Compare commits

...

91 Commits
0.7.0 ... main

Author SHA1 Message Date
57d7e8c8cc
rm py-clone 2025-04-13 00:09:28 +08:00
f4ceef050f
warn 2025-04-11 01:05:02 +08:00
56469f6fbb
写了个 schduler 2025-04-11 00:36:58 +08:00
1f92a62e45
update dep 2025-04-10 23:37:35 +08:00
12d6254c6b
update to edition 2024 2025-04-10 23:36:16 +08:00
e10dff96d2
fix + 简化 2025-04-10 22:56:49 +08:00
ff130d7f84
挪个地方 2025-04-10 22:29:17 +08:00
d4999d1ab3
改掉一点之前写的比较粗糙的地方 2025-04-10 22:28:49 +08:00
546214f52f
fmt! 2025-04-09 23:14:17 +08:00
efc0393ed5
加上value 2025-04-09 21:27:12 +08:00
d93e8f7c9e
可能不需要那么精确(? 2025-04-09 21:22:52 +08:00
9f6206aa5c
update dep 2025-04-09 13:03:34 +08:00
9e72c8a117
fmt 2025-04-09 12:35:27 +08:00
ad66657d37
some todo 2025-04-09 12:35:20 +08:00
8b5275608b
func -> consts 2025-04-09 00:12:09 +08:00
3435b8c0fb
就喜欢花哨的 2025-04-08 00:18:19 +08:00
1711270c9f
dev 就是 dev 2025-04-08 00:07:37 +08:00
4d0a32ca0e
fmt 2025-04-07 23:39:21 +08:00
0826daf410
news! 2025-04-07 23:24:11 +08:00
eb089273c4
更好看一些 2025-04-07 23:22:44 +08:00
4c814c45bc
更新依赖 2025-04-07 23:21:48 +08:00
3c5cf2e92c
更好看的状态显示 2025-04-07 23:21:35 +08:00
15b3e328d4
一些小的重构 2025-04-07 19:40:40 +08:00
5d93e38237
修复了 Python 插件停不下来就真的停不下来的问题 2025-04-07 05:28:29 +08:00
3ce0e0004d
补充 room api 2025-04-06 20:22:42 +08:00
86ab23e45d
添加新 api,我感觉可以发版了 2025-04-06 19:27:09 +08:00
3d08a7da91
处理一下 join request 2025-03-31 06:31:48 +08:00
e41279d843
ica 2.0.1 & dev 0.9.0 2025-03-29 00:20:29 +08:00
9745590c11
update dep 2025-03-27 02:04:19 +08:00
e02918b33d
dep update 2025-03-14 00:00:31 +08:00
befc822be7
cargo update 2025-03-11 23:17:18 +08:00
fe411ba137
fix linux unuse warning and fix linux build 2025-03-11 22:32:39 +08:00
15f08a8cfb
0.8.3 dev 2025-02-13 01:31:36 +08:00
2b1c366643
remove some tokio feature 2025-02-13 01:29:54 +08:00
a63423d545
fmt 2025-02-13 00:05:56 +08:00
65b8e92ce1
2.0.0 2025-02-12 13:43:14 +08:00
c23b3ee67a
ica 1.6.7 & tailchat 1.2.6 2025-02-12 00:02:05 +08:00
ede6640aa9
fmt + 改进 msg display 2025-02-11 23:36:27 +08:00
76a3628d2d
改进一下msg display 2025-02-11 23:31:52 +08:00
4866f2ec2e
我悟了,忘记 save thread了 2025-02-11 23:25:47 +08:00
4eb553473d
a ? 2025-02-11 23:15:20 +08:00
12a32da61e
cancel -> join 2025-02-11 23:07:33 +08:00
0cec518f1d
修复了停不住的问题 2025-02-11 23:06:17 +08:00
a114a92cba
wtf 2025-02-11 23:00:35 +08:00
56a6c39df7
add -env 2025-02-11 22:06:57 +08:00
f8cd207923
add default config 2025-02-11 22:06:49 +08:00
79b306d089
cargo update 2025-02-11 21:47:04 +08:00
d8b4fe06f9
ready for 0.8.2 2025-02-11 21:46:56 +08:00
e5f67475db
add .chain(Some(0)) 2025-02-11 21:44:02 +08:00
d94841b1bd
尝试进行一个 config 2025-02-11 21:21:06 +08:00
62a0a8d3fa
加个 abi3-py38 2025-01-18 12:35:24 +08:00
6638a1f645
0.8.1 2025-01-10 23:51:28 +08:00
073c711c7c
呐呐呐 2025-01-10 23:50:48 +08:00
0275863cfe
呐呐呐 2025-01-10 22:37:48 +08:00
3ed1c3e738
开始 0.8.1 周期了 2025-01-06 23:20:10 +08:00
d4c7a55dcc
呐呐呐 2025-01-06 21:24:20 +08:00
9711af9444
看着没问题? 2025-01-06 21:23:26 +08:00
974c2577c3
还是0.8.0…… 2025-01-06 21:02:52 +08:00
c80e938a78
理论上这还是0.8.0? 2025-01-06 20:54:05 +08:00
32958031d2
clean up 2025-01-06 20:13:20 +08:00
fcf88f0ebb
能跑了吗? 2025-01-06 20:12:35 +08:00
16fee092ba
hmmm 2024-12-06 21:34:38 +08:00
9da0b37db2
ica 1.6.5 api 2024-12-06 00:49:03 +08:00
86c19bc3db
进行一个pre 刀耕火种的修改 2024-11-22 00:01:17 +08:00
75832bfa2e
迁移一些 2024-11-19 22:57:31 +08:00
c5de63f02e
真好…… 2024-11-19 22:51:18 +08:00
686291755d
fmt 2024-11-19 22:18:43 +08:00
38cfd2dce7
进行一个虚心接受批评 2024-11-19 22:17:55 +08:00
98633aa5cc
稍微去除一点 info 之类的 2024-11-19 22:05:11 +08:00
4aa969adc5
多少有点毛病( 2024-11-16 14:05:24 +08:00
9d74853d1e
release 2024-11-08 19:02:35 +08:00
52aafcab19
更新依赖, release 0.7.4 2024-11-08 19:01:34 +08:00
4cf92356c8
cargo update 2024-10-21 23:04:08 +08:00
188c357378
2.12.23 适配 2024-10-21 22:52:28 +08:00
dc936bb7fe
fix action 2024-10-07 20:29:33 +08:00
a8129181a8
update ica version 2024-10-07 20:27:35 +08:00
6407f7eb86
cargo update 2024-10-07 20:23:14 +08:00
780c9aee8d
reee 2024-10-01 19:18:28 +08:00
6d883b0d7d
0.7.3 release 2024-10-01 19:15:42 +08:00
9ba8cfaba3
看起来行了? 2024-10-01 19:13:28 +08:00
b286765213
update dependes 2024-09-28 23:07:38 +08:00
e5ff010a69
0.7.3 2024-09-28 22:40:22 +08:00
ef3fb5a8cc
忽略 download相关room信息 2024-09-27 20:33:29 +08:00
c41d796f61
reee 2024-09-26 00:48:00 +08:00
91aa3f7bd9
更新到 2.12.20 2024-09-19 23:52:45 +08:00
cb87108804
0.7.2 2024-08-18 13:25:38 +08:00
e88d1fe435
去掉 ica-py 的部分 2024-08-18 13:17:48 +08:00
538f43869f
ok 2024-08-18 13:16:20 +08:00
f0cdac3299
版本号 2024-08-18 13:15:53 +08:00
880fc4c5dd
0.7.1? 2024-08-18 13:13:52 +08:00
32f1797edc
缝缝补补又三年 2024-08-18 12:35:37 +08:00
40 changed files with 3056 additions and 1252 deletions

View File

@ -37,7 +37,7 @@ jobs:
run: cargo build --release
- name: 上传
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ica-rs-b${{ github.run_number }}-${{ steps.get_version.outputs.value }}-py38-win-x64
path: ./target/release/ica-rs.exe

View File

@ -39,7 +39,7 @@ jobs:
run: cargo build --release
- name: 上传
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ica-rs-b${{ github.run_number }}-${{ steps.get_version.outputs.value }}-py38-win-x64
path: ./target/release/ica-rs.exe

4
.gitignore vendored
View File

@ -1,13 +1,13 @@
venv
env*
config.toml
config*.toml
.idea
*.pyc
*__pycache__/
make.ps1
make.*
# Added by cargo
/target

1402
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,6 @@ members = [
resolver = "2"
[patch.crates-io]
# rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "message_pack" }
rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "main" }
# rust_socketio = { path = "../../rust-socketio/socketio" }
# pyo3 = { git = "https://github.com/PyO3/pyo3.git", branch = "main" }

View File

@ -1,7 +1,7 @@
[package]
name = "ica-rs"
version = "0.7.0"
edition = "2021"
version = "0.9.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -25,8 +25,8 @@ ed25519-dalek = { version = "2.1", optional = true }
hex = { version = "0.4", optional = true }
# tailchat
reqwest = { version = "0.12.4", optional = true, features = ["multipart"] }
md-5 = { version = "0.10.6", optional = true }
reqwest = { version = "0.12", optional = true, features = ["multipart"] }
md-5 = { version = "0.10", optional = true }
# ica & tailchat (socketio)
rust_socketio = { version = "0.6.0", features = ["async"], optional = true }
@ -36,19 +36,19 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"
toml = "0.8"
colored = "2.1"
toml_edit = "0.22"
colored = "3.0"
# runtime
tokio = { version = "1.37", features = ["full"] }
futures-util = "0.3.30"
pyo3 = { version = "0.22.2", features = ["experimental-async", "py-clone"] }
tokio = { version = "1.43", features = ["rt-multi-thread", "time", "signal", "macros"] }
futures-util = "0.3"
pyo3 = { version = "0.24", features = ["experimental-async"] }
anyhow = { version = "1.0", features = ["backtrace"] }
# async 这玩意以后在搞
# pyo3-async = "0.3.2"
# pyo3-asyncio = { version = "0.20.0", features = ["attributes", "tokio-runtime"] }
# log
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["time"] }
thiserror = "1.0.63"
toml_edit = "0.22.20"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["time"] }
foldhash = "0.1.4"

View File

@ -21,3 +21,5 @@ use_field_init_shorthand = true
color = "Always"
edition = "2021"
# 这样不用 nightly 也可以使用 unstable 特性
unstable_features = true

View File

