├── rustfmt.toml
├── tests
└── test_all_features.sh
├── .gitignore
├── README.md
├── src
├── types.rs
├── error.rs
├── lib.rs
├── logger.rs
├── utils.rs
├── bot
│ ├── event
│ │ ├── notice_event.rs
│ │ ├── request_event.rs
│ │ ├── lifecycle_event.rs
│ │ ├── msg_send_from_kovi_event.rs
│ │ ├── private_msg_event.rs
│ │ ├── group_msg_event.rs
│ │ ├── admin_msg_event.rs
│ │ ├── msg_send_from_server_event.rs
│ │ └── msg_event.rs
│ ├── runtimebot.rs
│ ├── event.rs
│ ├── message
│ │ └── add.rs
│ ├── run.rs
│ ├── connect.rs
│ ├── handler.rs
│ ├── runtimebot
│ │ └── kovi_api.rs
│ └── message.rs
├── task.rs
├── plugin.rs
└── plugin
│ └── plugin_builder.rs
├── README_EN.md
├── Cargo.toml
├── .github
└── ISSUE_TEMPLATE
│ └── bug_report.yml
├── README_Cargo.md
└── LICENSE
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | empty_item_single_line = false
2 | format_code_in_doc_comments = true
3 | overflow_delimited_expr = true
4 |
--------------------------------------------------------------------------------
/tests/test_all_features.sh:
--------------------------------------------------------------------------------
1 | cargo hack test --feature-powerset --exclude-features native-tls-vendored,rustls-tls-webpki-roots,rustls-tls-native-roots -p kovi
2 |
3 | cargo hack clippy --feature-powerset --exclude-features native-tls-vendored,rustls-tls-webpki-roots,rustls-tls-native-roots -p kovi
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | debug/
4 | target/
5 |
6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8 | Cargo.lock
9 |
10 | # These are backup files generated by rustfmt
11 | **/*.rs.bk
12 |
13 | # MSVC Windows builds of rustc generate these, which store debugging information
14 | *.pdb
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |  [](https://qm.qq.com/q/kmpSBOVaCI)
4 |
5 | **简体中文** | [English](README_EN.md)
6 |
7 |
8 |
9 | # Kovi
10 |
11 | Kovi 是一个 OneBot V11 的插件框架,如果你想用 Rust 来开发 OneBot V11 机器人,那么 Kovi 是一个很好的选择。
12 |
13 | 🎯 目标是 Rust 最简单 OneBot 框架!复杂的 Rust 简化写法? Kovi 已经尽力了。
14 |
15 | 🤔 让我数数,文档里面的快速上手,居然9行代码就可以实现一个最简插件。
16 |
17 | 🥁 还有 CLI 工具,方便项目开发。
18 |
19 | 🛍️ 插件商店带来绝佳 Kovi 购物体验,一键接入插件开发者们的包裹📦。
20 |
21 | 😍 本项目文档非常简单易懂,跟着来一遍保证都会了。
22 |
23 | ### ↓ 文档在这里
24 |
25 | [Kovi Docs](https://thricecola.github.io/kovi-doc/)
26 |
27 | ### ↓ 商店在这里
28 |
29 | [Kovi Shop](https://thricecola.github.io/kovi-doc/start/plugins.html)
30 |
31 | **注意⚠️,项目目前只支持 OneBot V11 正向 WebSocket 协议**
32 |
33 | 球球啦,点个星星⭐吧,这是一个很大的鼓励。
34 |
35 | 还有欢迎加Q群玩。
36 |
--------------------------------------------------------------------------------
/src/types.rs:
--------------------------------------------------------------------------------
1 | use crate::{ApiReturn, bot::SendApi};
2 | use std::{pin::Pin, sync::Arc};
3 | use tokio::sync::oneshot;
4 |
5 | pub(crate) type KoviAsyncFn = dyn Fn() -> Pin + Send>> + Send + Sync;
6 |
7 | pub type PinFut = Pin + Send>>;
8 |
9 | // pub type MsgFn = Arc) -> PinFut + Send + Sync>;
10 |
11 | // pub type NoticeFn = Arc) -> PinFut + Send + Sync>;
12 |
13 | // pub type RequestFn = Arc) -> PinFut + Send + Sync>;
14 | //
15 | // pub type EventFn = Arc) -> PinFut + Send + Sync>;
16 |
17 | pub type NoArgsFn = Arc PinFut + Send + Sync>;
18 |
19 | pub type ApiOneshotSender = oneshot::Sender>;
20 | pub type ApiOneshotReceiver = oneshot::Receiver>;
21 |
22 | pub type ApiAndOneshot = (
23 | SendApi,
24 | Option>>,
25 | );
26 |
27 | pub type ApiAndRuturn = (SendApi, Result);
28 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use thiserror::Error;
2 |
3 | #[derive(Error, Debug)]
4 | pub enum MessageError {
5 | /// 解析出错
6 | #[error("Parse error: {0}")]
7 | ParseError(String),
8 | // #[error("Error, and no one knows why something went wrong")]
9 | // UnknownError(),
10 | }
11 |
12 | #[derive(Error, Debug)]
13 | pub enum BotError {
14 | /// 没有寻找到插件
15 | #[error("Plugin not found: {0}")]
16 | PluginNotFound(String),
17 | #[error("Bot's Weak reference has expired")]
18 | RefExpired,
19 | }
20 |
21 | #[derive(Error, Debug)]
22 | pub enum BotBuildError {
23 | /// 解析TOML文件失败
24 | #[error("Failed to parse TOML:\n{0}\nPlease reload the config file")]
25 | TomlParseError(String),
26 | /// 无法创建配置文件
27 | #[error("Failed to create config file: {0}")]
28 | FileCreateError(String),
29 | /// 无法读取TOML文件
30 | #[error("Failed to read TOML file: {0}")]
31 | FileReadError(String),
32 | }
33 |
34 | #[derive(Error, Debug)]
35 | pub enum EventBuildError {
36 | /// 解析出错
37 | #[error("Parse error: {0}")]
38 | ParseError(String),
39 | }
40 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |  [](https://qm.qq.com/q/kmpSBOVaCI)
4 |
5 | [简体中文](README.md) | **English**
6 |
7 |
8 |
9 | # Kovi
10 |
11 | Kovi is a plugin framework for OneBot V11. If you want to develop OneBot V11 bots using Rust, Kovi is a great choice.
12 |
13 | 🎯 The goal is to create the simplest OneBot framework in Rust! Simplifying complex Rust syntax? Kovi has done its best.
14 |
15 | 🤔 Let me count, the quick start in the documentation only requires 9 lines of code to create the simplest plugin.
16 |
17 | 🥁 There’s also a CLI tool to make project development easier.
18 |
19 | 🛍️ The plugin shop provides an excellent Kovi shopping experience, allowing you to easily access packages from plugin developers 📦.
20 |
21 | 😍 The project documentation is very simple and easy to understand. Follow it and you’ll be good to go.
22 |
23 | ### ↓ Documentation is here
24 |
25 | [Kovi Docs](https://thricecola.github.io/kovi-doc/)
26 |
27 | ### ↓ The shop is here
28 |
29 | [Kovi Shop](https://kovi.thricecola.com/start/plugins.html)
30 |
31 | **Note ⚠️: Currently, the project only supports OneBot V11's forward WebSocket protocol.**
32 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "kovi"
3 | version = "0.12.6"
4 | authors = ["ThriceCola "]
5 | edition = "2024"
6 | description = "A OneBot V11 bot plugin framework"
7 | documentation = "https://thricecola.github.io/kovi-doc/"
8 | readme = "README_Cargo.md"
9 | repository = "https://github.com/thricecola/Kovi"
10 | license = "MPL-2.0"
11 | keywords = ["framework", "kovi", "onebot"]
12 |
13 | [lib]
14 | name = "kovi"
15 | path = "src/lib.rs"
16 |
17 | [dependencies]
18 | ahash = "0.8"
19 | chrono = "0.4"
20 | croner = "2"
21 | dialoguer = { version = "0.11", features = ["fuzzy-select"] }
22 | env_logger = { version = "0.11", default-features = false, features = [
23 | "auto-color",
24 | "color",
25 | ], optional = true }
26 | futures-util = "0.3"
27 | http = "1"
28 | kovi-macros = { version = "0.5" }
29 | log = "0.4"
30 | parking_lot = "0.12"
31 | rand = "0.9"
32 | serde = { version = "1", features = ["derive"] }
33 | serde_json = "1"
34 | thiserror = "2"
35 | tokio = { version = "1", features = ["full", "windows-sys"] }
36 | tokio-tungstenite = "0.26"
37 | toml = "0.8"
38 | toml_edit = "0.22"
39 |
40 | [features]
41 | cqstring = []
42 | default = ["logger", "plugin-access-control", "save_bot_status"]
43 | logger = ["env_logger"]
44 | native-tls-vendored = ["tokio-tungstenite/native-tls-vendored"]
45 | plugin-access-control = []
46 | rustls-tls-native-roots = ["tokio-tungstenite/rustls-tls-native-roots"]
47 | rustls-tls-webpki-roots = ["tokio-tungstenite/rustls-tls-webpki-roots"]
48 | save_bot_admin = []
49 | save_bot_status = ["save_bot_admin", "save_plugin_status"]
50 | save_plugin_status = []
51 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! # Kovi
2 | //!
3 | //! A OneBot V11 bot framework developed using Rust.
4 | //!
5 | //! More documentation can be found at [Github-Kovi](https://github.com/ThriceCola/Kovi) Or [Kovi-doc](https://thricecola.github.io/kovi-doc/)
6 | //!
7 | //! 中文文档或更多文档请查看[Github-Kovi](https://github.com/ThriceCola/Kovi) 和 [Kovi-doc](https://thricecola.github.io/kovi-doc/)
8 | #![deny(clippy::unwrap_used)]
9 |
10 | /// Everything about bots is inside
11 | pub mod bot;
12 | /// 一些错误枚举
13 | pub mod error;
14 | /// 控制台输出日志
15 | pub mod logger;
16 | /// 关于插件的一切
17 | pub mod plugin;
18 | /// task 提供 kovi 运行时的多线程处理
19 | pub mod task;
20 | /// 这里包含一些集成类型
21 | pub mod types;
22 | /// 提供一些方便的插件开发函数
23 | pub mod utils;
24 |
25 | pub use bot::ApiReturn;
26 | pub use bot::Bot;
27 | pub use bot::event;
28 | pub use bot::message::Message;
29 | pub use bot::runtimebot::RuntimeBot;
30 | pub use error::MessageError;
31 | pub use kovi_macros::plugin;
32 | pub use plugin::plugin_builder::PluginBuilder;
33 | pub use plugin::plugin_builder::event::MsgEvent;
34 | pub use plugin::plugin_builder::event::NoticeEvent;
35 | pub use plugin::plugin_builder::event::RequestEvent;
36 | pub use task::spawn;
37 |
38 | #[deprecated(since = "0.11.0", note = "请使用 `MsgEvent` 代替")]
39 | pub type AllMsgEvent = bot::plugin_builder::event::MsgEvent;
40 | #[deprecated(since = "0.11.0", note = "请使用 `NoticeEvent` 代替")]
41 | pub type AllNoticeEvent = bot::plugin_builder::event::NoticeEvent;
42 | #[deprecated(since = "0.11.0", note = "请使用 `RequestEvent` 代替")]
43 | pub type AllRequestEvent = bot::plugin_builder::event::RequestEvent;
44 |
45 | pub use chrono;
46 | pub use croner;
47 | pub use futures_util;
48 | pub use log;
49 | pub use serde_json;
50 | pub use tokio;
51 | pub use toml;
52 |
53 | pub(crate) use crate::bot::run::RUNTIME as RT;
54 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug 反馈
3 | description: 报告可能的 Kovi 异常行为
4 | title: "[BUG] "
5 | labels: potential bug
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | 欢迎来到 Kovi 的 Issue Tracker!请填写以下表格来提交 Bug。
11 | 在提交新的 Bug 反馈前,请确保您:
12 | * 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
13 | * 不与现有的某一 issue 重复
14 |
15 | - type: input
16 | id: system-version
17 | attributes:
18 | label: 系统信息
19 | description: 运行 Kovi 的系统信息
20 | placeholder: Windows 10 Pro Workstation 22H2
21 | validations:
22 | required: true
23 | - type: input
24 | id: napcat-version
25 | attributes:
26 | label: Kovi 版本
27 | description: 可在 Cargo.toml 中找到
28 | placeholder: 1.0.0
29 | validations:
30 | required: true
31 | - type: input
32 | id: onebot-server-version
33 | attributes:
34 | label: OneBot 服务端
35 | description: 连接至 Kovi 的服务端版本信息
36 | placeholder: Napcat 2.5.0
37 | validations:
38 | required: true
39 | - type: textarea
40 | id: what-happened
41 | attributes:
42 | label: 发生了什么?
43 | description: 填写你认为的 Kovi 的不正常行为
44 | validations:
45 | required: true
46 | - type: textarea
47 | id: how-reproduce
48 | attributes:
49 | label: 如何复现
50 | description: 填写应当如何操作才能触发这个不正常行为
51 | placeholder: |
52 | 1. xxx
53 | 2. xxx
54 | 3. xxx
55 | validations:
56 | required: true
57 | - type: textarea
58 | id: what-expected
59 | attributes:
60 | label: 期望的结果?
61 | description: 填写你认为 Kovi 应当执行的正常行为
62 | validations:
63 | required: true
64 | - type: textarea
65 | id: kovi-log
66 | attributes:
67 | label: kovi 运行日志
68 | description: 粘贴相关日志内容到此处,通过从 kovi.conf.json 中打开 debug 模式获取更详细的日志
69 | render: shell
70 | validations:
71 | required: true
72 |
73 | - type: textarea
74 | id: any
75 | attributes:
76 | label: 你想填写的其它内容
77 | description: 填写其它想填写的内容
78 |
--------------------------------------------------------------------------------
/src/logger.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "logger")]
2 | pub fn set_logger() {
3 | use chrono::Local;
4 | use log::Level;
5 | use std::io::Write;
6 |
7 | macro_rules! _format {
8 | ($level:literal, $timestamp:ident, $record:ident, $($color:ident),+ $(,)?) => {{
9 | let color = dialoguer::console::style($level)$(.$color())+;
10 | format!("[{}] [{}]: {}", color, $timestamp, $record.args())
11 | }};
12 | }
13 |
14 | let init = env_logger::Builder::from_default_env()
15 | .format(|buf, record| {
16 | let t = Local::now().format("%m-%d %H:%M:%S");
17 |
18 | match record.level() {
19 | Level::Info => {
20 | writeln!(buf, "[{t}] {}", record.args())
21 | }
22 | Level::Debug => {
23 | writeln!(buf, "{}", _format!("Debug", t, record, yellow, italic))
24 | }
25 | Level::Warn => {
26 | writeln!(buf, "{}", _format!("Warn", t, record, yellow))
27 | }
28 | Level::Error => {
29 | writeln!(buf, "{}", _format!("Error", t, record, red))
30 | }
31 | Level::Trace => {
32 | writeln!(buf, "{}", _format!("Trace", t, record, magenta))
33 | }
34 | }
35 | })
36 | .try_init();
37 |
38 | if let Err(e) = init {
39 | println!(
40 | "Kovi init env_logger failed: {e}. Very likely you've already started a logger"
41 | );
42 | }
43 | }
44 |
45 | pub fn try_set_logger() {
46 | #[cfg(feature = "logger")]
47 | set_logger();
48 | }
49 |
50 | #[cfg(feature = "logger")]
51 | #[test]
52 | fn test_logger() {
53 | unsafe {
54 | std::env::set_var("RUST_LOG", "trace");
55 | }
56 |
57 | // Initialize the logger
58 | try_set_logger();
59 |
60 | // Test different log levels
61 | log::info!("This is an info message - should appear without color");
62 | log::debug!("This is a debug message - should appear in yellow");
63 | log::warn!("This is a warning message - should appear in yellow");
64 | log::error!("This is an error message - should appear in red");
65 | log::trace!("This is a trace message - should appear in red");
66 | }
67 |
--------------------------------------------------------------------------------
/src/utils.rs:
--------------------------------------------------------------------------------
1 | use serde::{Serialize, de::DeserializeOwned};
2 | use std::{
3 | fs::{self, File},
4 | io::{Read, Write},
5 | path::Path,
6 | };
7 |
8 | fn save_data(data: &[u8], file_path: &Path) -> Result<(), Box> {
9 | if let Some(parent) = file_path.parent()
10 | && !parent.exists()
11 | {
12 | fs::create_dir_all(parent)?;
13 | }
14 |
15 | let mut file = File::create(file_path)?;
16 | file.write_all(data)?;
17 |
18 | Ok(())
19 | }
20 |
21 | /// 加载本地json数据,如果没有则保存传入的数据进指定路径
22 | pub fn load_json_data(data: T, file_path: P) -> Result>
23 | where
24 | T: Serialize + DeserializeOwned,
25 | P: AsRef,
26 | {
27 | if !file_path.as_ref().exists() {
28 | let serialized_data = serde_json::to_string(&data)?;
29 | save_data(serialized_data.as_bytes(), file_path.as_ref())?;
30 | return Ok(data);
31 | }
32 |
33 | let mut file = File::open(&file_path)?;
34 | let mut contents = String::new();
35 | file.read_to_string(&mut contents)?;
36 | let deserialized_data = serde_json::from_str(&contents)?;
37 |
38 | Ok(deserialized_data)
39 | }
40 |
41 | /// 加载本地toml数据,如果没有则保存传入的数据进指定路径
42 | pub fn load_toml_data(data: T, file_path: P) -> Result>
43 | where
44 | T: Serialize + DeserializeOwned,
45 | P: AsRef,
46 | {
47 | if !file_path.as_ref().exists() {
48 | let serialized_data = toml::to_string(&data)?;
49 | save_data(serialized_data.as_bytes(), file_path.as_ref())?;
50 | return Ok(data);
51 | }
52 |
53 | let mut file = File::open(file_path)?;
54 | let mut contents = String::new();
55 | file.read_to_string(&mut contents)?;
56 | let deserialized_data = toml::from_str(&contents)?;
57 |
58 | Ok(deserialized_data)
59 | }
60 |
61 | /// 将json数据保存在传入的地址
62 | pub fn save_json_data(data: &T, file_path: P) -> Result<(), Box>
63 | where
64 | T: Serialize,
65 | P: AsRef,
66 | {
67 | let serialized_data = serde_json::to_string(data)?;
68 | save_data(serialized_data.as_bytes(), file_path.as_ref())?;
69 | Ok(())
70 | }
71 |
72 | /// 将toml数据保存在传入的地址
73 | pub fn save_toml_data(data: &T, file_path: P) -> Result<(), Box>
74 | where
75 | T: Serialize,
76 | P: AsRef,
77 | {
78 | let serialized_data = toml::to_string(data)?;
79 | save_data(serialized_data.as_bytes(), file_path.as_ref())?;
80 | Ok(())
81 | }
82 |
83 | /// 计算pskey值
84 | pub fn calculate_pskey(skey: &str) -> u32 {
85 | let mut hash: u32 = 5381;
86 | for character in skey.chars() {
87 | hash = (hash << 5)
88 | .wrapping_add(hash)
89 | .wrapping_add(character as u32);
90 | }
91 | hash & 0x7fffffff
92 | }
93 |
--------------------------------------------------------------------------------
/src/bot/event/notice_event.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | bot::{
3 | BotInformation,
4 | event::InternalEvent,
5 | plugin_builder::event::{Event, PostType},
6 | },
7 | error::EventBuildError,
8 | types::ApiAndOneshot,
9 | };
10 | use serde_json::{Value, value::Index};
11 | use tokio::sync::mpsc;
12 |
13 | #[derive(Debug, Clone)]
14 | pub struct NoticeEvent {
15 | /// 事件发生的时间戳
16 | pub time: i64,
17 | /// 收到事件的机器人 登陆号
18 | pub self_id: i64,
19 | /// 上报类型
20 | pub post_type: PostType,
21 | /// 通知类型
22 | pub notice_type: String,
23 |
24 | /// 原始的onebot消息,已处理成json格式
25 | pub original_json: Value,
26 | }
27 | impl Event for NoticeEvent {
28 | fn de(
29 | event: &InternalEvent,
30 | _: &BotInformation,
31 | _: &mpsc::Sender,
32 | ) -> Option {
33 | let InternalEvent::OneBotEvent(json_str) = event else {
34 | return None;
35 | };
36 | Self::new(json_str).ok()
37 | }
38 | }
39 |
40 | impl NoticeEvent {
41 | pub(crate) fn new(msg: &str) -> Result {
42 | let temp: Value =
43 | serde_json::from_str(msg).map_err(|e| EventBuildError::ParseError(e.to_string()))?;
44 | let time = temp
45 | .get("time")
46 | .and_then(Value::as_i64)
47 | .ok_or(EventBuildError::ParseError("time".to_string()))?;
48 | let self_id = temp
49 | .get("self_id")
50 | .and_then(Value::as_i64)
51 | .ok_or(EventBuildError::ParseError("self_id".to_string()))?;
52 | let post_type = temp
53 | .get("post_type")
54 | .and_then(|v| serde_json::from_value::(v.clone()).ok())
55 | .ok_or(EventBuildError::ParseError("Invalid post_type".to_string()))?;
56 | let notice_type = temp
57 | .get("notice_type")
58 | .and_then(Value::as_str)
59 | .map(String::from)
60 | .ok_or(EventBuildError::ParseError("notice_type".to_string()))?;
61 | Ok(NoticeEvent {
62 | time,
63 | self_id,
64 | post_type,
65 | notice_type,
66 | original_json: temp,
67 | })
68 | }
69 | }
70 |
71 | impl NoticeEvent {
72 | /// 直接从原始的 Json Value 获取某值
73 | ///
74 | /// # example
75 | ///
76 | /// ```ignore
77 | /// use kovi::PluginBuilder;
78 | ///
79 | /// PluginBuilder::on_notice(|event| async move {
80 | /// let time = event.get("time").and_then(|v| v.as_i64()).unwrap();
81 | ///
82 | /// assert_eq!(time, event.time);
83 | /// });
84 | /// ```
85 | pub fn get(&self, index: I) -> Option<&Value> {
86 | self.original_json.get(index)
87 | }
88 | }
89 |
90 | impl std::ops::Index for NoticeEvent
91 | where
92 | I: Index,
93 | {
94 | type Output = Value;
95 |
96 | fn index(&self, index: I) -> &Self::Output {
97 | &self.original_json[index]
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/bot/event/request_event.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | bot::{
3 | BotInformation,
4 | event::InternalEvent,
5 | plugin_builder::event::{Event, PostType},
6 | },
7 | error::EventBuildError,
8 | types::ApiAndOneshot,
9 | };
10 | use serde_json::{Value, value::Index};
11 | use tokio::sync::mpsc;
12 |
13 | #[derive(Debug, Clone)]
14 | pub struct RequestEvent {
15 | /// 事件发生的时间戳
16 | pub time: i64,
17 | /// 收到事件的机器人 登陆号
18 | pub self_id: i64,
19 | /// 上报类型
20 | pub post_type: PostType,
21 | /// 请求类型
22 | pub request_type: String,
23 |
24 | /// 原始的onebot消息,已处理成json格式
25 | pub original_json: Value,
26 | }
27 | impl Event for RequestEvent {
28 | fn de(
29 | event: &InternalEvent,
30 | _: &BotInformation,
31 | _: &mpsc::Sender,
32 | ) -> Option {
33 | let InternalEvent::OneBotEvent(json_str) = event else {
34 | return None;
35 | };
36 |
37 | Self::new(json_str).ok()
38 | }
39 | }
40 |
41 | impl RequestEvent {
42 | pub(crate) fn new(msg: &str) -> Result {
43 | let temp: Value =
44 | serde_json::from_str(msg).map_err(|e| EventBuildError::ParseError(e.to_string()))?;
45 | let time = temp
46 | .get("time")
47 | .and_then(Value::as_i64)
48 | .ok_or(EventBuildError::ParseError("time".to_string()))?;
49 | let self_id = temp
50 | .get("self_id")
51 | .and_then(Value::as_i64)
52 | .ok_or(EventBuildError::ParseError("self_id".to_string()))?;
53 | let post_type = temp
54 | .get("post_type")
55 | .and_then(|v| serde_json::from_value::(v.clone()).ok())
56 | .ok_or(EventBuildError::ParseError("Invalid post_type".to_string()))?;
57 | let request_type = temp
58 | .get("request_type")
59 | .and_then(Value::as_str)
60 | .map(String::from)
61 | .ok_or(EventBuildError::ParseError("request_type".to_string()))?;
62 | Ok(RequestEvent {
63 | time,
64 | self_id,
65 | post_type,
66 | request_type,
67 | original_json: temp,
68 | })
69 | }
70 | }
71 |
72 | impl RequestEvent {
73 | /// 直接从原始的 Json Value 获取某值
74 | ///
75 | /// # example
76 | ///
77 | /// ```ignore
78 | /// use kovi::PluginBuilder;
79 | ///
80 | /// PluginBuilder::on_request(|event| async move {
81 | /// let time = event.get("time").and_then(|v| v.as_i64()).unwrap();
82 | ///
83 | /// assert_eq!(time, event.time);
84 | /// });
85 | /// ```
86 | pub fn get(&self, index: I) -> Option<&Value> {
87 | self.original_json.get(index)
88 | }
89 | }
90 |
91 | impl std::ops::Index for RequestEvent
92 | where
93 | I: Index,
94 | {
95 | type Output = Value;
96 |
97 | fn index(&self, index: I) -> &Self::Output {
98 | &self.original_json[index]
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/bot/event/lifecycle_event.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | ApiReturn,
3 | bot::{
4 | BotInformation, SendApi,
5 | event::InternalEvent,
6 | plugin_builder::event::{Event, PostType},
7 | },
8 | types::ApiAndOneshot,
9 | };
10 | use log::{error, info};
11 | use serde::{Deserialize, Serialize};
12 | use serde_json::json;
13 | use tokio::sync::{mpsc, oneshot};
14 |
15 | #[derive(Debug, Clone, Deserialize)]
16 | pub struct LifecycleEvent {
17 | pub meta_event_type: String,
18 | pub post_type: PostType,
19 | pub self_id: i64,
20 | pub time: i64,
21 | pub sub_type: LifecycleAction,
22 | }
23 |
24 | #[derive(Debug, Copy, Clone, Deserialize, Serialize)]
25 | #[serde(rename_all = "lowercase")]
26 | pub enum LifecycleAction {
27 | Enable,
28 | Disable,
29 | Connect,
30 | }
31 |
32 | impl Event for LifecycleEvent {
33 | fn de(
34 | event: &InternalEvent,
35 | _: &BotInformation,
36 | _: &tokio::sync::mpsc::Sender,
37 | ) -> Option
38 | where
39 | Self: Sized,
40 | {
41 | let InternalEvent::OneBotEvent(json_str) = event else {
42 | return None;
43 | };
44 | let event: LifecycleEvent = serde_json::from_str(json_str).ok()?;
45 | if event.meta_event_type == "lifecycle" {
46 | Some(event)
47 | } else {
48 | None
49 | }
50 | }
51 | }
52 |
53 | pub(crate) async fn handler_lifecycle_log_bot_enable(api_tx_: mpsc::Sender) {
54 | let api_msg = SendApi::new("get_login_info", json!({}));
55 |
56 | #[allow(clippy::type_complexity)]
57 | let (api_tx, api_rx): (
58 | oneshot::Sender>,
59 | oneshot::Receiver>,
60 | ) = oneshot::channel();
61 |
62 | api_tx_
63 | .send((api_msg, Some(api_tx)))
64 | .await
65 | .expect("The api_tx channel closed");
66 |
67 | let receive = match api_rx.await {
68 | Ok(v) => v,
69 | Err(e) => {
70 | error!("Lifecycle Error, get bot info failed: {e}");
71 | return;
72 | }
73 | };
74 |
75 | let self_info_value = match receive {
76 | Ok(v) => v,
77 | Err(e) => {
78 | error!("Lifecycle Error, get bot info failed: {e}");
79 | return;
80 | }
81 | };
82 |
83 | let self_id = match self_info_value.data.get("user_id") {
84 | Some(user_id) => match user_id.as_i64() {
85 | Some(id) => id,
86 | None => {
87 | error!("Expected 'user_id' to be an integer");
88 | return;
89 | }
90 | },
91 | None => {
92 | error!("Missing 'user_id' in self_info_value data");
93 | return;
94 | }
95 | };
96 | let self_name = match self_info_value.data.get("nickname") {
97 | Some(nickname) => nickname.to_string(),
98 | None => {
99 | error!("Missing 'nickname' in self_info_value data");
100 | return;
101 | }
102 | };
103 | info!(
104 | "Bot connection successful,Nickname:{self_name},ID:{self_id}"
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/src/task.rs:
--------------------------------------------------------------------------------
1 | use crate::{RT, plugin::PLUGIN_NAME};
2 | use ahash::RandomState;
3 | use parking_lot::Mutex;
4 | use std::{
5 | borrow::BorrowMut,
6 | collections::HashMap,
7 | future::Future,
8 | sync::{Arc, LazyLock},
9 | time::Duration,
10 | };
11 | use tokio::{
12 | task::{AbortHandle, JoinHandle},
13 | time::interval,
14 | };
15 |
16 | pub(crate) static TASK_MANAGER: LazyLock = LazyLock::new(TaskManager::init);
17 |
18 | pub(crate) struct TaskManager {
19 | pub(crate) handles: Arc>,
20 | }
21 |
22 | impl TaskManager {
23 | pub(crate) fn init() -> Self {
24 | let handles = Arc::new(Mutex::new(TaskAbortHandles::default()));
25 |
26 | let handles_clone = handles.clone();
27 | RT.spawn(async move {
28 | let mut interval = interval(Duration::from_secs(20)); // 每>秒清理一次
29 | loop {
30 | interval.tick().await;
31 | log::debug!("Kovi task thread is cleaning up task handles");
32 |
33 | let mut handles_lock = handles_clone.lock();
34 |
35 | handles_lock.clear();
36 | }
37 | });
38 |
39 | Self { handles }
40 | }
41 |
42 | pub(crate) fn disable_plugin(&self, plugin_name: &str) {
43 | let mut task_manager = self.handles.lock();
44 |
45 | let map = task_manager.map.borrow_mut();
46 | let vec = match map.get(plugin_name) {
47 | Some(v) => v,
48 | None => return,
49 | };
50 |
51 | for abort in vec {
52 | abort.abort();
53 | }
54 | }
55 | }
56 |
57 | #[derive(Debug, Clone)]
58 | pub(crate) struct TaskAbortHandles {
59 | map: HashMap, RandomState>,
60 | }
61 |
62 | impl Default for TaskAbortHandles {
63 | fn default() -> Self {
64 | Self {
65 | map: HashMap::with_hasher(RandomState::new()),
66 | }
67 | }
68 | }
69 |
70 | impl TaskAbortHandles {
71 | pub(crate) fn clear(&mut self) {
72 | for vec in self.map.values_mut() {
73 | vec.retain(|abort| !abort.is_finished());
74 | vec.shrink_to_fit();
75 | }
76 | }
77 | }
78 |
79 | /// 生成一个新的异步线程并立即运行,另外,这个线程关闭句柄会被交给 Kovi 管理。
80 | ///
81 | /// **如果在 Kovi 管理之外的地方(新的tokio线程或者系统线程)运行此函数,此函数会 panic!**
82 | ///
83 | /// 由 Kovi 管理的地方:
84 | ///
85 | /// 1. 有 #[kovi::plugin] 的插件入口函数。
86 | /// 2. 插件的监听闭包。
87 | /// 3. 由 kovi::spawn() 创建的新线程。
88 | ///
89 | /// # panic!
90 | ///
91 | /// 如果在 Kovi 管理之外的地方(tokio线程或者系统线程)运行此函数,此函数会 panic!
92 | pub fn spawn(future: F) -> JoinHandle
93 | where
94 | F: Future + Send + 'static,
95 | F::Output: Send + 'static,
96 | {
97 | PLUGIN_NAME.with(|name| {
98 | let join = {
99 | let name = name.clone();
100 | RT.spawn(PLUGIN_NAME.scope(name, future))
101 | };
102 |
103 | let about_join = join.abort_handle();
104 |
105 | task_manager_handler(name, about_join);
106 |
107 | join
108 | })
109 | }
110 |
111 | pub(crate) fn task_manager_handler(name: &str, about_join: AbortHandle) {
112 | let mut task_abort_handles = TASK_MANAGER.handles.lock();
113 |
114 | let aborts = task_abort_handles.map.entry(name.to_string()).or_default();
115 |
116 | aborts.push(about_join);
117 | }
118 |
--------------------------------------------------------------------------------
/src/bot/runtimebot.rs:
--------------------------------------------------------------------------------
1 | use crate::types::{ApiAndOneshot, ApiOneshotReceiver, ApiOneshotSender};
2 |
3 | use super::{ApiReturn, Bot, Host, SendApi};
4 | use log::error;
5 | use parking_lot::RwLock;
6 | use serde_json::Value;
7 | use std::sync::Weak;
8 | use tokio::sync::{mpsc, oneshot};
9 |
10 | pub mod kovi_api;
11 | pub mod onebot_api;
12 |
13 | pub use kovi_api::SetAdmin;
14 |
15 | /// 运行时的Bot,可以用来发送api,需要从PluginBuilder的.get_runtime_bot()获取。
16 | /// # Examples
17 | /// ```ignore
18 | /// use kovi::PluginBuilder;
19 | ///
20 | /// let bot = PluginBuilder::get_runtime_bot();
21 | /// let user_id = bot.get_main_admin().unwrap();
22 | ///
23 | /// bot.send_private_msg(user_id, "bot online")
24 | /// ```
25 | #[derive(Clone)]
26 | pub struct RuntimeBot {
27 | pub host: Host,
28 | pub port: u16,
29 |
30 | pub(crate) bot: Weak>,
31 | pub(crate) plugin_name: String,
32 | pub api_tx: mpsc::Sender,
33 | }
34 |
35 | /// 提供给拓展 API 插件开发者的异步 API 请求发送函数,返回一个 Future ,用于等待在 Kovi 中已经缓存好的API响应。
36 | pub fn send_api_request_with_response(
37 | api_tx: &mpsc::Sender,
38 | send_api: SendApi,
39 | ) -> impl std::future::Future