Documentation

Onebot API

Crates.io Version GitHub Release

库如其名,这是一个Onebot V11协议的实现
目前已完成对Onebot V11协议所有API的实现

核心概念

Client

Client 是高层客户端的入口,封装了API调用、事件推送等核心逻辑层服务
Client 内部使用了 flume 作为API调用通道(mpsc),tokio broadcast 作为事件通道(mpmc
在与底层协议的交互方面,Client 内部使用了 特征对象依赖注入 ,这使得 Client 具备 协议无关 的特性
因此,Client 需要且仅需要专注于 API调用事件推送 等核心逻辑层服务,对于底层协议的交互,则使用外部依赖实现
由此,Client 实现了逻辑层与协议层的解耦,也使得 Client 具备运行时切换底层协议的能力
另外,由于 Client 与底层协议交互时使用了消息通道
因此,Client 天然具备 线程安全 并且不需要锁来防止竞态条件(Arc<Client>🤓☝️ | Arc<Mutex<Client>>👎😡)
对于资源管理方面,Client 实现了 Drop 特征,在 Client 析构时会自动清理其产生的所有资源
Client 并不会清理外部依赖所产生的资源,这依赖于外部依赖的析构函数(本库中所有实现了 CommunicationService 的结构都实现了 Drop 特征)

CommunicationService

CommunicationServiceClient 与底层协议交互的基础
任意实现了 CommunicationService 特征 的结构都可作为与 Client 交互的服务


目前已实现的协议:

  • 正向 WebSocket
  • 反向 WebSocket
  • SSE
  • Http
  • Http Post
flowchart LR
    A(Client) <-->|交互| B{CommunicationService}
    C([WebSocket])
    D([WebSocketReverse])
    E([Http])
    F([HttpPost])
    G([SSE])
    H([具体协议])
    B -->|API调用| H
    H -->|事件推送/API响应| B
    H -.- C & D & E & F & G

Usage

Client用法

use std::time::Duration;
use onebot_api::api::APISender;
use onebot_api::communication::utils::{Client, Event};
use onebot_api::communication::ws::WsService;
use onebot_api::event::EventReceiver;
use onebot_api::text;

#[tokio::main]
async fn main() {
	let ws_service = WsService::new("wss://example.com", Some("example_token".to_string())).unwrap();
	let client = Client::new(ws_service, Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();

	let msg_id = client.send_private_msg(123456, text!("this is a {}", "message"), None).await.unwrap();
	client.send_like(123456, Some(10)).await.unwrap();

	let mut event_receiver = client.get_receiver();
	while let Ok(event) = event_receiver.recv().await && let Event::Event(event) = &*event {
		println!("{:#?}", event)
	}
}

正向WebSocket

use std::time::Duration;
use onebot_api::communication::utils::Client;
use onebot_api::communication::ws::WsService;

#[tokio::main]
async fn main() {
	let ws_service = WsService::new("wss://example.com", Some("example_token".to_string())).unwrap();
	let client = Client::new(ws_service, Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();
}

反向WebSocket

use onebot_api::communication::utils::Client;
use onebot_api::communication::ws_reverse::WsReverseService;
use std::time::Duration;

#[tokio::main]
async fn main() {
	let ws_reverse_service = WsReverseService::new("0.0.0.0:8080", Some("example_token".to_string()));
	let client = Client::new(ws_reverse_service, Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();
}

Http

use onebot_api::communication::utils::Client;
use std::time::Duration;
use onebot_api::communication::http::HttpService;

#[tokio::main]
async fn main() {
	let http_service = HttpService::new("https://example.com", Some("example_token".to_string())).unwrap();
	let client = Client::new(http_service, Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();
}

Http Post

use onebot_api::communication::utils::Client;
use std::time::Duration;
use onebot_api::communication::http_post::HttpPostService;

#[tokio::main]
async fn main() {
	let http_post_service = HttpPostService::new("0.0.0.0:8080", None, Some("example_secret".to_string())).unwrap();
	let client = Client::new(http_post_service, Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();
}

SSE

use onebot_api::communication::utils::Client;
use std::time::Duration;
use onebot_api::communication::sse::SseService;

#[tokio::main]
async fn main() {
	let sse_service = SseService::new("https://example.com/_events", Some("example_token".to_string())).unwrap();
	let client = Client::new(sse_service, Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();
}

组合器

同时,该库设计了组合器来将不同的底层连接放在同一个Client上
例如,你可以创建一个SseService和一个HttpService,同时通过组合器将它们放在同一个Client上
其行为与直接用WsService并无差别

SplitCombiner

将事件接收与API发送分为两个不同服务实现
服务分为 send_sideread_side
其中,send_side 负责API发送服务,read_side 负责事件接收服务
send_side 的事件通道由一个 processor task 负责
processor 将 send_side 的API响应事件并入原事件通道,其余事件丢弃

use onebot_api::communication::utils::Client;
use std::time::Duration;
use onebot_api::communication::combiner::SplitCombiner;
use onebot_api::communication::http::HttpService;
use onebot_api::communication::sse::SseService;

#[tokio::main]
async fn main() {
	let sse_service = SseService::new("https://example.com/_events", Some("example_token".to_string())).unwrap();
	let http_service = HttpService::new("https://example.com", Some("example_token".to_string())).unwrap();
	let combiner = SplitCombiner::new(http_service, sse_service);
	let client = Client::new(combiner, Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();
}
flowchart LR
    A(Client) <-->|交互| B{CommunicationService}
    B -->|API调用| C[[SplitCombiner]]
    C -->|事件推送/API响应| B
    C -->|API调用| E([HttpService])
    E -->|API响应| C
    C ~~~ D([SseService])
    D -->|事件推送| C

TIPS

传统的 WebSocket 并不支持 HTTP 3,但是 SSE 支持 HTTP 3
因此,最初设计 SplitCombiner 时,就是用来组合 HttpServiceSseService
这样既可以享受 HTTP 3 带来的优势,同时在使用体验上也不输 WebSocket

BothEventCombiner

详见 SplitCombiner
SplitCombiner 的区别在于
BothEventCombiner 会将 send_side 的所有事件均并入原事件通道
因此,BothEventCombiner 不存在 processor task

use onebot_api::communication::combiner::BothEventCombiner;
use onebot_api::communication::ws_reverse::WsReverseService;
use onebot_api::communication::utils::Client;
use onebot_api::communication::ws::WsService;
use std::time::Duration;

#[tokio::main]
async fn main() {
	let ws_service = WsService::new("wss://example.com", Some("example_token".to_string())).unwrap();
	let ws_reverse_service = WsReverseService::new("0.0.0.0:8080", Some("example_token".to_string()));
	let combiner = BothEventCombiner::new(ws_service, ws_reverse_service);
	let client = Client::new(combiner, Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();
}
flowchart LR
    A(Client) <-->|交互| B{CommunicationService}
    B -->|API调用| C[[BothEventCombiner]]
    C -->|事件推送/API响应| B
    C -->|API调用| E([WsService])
    E -->|事件推送/API响应| C
    C ~~~ D([WsReverseService])
    D -->|事件推送| C

TIPS

对于组合器,组合器与组合器之间也是可以被组合器所连接的
因此,对于一个bot消息集群,可以通过多个 BothEventCombiner 来实现同一个client接收所有消息

use std::time::Duration;
use onebot_api::communication::combiner::BothEventCombiner;
use onebot_api::communication::http_post::HttpPostService;
use onebot_api::communication::sse::SseService;
use onebot_api::communication::utils::Client;
use onebot_api::communication::ws::WsService;
use onebot_api::communication::ws_reverse::WsReverseService;

#[tokio::main]
async fn main() {
	let bot_1 = WsService::new("ws://127.0.0.1:5000", None).unwrap();
	let bot_2 = WsReverseService::new("127.0.0.1:6000", None);
	let bot_3 = SseService::new("http://127.0.0.1:7000", None).unwrap();
	let bot_4 = HttpPostService::new("127.0.0.1:8000", None, None).unwrap();

	let combiner_1 = BothEventCombiner::new(bot_1, bot_2);
	let combiner_2 = BothEventCombiner::new(bot_3, bot_4);

	let combiner = BothEventCombiner::new(combiner_1, combiner_2);

	let client = Client::new(combiner, Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();
}
flowchart TD
    A(Client) <-->|交互| B{CommunicationService}
    C[[combiner]]
    D[[combiner_1]]
    E[[combiner_2]]
    F([bot_1])
    G([bot_2])
    H([bot_3])
    I([bot_4])
    B -->|API调用| C
    C -->|事件推送/API响应| B
    C -->|API调用| D
    D -->|事件推送/API响应| C
    C ~~~ E
    E -->|事件推送| C
    D -->|API调用| F
    F -->|事件推送/API响应| D
    D ~~~ G
    G -->|事件推送| D
    E -->|API调用| H
    H -->|事件推送/API响应| E
    H ~~~ I
    I -->|事件推送| E

何时使用哪种组合器?

  • 使用 SplitCombiner:当你明确分离 发送接收 时(例如刚才提到的 SseServiceHttpService
  • 使用 BothEventCombiner:当你需要聚合多个独立bot实例的事件流

SegmentBuilder

Onebot V11协议中,在发送消息时需要构造Segment Array
库提供了所有Send Segment的类型,但手动构造它们还是太麻烦了
于是就有了 SegmentBuilder

use std::time::Duration;
use onebot_api::api::APISender;
use onebot_api::communication::utils::Client;
use onebot_api::communication::ws::WsService;
use onebot_api::message::SegmentBuilder;

#[tokio::main]
async fn main() {
	let client = Client::new(WsService::new("ws://localhost:8080", None).unwrap(), Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();

	let segment = SegmentBuilder::new()
		.text("this is an apple")
		.image("https://example.com/apple.png")
		.text("\n")
		.text("this is a banana")
		.image("https://example.com/banana.png")
		.build();

	client.send_private_msg(123456, segment, None).await.unwrap();
}

当然,image 中的选项很多,如果你希望的话,库也提供了部分 segmentbuilder

use onebot_api::message::SegmentBuilder;

#[tokio::main]
async fn main() {
	let segment = SegmentBuilder::new()
		.text("this")
		.image_builder("https://example.com/apple.png")
		.cache(true)
		.timeout(5)
		.proxy(true)
		.build()
		.text("is an apple")
		.build();
}

当然,bot发送消息大部分情况都只是文本
每次都要创建 SegmentBuilder 还是太麻烦了
于是就有了 text

use std::time::Duration;
use onebot_api::api::APISender;
use onebot_api::communication::utils::Client;
use onebot_api::communication::ws::WsService;
use onebot_api::text;

#[tokio::main]
async fn main() {
	let client = Client::new(WsService::new("ws://localhost:8080", None).unwrap(), Some(Duration::from_secs(5)), None, None);
	client.start_service().await.unwrap();

	let msg = "123456".to_string();
	client.send_private_msg(123456, text!("this is a message: {}", msg), None).await.unwrap();
}

text 宏的内部使用了 format
因此,你可以像使用 println 宏一样使用 text

quick_operation

有时候,我们收到了一个事件,我们想直接对这个事件进行操作
此时,若调用 client 上的对应api,效率未免太低了,还要一个个传参
于是,我们提供了 quick_operation
quick_operation 是一系列快速操作trait(虽然目前还没实现多少,但未来肯定会实现的!)
用户可以直接在事件上进行对应的操作

Selector

实际上,因为事件分发使用了Arc的原因,同时为了追求事件零拷贝分发
这就使得事件的处理变得十分困难(至少实现起来非常繁琐)
于是,我们提供了 Selector 工具
Selector 提供了类似迭代器的链式调用能力

use onebot_api::communication::utils::Client;
use onebot_api::communication::ws::WsService;
use onebot_api::quick_operation::QuickSendMsg;
use onebot_api::text;

#[tokio::main]
async fn main() {
	let ws_service = WsService::new("wss://example.com", Some("example_token".to_string())).unwrap();
	let client = Client::new(ws_service);
	client.start_service().await.unwrap();
	let mut r = client.subscribe_normal_event();
	while let Ok(event) = r.recv().await {
		event
			.selector()
			.message()
			.and_filter_self_id(|self_id| self_id == 123456)
			.message_event_selector()
			.group()
			.and_filter_user_id(|user_id| user_id == 114514)
			.and_filter_raw_message(|msg| msg.starts_with("/command"))
			.and_normal()
			.select_async(async |event| {
				event
					.send_msg(&client, text!("this is a command"), None)
					.await
			})
			.await
			.expect("not matched")
			.expect("can not send message");
	}
}

在这里,我们接收到了一个事件,此时,我们要求该事件:

  1. 属于 message 类型
  2. 要求机器人id为 123456
  3. 要求该 message 为群组消息
  4. 要求发送用户id为 114514
  5. 要求原始消息内容前缀为 /command
  6. 要求为正常消息

在以上条件全部满足后,将会执行 select_async 中传入的闭包
其中,若任何一个条件不满足,则会直接返回 None ,不执行闭包
对于以上所有 filter 函数,我们都提供了以下几个版本:

  • 普通版本
  • 链式调用版本(前缀 and
  • 异步版本(后缀 async
  • 链式调用异步版本(前缀 and + 后缀 async

当然,最后的 select_async 也有对应的异步版本

Todo List

  • WsService 自动重连 ✅
  • SseService 自动重连 ❌ (目前还没有方法能够在SSE连接突然断开后获得通知)
  • 更精细化的错误处理
    • Client 实现无 anyhow::Result
    • 服务 task 实现无 anyhow::Result
    • 取消服务 task 错误静默处理
  • 更完善的文档注释
  • 自定义Event反序列化
  • 更多的API!
    • napcat API
    • go-cqhttp API