@ -102,7 +102,7 @@ impl BotConfig {
pub fn new_from_cli() -> Self {
// let config_file_path = env::args().nth(1).expect("No config path given");
// -c <config_file_path>
let mut config_file_path = String::new();
let mut config_file_path = "./config.toml".to_string();
let mut args = env::args();
while let Some(arg) = args.next() {
if arg == "-c" {

View File

@ -13,7 +13,9 @@ pub type MessageId = String;
#[allow(unused)]
pub trait RoomIdTrait {
/// 判断是否是群聊
fn is_room(&self) -> bool;
/// 判断是否是私聊
fn is_chat(&self) -> bool { !self.is_room() }
fn as_room_id(&self) -> RoomId;
fn as_chat_id(&self) -> RoomId;

View File

@ -1,8 +1,8 @@
use crate::data_struct::ica::messages::{At, LastMessage};
use crate::data_struct::ica::RoomId;
use crate::data_struct::ica::messages::{At, LastMessage, SendMessage};
use crate::data_struct::ica::{RoomId, UserId};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use serde_json::{Number, Value as JsonValue};
/// export default interface Room {
/// roomId: number
@ -32,14 +32,30 @@ pub struct Room {
// pub users: JsonValue,
pub at: At,
pub last_message: LastMessage,
pub auto_download: Option<String>,
pub download_path: Option<String>,
// 这俩都没啥用
// pub auto_download: Option<String>,
// pub download_path: Option<String>,
}
impl Room {
pub fn new_from_json(json: &JsonValue) -> Self {
let inner = serde_json::from_value::<InnerRoom>(json.clone()).unwrap();
let at = At::new_from_json(&json["at"]);
pub fn new_from_json(raw_json: &JsonValue) -> Self {
let mut parse_json = raw_json.clone();
// 手动 patch 一下 roomId
// ica issue: https://github.com/Icalingua-plus-plus/Icalingua-plus-plus/issues/793
if parse_json.get("roomId").is_none_or(|id| id.is_null()) {
use tracing::warn;
warn!("Room::new_from_json roomId is None, patching it to -1, raw: {:?}", raw_json);
parse_json["roomId"] = JsonValue::Number(Number::from(-1));
}
// 现在 fix 了
let inner = match serde_json::from_value::<InnerRoom>(parse_json) {
Ok(data) => data,
Err(e) => {
panic!("Room::new_from_json error: {}, raw: {:#?}", e, raw_json);
}
};
let at = At::new_from_json(&raw_json["at"]);
Self {
room_id: inner.room_id,
room_name: inner.room_name,
@ -50,15 +66,19 @@ impl Room {
// users: inner.users,
at,
last_message: inner.last_message,
auto_download: inner.auto_download,
download_path: inner.download_path,
// download_path: inner.download_path,
}
}
pub fn new_message_to(&self, content: String) -> SendMessage {
SendMessage::new(content, self.room_id, None)
}
}
fn room_id_default() -> RoomId { -1 }
#[derive(Debug, Clone, Deserialize, Serialize)]
struct InnerRoom {
#[serde(rename = "roomId")]
#[serde(rename = "roomId", default = "room_id_default")]
pub room_id: RoomId,
#[serde(rename = "roomName")]
pub room_name: String,
@ -75,8 +95,43 @@ struct InnerRoom {
// 忽略 at
#[serde(rename = "lastMessage")]
pub last_message: LastMessage,
#[serde(rename = "autoDownload")]
pub auto_download: Option<String>,
#[serde(rename = "downloadPath")]
pub download_path: Option<String>,
// 这俩都没啥用
// #[serde(rename = "autoDownload")]
// pub auto_download: Option<String>,
// #[serde(rename = "downloadPath")]
// pub download_path: Option<String>,
}
/// ```json
/// {
/// "comment": "问题:从哪里了解到的本群\n答案aaa",
/// "flag": "e4cd5a892ba34bed063196a0cc47a8",
/// "group_id": xxxxx,
/// "group_name": "Nuitka 和 Python 打包",
/// "nickname": "jashcken",
/// "post_type": "request",
/// "request_type": "group",
/// "self_id": 45620725,
/// "sub_type": "add",
/// "time": 1743372872,
/// "tips": "",
/// "user_id": 3838663305
/// }
/// ```
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JoinRequestRoom {
/// 问题+答案
pub comment: String,
pub group_id: RoomId,
pub group_name: String,
pub user_id: UserId,
pub nickname: String,
// 剩下的应该没用了……吧?
pub request_type: String,
pub post_type: String,
pub sub_type: String,
pub time: i64,
pub tips: String,
pub flag: String,
}

View File

@ -3,7 +3,7 @@ use crate::data_struct::ica::{MessageId, RoomId, UserId};
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use serde_json::{Value as JsonValue, json};
use tracing::warn;
pub mod msg_trait;
@ -41,8 +41,9 @@ pub struct LastMessage {
pub content: Option<String>,
pub timestamp: Option<String>,
pub username: Option<String>,
#[serde(rename = "userId")]
pub user_id: Option<i64>,
// 因为这玩意可能返回 raw buffer, 所以先不解析了
// #[serde(rename = "userId")]
// pub user_id: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@ -330,7 +331,7 @@ impl SendMessage {
/// file_type: 图片类型(MIME) (image/png; image/jpeg)
pub fn set_img(&mut self, file: &Vec<u8>, file_type: &str, as_sticker: bool) {
self.sticker = as_sticker;
use base64::{engine::general_purpose, Engine as _};
use base64::{Engine as _, engine::general_purpose};
let base64_data = general_purpose::STANDARD.encode(file);
self.file_data = Some(format!("data:{};base64,{}", file_type, base64_data));
}

View File

@ -4,9 +4,9 @@ use chrono::DateTime;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::MainStatus;
use crate::data_struct::ica::messages::{At, Message, NewMessage};
use crate::data_struct::ica::{MessageId, UserId};
use crate::MainStatus;
impl Serialize for At {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@ -88,7 +88,26 @@ impl<'de> Deserialize<'de> for Message {
impl Display for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}|{}|{}|{}", self.msg_id(), self.sender_id, self.sender_name, self.content)
if !self.content.is_empty() && !self.content.trim().is_empty() {
write!(f, "{}|{}|{}|{}", self.msg_id(), self.sender_id, self.sender_name, self.content)
} else if !self.files.is_empty() {
write!(
f,
"{}|{}|{}|{:?}",
self.msg_id(),
self.sender_id,
self.sender_name,
self.files[0].name
)
} else {
write!(
f,
"{}|{}|{}|empty content & empty files",
self.msg_id(),
self.sender_id,
self.sender_name
)
}
}
}
@ -114,14 +133,32 @@ impl MessageTrait for NewMessage {
impl Display for NewMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}|{}|{}|{}|{}",
self.msg_id(),
self.room_id,
self.msg.sender_id,
self.msg.sender_name,
self.msg.content
)
if !self.msg.content.trim().is_empty() {
write!(
f,
"{}|{}|{}|{}|{}",
self.msg.msg_id,
self.room_id,
self.msg.sender_id,
self.msg.sender_name,
self.msg.content
)
} else if !self.msg.files.is_empty() {
write!(
f,
"{}|{}|{}|{}|{:?}",
self.msg.msg_id,
self.room_id,
self.msg.sender_id,
self.msg.sender_name,
self.msg.files[0]
)
} else {
write!(
f,
"{}|{}|{}|{}|empty content & empty files",
self.msg.msg_id, self.room_id, self.msg.sender_id, self.msg.sender_name
)
}
}
}

View File

@ -1,7 +1,7 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use serde_json::{Value as JsonValue, json};
use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};

View File

@ -51,11 +51,13 @@ pub struct UpdateDMConverse {
#[allow(unused)]
pub type Writeable<T> = Arc<RwLock<T>>;
#[allow(unused)]
#[derive(Debug, Clone)]
pub struct BotStatus {
user_id: UserId,
}
#[allow(unused)]
impl BotStatus {
pub fn new(user_id: UserId) -> Self { Self { user_id } }

View File

@ -33,6 +33,8 @@ pub enum PyPluginError {
/// 插件内函数调用错误
/// pyerr, func_name, module_name
FuncCallError(pyo3::PyErr, String, String),
/// 插件停不下来!
PluginNotStopped,
}
impl From<rust_socketio::Error> for IcaError {
@ -81,6 +83,9 @@ impl std::fmt::Display for PyPluginError {
PyPluginError::FuncCallError(py_err, name, module) => {
write!(f, "插件内函数调用错误: {:#?}|{} in {}", py_err, name, module)
}
PyPluginError::PluginNotStopped => {
write!(f, "插件未停止")
}
}
}
}
@ -111,6 +116,7 @@ impl std::error::Error for PyPluginError {
PyPluginError::CouldNotGetFunc(e, _, _) => Some(e),
PyPluginError::FuncNotCallable(_, _) => None,
PyPluginError::FuncCallError(e, _, _) => Some(e),
PyPluginError::PluginNotStopped => None,
}
}
}

View File

@ -1,17 +1,46 @@
pub mod client;
pub mod events;
// use std::sync::OnceLock;
use colored::Colorize;
use rust_socketio::asynchronous::{Client, ClientBuilder};
use rust_socketio::{async_any_callback, async_callback};
use rust_socketio::{Event, Payload, TransportType};
use tracing::{event, span, Level};
use rust_socketio::{async_any_callback, async_callback};
use tracing::{Level, event, span};
use crate::config::IcaConfig;
use crate::error::{ClientResult, IcaError};
use crate::{version_str, StopGetter};
use crate::{StopGetter, version_str};
/// icalingua 客户端的兼容版本号
pub const ICA_PROTOCOL_VERSION: &str = "2.12.12";
pub const ICA_PROTOCOL_VERSION: &str = "2.12.28";
// mod status {
// use crate::data_struct::ica::all_rooms::Room;
// pub use crate::data_struct::ica::online_data::OnlineData;
// #[derive(Debug, Clone)]
// pub struct MainStatus {
// /// 是否启用 ica
// pub enable: bool,
// /// qq 是否登录
// pub qq_login: bool,
// /// 当前已加载的消息数量
// pub current_loaded_messages_count: u64,
// /// 房间数据
// pub rooms: Vec<Room>,
// /// 在线数据 (Icalingua 信息)
// pub online_status: OnlineData,
// }
// impl MainStatus {
// pub fn update_rooms(&mut self, room: Vec<Room>) { self.rooms = room; }
// pub fn update_online_status(&mut self, status: OnlineData) { self.online_status = status; }
// }
// }
// static ICA_STATUS: OnceLock<status::MainStatus> = OnceLock::new();
pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientResult<(), IcaError> {
let span = span!(Level::INFO, "Icalingua Client");
@ -19,6 +48,7 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
event!(Level::INFO, "ica-async-rs v{} initing", crate::ICA_VERSION);
let start_connect_time = std::time::Instant::now();
let socket = match ClientBuilder::new(config.host.clone())
.transport_type(TransportType::Websocket)
.on_any(async_any_callback!(events::any_event))
@ -33,11 +63,16 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
.on("setMessages", async_callback!(events::set_messages))
.on("addMessage", async_callback!(events::add_message))
.on("deleteMessage", async_callback!(events::delete_message))
.on("handleRequest", async_callback!(events::join_request))
.connect()
.await
{
Ok(client) => {
event!(Level::INFO, "socketio connected");
event!(
Level::INFO,
"{}",
format!("socketio connected time: {:?}", start_connect_time.elapsed()).on_cyan()
);
client
}
Err(e) => {
@ -66,11 +101,12 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
}
}
// 等待停止信号
event!(Level::INFO, "{}", "ica client waiting for stop signal".purple());
stop_reciver.await.ok();
event!(Level::INFO, "socketio client stopping");
event!(Level::INFO, "{}", "socketio client stopping".yellow());
match socket.disconnect().await {
Ok(_) => {
event!(Level::INFO, "socketio client stopped");
event!(Level::INFO, "{}", "socketio client stopped".green());
Ok(())
}
Err(e) => {
@ -78,7 +114,7 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
match e {
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e) => {
if inner_e.to_string().contains("AlreadyClosed") {
event!(Level::INFO, "socketio client stopped");
event!(Level::INFO, "{}", "socketio client stopped".green());
Ok(())
} else {
event!(Level::ERROR, "socketio 客户端出现了 Error: {:?}", inner_e);

View File

@ -1,50 +1,52 @@
use crate::data_struct::ica::messages::{DeleteMessage, SendMessage};
use crate::error::{ClientResult, IcaError};
use crate::MainStatus;
use crate::data_struct::ica::messages::{DeleteMessage, SendMessage};
use crate::data_struct::ica::{RoomId, RoomIdTrait, UserId};
use crate::error::{ClientResult, IcaError};
use colored::Colorize;
use ed25519_dalek::{Signature, Signer, SigningKey};
use rust_socketio::asynchronous::Client;
use rust_socketio::Payload;
use serde_json::Value;
use tracing::{debug, event, span, warn, Level};
use rust_socketio::asynchronous::Client;
use serde_json::{Value, json};
use tracing::{Level, event, span};
/// "安全" 的 发送一条消息
pub async fn send_message(client: &Client, message: &SendMessage) -> bool {
let value = message.as_value();
match client.emit("sendMessage", value).await {
Ok(_) => {
debug!("send_message {}", format!("{:#?}", message).cyan());
event!(Level::INFO, "send_message {}", format!("{:#?}", message).cyan());
true
}
Err(e) => {
warn!("send_message faild:{}", format!("{:#?}", e).red());
event!(Level::WARN, "send_message faild:{}", format!("{:#?}", e).red());
false
}
}
}
/// "安全" 的 删除一条消息
pub async fn delete_message(client: &Client, message: &DeleteMessage) -> bool {
let value = message.as_value();
match client.emit("deleteMessage", value).await {
Ok(_) => {
debug!("delete_message {}", format!("{:#?}", message).yellow());
event!(Level::DEBUG, "delete_message {}", format!("{:#?}", message).yellow());
true
}
Err(e) => {
warn!("delete_message faild:{}", format!("{:#?}", e).red());
event!(Level::WARN, "delete_message faild:{}", format!("{:#?}", e).red());
false
}
}
}
/// "安全" 的 获取历史消息
/// ```typescript
/// async fetchHistory(messageId: string, roomId: number, currentLoadedMessagesCount: number)
/// ```
// #[allow(dead_code)]
// pub async fn fetch_history(client: &Client, roomd_id: RoomId) -> bool { false }
async fn inner_sign(payload: Payload, client: Client) -> ClientResult<(), IcaError> {
async fn inner_sign(payload: Payload, client: &Client) -> ClientResult<(), IcaError> {
let span = span!(Level::INFO, "signing icalingua");
let _guard = span.enter();
@ -69,7 +71,8 @@ async fn inner_sign(payload: Payload, client: Client) -> ClientResult<(), IcaErr
.as_str()
.unwrap_or("unknow");
if server_protocol_version != crate::ica::ICA_PROTOCOL_VERSION {
warn!(
event!(
Level::WARN,
"服务器版本与兼容版本不一致\n服务器协议版本:{:?}\n兼容版本:{}",
version.get("protocolVersion"),
crate::ica::ICA_PROTOCOL_VERSION
@ -101,5 +104,41 @@ async fn inner_sign(payload: Payload, client: Client) -> ClientResult<(), IcaErr
/// 签名回调
/// 失败的时候得 panic
pub async fn sign_callback(payload: Payload, client: Client) {
inner_sign(payload, client).await.expect("Faild to sign");
inner_sign(payload, &client).await.expect("Faild to sign");
}
/// 向指定群发送签到信息
///
/// 只能是群啊, 不能是私聊
pub async fn send_room_sign_in(client: &Client, room_id: RoomId) -> bool {
if room_id.is_chat() {
event!(Level::WARN, "不能向私聊发送签到信息");
return false;
}
let data = json!(room_id.abs());
match client.emit("sendGroupSign", data).await {
Ok(_) => {
event!(Level::INFO, "已向群 {} 发送签到信息", room_id);
true
}
Err(e) => {
event!(Level::ERROR, "向群 {} 发送签到信息失败: {}", room_id, e);
false
}
}
}
/// 向某个群/私聊的某个人发送戳一戳
pub async fn send_poke(client: &Client, room_id: RoomId, target: UserId) -> bool {
let data = vec![json!(room_id), json!(target)];
match client.emit("sendGroupPoke", data).await {
Ok(_) => {
event!(Level::INFO, "已向 {} 的 {} 发送戳一戳", room_id, target);
true
}
Err(e) => {
event!(Level::ERROR, "向 {} 的 {} 发送戳一戳失败: {}", room_id, target, e);
false
}
}
}

View File

@ -1,15 +1,15 @@
use std::path::PathBuf;
use colored::Colorize;
use rust_socketio::asynchronous::Client;
use rust_socketio::{Event, Payload};
use tracing::{event, info, span, warn, Level};
use serde_json::json;
use tracing::{Level, event, info, span, warn};
use crate::data_struct::ica::all_rooms::Room;
use crate::data_struct::ica::RoomId;
use crate::data_struct::ica::all_rooms::{JoinRequestRoom, Room};
use crate::data_struct::ica::messages::{Message, MessageTrait, NewMessage};
use crate::data_struct::ica::online_data::OnlineData;
use crate::ica::client::send_message;
use crate::{help_msg, py, version_str, MainStatus, VERSION};
use crate::{MainStatus, VERSION, client_id, help_msg, py, version_str};
/// 获取在线数据
pub async fn get_online_data(payload: Payload, _client: Client) {
@ -32,7 +32,7 @@ pub async fn add_message(payload: Payload, client: Client) {
return;
}
event!(Level::INFO, "new_msg {}", message.to_string().cyan());
println!("new_msg {}", message.to_string().cyan());
// 就在这里处理掉最基本的消息
// 之后的处理交给插件
if !message.is_from_self() && !message.is_reply() {
@ -41,8 +41,9 @@ pub async fn add_message(payload: Payload, client: Client) {
send_message(&client, &reply).await;
} else if message.content() == "/bot-ls" {
let reply = message.reply_with(&format!(
"shenbot-py v{}\n{}",
"shenbot-py v{}-{}\n{}",
VERSION,
client_id(),
if MainStatus::global_config().check_py() {
py::PyStatus::display()
} else {
@ -54,16 +55,24 @@ pub async fn add_message(payload: Payload, client: Client) {
let reply = message.reply_with(&help_msg());
send_message(&client, &reply).await;
}
// else if message.content() == "/bot-uptime" {
// let duration = match start_up_time().elapsed() {
// Ok(d) => format!("{:?}", d),
// Err(e) => format!("出问题啦 {:?}", e),
// };
// let reply = message.reply_with(&format!(
// "shenbot 已运行: {}", duration
// ));
// send_message(&client, &reply).await;
// }
if MainStatus::global_config().ica().admin_list.contains(&message.sender_id()) {
// admin 区
if message.content().starts_with("/bot-enable") {
// 先判定是否为 admin
// 先判定是否为 admin
let client_id = client_id();
if message.content().starts_with(&format!("/bot-enable-{}", client_id)) {
// 尝试获取后面的信息
let mut content = message.content().split_whitespace();
content.next();
if let Some(name) = content.next() {
let path_name = PathBuf::from(name);
match py::PyStatus::get_status(&path_name) {
if let Some((_, name)) = message.content().split_once(" ") {
match py::PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
@ -73,18 +82,16 @@ pub async fn add_message(payload: Payload, client: Client) {
send_message(&client, &reply).await;
}
Some(false) => {
py::PyStatus::set_status(&path_name, true);
py::PyStatus::get_mut().set_status(name, true);
let reply = message.reply_with("启用插件完成");
send_message(&client, &reply).await;
}
}
}
} else if message.content().starts_with("/bot-disable") {
let mut content = message.content().split_whitespace();
content.next();
if let Some(name) = content.next() {
let path_name = PathBuf::from(name);
match py::PyStatus::get_status(&path_name) {
} else if message.content().starts_with(&format!("/bot-disable-{}", client_id))
{
if let Some((_, name)) = message.content().split_once(" ") {
match py::PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
@ -94,12 +101,16 @@ pub async fn add_message(payload: Payload, client: Client) {
send_message(&client, &reply).await;
}
Some(true) => {
py::PyStatus::set_status(&path_name, false);
py::PyStatus::get_mut().set_status(name, false);
let reply = message.reply_with("禁用插件完成");
send_message(&client, &reply).await;
}
}
}
} else if message.content() == "/bot-fetch" {
let reply = message.reply_with("正在更新当前群消息");
send_message(&client, &reply).await;
fetch_messages(&client, message.room_id).await;
}
}
}
@ -164,6 +175,41 @@ pub async fn failed_message(payload: Payload, _client: Client) {
}
}
/// 处理加群申请
///
/// add: 2.0.1
pub async fn join_request(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
match serde_json::from_value::<JoinRequestRoom>(value.clone()) {
Ok(join_room) => {
event!(Level::INFO, "{}", format!("收到加群申请 {:?}", join_room).on_blue());
}
Err(e) => {
event!(
Level::WARN,
"呼叫 shenjack! JoinRequestRoom 的 serde 没写好! {}\nraw: {:#?}",
e,
value
)
}
}
}
}
}
pub async fn fetch_history(client: Client, room: RoomId) { let mut request_body = json!(room); }
pub async fn fetch_messages(client: &Client, room: RoomId) {
let mut request_body = json!(room);
match client.emit("fetchMessages", request_body).await {
Ok(_) => {}
Err(e) => {
event!(Level::WARN, "fetch_messages {}", e);
}
}
}
/// 所有
pub async fn any_event(event: Event, payload: Payload, _client: Client) {
let handled = vec![
@ -177,18 +223,19 @@ pub async fn any_event(event: Event, payload: Payload, _client: Client) {
"deleteMessage",
"setAllRooms",
"setMessages",
"handleRequest", // 处理验证消息 (加入请求之类的)
// 也许以后会用到
"messageSuccess",
"messageFailed",
"setAllChatGroups",
"handleRequest", // 处理验证消息 (加入请求之类的)
// 忽略的
"notify",
"setShutUp", // 禁言
"syncRead", // 同步已读
"closeLoading", // 发送消息/加载新聊天 有一个 loading
"renewMessage", // 我也不确定到底是啥事件
"requestSetup", // 需要登录
"updateRoom",
"updateRoom", // 更新房间
];
match &event {
Event::Custom(event_name) => {

View File

@ -1,24 +1,30 @@
use std::time::Duration;
use std::{
hash::{DefaultHasher, Hash, Hasher},
sync::OnceLock,
time::{Duration, SystemTime},
};
mod config;
mod data_struct;
mod error;
mod py;
mod status;
mod wasms;
#[cfg(feature = "ica")]
mod ica;
#[cfg(feature = "tailchat")]
mod tailchat;
use colored::Colorize;
use config::BotConfig;
use tracing::{event, span, Level};
use error::PyPluginError;
use tracing::{Level, event, span};
pub static mut MAIN_STATUS: status::BotStatus = status::BotStatus {
config: None,
ica_status: None,
tailchat_status: None,
startup_time: None,
};
pub type MainStatus = status::BotStatus;
@ -26,8 +32,8 @@ pub type MainStatus = status::BotStatus;
pub type StopGetter = tokio::sync::oneshot::Receiver<()>;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const ICA_VERSION: &str = "1.6.2";
pub const TAILCHAT_VERSION: &str = "1.2.2";
pub const ICA_VERSION: &str = "2.0.1";
pub const TAILCHAT_VERSION: &str = "2.0.0";
const HELP_MSG: &str = r#"/bot-rs
rust
@ -35,19 +41,39 @@ const HELP_MSG: &str = r#"/bot-rs
python (python插件启用了的话)
/bot-ls
/bot-enable <plugin>
/bot-disable <plugin>
/bot-enable-<client-id> <plugin>
()
/bot-disable-<client-id> <plugin>
()
by shenjackyuanjie"#;
pub fn help_msg() -> String { format!("{}\n{}", version_str(), HELP_MSG) }
/// 获取帮助信息
pub fn help_msg() -> String {
format!("{}\n{}", version_str(), HELP_MSG).replace("<client-id>", client_id().as_str())
}
static STARTUP_TIME: OnceLock<SystemTime> = OnceLock::new();
pub fn start_up_time() -> SystemTime { *STARTUP_TIME.get().expect("WTF, why did you panic?") }
/// 获得当前客户端的 id
/// 防止串号
pub fn client_id() -> String {
let mut hasher = DefaultHasher::new();
start_up_time().hash(&mut hasher);
let data = hasher.finish();
// 取后6位
format!("{:06}", data % 1_000_000)
}
/// 获取版本信息
pub fn version_str() -> String {
format!(
"shenbot-rs v{}-{} ica v{}({}) tailchat v{}",
"shenbot-rs v{}{}-[{}] ica v{}({}) tailchat v{}",
VERSION,
if STABLE { "" } else { "开发版" },
if STABLE { "" } else { "-开发版" },
client_id(),
ICA_VERSION,
ica::ICA_PROTOCOL_VERSION,
TAILCHAT_VERSION,
@ -56,7 +82,7 @@ pub fn version_str() -> String {
/// 是否为稳定版本
/// 会在 release 的时候设置为 true
pub const STABLE: bool = true;
pub const STABLE: bool = false;
#[macro_export]
macro_rules! async_callback_with_state {
@ -78,15 +104,32 @@ macro_rules! async_any_callback_with_state {
}};
}
#[tokio::main]
async fn main() -> anyhow::Result<()> { inner_main().await }
const CLI_HELP_MSG: &str = r#"{VERSION}
-d
debug
-t
trace
-h
-env <env>
-c <config_file_path>
"#;
fn main() -> anyhow::Result<()> {
let start_up_time = SystemTime::now();
STARTUP_TIME.set(start_up_time).expect("WTF, why did you panic?");
async fn inner_main() -> anyhow::Result<()> {
// -d -> debug
// none -> info
let args = std::env::args();
let args = args.collect::<Vec<String>>();
if args.contains(&"-h".to_string()) {
println!("{}", CLI_HELP_MSG.replace("{VERSION}", version_str().as_str()));
return Ok(());
}
let level = {
let args = std::env::args();
let args = args.collect::<Vec<String>>();
if args.contains(&"-d".to_string()) {
Level::DEBUG
} else if args.contains(&"-t".to_string()) {
@ -97,7 +140,35 @@ async fn inner_main() -> anyhow::Result<()> {
};
tracing_subscriber::fmt().with_max_level(level).init();
let span = span!(Level::INFO, "Shenbot Main");
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_name("shenbot-rs")
.worker_threads(10)
.build()
.unwrap();
let result = rt.block_on(inner_main());
event!(Level::INFO, "shenbot-rs v{} exiting", VERSION);
match result {
Ok(_) => {}
Err(e) => {
if let Some(PyPluginError::PluginNotStopped) = e.downcast_ref::<PyPluginError>() {
event!(Level::WARN, "Python 插件停不下来, 3s 后终止 tokio rt");
rt.shutdown_timeout(Duration::from_secs(3));
} else {
event!(Level::ERROR, "shenbot-rs v{} exiting with error: {}", VERSION, e);
}
}
}
Ok(())
}
async fn inner_main() -> anyhow::Result<()> {
let span = span!(Level::INFO, "bot-main");
let _enter = span.enter();
event!(Level::INFO, "shenbot-rs v{} starting", VERSION);
@ -114,43 +185,43 @@ async fn inner_main() -> anyhow::Result<()> {
}
// 准备一个用于停止 socket 的变量
event!(Level::INFO, "启动 ICA");
let (ica_send, ica_recv) = tokio::sync::oneshot::channel::<()>();
if bot_config.check_ica() {
event!(Level::INFO, "启动 ica");
event!(Level::INFO, "{}", "开始启动 ICA".green());
let config = bot_config.ica();
tokio::spawn(async move {
ica::start_ica(&config, ica_recv).await.unwrap();
});
} else {
event!(Level::INFO, "未启用 ica");
event!(Level::INFO, "{}", "ica 未启用, 不管他".cyan());
}
let (tailchat_send, tailchat_recv) = tokio::sync::oneshot::channel::<()>();
if bot_config.check_tailchat() {
event!(Level::INFO, "启动 Tailchat");
event!(Level::INFO, "{}", "开始启动 tailchat".green());
let config = bot_config.tailchat();
tokio::spawn(async move {
tailchat::start_tailchat(config, tailchat_recv).await.unwrap();
});
} else {
event!(Level::INFO, "未启用 Tailchat");
event!(Level::INFO, "{}", "tailchat 未启用, 不管他".bright_magenta());
}
tokio::time::sleep(Duration::from_secs(2)).await;
tokio::time::sleep(Duration::from_secs(1)).await;
// 等待一个输入
event!(Level::INFO, "Press any key to exit");
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
event!(Level::INFO, "Press ctrl+c to exit, second ctrl+c to force exit");
tokio::signal::ctrl_c().await.ok();
ica_send.send(()).ok();
tailchat_send.send(()).ok();
event!(Level::INFO, "Disconnected");
py::post_py()?;
py::post_py().await?;
event!(Level::INFO, "Shenbot-rs exiting");
Ok(())
}
@ -162,8 +233,8 @@ async fn test_macro() {
use std::sync::Arc;
use tokio::sync::RwLock;
use rust_socketio::asynchronous::{Client, ClientBuilder};
use rust_socketio::Payload;
use rust_socketio::asynchronous::{Client, ClientBuilder};
/// 一个简单的例子
#[derive(Clone)]

View File

@ -1,13 +1,84 @@
use std::path::PathBuf;
use std::sync::LazyLock;
use pyo3::prelude::*;
use rust_socketio::asynchronous::Client;
use tracing::{event, info, warn, Level};
use tokio::sync::Mutex;
use tracing::{Level, event, info, warn};
use crate::MainStatus;
use crate::data_struct::{ica, tailchat};
use crate::error::PyPluginError;
use crate::py::{class, PyPlugin, PyStatus};
use crate::MainStatus;
use crate::py::consts::events_func;
use crate::py::{PyPlugin, PyStatus, class};
pub struct PyTasks {
pub ica_new_message: Vec<tokio::task::JoinHandle<()>>,
pub ica_delete_message: Vec<tokio::task::JoinHandle<()>>,
pub tailchat_new_message: Vec<tokio::task::JoinHandle<()>>,
}
impl PyTasks {
pub fn push_ica_new_message(&mut self, handle: tokio::task::JoinHandle<()>) {
self.ica_new_message.push(handle);
self.ica_new_message.retain(|handle| !handle.is_finished());
}
pub fn push_ica_delete_message(&mut self, handle: tokio::task::JoinHandle<()>) {
self.ica_delete_message.push(handle);
self.ica_delete_message.retain(|handle| !handle.is_finished());
}
pub fn push_tailchat_new_message(&mut self, handle: tokio::task::JoinHandle<()>) {
self.tailchat_new_message.push(handle);
self.tailchat_new_message.retain(|handle| !handle.is_finished());
}
pub async fn join_all(&mut self) {
for handle in self.ica_new_message.drain(..) {
let _ = handle.await;
}
for handle in self.ica_delete_message.drain(..) {
let _ = handle.await;
}
for handle in self.tailchat_new_message.drain(..) {
let _ = handle.await;
}
}
pub fn len_check(&mut self) -> usize {
self.ica_delete_message.retain(|handle| !handle.is_finished());
self.ica_new_message.retain(|handle| !handle.is_finished());
self.tailchat_new_message.retain(|handle| !handle.is_finished());
self.ica_new_message.len() + self.ica_delete_message.len() + self.tailchat_new_message.len()
}
pub fn len(&self) -> usize {
self.ica_new_message.len() + self.ica_delete_message.len() + self.tailchat_new_message.len()
}
pub fn is_empty(&self) -> bool { self.len() == 0 }
pub fn cancel_all(&mut self) {
for handle in self.ica_new_message.drain(..) {
handle.abort();
}
for handle in self.ica_delete_message.drain(..) {
handle.abort();
}
for handle in self.tailchat_new_message.drain(..) {
handle.abort();
}
}
}
pub static PY_TASKS: LazyLock<Mutex<PyTasks>> = LazyLock::new(|| {
Mutex::new(PyTasks {
ica_new_message: Vec::new(),
ica_delete_message: Vec::new(),
tailchat_new_message: Vec::new(),
})
});
pub fn get_func<'py>(
py_module: &Bound<'py, PyAny>,
@ -64,10 +135,18 @@ pub fn verify_and_reload_plugins() {
let mut need_reload_files: Vec<PathBuf> = Vec::new();
let plugin_path = MainStatus::global_config().py().plugin_path.clone();
// 先检查是否有插件被删除
for path in PyStatus::get().files.keys() {
if !path.exists() {
event!(Level::INFO, "Python 插件: {:?} 已被删除", path);
PyStatus::get_mut().delete_file(path);
}
}
for entry in std::fs::read_dir(plugin_path).unwrap().flatten() {
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "py" && !PyStatus::verify_file(&path) {
if ext == "py" && !PyStatus::get().verify_file(&path) {
need_reload_files.push(path);
}
}
@ -76,16 +155,16 @@ pub fn verify_and_reload_plugins() {
if need_reload_files.is_empty() {
return;
}
info!("file change list: {:?}", need_reload_files);
let exist_plugins = PyStatus::get_map_mut();
event!(Level::INFO, "更改列表: {:?}", need_reload_files);
let plugins = PyStatus::get_mut();
for reload_file in need_reload_files {
if let Some(plugin) = exist_plugins.get_mut(&reload_file) {
if let Some(plugin) = plugins.files.get_mut(&reload_file) {
plugin.reload_from_file();
event!(Level::INFO, "重载 Python 插件: {:?} 完成", reload_file);
} else {
match PyPlugin::new_from_path(&reload_file) {
Some(plugin) => {
PyStatus::add_file(reload_file.clone(), plugin);
plugins.add_file(reload_file.clone(), plugin);
info!("加载 Python 插件: {:?} 完成", reload_file);
}
None => {
@ -96,11 +175,6 @@ pub fn verify_and_reload_plugins() {
}
}
pub const ICA_NEW_MESSAGE_FUNC: &str = "on_ica_message";
pub const ICA_DELETE_MESSAGE_FUNC: &str = "on_ica_delete_message";
pub const TAILCHAT_NEW_MESSAGE_FUNC: &str = "on_tailchat_message";
macro_rules! call_py_func {
($args:expr, $plugin:expr, $plugin_path:expr, $func_name:expr, $client:expr) => {
tokio::spawn(async move {
@ -119,7 +193,7 @@ macro_rules! call_py_func {
e,
// 获取 traceback
match &e {
PyPluginError::FuncCallError(py_err, _, _) => match py_err.traceback_bound(py) {
PyPluginError::FuncCallError(py_err, _, _) => match py_err.traceback(py) {
Some(traceback) => match traceback.format() {
Ok(trace) => trace,
Err(trace_e) => format!("failed to format traceback: {:?}", trace_e),
@ -141,25 +215,26 @@ pub async fn ica_new_message_py(message: &ica::messages::NewMessage, client: &Cl
// 验证插件是否改变
verify_and_reload_plugins();
let plugins = PyStatus::get_map();
for (path, plugin) in plugins.iter().filter(|(_, plugin)| plugin.enabled) {
let plugins = PyStatus::get();
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
let msg = class::ica::NewMessagePy::new(message);
let client = class::ica::IcaClientPy::new(client);
let args = (msg, client);
// 甚至实际上压根不需要await这个spawn, 直接让他自己跑就好了(离谱)
call_py_func!(args, plugin, path, ICA_NEW_MESSAGE_FUNC, client);
let task = call_py_func!(args, plugin, path, events_func::ICA_NEW_MESSAGE, client);
PY_TASKS.lock().await.push_ica_new_message(task);
}
}
pub async fn ica_delete_message_py(msg_id: ica::MessageId, client: &Client) {
verify_and_reload_plugins();
let plugins = PyStatus::get_map();
for (path, plugin) in plugins.iter().filter(|(_, plugin)| plugin.enabled) {
let plugins = PyStatus::get();
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
let msg_id = msg_id.clone();
let client = class::ica::IcaClientPy::new(client);
let args = (msg_id.clone(), client);
call_py_func!(args, plugin, path, ICA_DELETE_MESSAGE_FUNC, client);
let task = call_py_func!(args, plugin, path, events_func::ICA_DELETE_MESSAGE, client);
PY_TASKS.lock().await.push_ica_delete_message(task);
}
}
@ -169,11 +244,12 @@ pub async fn tailchat_new_message_py(
) {
verify_and_reload_plugins();
let plugins = PyStatus::get_map();
for (path, plugin) in plugins.iter().filter(|(_, plugin)| plugin.enabled) {
let plugins = PyStatus::get();
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
let msg = class::tailchat::TailchatReceiveMessagePy::from_recive_message(message);
let client = class::tailchat::TailchatClientPy::new(client);
let args = (msg, client);
call_py_func!(args, plugin, path, TAILCHAT_NEW_MESSAGE_FUNC, client);
let task = call_py_func!(args, plugin, path, events_func::TAILCHAT_NEW_MESSAGE, client);
PY_TASKS.lock().await.push_tailchat_new_message(task);
}
}

View File

@ -1,23 +1,17 @@
pub mod commander;
pub mod config;
pub mod ica;
pub mod schdule;
pub mod tailchat;
use pyo3::prelude::*;
use pyo3::{
Bound, IntoPyObject, PyAny, PyRef, PyResult, pyclass, pymethods, pymodule,
types::{PyBool, PyModule, PyModuleMethods, PyString},
};
use toml::Value as TomlValue;
use tracing::{Level, event};
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "ConfigRequest")]
pub struct ConfigRequestPy {
pub path: String,
}
#[pymethods]
impl ConfigRequestPy {
#[new]
pub fn py_new(path: String) -> Self { Self { path } }
}
#[derive(Clone)]
// #[derive(Clone)]
#[pyclass]
#[pyo3(name = "ConfigData")]
pub struct ConfigDataPy {
@ -26,21 +20,24 @@ pub struct ConfigDataPy {
#[pymethods]
impl ConfigDataPy {
pub fn __getitem__(self_: PyRef<'_, Self>, key: String) -> Option<Py<PyAny>> {
pub fn __getitem__(self_: PyRef<'_, Self>, key: String) -> Option<Bound<PyAny>> {
match self_.data.get(&key) {
Some(value) => match value {
TomlValue::String(s) => Some(s.into_py(self_.py())),
TomlValue::Integer(i) => Some(i.into_py(self_.py())),
TomlValue::Float(f) => Some(f.into_py(self_.py())),
TomlValue::Boolean(b) => Some(b.into_py(self_.py())),
TomlValue::String(s) => Some(PyString::new(self_.py(), s).into_any()),
TomlValue::Integer(i) => Some(i.into_pyobject(self_.py()).unwrap().into_any()),
TomlValue::Float(f) => Some(f.into_pyobject(self_.py()).unwrap().into_any()),
TomlValue::Boolean(b) => {
let py_value = PyBool::new(self_.py(), *b);
Some(py_value.as_any().clone())
}
TomlValue::Array(a) => {
let new_self = Self::new(TomlValue::Array(a.clone()));
let py_value = new_self.into_py(self_.py());
let py_value = new_self.into_pyobject(self_.py()).unwrap().into_any();
Some(py_value)
}
TomlValue::Table(t) => {
let new_self = Self::new(TomlValue::Table(t.clone()));
let py_value = new_self.into_py(self_.py());
let py_value = new_self.into_pyobject(self_.py()).unwrap().into_any();
Some(py_value)
}
_ => None,
@ -54,3 +51,35 @@ impl ConfigDataPy {
impl ConfigDataPy {
pub fn new(data: TomlValue) -> Self { Self { data } }
}
/// Rust 侧向 Python 侧提供的 api
#[pymodule]
#[pyo3(name = "shenbot_api")]
fn rs_api_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add("__version__", crate::VERSION)?;
m.add("_version_", crate::VERSION)?;
m.add("_ica_version_", crate::ICA_VERSION)?;
m.add("_tailchat_version_", crate::TAILCHAT_VERSION)?;
m.add_class::<ConfigDataPy>()?;
m.add_class::<config::ConfigStoragePy>()?;
m.add_class::<schdule::SchedulerPy>()?;
Ok(())
}
/// 在 python 初始化之前注册所有需要的类
///
/// WARNING: 这个函数需要在 Python 初始化之前调用,否则会导致报错
///
/// (pyo3 提供的宏会检查一遍, 不过我这里就直接用原始形式了)
pub fn regist_class() {
event!(Level::INFO, "向 Python 注册 Rust 侧模块/函数");
unsafe {
// 单纯没用 macro 而已
pyo3::ffi::PyImport_AppendInittab(
rs_api_module::__PYO3_NAME.as_ptr(),
Some(rs_api_module::__pyo3_init),
);
}
event!(Level::INFO, "注册完成");
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,343 @@
use std::collections::HashMap;
use pyo3::{
Bound, PyAny, PyResult, pyclass, pymethods,
types::{
PyAnyMethods, PyBool, PyBoolMethods, PyDict, PyDictMethods, PyFloat, PyInt, PyList,
PyListMethods, PyString, PyStringMethods, PyTypeMethods,
},
};
use tracing::{Level, event};
#[derive(Debug, Clone)]
pub enum ConfigItem {
None,
String(String),
Int(i64),
Float(f64),
Bool(bool),
Array(Vec<ConfigItemPy>),
Table(HashMap<String, ConfigItemPy>),
}
#[derive(Clone, Debug)]
#[pyclass]
#[pyo3(name = "ConfigItem")]
pub struct ConfigItemPy {
pub item: ConfigItem,
pub default_value: ConfigItem,
}
impl ConfigItemPy {
pub fn new(item: ConfigItem, default_value: ConfigItem) -> Self {
Self {
item,
default_value,
}
}
pub fn new_uninit(default_value: ConfigItem) -> Self {
Self {
item: ConfigItem::None,
default_value,
}
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "ConfigStorage")]
pub struct ConfigStoragePy {
pub keys: HashMap<String, ConfigItemPy>,
}
/// Storage 里允许的最大层级深度
///
/// 我也不知道为啥就突然有这玩意了(
pub const MAX_CFG_DEPTH: usize = 10;
fn parse_py_string(obj: &Bound<'_, PyAny>) -> PyResult<String> {
let py_str = obj.downcast::<PyString>()?;
let value = py_str.to_str()?;
Ok(value.to_string())
}
fn parse_py_bool(obj: &Bound<'_, PyAny>) -> PyResult<bool> {
let py_bool = obj.downcast::<PyBool>()?;
Ok(py_bool.is_true())
}
fn parse_py_int(obj: &Bound<'_, PyAny>) -> PyResult<i64> {
let py_int = obj.downcast::<PyInt>()?;
py_int.extract::<i64>()
}
fn parse_py_float(obj: &Bound<'_, PyAny>) -> PyResult<f64> {
let py_float = obj.downcast::<PyFloat>()?;
py_float.extract::<f64>()
}
impl ConfigStoragePy {
/// 递归 list 解析配置
///
/// 用个 Result 来标记递归过深
fn parse_py_list(
args: &Bound<'_, PyList>,
list: &mut Vec<ConfigItemPy>,
current_deepth: usize,
) -> Result<(), usize> {
if current_deepth > MAX_CFG_DEPTH {
return Err(current_deepth);
} else {
for value in args.iter() {
// 匹配 item
let value_type = value.get_type();
if value_type.is_instance_of::<PyDict>() {
let py_dict = value.downcast::<PyDict>().unwrap();
let mut new_map = HashMap::new();
match Self::parse_py_dict(py_dict, &mut new_map, current_deepth + 1) {
Ok(_) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Table(new_map)));
}
Err(e) => {
event!(
Level::WARN,
"value(dict) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyList>() {
let py_list = value.downcast::<PyList>().unwrap();
let mut new_list = Vec::new();
match Self::parse_py_list(py_list, &mut new_list, current_deepth + 1) {
Ok(_) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Array(new_list)));
}
Err(e) => {
event!(
Level::WARN,
"value(list) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyString>() {
match parse_py_string(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::String(value)));
}
Err(e) => {
event!(
Level::WARN,
"value(string) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyBool>() {
match parse_py_bool(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Bool(value)));
}
Err(e) => {
event!(
Level::WARN,
"value(bool) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyInt>() {
match parse_py_int(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Int(value)));
}
Err(e) => {
event!(Level::WARN, "value(int) 解析时出现错误: {}\nraw: {}", e, value);
}
}
} else if value_type.is_instance_of::<PyFloat>() {
match parse_py_float(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Float(value)));
}
Err(e) => {
event!(
Level::WARN,
"value(float) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else {
// 先丢个 warning 出去
match value_type.name() {
Ok(type_name) => {
event!(
Level::WARN,
"value 为不支持的 {} 类型\nraw: {}",
type_name,
value
)
}
Err(e) => {
event!(
Level::WARN,
"value 为不支持的类型 (获取类型名失败: {})\nraw: {}",
e,
value
)
}
}
}
}
}
Ok(())
}
/// 递归 dict 解析配置
///
/// 用个 Result 来标记递归过深
fn parse_py_dict(
kwargs: &Bound<'_, PyDict>,
map: &mut HashMap<String, ConfigItemPy>,
current_deepth: usize,
) -> Result<(), usize> {
if current_deepth > MAX_CFG_DEPTH {
Err(current_deepth)
} else {
for (key, value) in kwargs.iter() {
if let Ok(name) = key.downcast::<PyString>() {
let name = name.to_string();
// 匹配 item
let value_type = value.get_type();
if value_type.is_instance_of::<PyDict>() {
let py_dict = value.downcast::<PyDict>().unwrap();
let mut new_map = HashMap::new();
match Self::parse_py_dict(py_dict, &mut new_map, current_deepth + 1) {
Ok(_) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Table(new_map)),
);
}
Err(e) => {
event!(Level::WARN, "value(dict) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyList>() {
let py_list = value.downcast::<PyList>().unwrap();
let mut new_list = Vec::new();
match Self::parse_py_list(py_list, &mut new_list, current_deepth + 1) {
Ok(_) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Array(new_list)),
);
}
Err(e) => {
event!(Level::WARN, "value(list) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyString>() {
match parse_py_string(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::String(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(string) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyBool>() {
match parse_py_bool(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Bool(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(bool) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyInt>() {
match parse_py_int(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Int(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(int) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyFloat>() {
match parse_py_float(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Float(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(float) {} 解析时出现错误: {}", name, e);
}
}
} else {
// 先丢个 warning 出去
match value_type.name() {
Ok(type_name) => {
event!(Level::WARN, "value {} 为不支持的 {} 类型", name, type_name)
}
Err(e) => event!(
Level::WARN,
"value {} 为不支持的类型 (获取类型名失败: {})",
name,
e
),
}
continue;
}
}
}
Ok(())
}
}
}
#[pymethods]
impl ConfigStoragePy {
#[new]
#[pyo3(signature = (**kwargs))]
pub fn new(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
match kwargs {
Some(kwargs) => {
let mut keys = HashMap::new();
// 解析 kwargs
Self::parse_py_dict(kwargs, &mut keys, 0).map_err(|e| {
event!(Level::ERROR, "配置解析过深: {}", e);
pyo3::exceptions::PyValueError::new_err(format!("配置解析过深: {}", e))
})?;
// 解析完成
Ok(Self { keys })
}
None => Ok(Self {
keys: HashMap::new(),
}),
}
}
#[getter]
/// 获取最大允许的层级深度
pub fn get_max_allowed_depth(&self) -> usize { MAX_CFG_DEPTH }
}

View File

@ -1,16 +1,17 @@
use std::time::SystemTime;
use pyo3::prelude::*;
use pyo3::{pyclass, pymethods};
use rust_socketio::asynchronous::Client;
use tokio::runtime::Runtime;
use tracing::{debug, info, warn};
use tracing::{Level, event};
use crate::MainStatus;
use crate::data_struct::ica::messages::{
DeleteMessage, MessageTrait, NewMessage, ReplyMessage, SendMessage,
};
use crate::data_struct::ica::{MessageId, RoomId, RoomIdTrait};
use crate::ica::client::{delete_message, send_message};
use crate::MainStatus;
use crate::data_struct::ica::{MessageId, RoomId, RoomIdTrait, UserId};
use crate::ica::client::{delete_message, send_message, send_poke, send_room_sign_in};
use crate::py::PyStatus;
#[pyclass]
#[pyo3(name = "IcaStatus")]
@ -62,6 +63,27 @@ impl IcaStatusPy {
pub fn get_load(&self) -> String {
MainStatus::global_ica_status().online_status.icalingua_info.load.clone()
}
#[getter]
/// 获取当前用户加入的所有房间
///
/// 添加自 2.0.1
pub fn get_rooms(&self) -> Vec<IcaRoomPy> {
MainStatus::global_ica_status().rooms.iter().map(|r| r.into()).collect()
}
#[getter]
/// 获取所有管理员
///
/// 添加自 2.0.1
pub fn get_admins(&self) -> Vec<UserId> { MainStatus::global_config().ica().admin_list.clone() }
#[getter]
/// 获取所有被屏蔽的人
///
/// (好像没啥用就是了, 反正被过滤的不会给到插件)
///
/// 添加自 2.0.1
pub fn get_filtered(&self) -> Vec<UserId> {
MainStatus::global_config().ica().filter_list.clone()
}
}
impl Default for IcaStatusPy {
@ -72,6 +94,47 @@ impl IcaStatusPy {
pub fn new() -> Self { Self {} }
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "IcaRoom")]
/// Room api
///
/// 添加自 2.0.1
pub struct IcaRoomPy {
pub inner: crate::data_struct::ica::all_rooms::Room,
}
impl From<crate::data_struct::ica::all_rooms::Room> for IcaRoomPy {
fn from(inner: crate::data_struct::ica::all_rooms::Room) -> Self { Self { inner } }
}
impl From<&crate::data_struct::ica::all_rooms::Room> for IcaRoomPy {
fn from(inner: &crate::data_struct::ica::all_rooms::Room) -> Self {
Self {
inner: inner.clone(),
}
}
}
#[pymethods]
impl IcaRoomPy {
#[getter]
pub fn get_room_id(&self) -> i64 { self.inner.room_id }
#[getter]
pub fn get_room_name(&self) -> String { self.inner.room_name.clone() }
#[getter]
pub fn get_unread_count(&self) -> u64 { self.inner.unread_count }
#[getter]
pub fn get_priority(&self) -> u8 { self.inner.priority }
#[getter]
pub fn get_utime(&self) -> i64 { self.inner.utime }
pub fn is_group(&self) -> bool { self.inner.room_id.is_room() }
pub fn is_chat(&self) -> bool { self.inner.room_id.is_chat() }
pub fn new_message_to(&self, content: String) -> SendMessagePy {
SendMessagePy::new(self.inner.new_message_to(content))
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "NewMessage")]
@ -145,10 +208,18 @@ impl SendMessagePy {
pub fn get_content(&self) -> String { self.msg.content.clone() }
#[setter]
pub fn set_content(&mut self, content: String) { self.msg.content = content; }
#[getter]
pub fn get_room_id(&self) -> RoomId { self.msg.room_id }
#[setter]
pub fn set_room_id(&mut self, room_id: RoomId) { self.msg.room_id = room_id; }
/// 设置消息图片
pub fn set_img(&mut self, file: Vec<u8>, file_type: String, as_sticker: bool) {
self.msg.set_img(&file, &file_type, as_sticker);
}
pub fn remove_reply(&mut self) -> Self {
self.msg.reply_to = None;
self.clone()
}
}
impl SendMessagePy {
@ -180,6 +251,26 @@ pub struct IcaClientPy {
#[pymethods]
impl IcaClientPy {
/// 签到
///
/// 添加自 1.6.5 版本
pub fn send_room_sign_in(&self, room_id: RoomId) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(send_room_sign_in(&self.client, room_id))
})
}
/// 戳一戳
///
/// 添加自 1.6.5 版本
pub fn send_poke(&self, room_id: RoomId, user_id: UserId) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(send_poke(&self.client, room_id, user_id))
})
}
pub fn send_message(&self, message: SendMessagePy) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
@ -188,7 +279,7 @@ impl IcaClientPy {
}
pub fn send_and_warn(&self, message: SendMessagePy) -> bool {
warn!(message.msg.content);
event!(Level::WARN, message.msg.content);
self.send_message(message)
}
@ -218,18 +309,54 @@ impl IcaClientPy {
#[getter]
pub fn get_version(&self) -> String { crate::VERSION.to_string() }
#[getter]
pub fn get_version_str(&self) -> String { crate::version_str() }
#[getter]
pub fn get_client_id(&self) -> String { crate::client_id() }
#[getter]
pub fn get_ica_version(&self) -> String { crate::ICA_VERSION.to_string() }
#[getter]
pub fn get_startup_time(&self) -> SystemTime { crate::MainStatus::get_startup_time() }
pub fn get_startup_time(&self) -> SystemTime { crate::start_up_time() }
#[getter]
pub fn get_py_tasks_count(&self) -> usize {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(async { crate::py::call::PY_TASKS.lock().await.len_check() })
})
}
/// 重新加载插件状态
/// 返回是否成功
pub fn reload_plugin_status(&self) -> bool { PyStatus::get_mut().config.reload_from_default() }
/// 设置某个插件的状态
pub fn set_plugin_status(&self, plugin_name: String, status: bool) {
PyStatus::get_mut().set_status(&plugin_name, status);
}
pub fn get_plugin_status(&self, plugin_name: String) -> Option<bool> {
PyStatus::get().get_status(&plugin_name)
}
/// 同步状态到配置文件
/// 这样关闭的时候就会保存状态
pub fn sync_status_to_config(&self) { PyStatus::get_mut().config.sync_status_to_config(); }
/// 重新加载插件
///
/// 返回是否成功
pub fn reload_plugin(&self, plugin_name: String) -> bool {
PyStatus::get_mut().reload_plugin(&plugin_name)
}
pub fn debug(&self, content: String) {
debug!("{}", content);
event!(Level::DEBUG, "{}", content);
}
pub fn info(&self, content: String) {
info!("{}", content);
event!(Level::INFO, "{}", content);
}
pub fn warn(&self, content: String) {
warn!("{}", content);
event!(Level::WARN, "{}", content);
}
}

View File

@ -0,0 +1,59 @@
use std::time::Duration;
use pyo3::{Bound, Py, PyTraverseError, PyVisit, Python, pyclass, pymethods, types::PyFunction};
use tracing::{Level, event};
#[derive(Debug)]
#[pyclass]
#[pyo3(name = "Scheduler")]
/// 用于计划任务的类
///
/// 给 Python 侧使用
///
/// add: 0.9.0
pub struct SchedulerPy {
/// 回调函数
///
/// 你最好不要把他清理掉
pub callback: Py<PyFunction>,
/// 预计等待时间
pub schdule_time: Duration,
}
#[pymethods]
impl SchedulerPy {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self.callback)?;
Ok(())
}
#[new]
pub fn new(func: Bound<'_, PyFunction>, schdule_time: Duration) -> Self {
Self {
callback: func.unbind(),
schdule_time,
}
}
/// 开始
pub fn start(&self, py: Python<'_>) {
let wait = self.schdule_time;
let cb = self.callback.clone_ref(py);
tokio::spawn(async move {
let second = Duration::from_secs(1);
if wait > second {
let big_sleep = wait.checked_sub(second).unwrap();
tokio::time::sleep(big_sleep).await;
tokio::time::sleep(second).await;
} else {
tokio::time::sleep(wait).await;
}
Python::with_gil(|py| {
event!(Level::INFO, "正在调用计划 {:?}", wait);
if let Err(e) = cb.call0(py) {
event!(Level::WARN, "调用时出现错误 {}", e);
}
});
});
}
}

View File

@ -3,10 +3,12 @@ use std::time::SystemTime;
use pyo3::prelude::*;
use rust_socketio::asynchronous::Client;
use tokio::runtime::Runtime;
use tracing::{debug, info, warn};
use crate::data_struct::tailchat::messages::{ReceiveMessage, SendingFile, SendingMessage};
use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};
use crate::py::PyStatus;
use crate::tailchat::client::send_message;
#[pyclass]
@ -65,9 +67,57 @@ impl TailchatClientPy {
#[getter]
pub fn get_version(&self) -> String { crate::VERSION.to_string() }
#[getter]
pub fn get_version_str(&self) -> String { crate::version_str() }
#[getter]
pub fn get_client_id(&self) -> String { crate::client_id() }
#[getter]
pub fn get_tailchat_version(&self) -> String { crate::TAILCHAT_VERSION.to_string() }
#[getter]
pub fn get_startup_time(&self) -> SystemTime { crate::MainStatus::get_startup_time() }
pub fn get_startup_time(&self) -> SystemTime { crate::start_up_time() }
#[getter]
pub fn get_py_tasks_count(&self) -> usize {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(async { crate::py::call::PY_TASKS.lock().await.len_check() })
})
}
/// 重新加载插件状态
/// 返回是否成功
pub fn reload_plugin_status(&self) -> bool { PyStatus::get_mut().config.reload_from_default() }
/// 设置某个插件的状态
pub fn set_plugin_status(&self, plugin_name: String, status: bool) {
PyStatus::get_mut().set_status(&plugin_name, status);
}
pub fn get_plugin_status(&self, plugin_name: String) -> Option<bool> {
PyStatus::get().get_status(&plugin_name)
}
/// 同步状态到配置文件
/// 这样关闭的时候就会保存状态
pub fn sync_status_to_config(&self) { PyStatus::get_mut().config.sync_status_to_config(); }
/// 重新加载插件
///
/// 返回是否成功
pub fn reload_plugin(&self, plugin_name: String) -> bool {
PyStatus::get_mut().reload_plugin(&plugin_name)
}
#[pyo3(signature = (content, converse_id, group_id = None))]
pub fn new_message(
&self,
content: String,
converse_id: ConverseId,
group_id: Option<GroupId>,
) -> TailchatSendingMessagePy {
TailchatSendingMessagePy {
message: SendingMessage::new(content, converse_id, group_id, None),
}
}
pub fn debug(&self, content: String) {
debug!("{}", content);
}
@ -128,6 +178,10 @@ impl TailchatSendingMessagePy {
self.message.content = content;
self.clone()
}
pub fn clear_meta(&mut self) -> Self {
self.message.meta = None;
self.clone()
}
pub fn set_img(&mut self, file: Vec<u8>, file_name: String) {
let file = SendingFile::Image {
file,

View File

@ -1,11 +1,10 @@
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use std::{path::Path, str::FromStr};
use toml_edit::{value, DocumentMut, Key, Table, TomlError, Value};
use tracing::{event, Level};
use colored::Colorize;
use toml_edit::{DocumentMut, Key, Table, TomlError, Value, value};
use tracing::{Level, event};
use crate::MainStatus;
use crate::py::PyStatus;
/// ```toml
@ -29,16 +28,41 @@ pub const DEFAULT_CONFIG: &str = r#"
[plugins]
"#;
#[allow(unused)]
impl PluginConfigFile {
pub fn from_str(data: &str) -> Result<Self, TomlError> {
let data = DocumentMut::from_str(data)?;
let mut data = DocumentMut::from_str(data)?;
if data.get(CONFIG_KEY).is_none() {
event!(Level::WARN, "插件配置文件缺少 plugins 字段, 正在初始化");
data.insert_formatted(
&Key::from_str(CONFIG_KEY).unwrap(),
toml_edit::Item::Table(Table::new()),
);
}
Ok(Self { data })
}
pub fn default_init() -> anyhow::Result<Self> {
let config_path = MainStatus::global_config().py().config_path.clone();
let path = Path::new(&config_path);
Self::from_config_path(path)
}
pub fn reload_from_default(&mut self) -> bool {
let new_config = Self::default_init();
if let Err(e) = new_config {
event!(Level::ERROR, "从配置文件重加载时遇到错误: {}", e);
return false;
}
let new_config = new_config.unwrap();
self.data = new_config.data;
true
}
pub fn from_config_path(path: &Path) -> anyhow::Result<Self> {
let config_path = path.join(CONFIG_FILE_NAME);
if !config_path.exists() {
event!(Level::INFO, "插件配置文件不存在, 正在创建");
event!(Level::WARN, "插件配置文件不存在, 正在创建");
std::fs::write(&config_path, DEFAULT_CONFIG)?;
Ok(Self::from_str(DEFAULT_CONFIG)?)
} else {
@ -47,23 +71,20 @@ impl PluginConfigFile {
}
}
pub fn verify_and_init(&mut self) {
if self.data.get(CONFIG_KEY).is_none() {
event!(Level::INFO, "插件配置文件缺少 plugins 字段, 正在初始化");
self.data.insert_formatted(
&Key::from_str(CONFIG_KEY).unwrap(),
toml_edit::Item::Table(Table::new()),
);
}
fn get_table(&self) -> Option<&Table> {
self.data.get(CONFIG_KEY).and_then(|item| item.as_table())
}
fn get_table_mut(&mut self) -> Option<&mut Table> {
self.data.get_mut(CONFIG_KEY).and_then(|item| item.as_table_mut())
}
/// 获取插件状态
/// 默认为 true
pub fn get_status(&self, path: &Path) -> bool {
let path_str = path.to_str().unwrap();
pub fn get_status(&self, plugin_id: &str) -> bool {
if let Some(item) = self.data.get(CONFIG_KEY) {
if let Some(table) = item.as_table() {
if let Some(item) = table.get(path_str) {
if let Some(item) = table.get(plugin_id) {
if let Some(bool) = item.as_bool() {
return bool;
}
@ -73,9 +94,25 @@ impl PluginConfigFile {
true
}
/// 删掉一个状态
pub fn remove_status(&mut self, path: &Path) -> Option<bool> {
let path_str = path.to_str().unwrap();
if let Some(table) = self.get_table_mut() {
if let Some(item) = table.get_mut(path_str) {
if let Some(bool) = item.as_bool() {
table.remove(path_str);
return Some(bool);
} else {
table.remove(path_str);
return Some(false);
}
}
}
None
}
/// 设置插件状态
pub fn set_status(&mut self, path: &Path, status: bool) {
self.verify_and_init();
let path_str = path.to_str().unwrap();
let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap();
if table.contains_key(path_str) {
@ -90,27 +127,64 @@ impl PluginConfigFile {
}
}
pub fn sync_status_from_config(&mut self) {
let plugins = PyStatus::get_map_mut();
self.verify_and_init();
plugins.iter_mut().for_each(|(path, status)| {
let config_status = self.get_status(path);
event!(Level::INFO, "插件状态: {:?} {} -> {}", path, status.enabled, config_status);
status.enabled = config_status;
/// 从默认文件读取状态
///
/// 返回是否成功
pub fn read_status_from_default(&mut self) -> bool {
if !self.reload_from_default() {
return false;
}
event!(Level::INFO, "同步插件状态");
let plugins = PyStatus::get_mut();
fn fmt_bool(b: bool) -> String {
if b {
"启用".green().to_string()
} else {
"禁用".red().to_string()
}
}
plugins.files.iter_mut().for_each(|(path, status)| {
let plugin_id = status.get_id();
let config_status = self.get_status(&plugin_id);
if config_status != status.enabled {
event!(
Level::INFO,
"插件状态: {} {} -> {}",
status.get_id(),
fmt_bool(status.enabled),
fmt_bool(config_status)
);
status.enabled = config_status;
} else {
event!(
Level::INFO,
"插件状态: {} {} (没变)",
status.get_id(),
fmt_bool(status.enabled)
);
}
});
true
}
pub fn sync_status_to_config(&mut self) {
let plugins = PyStatus::get_map();
self.verify_and_init();
let plugins = PyStatus::get();
let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap();
table.clear();
plugins.iter().for_each(|(path, status)| {
table.insert(path.to_str().unwrap(), value(status.enabled));
plugins.files.iter().for_each(|(_, status)| {
table.insert(&status.get_id(), value(status.enabled));
});
}
pub fn write_to_file(&self, path: &PathBuf) -> Result<(), std::io::Error> {
pub fn write_to_default(&self) -> Result<(), std::io::Error> {
let config_path = MainStatus::global_config().py().config_path.clone();
let config_path = Path::new(&config_path);
self.write_to_file(config_path)
}
pub fn write_to_file(&self, path: &Path) -> Result<(), std::io::Error> {
let config_path = path.join(CONFIG_FILE_NAME);
std::fs::write(config_path, self.data.to_string())?;
Ok(())

21
ica-rs/src/py/consts.rs Normal file
View File

@ -0,0 +1,21 @@
pub mod events_func {
/// icalingua 的 加群请求
///
/// added: 2.0.1
pub const ICA_JOIN_REQUEST: &str = "on_ica_join_request";
/// icalingua 的 新消息
pub const ICA_NEW_MESSAGE: &str = "on_ica_message";
/// icalingua 的 消息撤回
pub const ICA_DELETE_MESSAGE: &str = "on_ica_delete_message";
/// tailchat 的 新消息
pub const TAILCHAT_NEW_MESSAGE: &str = "on_tailchat_message";
}
pub mod config_func {
/// 请求配置用的函数
pub const REQUIRE_CONFIG: &str = "require_config";
/// 接受配置用的函数
pub const ON_CONFIG: &str = "on_config";
}

View File

@ -1,120 +1,108 @@
pub mod call;
pub mod class;
pub mod config;
pub mod consts;
use std::ffi::CString;
use std::fmt::Display;
use std::path::Path;
use std::sync::OnceLock;
use std::time::SystemTime;
use std::{collections::HashMap, path::PathBuf};
use colored::Colorize;
use pyo3::prelude::*;
use pyo3::types::PyTuple;
use tracing::{event, info, span, warn, Level};
use pyo3::{
Bound, Py, PyErr, PyResult, Python,
exceptions::PyTypeError,
intern,
types::{PyAnyMethods, PyModule, PyTracebackMethods, PyTuple},
};
use tracing::{Level, event, span, warn};
use crate::MainStatus;
use crate::error::PyPluginError;
#[derive(Debug, Clone)]
use consts::config_func;
#[derive(Debug)]
pub struct PyStatus {
pub files: Option<PyPlugins>,
pub config: Option<config::PluginConfigFile>,
pub files: PyPlugins,
pub config: config::PluginConfigFile,
}
pub type PyPlugins = HashMap<PathBuf, PyPlugin>;
pub type RawPyPlugin = (PathBuf, Option<SystemTime>, String);
#[allow(non_upper_case_globals)]
static mut PyPluginStatus: OnceLock<PyStatus> = OnceLock::new();
#[allow(static_mut_refs)]
impl PyStatus {
pub fn init() {
unsafe {
if PYSTATUS.files.is_none() {
PYSTATUS.files = Some(HashMap::new());
}
if PYSTATUS.config.is_none() {
let plugin_path = MainStatus::global_config().py().config_path.clone();
let mut config =
config::PluginConfigFile::from_config_path(&PathBuf::from(plugin_path))
.unwrap();
config.verify_and_init();
PYSTATUS.config = Some(config);
let config =
config::PluginConfigFile::default_init().expect("初始化 Python 插件配置文件失败");
let status = PyStatus {
files: HashMap::new(),
config,
};
let _ = unsafe { PyPluginStatus.get_or_init(|| status) };
}
pub fn get() -> &'static PyStatus { unsafe { PyPluginStatus.get().unwrap() } }
pub fn get_mut() -> &'static mut PyStatus { unsafe { PyPluginStatus.get_mut().unwrap() } }
/// 添加一个插件
pub fn add_file(&mut self, path: PathBuf, plugin: PyPlugin) { self.files.insert(path, plugin); }
/// 重新加载一个插件
pub fn reload_plugin(&mut self, plugin_name: &str) -> bool {
let plugin = self.files.iter_mut().find_map(|(_, plugin)| {
if plugin.get_id() == plugin_name {
Some(plugin)
} else {
None
}
});
if let Some(plugin) = plugin {
plugin.reload_from_file()
} else {
event!(Level::WARN, "没有找到插件: {}", plugin_name);
false
}
}
pub fn add_file(path: PathBuf, plugin: PyPlugin) { Self::get_map_mut().insert(path, plugin); }
/// 删除一个插件
pub fn delete_file(&mut self, path: &PathBuf) -> Option<PyPlugin> { self.files.remove(path) }
pub fn verify_file(path: &PathBuf) -> bool {
Self::get_map().get(path).map_or(false, |plugin| plugin.verifiy())
}
pub fn get_map() -> &'static PyPlugins {
unsafe {
match PYSTATUS.files.as_ref() {
Some(files) => files,
None => {
Self::init();
PYSTATUS.files.as_ref().unwrap()
}
pub fn get_status(&self, pluging_id: &str) -> Option<bool> {
self.files.iter().find_map(|(_, plugin)| {
if plugin.get_id() == pluging_id {
return Some(plugin.enabled);
}
}
None
})
}
pub fn get_map_mut() -> &'static mut PyPlugins {
unsafe {
match PYSTATUS.files.as_mut() {
Some(files) => files,
None => {
Self::init();
PYSTATUS.files.as_mut().unwrap()
}
pub fn set_status(&mut self, pluging_id: &str, status: bool) {
self.files.iter_mut().for_each(|(_, plugin)| {
if plugin.get_id() == pluging_id {
plugin.enabled = status;
}
}
});
}
pub fn get_config() -> &'static config::PluginConfigFile {
unsafe {
match PYSTATUS.config.as_ref() {
Some(config) => config,
None => {
Self::init();
PYSTATUS.config.as_ref().unwrap()
}
}
}
}
pub fn get_config_mut() -> &'static mut config::PluginConfigFile {
unsafe {
match PYSTATUS.config.as_mut() {
Some(config) => config,
None => {
Self::init();
PYSTATUS.config.as_mut().unwrap()
}
}
}
}
/// 获取某个插件的状态
/// 以 config 优先
pub fn get_status(path: &PathBuf) -> Option<bool> {
Self::get_config_mut().sync_status_from_config();
Self::get_map().get(path).map(|plugin| plugin.enabled)
}
pub fn set_status(path: &Path, status: bool) {
let cfg = Self::get_config_mut();
cfg.set_status(path, status);
let map = Self::get_map_mut();
if let Some(plugin) = map.get_mut(path) {
plugin.enabled = status;
}
pub fn verify_file(&self, path: &PathBuf) -> bool {
self.files.get(path).is_some_and(|plugin| plugin.verifiy())
}
pub fn display() -> String {
let map = Self::get_map();
format!(
"Python 插件 {{ {} }}",
map.iter()
.map(|(k, v)| format!("{:?}-{}", k, v.enabled))
Self::get()
.files
.values()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join("\n")
)
@ -122,7 +110,7 @@ impl PyStatus {
}
pub fn get_py_err_traceback(py_err: &PyErr) -> String {
Python::with_gil(|py| match py_err.traceback_bound(py) {
Python::with_gil(|py| match py_err.traceback(py) {
Some(traceback) => match traceback.format() {
Ok(trace) => trace,
Err(e) => format!("{:?}", e),
@ -133,15 +121,24 @@ pub fn get_py_err_traceback(py_err: &PyErr) -> String {
.to_string()
}
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct PyPlugin {
pub file_path: PathBuf,
pub changed_time: Option<SystemTime>,
pub py_module: Py<PyAny>,
pub modify_time: Option<SystemTime>,
pub py_module: Py<PyModule>,
pub enabled: bool,
}
impl PyPlugin {
pub fn new(path: PathBuf, modify_time: Option<SystemTime>, module: Py<PyModule>) -> Self {
PyPlugin {
file_path: path.clone(),
modify_time,
py_module: module,
enabled: false,
}
}
/// 从文件创建一个新的
pub fn new_from_path(path: &PathBuf) -> Option<Self> {
let raw_file = load_py_file(path);
@ -166,15 +163,16 @@ impl PyPlugin {
}
/// 从文件更新
pub fn reload_from_file(&mut self) {
pub fn reload_from_file(&mut self) -> bool {
let raw_file = load_py_file(&self.file_path);
match raw_file {
Ok(raw_file) => match Self::try_from(raw_file) {
Ok(plugin) => {
self.py_module = plugin.py_module;
self.changed_time = plugin.changed_time;
self.enabled = PyStatus::get_config().get_status(self.file_path.as_path());
self.modify_time = plugin.modify_time;
self.enabled = PyStatus::get().config.get_status(&self.get_id());
event!(Level::INFO, "更新 Python 插件文件 {:?} 完成", self.file_path);
true
}
Err(e) => {
warn!(
@ -183,10 +181,12 @@ impl PyPlugin {
e,
get_py_err_traceback(&e)
);
false
}
},
Err(e) => {
warn!("更新插件 {:?}: {:?} 失败", self.file_path, e);
false
}
}
}
@ -196,7 +196,7 @@ impl PyPlugin {
match get_change_time(&self.file_path) {
None => false,
Some(time) => {
if let Some(changed_time) = self.changed_time {
if let Some(changed_time) = self.modify_time {
time.eq(&changed_time)
} else {
true
@ -204,15 +204,159 @@ impl PyPlugin {
}
}
}
pub fn get_id(&self) -> String { plugin_path_as_id(&self.file_path) }
}
impl Display for PyPlugin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}({:?})-{}", self.get_id(), self.file_path, self.enabled)
}
}
pub const CONFIG_DATA_NAME: &str = "CONFIG_DATA";
fn set_str_cfg_default_plugin(
module: &Bound<'_, PyModule>,
default: String,
path: String,
) -> PyResult<()> {
let base_path = MainStatus::global_config().py().config_path;
let mut base_path: PathBuf = PathBuf::from(base_path);
if !base_path.exists() {
event!(Level::WARN, "python 插件路径不存在, 创建: {:?}", base_path);
std::fs::create_dir_all(&base_path)?;
}
base_path.push(&path);
let config_str: String = if base_path.exists() {
event!(Level::INFO, "加载 {:?} 的配置文件 {:?} 中", path, base_path);
match std::fs::read_to_string(&base_path) {
Ok(v) => v,
Err(e) => {
event!(Level::WARN, "配置文件 {:?} 读取失败 {}, 创建默认配置", base_path, e);
// 写入默认配置
std::fs::write(&base_path, &default)?;
default
}
}
} else {
event!(Level::WARN, "配置文件 {:?} 不存在, 创建默认配置", base_path);
// 写入默认配置
std::fs::write(base_path, &default)?;
default
};
if let Err(e) = module.setattr(intern!(module.py(), CONFIG_DATA_NAME), &config_str) {
event!(Level::WARN, "Python 插件 {:?} 的配置文件信息设置失败:{:?}", path, e);
return Err(PyTypeError::new_err(format!(
"Python 插件 {:?} 的配置文件信息设置失败:{:?}",
path, e
)));
}
// 给到 on config
if let Ok(attr) = module.getattr(intern!(module.py(), config_func::ON_CONFIG)) {
if !attr.is_callable() {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数不是 Callable",
path,
config_func::ON_CONFIG
);
return Ok(());
}
let args = (config_str.as_bytes(),);
if let Err(e) = attr.call1(args) {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数返回了一个报错 {}",
path,
config_func::ON_CONFIG,
e
);
}
}
Ok(())
}
fn set_bytes_cfg_default_plugin(
module: &Bound<'_, PyModule>,
default: Vec<u8>,
path: String,
) -> PyResult<()> {
let base_path = MainStatus::global_config().py().config_path;
let mut base_path: PathBuf = PathBuf::from(base_path);
if !base_path.exists() {
event!(Level::WARN, "python 插件路径不存在, 创建: {:?}", base_path);
std::fs::create_dir_all(&base_path)?;
}
base_path.push(&path);
let config_vec: Vec<u8> = if base_path.exists() {
event!(Level::INFO, "加载 {:?} 的配置文件 {:?} 中", path, base_path);
match std::fs::read(&base_path) {
Ok(v) => v,
Err(e) => {
event!(Level::WARN, "配置文件 {:?} 读取失败 {}, 创建默认配置", base_path, e);
// 写入默认配置
std::fs::write(&base_path, &default)?;
default
}
}
} else {
event!(Level::WARN, "配置文件 {:?} 不存在, 创建默认配置", base_path);
// 写入默认配置
std::fs::write(base_path, &default)?;
default
};
match module.setattr(intern!(module.py(), CONFIG_DATA_NAME), &config_vec) {
Ok(()) => (),
Err(e) => {
warn!("Python 插件 {:?} 的配置文件信息设置失败:{:?}", path, e);
return Err(PyTypeError::new_err(format!(
"Python 插件 {:?} 的配置文件信息设置失败:{:?}",
path, e
)));
}
}
// 给到 on config
if let Ok(attr) = module.getattr(intern!(module.py(), config_func::ON_CONFIG)) {
if !attr.is_callable() {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数不是 Callable",
path,
config_func::ON_CONFIG
);
return Ok(());
}
let args = (&config_vec,);
if let Err(e) = attr.call1(args) {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数返回了一个报错 {}",
path,
config_func::ON_CONFIG,
e
);
}
}
Ok(())
}
impl TryFrom<RawPyPlugin> for PyPlugin {
type Error = PyErr;
fn try_from(value: RawPyPlugin) -> Result<Self, Self::Error> {
let (path, changed_time, content) = value;
let py_module = match py_module_from_code(&content, &path) {
let (path, modify_time, content) = value;
let py_module: Py<PyModule> = match py_module_from_code(&content, &path) {
Ok(module) => module,
Err(e) => {
warn!("加载 Python 插件: {:?} 失败", e);
@ -221,110 +365,37 @@ impl TryFrom<RawPyPlugin> for PyPlugin {
};
Python::with_gil(|py| {
let module = py_module.bind(py);
if let Ok(config_func) = call::get_func(module, "on_config") {
if let Ok(config_func) = call::get_func(module, config_func::REQUIRE_CONFIG) {
match config_func.call0() {
Ok(config) => {
if config.is_instance_of::<PyTuple>() {
let (config, default) = config.extract::<(String, String)>().unwrap();
let base_path = MainStatus::global_config().py().config_path;
let mut base_path: PathBuf = PathBuf::from(base_path);
if !base_path.exists() {
warn!("python 插件路径不存在, 创建: {:?}", base_path);
std::fs::create_dir_all(&base_path)?;
}
base_path.push(&config);
let config_value = if base_path.exists() {
info!("加载 {:?} 的配置文件 {:?} 中", path, base_path);
let content = std::fs::read_to_string(&base_path)?;
toml::from_str(&content)
// let (config, default) = config.extract::<(String, Vec<u8>)>().unwrap();
// let (config, default) = config.extract::<(String, String)>().unwrap();
if let Ok((config, default)) = config.extract::<(String, String)>() {
set_str_cfg_default_plugin(module, default, config)?;
} else if let Ok((config, default)) =
config.extract::<(String, Vec<u8>)>()
{
set_bytes_cfg_default_plugin(module, default, config)?;
} else {
warn!("配置文件 {:?} 不存在, 创建默认配置", base_path);
// 写入默认配置
std::fs::write(base_path, &default)?;
toml::from_str(&default)
};
match config_value {
Ok(config) => {
let py_config =
Bound::new(py, class::ConfigDataPy::new(config));
if let Err(e) = py_config {
warn!("添加配置文件信息失败: {:?}", e);
return Err(e);
}
let py_config = py_config.unwrap();
// 先判定一下原来有没有
if let Ok(true) = module.hasattr(CONFIG_DATA_NAME) {
// get 过来, 后面直接覆盖, 这里用于发个警告
match module.getattr(CONFIG_DATA_NAME) {
Ok(old_config) => {
// 先判断是不是 None, 直接忽略掉 None
// 毕竟有可能有占位
if !old_config.is_none() {
warn!(
"Python 插件 {:?} 的配置文件信息已经存在\n原始内容: {}",
path, old_config
);
}
}
Err(e) => {
warn!(
"Python 插件 {:?} 的配置文件信息已经存在, 但获取失败:{:?}",
path, e
);
}
}
}
match module.setattr(CONFIG_DATA_NAME, py_config) {
Ok(()) => Ok(PyPlugin {
file_path: path,
changed_time,
py_module: module.into_py(py),
enabled: true,
}),
Err(e) => {
warn!(
"Python 插件 {:?} 的配置文件信息设置失败:{:?}",
path, e
);
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
format!(
"Python 插件 {:?} 的配置文件信息设置失败:{:?}",
path, e
),
))
}
}
}
Err(e) => {
warn!(
"加载 Python 插件 {:?} 的配置文件信息时失败:{:?}",
path, e
);
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(format!(
"加载 Python 插件 {:?} 的配置文件信息时失败:{:?}",
path, e
)))
}
warn!(
"加载 Python 插件 {:?} 的配置文件信息时失败:返回的不是 [str, bytes | str]",
path
);
return Err(PyTypeError::new_err(
"返回的不是 [str, bytes | str]".to_string(),
));
}
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
} else if config.is_none() {
// 没有配置文件
Ok(PyPlugin {
file_path: path,
changed_time,
py_module: module.into_py(py),
enabled: true,
})
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
} else {
warn!(
"加载 Python 插件 {:?} 的配置文件信息时失败:返回的不是 [str, str]",
path
);
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
"返回的不是 [str, str]".to_string(),
))
Err(PyTypeError::new_err("返回的不是 [str, str]".to_string()))
}
}
Err(e) => {
@ -333,23 +404,23 @@ impl TryFrom<RawPyPlugin> for PyPlugin {
}
}
} else {
Ok(PyPlugin {
file_path: path,
changed_time,
py_module: module.into_py(py),
enabled: true,
})
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
}
})
}
}
pub static mut PYSTATUS: PyStatus = PyStatus {
files: None,
config: None,
};
/// 插件路径转换为 id
pub fn plugin_path_as_id(path: &Path) -> String {
path.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or("decode-failed")
.to_string()
}
pub fn load_py_plugins(path: &PathBuf) {
let plugins = PyStatus::get_mut();
if path.exists() {
event!(Level::INFO, "找到位于 {:?} 的插件", path);
// 搜索所有的 py 文件 和 文件夹单层下面的 py 文件
@ -364,7 +435,7 @@ pub fn load_py_plugins(path: &PathBuf) {
if let Some(ext) = path.extension() {
if ext == "py" {
if let Some(plugin) = PyPlugin::new_from_path(&path) {
PyStatus::add_file(path, plugin);
plugins.add_file(path, plugin);
}
}
}
@ -374,28 +445,30 @@ pub fn load_py_plugins(path: &PathBuf) {
} else {
event!(Level::WARN, "插件加载目录不存在: {:?}", path);
}
PyStatus::get_config_mut().sync_status_from_config();
plugins.config.read_status_from_default();
plugins.config.sync_status_to_config();
event!(
Level::INFO,
"python 插件目录: {:?} 加载完成, 加载到 {} 个插件",
path,
PyStatus::get_map().len()
plugins.files.len()
);
}
pub fn get_change_time(path: &Path) -> Option<SystemTime> { path.metadata().ok()?.modified().ok() }
pub fn py_module_from_code(content: &str, path: &Path) -> PyResult<Py<PyAny>> {
Python::with_gil(|py| -> PyResult<Py<PyAny>> {
let module: PyResult<Py<PyAny>> = PyModule::from_code_bound(
pub fn py_module_from_code(content: &str, path: &Path) -> PyResult<Py<PyModule>> {
Python::with_gil(|py| -> PyResult<Py<PyModule>> {
let module = PyModule::from_code(
py,
content,
&path.to_string_lossy(),
&path.file_name().unwrap().to_string_lossy(),
CString::new(content).unwrap().as_c_str(),
CString::new(path.to_string_lossy().as_bytes()).unwrap().as_c_str(),
CString::new(path.file_name().unwrap().to_string_lossy().as_bytes())
.unwrap()
.as_c_str(),
// !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的
)
.map(|module| module.into());
module
)?;
Ok(module.unbind())
})
}
@ -407,28 +480,122 @@ pub fn load_py_file(path: &PathBuf) -> std::io::Result<RawPyPlugin> {
Ok((path.clone(), changed_time, content))
}
fn init_py_with_env_path(path: &str) {
unsafe {
#[cfg(target_os = "windows")]
use std::ffi::OsStr;
#[cfg(target_os = "windows")]
use std::os::windows::ffi::OsStrExt;
let mut config = std::mem::zeroed::<pyo3::ffi::PyConfig>();
let config_ptr = &mut config as *mut pyo3::ffi::PyConfig;
// 初始化配置
// pyo3::ffi::PyConfig_InitIsolatedConfig(config_ptr);
pyo3::ffi::PyConfig_InitPythonConfig(config_ptr);
#[cfg(target_os = "linux")]
let wide_path = path.as_bytes().iter().map(|i| *i as i32).collect::<Vec<i32>>();
#[cfg(target_os = "windows")]
let wide_path = OsStr::new(path).encode_wide().chain(Some(0)).collect::<Vec<u16>>();
// 设置 prefix 和 exec_prefix
pyo3::ffi::PyConfig_SetString(config_ptr, &mut config.prefix as *mut _, wide_path.as_ptr());
pyo3::ffi::PyConfig_SetString(
config_ptr,
&mut config.exec_prefix as *mut _,
wide_path.as_ptr(),
);
// 使用 Py_InitializeFromConfig 初始化 python
let status = pyo3::ffi::Py_InitializeFromConfig(&config as *const _);
pyo3::ffi::PyEval_SaveThread();
// 清理配置
pyo3::ffi::PyConfig_Clear(config_ptr);
match status._type {
pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_OK => {
event!(Level::INFO, "根据配置初始化 python 完成");
}
pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_EXIT => {
event!(Level::ERROR, "不对啊, 怎么刚刚初始化 Python 就 EXIT 了");
}
pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_ERROR => {
event!(Level::ERROR, "初始化 python 时发生错误: ERROR");
pyo3::ffi::Py_ExitStatusException(status);
}
}
}
}
/// Python 侧初始化
pub fn init_py() {
// 从 全局配置中获取 python 插件路径
let span = span!(Level::INFO, "初始化 python 及其插件.ing");
let span = span!(Level::INFO, "py init");
let _enter = span.enter();
event!(Level::INFO, "开始初始化 python");
// 注册东西
class::regist_class();
let plugin_path = MainStatus::global_config().py().plugin_path;
event!(Level::INFO, "正在初始化 python");
pyo3::prepare_freethreaded_python();
let cli_args = std::env::args().collect::<Vec<String>>();
if cli_args.contains(&"-env".to_string()) {
let env_path = cli_args.iter().find(|&arg| arg != "-env").expect("未找到 -env 参数的值");
event!(Level::INFO, "找到 -env 参数: {} 正在初始化", env_path);
// 判断一下是否有 VIRTUAL_ENV 环境变量
if let Ok(virtual_env) = std::env::var("VIRTUAL_ENV") {
event!(Level::WARN, "找到 VIRTUAL_ENV 环境变量: {} 将会被 -env 参数覆盖", virtual_env);
}
init_py_with_env_path(env_path);
} else {
// 根据 VIRTUAL_ENV 环境变量 进行一些处理
match std::env::var("VIRTUAL_ENV") {
Ok(virtual_env) => {
event!(Level::INFO, "找到 VIRTUAL_ENV 环境变量: {} 正在初始化", virtual_env);
init_py_with_env_path(&virtual_env);
}
Err(_) => {
event!(Level::INFO, "未找到 VIRTUAL_ENV 环境变量, 正常初始化");
pyo3::prepare_freethreaded_python();
event!(Level::INFO, "prepare_freethreaded_python 完成");
}
}
}
PyStatus::init();
let plugin_path = PathBuf::from(plugin_path);
load_py_plugins(&plugin_path);
event!(Level::DEBUG, "python 插件列表: {}", PyStatus::display());
info!("python inited")
event!(Level::INFO, "python 初始化完成")
}
pub fn post_py() -> anyhow::Result<()> {
PyStatus::get_config_mut().sync_status_to_config();
PyStatus::get_config()
.write_to_file(&PathBuf::from(MainStatus::global_config().py().config_path))?;
pub async fn post_py() -> anyhow::Result<()> {
let status = PyStatus::get_mut();
status.config.sync_status_to_config();
status.config.write_to_default()?;
stop_tasks().await?;
Ok(())
}
async fn stop_tasks() -> Result<(), PyPluginError> {
if call::PY_TASKS.lock().await.is_empty() {
return Ok(());
}
let waiter = tokio::spawn(async {
call::PY_TASKS.lock().await.join_all().await;
});
tokio::select! {
_ = waiter => {
event!(Level::INFO, "Python 任务完成");
Ok(())
}
_ = tokio::signal::ctrl_c() => {
event!(Level::WARN, "正在强制结束 Python 任务");
Err(PyPluginError::PluginNotStopped)
}
}
}

View File

@ -1,12 +1,11 @@
use crate::config::BotConfig;
use crate::MAIN_STATUS;
use crate::config::BotConfig;
#[derive(Debug, Clone)]
pub struct BotStatus {
pub config: Option<BotConfig>,
pub ica_status: Option<ica::MainStatus>,
pub tailchat_status: Option<tailchat::MainStatus>,
pub startup_time: Option<std::time::SystemTime>,
}
impl BotStatus {
@ -36,28 +35,40 @@ impl BotStatus {
online_status: ica::OnlineData::default(),
});
MAIN_STATUS.config = Some(config);
MAIN_STATUS.startup_time = Some(std::time::SystemTime::now());
}
}
pub fn get_startup_time() -> std::time::SystemTime {
unsafe { MAIN_STATUS.startup_time.unwrap() }
pub fn global_config() -> &'static BotConfig {
unsafe {
let ptr = &raw const MAIN_STATUS.config;
(*ptr).as_ref().unwrap()
}
}
pub fn global_config() -> &'static BotConfig { unsafe { MAIN_STATUS.config.as_ref().unwrap() } }
pub fn global_ica_status() -> &'static ica::MainStatus {
unsafe { MAIN_STATUS.ica_status.as_ref().unwrap() }
unsafe {
let ptr = &raw const MAIN_STATUS.ica_status;
(*ptr).as_ref().unwrap()
}
}
pub fn global_tailchat_status() -> &'static tailchat::MainStatus {
unsafe { MAIN_STATUS.tailchat_status.as_ref().unwrap() }
unsafe {
let ptr = &raw const MAIN_STATUS.tailchat_status;
(*ptr).as_ref().unwrap()
}
}
pub fn global_ica_status_mut() -> &'static mut ica::MainStatus {
unsafe { MAIN_STATUS.ica_status.as_mut().unwrap() }
unsafe {
let ptr = &raw mut MAIN_STATUS.ica_status;
(*ptr).as_mut().unwrap()
}
}
pub fn global_tailchat_status_mut() -> &'static mut tailchat::MainStatus {
unsafe { MAIN_STATUS.tailchat_status.as_mut().unwrap() }
unsafe {
let ptr = &raw mut MAIN_STATUS.tailchat_status;
(*ptr).as_mut().unwrap()
}
}
}

View File

@ -9,13 +9,13 @@ use reqwest::ClientBuilder as reqwest_ClientBuilder;
use rust_socketio::async_callback;
use rust_socketio::asynchronous::{Client, ClientBuilder};
use rust_socketio::{Event, Payload, TransportType};
use serde_json::{json, Value};
use tracing::{event, span, Level};
use serde_json::{Value, json};
use tracing::{Level, event, span};
use crate::config::TailchatConfig;
use crate::data_struct::tailchat::status::{BotStatus, LoginData};
use crate::error::{ClientResult, TailchatError};
use crate::{async_any_callback_with_state, async_callback_with_state, version_str, StopGetter};
use crate::{StopGetter, async_any_callback_with_state, async_callback_with_state, version_str};
pub async fn start_tailchat(
config: TailchatConfig,

View File

@ -4,8 +4,8 @@ use crate::data_struct::tailchat::messages::SendingMessage;
use colored::Colorize;
use reqwest::multipart;
use rust_socketio::asynchronous::Client;
use serde_json::{json, Value};
use tracing::{event, span, Level};
use serde_json::{Value, json};
use tracing::{Level, event, span};
pub async fn send_message(client: &Client, message: &SendingMessage) -> bool {
let span = span!(Level::INFO, "tailchat send message");

View File

@ -1,15 +1,16 @@
use std::path::PathBuf;
use std::sync::Arc;
use colored::Colorize;
use rust_socketio::asynchronous::Client;
use rust_socketio::{Event, Payload};
use tracing::{event, info, Level};
use tracing::{Level, event, info};
use crate::data_struct::tailchat::messages::ReceiveMessage;
use crate::data_struct::tailchat::status::{BotStatus, UpdateDMConverse};
use crate::py::PyStatus;
use crate::py::call::tailchat_new_message_py;
use crate::tailchat::client::{emit_join_room, send_message};
use crate::{help_msg, py, MainStatus, TAILCHAT_VERSION, VERSION};
use crate::{MainStatus, VERSION, client_id, help_msg, version_str};
/// 所有
pub async fn any_event(event: Event, payload: Payload, _client: Client, _status: Arc<BotStatus>) {
@ -78,17 +79,15 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
if !message.is_reply() {
if message.content == "/bot-rs" {
let reply = message.reply_with(&format!(
"shenbot v{}\ntailchat-async-rs pong v{}",
VERSION, TAILCHAT_VERSION
));
let reply = message.reply_with(&version_str());
send_message(&client, &reply).await;
} else if message.content == "/bot-ls" {
let reply = message.reply_with(&format!(
"shenbot-py v{}\n{}",
"shenbot-py v{}-{}\n{}",
VERSION,
client_id(),
if MainStatus::global_config().check_py() {
py::PyStatus::display()
PyStatus::display()
} else {
"未启用 Python 插件".to_string()
}
@ -100,14 +99,12 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
}
if MainStatus::global_config().tailchat().admin_list.contains(&message.sender_id) {
// admin 区
if message.content.starts_with("/bot-enable") {
let client_id = client_id();
if message.content.starts_with(&format!("/bot-enable-{}", client_id)) {
// 先判定是否为 admin
// 尝试获取后面的信息
let mut content = message.content.split_whitespace();
content.next();
if let Some(name) = content.next() {
let path_name = PathBuf::from(name);
match py::PyStatus::get_status(&path_name) {
if let Some((_, name)) = message.content.split_once(" ") {
match PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
@ -117,18 +114,15 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
send_message(&client, &reply).await;
}
Some(false) => {
py::PyStatus::set_status(&path_name, true);
PyStatus::get_mut().set_status(name, true);
let reply = message.reply_with("启用插件完成");
send_message(&client, &reply).await;
}
}
}
} else if message.content.starts_with("/bot-disable") {
let mut content = message.content.split_whitespace();
content.next();
if let Some(name) = content.next() {
let path_name = PathBuf::from(name);
match py::PyStatus::get_status(&path_name) {
} else if message.content.starts_with(&format!("/bot-disable-{}", client_id)) {
if let Some((_, name)) = message.content.split_once(" ") {
match PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
@ -138,7 +132,7 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
send_message(&client, &reply).await;
}
Some(true) => {
py::PyStatus::set_status(&path_name, false);
PyStatus::get_mut().set_status(name, false);
let reply = message.reply_with("禁用插件完成");
send_message(&client, &reply).await;
}
@ -147,7 +141,7 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
}
}
}
py::call::tailchat_new_message_py(&message, &client).await;
tailchat_new_message_py(&message, &client).await;
}
}
}

1
ica-rs/src/wasms.rs Normal file
View File

@ -0,0 +1 @@

244
news.md
View File

@ -1,245 +1,5 @@
# 更新日志
## 0.7.0
## [0.9](./news/0-9.md)
> 我决定叫他 0.7.0
> 因为修改太多了.png
- 加入了 禁用/启用 插件功能
- 现在会在插件加载时警告你的插件原来定义了 `CONFIG_DATA` 这一项
- `IcaNewMessage` 添加了新的 api
- `get_sender_name` 获取发送人昵称
- `ica` 兼容版本号 `2.12.11` -> `2.12.12`
- 加入了 `STABLE` 信息, 用于标记稳定版本
- 不少配置文件项加上了默认值
- 添加了 `version_str() -> String` 用于方便的获取版本信息
- 同样在 `py` 侧也有 `version_str` 方法
- 加入了 `/help` 命令
- 用于获取帮助信息
- 加入了 `/bot-ls`
- 用于展示所有插件的信息
- 加入了 `/bot-enable``/bot-disable`
- 用于启用/禁用插件
## 0.6.10
- 加了点东西 (?)
## 0.6.9
我决定立即发布 0.6.9
- 添加了 `Client.startup_time() -> datetime` 方法
- 用于获取 bot 启动时间
- 这样就可以经常吹我 bot 跑了多久了 ( ˘•ω•˘ )
- 顺手加上了 `/bot-uptime` 命令
- 可以获取 bot 运行时间
- 谢谢 GitHub Copilot 的帮助
## 0.6.8
- 修复了一堆拼写错误
- 太难绷了
- `TailchatReciveMessagePy` -> `TailchatReceiveMessagePy`
- `ReciveMessage` -> `ReceiveMessage`
- `ReceiveMessage::meta`
- 从 `JsonValue` 改成 `Option<JsonValue>`
- 用来解决发图片的时候没有 `meta` 字段的问题
- 去除了自带的两个 macro
- `wrap_callback``wrap_any_callback`
- 因为现在他俩已经进到 `rust_socketio` 里啦
- 添加了新的 macro
- ``
- 支持了 `TailchatReceiveMessagePy``is_from_self` 方法
- 用于判断是否是自己发的消息
## 0.6.7
游学回来啦
- 处理了一些 tailchat 的特殊情况
- 比如 message 里面的 `GroupId` 实际上是可选的, 在私聊中没有这一项
- 忽略了所有的 `__v` (用于数据库记录信息的, bot不需要管)
- 作者原话 `不用管。数据库记录版本`
- 修复了如果没法解析新的信息, 会 panic 的问题
- `ica_typing.py`
- 补充了 `TailchatSendingMessage``group_id``converse_id` 字段
- 把 `group_id` 的设置和返回都改成了 `Optional[GroupId]`
- tailchat 的 API 也差点意思就是了(逃)
- 处理了 icalingua 的 `renewMessage` 事件 (其实就是直接忽略掉了)
## 0.6.6
游学之前最后一次更新
其实也就五天
正式支持了 tailchat 端
好耶!
[!note]
```text
notice_room = []
notice_start = true
admin_list = []
filter_list = []
```
的功能暂时不支持
## 0.6.5
怎么就突然 0.6.5 了
我也不造啊
- 反正支持了 tailchat 的信息接受
- 但是需要你在对面服务端打开 `DISABLE_MESSAGEPACK` 环境变量
- 能用就行
- 现在 `update_online_data` 不会再以 INFO 级别显示了
- `update_all_room` 同上
## 0.6.2
- 添加 API
- `NewMessage.set_img` 用于设置消息的图片
- `IcaSendMessage.set_img` 用于设置消息的图片 (python)
## 0.6.1
还是没写完 tailchat 支持
因为 rust_socketio 还是没写好 serdelizer 的支持
- 正在添加发送图片的 api
## 0.6.0-dev
- 去除了 matrix 的支持
- 淦哦
- 去除了相应代码和依赖
- 去除了 Python 侧代码
- 向 tailchat (typescript 低头)
- 修复了没法编译的问题(
## 0.5.3
修复了 Icalingua 断开时 如果 socketio 已经断开会导致程序 返回 Error 的问题
以及还有一些别的修复就是了
- Python 端修改
- `on_message` -> `on_ica_message`
- `on_delete_message` -> `on_ica_delete_message`
- 添加 `on_matrix_message`
## 0.5.1/2
重构了一整波, 还没改 `ica-typing.py` 的代码
但至少能用了
- Ica 版本号 `1.4.0`
- Matrix 版本号 `0.1.0`
## 0.5.0
准备接入 `Matrix`
去掉 `pyo3-async` 的依赖
## 0.4.12
把 0.4.11 的遗留问题修完了
## 0.4.11
这几天就是在刷版本号的感觉
- 添加
- `DeleteMessage` 用于删除消息
- `NewMessage.as_delete` 用于将消息转换为删除消息
- `client::delete_message` 用于删除消息
- `client::fetch_history` 用于获取历史消息 TODO
- `py::class::DeleteMessagePy` 用于删除消息 的 Python 侧 API
- `py::class::IcaClientPy.delete_message` 用于删除消息 的 Python 侧 API
- `IcalinguaStatus.current_loaded_messages_count`
- 用于以后加载信息计数
- 修改
- `py::class::IcaStatusPy`
- 大部分方法从手动 `unsafe` + `Option`
- 改成直接调用 `IcalinguaStatus` 的方法
- `IcalinguaStatus`
- 所有方法均改成 直接对着 `IcalinguaStatus` 的方法调用
- 补全没有的方法
## 0.4.10
好家伙, 我感觉都快能叫 0.5 了
修改了一些内部数据结构, 使得插件更加稳定
添加了 `rustfmt.toml` 用于格式化代码
**注意**: 请在提交代码前使用 `cargo +nightly fmt` 格式化代码
修复了 `Message` 解析 `replyMessage` 字段是 如果是 null 则会解析失败的问题
## 0.4.9
修复了 Python 插件运行错误会导致整个程序崩溃的问题
## 0.4.8
添加了 `filter_list` 用于过滤特定人的消息
## 0.4.7
修复了重载时如果代码有问题会直接 panic 的问题
## 0.4.6
现在更适合部署了
## 0.4.5
添加 `is_reply` api 到 `NewMessagePy`
## 0.4.4
现在正式支持 Python 插件了
`/bmcl` 也迁移到了 Python 插件版本
## 0.4.3
噫! 好! 我成了!
## 0.4.2
现在是 async 版本啦!
## 0.4.1
现在能发送登录信息啦
## 0.4.0
使用 Rust 从头实现一遍
\能登录啦/
## 0.3.3
适配 Rust 端的配置文件修改
## 0.3.1/2
改进 `/bmcl` 的细节
## 0.3.0
合并了 dongdigua 的代码, 把消息处理部分分离
现在代码更阳间了(喜
## 0.2.3
添加了 `/bmcl` 请求 bmclapi 状态
## 0.2.2
重构了一波整体代码
## [0.2 ~ 0.8](./news/old.md)

38
news/0-9.md Normal file
View File

@ -0,0 +1,38 @@
# 0.9 更新日志
## 0.9.0
- 修复了 Python 插件停不下来就真的停不下来的问题
- 让初始化的时候 插件启/禁状态显示更明显了
- 有颜色啦!
- 加了不少颜色
### ica 2.0.1
> 添加了 `Room` 相关的 api
- `IcaStatus` 添加了 `rooms(self) -> list[IcaRoom]` 方法
- 用于获取当前所有的房间
- 添加了 `IcaRoom`
- 用于表示一个房间
- `room_id -> int` 群号 (i64)
- `def is_group(self) -> bool` 是否为群聊
- `def is_chat(self) -> bool` 是否为私聊
- `room_name -> int` 群名 (String)
- `unread_count -> int` 未读消息数 (u64)
- `priority -> int` 优先级 (u8)
- `utime -> int` 最后活跃时间 (unix sec * 1000)
- `def new_message_to(self, content: str) -> IcaSendMessage`
- 用于创建一条指向这个房间的消息
> 添加了 Ica 侧的相关配置获取
- `IcaStatus` 添加了 `admins(self) -> list[UserId]` 方法
- 用于获取当前所有的管理员
- `IcaStatus` 添加了 `blocked(self) -> list[UserId]` 方法
- 用于获取当前所有的被屏蔽的人

383
news/olds.md Normal file
View File

@ -0,0 +1,383 @@
# 更新日志 (0.2 ~ 0.8)
## 0.8.2
- ica 兼容版本号更新到 `2.12.28`
- 现在支持通过读取环境变量里的 `VIRTUAL_ENV` 来获取虚拟环境路径
- 用于在虚拟环境中运行插件
- 添加了 `-h` 参数
- 用于展示帮助信息
- 添加了 `-env` 参数
- 用于指定 python 插件的虚拟环境路径
- 会覆盖环境变量中的 `VIRTUAL_ENV`
- 现在加入了默认的配置文件路径 `./config.toml`
- 现在会记录所有的 python 运行中 task 了
- 也会在退出的时候等待所有的 task 结束
- 二次 ctrl-c 会立即退出
- 改进了一下 ica 的新消息显示
- 添加了 ica 链接用时的显示
### ica&tailchat 2.0.0
- BREAKING CHANGE
- 现在 `CONFIG_DATA` 为一个 `str | bytes`
- 用于存储插件的配置信息
- 需要自行解析
- 现在 `on_config` 函数签名为 `on_config = Callable[[bytes], None]`
- 用于接收配置信息
- 现在使用 `require_config = Callable[[None], str, bytes | str]` 函数来请求配置信息
- 用于请求配置信息
- `str` 为配置文件名
- `bytes | str` 为配置文件默认内容
- 以 `bytes` 形式或者 `str` 形式均可
### ica 1.6.7
- 为 `IcaClinet` 添加了 `py_tasks_count -> int` 属性
- 用于获取当前运行中的 python task 数量
### tailchat 1.2.6
- 为 `TailchatClient` 添加了 `py_tasks_count -> int` 属性
- 用于获取当前运行中的 python task 数量
## 0.8.1
- 修复了 Python 插件状态写入的时候写入路径错误的问题
- `ica-typing` 加入了 `from __future__ import annotations`
- 这样就可以随便用 typing 了
- 把 NewType 都扬了
### ica 1.6.6
- 修复了 `send_poke` api 的问题
- 现在可以正常使用了
## 0.8.0
- ica 兼容版本号更新到 ~~`2.12.24`~~ `2.12.26`
- 从 `py::PyStatus` 开始进行一个 `static mut` -> `static mut OnceLock` 的改造
- 用于看着更舒服(逃)
- 部分重构了一下 读取插件启用状态 的配置文件的代码
- 现在 `/bot-help` 会直接输出实际的 client id, 而不是给你一个默认的 `<client-id>`
### ica 1.6.5
- 添加了 `send_room_sign_in` api
- 用于发送群签到信息
- socketio event: `sendGroupSign`
- 添加了 `send_poke` api
- 用于发送戳一戳
- 可以指定群的某个人
- 或者指定好友
- 目前还是有点问题
- socketio event: `sendGroupPoke`
- 添加了 `reload_plugin_status` api
- 用于重新加载插件状态
- 添加了 `reload_plugin(plugin_name: str)` api
- 用于重新加载指定插件
- 添加了 `set_plugin_status(plugin_name: str, status: bool)` api
- 用于设置插件的启用状态
- 添加了 `get_plugin_status(plugin_name: str) -> bool` api
- 用于获取插件的启用状态
- 添加了 `sync_status_to_config` api
- 用于将内存中的插件状态同步到配置文件中
### tailchat 1.2.5
- 添加了 `reload_plugin_status` api
- 用于重新加载插件状态
- 添加了 `reload_plugin(plugin_name: str)` api
- 用于重新加载指定插件
- 添加了 `set_plugin_status(plugin_name: str, status: bool)` api
- 用于设置插件的启用状态
- 添加了 `get_plugin_status(plugin_name: str) -> bool` api
- 用于获取插件的启用状态
- 添加了 `sync_status_to_config` api
- 用于将内存中的插件状态同步到配置文件中
## 0.7.4
- ica 兼容版本号更新到 `2.12.23`
- 通过一个手动 json patch 修复了因为 icalingua 的奇怪类型问题导致的 bug
- [icalingua issue](https://github.com/Icalingua-plus-plus/Icalingua-plus-plus/issues/793)
## 0.7.3
- 也许修复了删除插件不会立即生效的问题
- ica 兼容版本号更新到 `2.12.21`
添加了一些新的 api
### ica 1.6.4
- 给 `SendMessagePy`
- 添加了 `remove_reply` 方法
- 用于取消回复状态
- 删除了 `Room``auto_download``download_path` 字段
- 因为这两个字段也没啥用
### tailcaht 1.2.4
- 给 `TailchatClientPy`
- 添加了 `new_message` 方法
- 用于创建新的消息
- 给 `TailchatSendingMessagePy`
- 添加了 `clear_meta` 功能
- 用于清除 `meta` 字段
- 可以用来取消回复状态
## 0.7.2
- 修复了一些 ica 和 tailchat 表现不一致的问题(捂脸)
## 0.7.1
- 两个 api 版本号分别升级到 `1.6.3(ica)``1.2.3(tailchat)`
- 加入了 `client_id`
- 用的 startup time hash 一遍取后六位
- 以及也有 python 侧的 `client_id` api
- 修复了上个版本其实没有写 python 侧 `version_str` api 的问题
## 0.7.0
> 我决定叫他 0.7.0
> 因为修改太多了.png
- 加入了 禁用/启用 插件功能
- 现在会在插件加载时警告你的插件原来定义了 `CONFIG_DATA` 这一项
- `IcaNewMessage` 添加了新的 api
- `get_sender_name` 获取发送人昵称
- `ica` 兼容版本号 `2.12.11` -> `2.12.12`
- 加入了 `STABLE` 信息, 用于标记稳定版本
- 不少配置文件项加上了默认值
- 添加了 `version_str() -> String` 用于方便的获取版本信息
- 同样在 `py` 侧也有 `version_str` 方法
- 加入了 `/help` 命令
- 用于获取帮助信息
- 加入了 `/bot-ls`
- 用于展示所有插件的信息
- 加入了 `/bot-enable``/bot-disable`
- 用于启用/禁用插件
## 0.6.10
- 加了点东西 (?)
## 0.6.9
我决定立即发布 0.6.9
- 添加了 `Client.startup_time() -> datetime` 方法
- 用于获取 bot 启动时间
- 这样就可以经常吹我 bot 跑了多久了 ( ˘•ω•˘ )
- 顺手加上了 `/bot-uptime` 命令
- 可以获取 bot 运行时间
- 谢谢 GitHub Copilot 的帮助
## 0.6.8
- 修复了一堆拼写错误
- 太难绷了
- `TailchatReciveMessagePy` -> `TailchatReceiveMessagePy`
- `ReciveMessage` -> `ReceiveMessage`
- `ReceiveMessage::meta`
- 从 `JsonValue` 改成 `Option<JsonValue>`
- 用来解决发图片的时候没有 `meta` 字段的问题
- 去除了自带的两个 macro
- `wrap_callback``wrap_any_callback`
- 因为现在他俩已经进到 `rust_socketio` 里啦
- 添加了新的 macro
- 支持了 `TailchatReceiveMessagePy``is_from_self` 方法
- 用于判断是否是自己发的消息
## 0.6.7
游学回来啦
- 处理了一些 tailchat 的特殊情况
- 比如 message 里面的 `GroupId` 实际上是可选的, 在私聊中没有这一项
- 忽略了所有的 `__v` (用于数据库记录信息的, bot不需要管)
- 作者原话 `不用管。数据库记录版本`
- 修复了如果没法解析新的信息, 会 panic 的问题
- `ica_typing.py`
- 补充了 `TailchatSendingMessage``group_id``converse_id` 字段
- 把 `group_id` 的设置和返回都改成了 `Optional[GroupId]`
- tailchat 的 API 也差点意思就是了(逃)
- 处理了 icalingua 的 `renewMessage` 事件 (其实就是直接忽略掉了)
## 0.6.6
游学之前最后一次更新
其实也就五天
正式支持了 tailchat 端
好耶!
[!note]
```text
notice_room = []
notice_start = true
admin_list = []
filter_list = []
```
的功能暂时不支持
## 0.6.5
怎么就突然 0.6.5 了
我也不造啊
- 反正支持了 tailchat 的信息接受
- 但是需要你在对面服务端打开 `DISABLE_MESSAGEPACK` 环境变量
- 能用就行
- 现在 `update_online_data` 不会再以 INFO 级别显示了
- `update_all_room` 同上
## 0.6.2
- 添加 API
- `NewMessage.set_img` 用于设置消息的图片
- `IcaSendMessage.set_img` 用于设置消息的图片 (python)
## 0.6.1
还是没写完 tailchat 支持
因为 rust_socketio 还是没写好 serdelizer 的支持
- 正在添加发送图片的 api
## 0.6.0-dev
- 去除了 matrix 的支持
- 淦哦
- 去除了相应代码和依赖
- 去除了 Python 侧代码
- 向 tailchat (typescript 低头)
- 修复了没法编译的问题(
## 0.5.3
修复了 Icalingua 断开时 如果 socketio 已经断开会导致程序 返回 Error 的问题
以及还有一些别的修复就是了
- Python 端修改
- `on_message` -> `on_ica_message`
- `on_delete_message` -> `on_ica_delete_message`
- 添加 `on_matrix_message`
## 0.5.1/2
重构了一整波, 还没改 `ica-typing.py` 的代码
但至少能用了
- Ica 版本号 `1.4.0`
- Matrix 版本号 `0.1.0`
## 0.5.0
准备接入 `Matrix`
去掉 `pyo3-async` 的依赖
## 0.4.12
把 0.4.11 的遗留问题修完了
## 0.4.11
这几天就是在刷版本号的感觉
- 添加
- `DeleteMessage` 用于删除消息
- `NewMessage.as_delete` 用于将消息转换为删除消息
- `client::delete_message` 用于删除消息
- `client::fetch_history` 用于获取历史消息 TODO
- `py::class::DeleteMessagePy` 用于删除消息 的 Python 侧 API
- `py::class::IcaClientPy.delete_message` 用于删除消息 的 Python 侧 API
- `IcalinguaStatus.current_loaded_messages_count`
- 用于以后加载信息计数
- 修改
- `py::class::IcaStatusPy`
- 大部分方法从手动 `unsafe` + `Option`
- 改成直接调用 `IcalinguaStatus` 的方法
- `IcalinguaStatus`
- 所有方法均改成 直接对着 `IcalinguaStatus` 的方法调用
- 补全没有的方法
## 0.4.10
好家伙, 我感觉都快能叫 0.5 了
修改了一些内部数据结构, 使得插件更加稳定
添加了 `rustfmt.toml` 用于格式化代码
**注意**: 请在提交代码前使用 `cargo +nightly fmt` 格式化代码
修复了 `Message` 解析 `replyMessage` 字段是 如果是 null 则会解析失败的问题
## 0.4.9
修复了 Python 插件运行错误会导致整个程序崩溃的问题
## 0.4.8
添加了 `filter_list` 用于过滤特定人的消息
## 0.4.7
修复了重载时如果代码有问题会直接 panic 的问题
## 0.4.6
现在更适合部署了
## 0.4.5
添加 `is_reply` api 到 `NewMessagePy`
## 0.4.4
现在正式支持 Python 插件了
`/bmcl` 也迁移到了 Python 插件版本
## 0.4.3
噫! 好! 我成了!
## 0.4.2
现在是 async 版本啦!
## 0.4.1
现在能发送登录信息啦
## 0.4.0
使用 Rust 从头实现一遍
\能登录啦/
## 0.3.3
适配 Rust 端的配置文件修改
## 0.3.1/2
改进 `/bmcl` 的细节
## 0.3.0
合并了 dongdigua 的代码, 把消息处理部分分离
现在代码更阳间了(喜
## 0.2.3
添加了 `/bmcl` 请求 bmclapi 状态
## 0.2.2
重构了一波整体代码

View File

@ -1,31 +1,33 @@
# icalingua bot
这是一个基于 icalingua docker 版的 bot
这是一个基于 icalingua-bridge 的 bot
> 出于某个企鹅, 和保护 作者 和 原项目 ( ica ) 的原因 \
> 功能请自行理解
[插件市场(确信)](https://github.com/shenjackyuanjie/shenbot-plugins)
## 通用环境准备
- 安装 Python 3.8+
```powershell
# 你可以使用你自己的方法安装 Python
# 例如
choco install python
# 或者
scoop install python
# 又或者
uv venv
```
- 启动 icalingua 后端
```bash
# 用你自己的方法启动你的 icalingua 后端
# 用你自己的方法启动你的 icalingua-bridge
# 例如
docker start icalingua
docker-compose up -d
```
## 使用方法 ( Rust 版 )
## 使用方法
- 准备一个 Python 环境
@ -46,42 +48,3 @@ cargo build --release
```powershell
cargo run --release -- -c config.toml
```
## 使用方法 ( Python 版 )
> [!WARNING]
> 请注意 Python 的 bot 已经不再维护
- 安装依赖
```powershell
python -m pip install -r requirements.txt
```
> 如果你想使用虚拟环境 \
> 可以使用 `python -m venv venv` 创建虚拟环境 \
> 然后使用 `venv\Scripts\activate` 激活虚拟环境 \
> 最后使用 `python -m pip install -r requirements.txt` 安装依赖
- 修改配置文件
```powershell
Copy-Item config-temp.toml config.toml
# 欸我就用 powershell
```
- icalingua 启动!
```bash
# 用你自己的方法启动你的 icalingua 后端
# 例如
docker start icalingua
# 或者
docker up -d
```
- bot 启动!
```powershell
python connect.py
```

View File

@ -1 +0,0 @@
nightly

2
rust-toolchain.toml Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "stable"