Compare commits

...

25 Commits

Author SHA1 Message Date
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
16 changed files with 883 additions and 442 deletions

538
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ica-rs" name = "ica-rs"
version = "0.8.0" version = "0.8.2"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -36,12 +36,12 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
chrono = "0.4" chrono = "0.4"
toml = "0.8" toml = "0.8"
toml_edit = "0.22.20" toml_edit = "0.22"
colored = "2.1" colored = "3.0"
# runtime # runtime
tokio = { version = "1.41", features = ["full"] } tokio = { version = "1.43", features = ["full"] }
futures-util = "0.3.30" futures-util = "0.3"
pyo3 = { version = "0.23", features = ["experimental-async", "py-clone"] } pyo3 = { version = "0.23", features = ["experimental-async", "py-clone"] }
anyhow = { version = "1.0", features = ["backtrace"] } anyhow = { version = "1.0", features = ["backtrace"] }
# async 这玩意以后在搞 # async 这玩意以后在搞
@ -49,5 +49,6 @@ anyhow = { version = "1.0", features = ["backtrace"] }
# pyo3-asyncio = { version = "0.20.0", features = ["attributes", "tokio-runtime"] } # pyo3-asyncio = { version = "0.20.0", features = ["attributes", "tokio-runtime"] }
# log # log
tracing = "0.1.40" tracing = "0.1"
tracing-subscriber = { version = "0.3.18", features = ["time"] } tracing-subscriber = { version = "0.3", features = ["time"] }
foldhash = "0.1.4"

View File

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

View File

@ -44,9 +44,11 @@ impl Room {
// ica issue: https://github.com/Icalingua-plus-plus/Icalingua-plus-plus/issues/793 // 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()) { if parse_json.get("roomId").is_none_or(|id| id.is_null()) {
use tracing::warn; use tracing::warn;
warn!("Room::new_from_json roomId is None, patching it to -1, raw: {:#?}", raw_json); warn!("Room::new_from_json roomId is None, patching it to -1, raw: {:?}", raw_json);
parse_json["roomId"] = JsonValue::Number(Number::from(-1)); parse_json["roomId"] = JsonValue::Number(Number::from(-1));
} }
// 现在 fix 了
let inner = match serde_json::from_value::<InnerRoom>(parse_json) { let inner = match serde_json::from_value::<InnerRoom>(parse_json) {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {

View File

@ -88,7 +88,26 @@ impl<'de> Deserialize<'de> for Message {
impl Display for Message { impl Display for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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 { impl Display for NewMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( if !self.msg.content.trim().is_empty() {
f, write!(
"{}|{}|{}|{}|{}", f,
self.msg_id(), "{}|{}|{}|{}|{}",
self.room_id, self.msg.msg_id,
self.msg.sender_id, self.room_id,
self.msg.sender_name, self.msg.sender_id,
self.msg.content 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,8 +1,9 @@
pub mod client; pub mod client;
pub mod events; pub mod events;
use std::sync::OnceLock; // use std::sync::OnceLock;
use colored::Colorize;
use rust_socketio::asynchronous::{Client, ClientBuilder}; use rust_socketio::asynchronous::{Client, ClientBuilder};
use rust_socketio::{async_any_callback, async_callback}; use rust_socketio::{async_any_callback, async_callback};
use rust_socketio::{Event, Payload, TransportType}; use rust_socketio::{Event, Payload, TransportType};
@ -13,33 +14,33 @@ use crate::error::{ClientResult, IcaError};
use crate::{version_str, StopGetter}; use crate::{version_str, StopGetter};
/// icalingua 客户端的兼容版本号 /// icalingua 客户端的兼容版本号
pub const ICA_PROTOCOL_VERSION: &str = "2.12.24"; pub const ICA_PROTOCOL_VERSION: &str = "2.12.28";
mod status { // mod status {
use crate::data_struct::ica::all_rooms::Room; // use crate::data_struct::ica::all_rooms::Room;
pub use crate::data_struct::ica::online_data::OnlineData; // pub use crate::data_struct::ica::online_data::OnlineData;
#[derive(Debug, Clone)] // #[derive(Debug, Clone)]
pub struct MainStatus { // pub struct MainStatus {
/// 是否启用 ica // /// 是否启用 ica
pub enable: bool, // pub enable: bool,
/// qq 是否登录 // /// qq 是否登录
pub qq_login: bool, // pub qq_login: bool,
/// 当前已加载的消息数量 // /// 当前已加载的消息数量
pub current_loaded_messages_count: u64, // pub current_loaded_messages_count: u64,
/// 房间数据 // /// 房间数据
pub rooms: Vec<Room>, // pub rooms: Vec<Room>,
/// 在线数据 (Icalingua 信息) // /// 在线数据 (Icalingua 信息)
pub online_status: OnlineData, // pub online_status: OnlineData,
} // }
impl MainStatus { // impl MainStatus {
pub fn update_rooms(&mut self, room: Vec<Room>) { self.rooms = room; } // 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; } // pub fn update_online_status(&mut self, status: OnlineData) { self.online_status = status; }
} // }
} // }
static ICA_STATUS: OnceLock<status::MainStatus> = OnceLock::new(); // static ICA_STATUS: OnceLock<status::MainStatus> = OnceLock::new();
pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientResult<(), IcaError> { pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientResult<(), IcaError> {
let span = span!(Level::INFO, "Icalingua Client"); let span = span!(Level::INFO, "Icalingua Client");
@ -47,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); 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()) let socket = match ClientBuilder::new(config.host.clone())
.transport_type(TransportType::Websocket) .transport_type(TransportType::Websocket)
.on_any(async_any_callback!(events::any_event)) .on_any(async_any_callback!(events::any_event))
@ -65,7 +67,11 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
.await .await
{ {
Ok(client) => { Ok(client) => {
event!(Level::INFO, "socketio connected"); event!(
Level::INFO,
"{}",
format!("socketio connected time: {:?}", start_connect_time.elapsed()).on_cyan()
);
client client
} }
Err(e) => { Err(e) => {
@ -94,6 +100,7 @@ pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientRe
} }
} }
// 等待停止信号 // 等待停止信号
event!(Level::INFO, "ica client waiting for stop signal");
stop_reciver.await.ok(); stop_reciver.await.ok();
event!(Level::INFO, "socketio client stopping"); event!(Level::INFO, "socketio client stopping");
match socket.disconnect().await { match socket.disconnect().await {

View File

@ -7,7 +7,7 @@ use colored::Colorize;
use ed25519_dalek::{Signature, Signer, SigningKey}; use ed25519_dalek::{Signature, Signer, SigningKey};
use rust_socketio::asynchronous::Client; use rust_socketio::asynchronous::Client;
use rust_socketio::Payload; use rust_socketio::Payload;
use serde_json::{Value, json}; use serde_json::{json, Value};
use tracing::{debug, event, span, warn, Level}; use tracing::{debug, event, span, warn, Level};
/// "安全" 的 发送一条消息 /// "安全" 的 发送一条消息
@ -129,10 +129,8 @@ pub async fn send_room_sign_in(client: &Client, room_id: RoomId) -> bool {
/// 向某个群/私聊的某个人发送戳一戳 /// 向某个群/私聊的某个人发送戳一戳
pub async fn send_poke(client: &Client, room_id: RoomId, target: UserId) -> bool { pub async fn send_poke(client: &Client, room_id: RoomId, target: UserId) -> bool {
let data = json!([room_id, target]); let data = vec![json!(room_id), json!(target)];
match client.emit( match client.emit("sendGroupPoke", data).await {
"sendGroupPoke", data
).await {
Ok(_) => { Ok(_) => {
event!(Level::INFO, "已向 {} 的 {} 发送戳一戳", room_id, target); event!(Level::INFO, "已向 {} 的 {} 发送戳一戳", room_id, target);
true true

View File

@ -1,15 +1,15 @@
use std::path::PathBuf;
use colored::Colorize; use colored::Colorize;
use rust_socketio::asynchronous::Client; use rust_socketio::asynchronous::Client;
use rust_socketio::{Event, Payload}; use rust_socketio::{Event, Payload};
use serde_json::json;
use tracing::{event, info, span, warn, Level}; use tracing::{event, info, span, warn, Level};
use crate::data_struct::ica::all_rooms::Room; use crate::data_struct::ica::all_rooms::Room;
use crate::data_struct::ica::messages::{Message, MessageTrait, NewMessage}; use crate::data_struct::ica::messages::{Message, MessageTrait, NewMessage};
use crate::data_struct::ica::online_data::OnlineData; use crate::data_struct::ica::online_data::OnlineData;
use crate::data_struct::ica::RoomId;
use crate::ica::client::send_message; use crate::ica::client::send_message;
use crate::{client_id, help_msg, py, start_up_time, version_str, MainStatus, VERSION}; use crate::{client_id, help_msg, py, version_str, MainStatus, VERSION};
/// 获取在线数据 /// 获取在线数据
pub async fn get_online_data(payload: Payload, _client: Client) { pub async fn get_online_data(payload: Payload, _client: Client) {
@ -32,7 +32,7 @@ pub async fn add_message(payload: Payload, client: Client) {
return; 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() { if !message.is_from_self() && !message.is_reply() {
@ -72,8 +72,7 @@ pub async fn add_message(payload: Payload, client: Client) {
if message.content().starts_with(&format!("/bot-enable-{}", client_id)) { if message.content().starts_with(&format!("/bot-enable-{}", client_id)) {
// 尝试获取后面的信息 // 尝试获取后面的信息
if let Some((_, name)) = message.content().split_once(" ") { if let Some((_, name)) = message.content().split_once(" ") {
let path_name = PathBuf::from(name); match py::PyStatus::get().get_status(name) {
match py::PyStatus::get().get_status(&path_name) {
None => { None => {
let reply = message.reply_with("未找到插件"); let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await; send_message(&client, &reply).await;
@ -83,7 +82,7 @@ pub async fn add_message(payload: Payload, client: Client) {
send_message(&client, &reply).await; send_message(&client, &reply).await;
} }
Some(false) => { Some(false) => {
py::PyStatus::get_mut().set_status(&path_name, true); py::PyStatus::get_mut().set_status(name, true);
let reply = message.reply_with("启用插件完成"); let reply = message.reply_with("启用插件完成");
send_message(&client, &reply).await; send_message(&client, &reply).await;
} }
@ -92,8 +91,7 @@ pub async fn add_message(payload: Payload, client: Client) {
} else if message.content().starts_with(&format!("/bot-disable-{}", client_id)) } else if message.content().starts_with(&format!("/bot-disable-{}", client_id))
{ {
if let Some((_, name)) = message.content().split_once(" ") { if let Some((_, name)) = message.content().split_once(" ") {
let path_name = PathBuf::from(name); match py::PyStatus::get().get_status(name) {
match py::PyStatus::get().get_status(&path_name) {
None => { None => {
let reply = message.reply_with("未找到插件"); let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await; send_message(&client, &reply).await;
@ -103,12 +101,16 @@ pub async fn add_message(payload: Payload, client: Client) {
send_message(&client, &reply).await; send_message(&client, &reply).await;
} }
Some(true) => { Some(true) => {
py::PyStatus::get_mut().set_status(&path_name, false); py::PyStatus::get_mut().set_status(name, false);
let reply = message.reply_with("禁用插件完成"); let reply = message.reply_with("禁用插件完成");
send_message(&client, &reply).await; 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;
} }
} }
} }
@ -173,6 +175,18 @@ pub async fn failed_message(payload: Payload, _client: Client) {
} }
} }
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) { pub async fn any_event(event: Event, payload: Payload, _client: Client) {
let handled = vec![ let handled = vec![
@ -193,6 +207,7 @@ pub async fn any_event(event: Event, payload: Payload, _client: Client) {
"handleRequest", // 处理验证消息 (加入请求之类的) "handleRequest", // 处理验证消息 (加入请求之类的)
// 忽略的 // 忽略的
"notify", "notify",
"setShutUp", // 禁言
"syncRead", // 同步已读 "syncRead", // 同步已读
"closeLoading", // 发送消息/加载新聊天 有一个 loading "closeLoading", // 发送消息/加载新聊天 有一个 loading
"renewMessage", // 我也不确定到底是啥事件 "renewMessage", // 我也不确定到底是啥事件

View File

@ -29,8 +29,8 @@ pub type MainStatus = status::BotStatus;
pub type StopGetter = tokio::sync::oneshot::Receiver<()>; pub type StopGetter = tokio::sync::oneshot::Receiver<()>;
pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const ICA_VERSION: &str = "1.6.5"; pub const ICA_VERSION: &str = "1.6.7";
pub const TAILCHAT_VERSION: &str = "1.2.4"; pub const TAILCHAT_VERSION: &str = "1.2.6";
const HELP_MSG: &str = r#"/bot-rs const HELP_MSG: &str = r#"/bot-rs
rust rust
@ -46,7 +46,9 @@ const HELP_MSG: &str = r#"/bot-rs
by shenjackyuanjie"#; 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(); static STARTUP_TIME: OnceLock<SystemTime> = OnceLock::new();
@ -77,7 +79,7 @@ pub fn version_str() -> String {
/// 是否为稳定版本 /// 是否为稳定版本
/// 会在 release 的时候设置为 true /// 会在 release 的时候设置为 true
pub const STABLE: bool = false; pub const STABLE: bool = true;
#[macro_export] #[macro_export]
macro_rules! async_callback_with_state { macro_rules! async_callback_with_state {
@ -99,25 +101,32 @@ macro_rules! async_any_callback_with_state {
}}; }};
} }
const CLI_HELP_MSG: &str = r#"{VERSION}
-d
debug
-t
trace
-h
-env <env>
-c <config_file_path>
"#;
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let start_up_time = SystemTime::now(); let start_up_time = SystemTime::now();
STARTUP_TIME.set(start_up_time).expect("WTF, why did you panic?"); STARTUP_TIME.set(start_up_time).expect("WTF, why did you panic?");
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_name("shenbot-rs")
.worker_threads(4)
.build()
.unwrap()
.block_on(inner_main())
}
async fn inner_main() -> anyhow::Result<()> {
// -d -> debug // -d -> debug
// none -> info // 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 level = {
let args = std::env::args();
let args = args.collect::<Vec<String>>();
if args.contains(&"-d".to_string()) { if args.contains(&"-d".to_string()) {
Level::DEBUG Level::DEBUG
} else if args.contains(&"-t".to_string()) { } else if args.contains(&"-t".to_string()) {
@ -128,6 +137,20 @@ async fn inner_main() -> anyhow::Result<()> {
}; };
tracing_subscriber::fmt().with_max_level(level).init(); tracing_subscriber::fmt().with_max_level(level).init();
let _ = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_name("shenbot-rs")
.worker_threads(10)
.build()
.unwrap()
.block_on(inner_main());
event!(Level::INFO, "shenbot-rs v{} exiting", VERSION);
Ok(())
}
async fn inner_main() -> anyhow::Result<()> {
let span = span!(Level::INFO, "Shenbot Main"); let span = span!(Level::INFO, "Shenbot Main");
let _enter = span.enter(); let _enter = span.enter();
@ -149,7 +172,6 @@ async fn inner_main() -> anyhow::Result<()> {
let (ica_send, ica_recv) = tokio::sync::oneshot::channel::<()>(); let (ica_send, ica_recv) = tokio::sync::oneshot::channel::<()>();
if bot_config.check_ica() { if bot_config.check_ica() {
event!(Level::INFO, "启动 ica");
let config = bot_config.ica(); let config = bot_config.ica();
tokio::spawn(async move { tokio::spawn(async move {
ica::start_ica(&config, ica_recv).await.unwrap(); ica::start_ica(&config, ica_recv).await.unwrap();
@ -170,18 +192,19 @@ async fn inner_main() -> anyhow::Result<()> {
event!(Level::INFO, "未启用 Tailchat"); event!(Level::INFO, "未启用 Tailchat");
} }
tokio::time::sleep(Duration::from_secs(2)).await; tokio::time::sleep(Duration::from_secs(1)).await;
// 等待一个输入 // 等待一个输入
event!(Level::INFO, "Press any key to exit"); event!(Level::INFO, "Press ctrl+c to exit, second ctrl+c to force exit");
let mut input = String::new(); tokio::signal::ctrl_c().await.ok();
std::io::stdin().read_line(&mut input).unwrap();
ica_send.send(()).ok(); ica_send.send(()).ok();
tailchat_send.send(()).ok(); tailchat_send.send(()).ok();
event!(Level::INFO, "Disconnected"); event!(Level::INFO, "Disconnected");
py::post_py()?; py::post_py().await?;
event!(Level::INFO, "Shenbot-rs exiting");
Ok(()) Ok(())
} }

View File

@ -1,7 +1,9 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::LazyLock;
use pyo3::prelude::*; use pyo3::prelude::*;
use rust_socketio::asynchronous::Client; use rust_socketio::asynchronous::Client;
use tokio::sync::Mutex;
use tracing::{event, info, warn, Level}; use tracing::{event, info, warn, Level};
use crate::data_struct::{ica, tailchat}; use crate::data_struct::{ica, tailchat};
@ -9,6 +11,74 @@ use crate::error::PyPluginError;
use crate::py::{class, PyPlugin, PyStatus}; use crate::py::{class, PyPlugin, PyStatus};
use crate::MainStatus; use crate::MainStatus;
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>( pub fn get_func<'py>(
py_module: &Bound<'py, PyAny>, py_module: &Bound<'py, PyAny>,
name: &'py str, name: &'py str,
@ -154,8 +224,8 @@ pub async fn ica_new_message_py(message: &ica::messages::NewMessage, client: &Cl
let msg = class::ica::NewMessagePy::new(message); let msg = class::ica::NewMessagePy::new(message);
let client = class::ica::IcaClientPy::new(client); let client = class::ica::IcaClientPy::new(client);
let args = (msg, client); let args = (msg, client);
// 甚至实际上压根不需要await这个spawn, 直接让他自己跑就好了(离谱) let task = call_py_func!(args, plugin, path, ICA_NEW_MESSAGE_FUNC, client);
call_py_func!(args, plugin, path, ICA_NEW_MESSAGE_FUNC, client); PY_TASKS.lock().await.push_ica_new_message(task);
} }
} }
@ -167,7 +237,8 @@ pub async fn ica_delete_message_py(msg_id: ica::MessageId, client: &Client) {
let msg_id = msg_id.clone(); let msg_id = msg_id.clone();
let client = class::ica::IcaClientPy::new(client); let client = class::ica::IcaClientPy::new(client);
let args = (msg_id.clone(), 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, ICA_DELETE_MESSAGE_FUNC, client);
PY_TASKS.lock().await.push_ica_delete_message(task);
} }
} }
@ -182,6 +253,7 @@ pub async fn tailchat_new_message_py(
let msg = class::tailchat::TailchatReceiveMessagePy::from_recive_message(message); let msg = class::tailchat::TailchatReceiveMessagePy::from_recive_message(message);
let client = class::tailchat::TailchatClientPy::new(client); let client = class::tailchat::TailchatClientPy::new(client);
let args = (msg, client); let args = (msg, client);
call_py_func!(args, plugin, path, TAILCHAT_NEW_MESSAGE_FUNC, client); let task = call_py_func!(args, plugin, path, TAILCHAT_NEW_MESSAGE_FUNC, client);
PY_TASKS.lock().await.push_tailchat_new_message(task);
} }
} }

View File

@ -10,6 +10,7 @@ use crate::data_struct::ica::messages::{
}; };
use crate::data_struct::ica::{MessageId, RoomId, RoomIdTrait, UserId}; 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::ica::client::{delete_message, send_message, send_poke, send_room_sign_in};
use crate::py::PyStatus;
use crate::MainStatus; use crate::MainStatus;
#[pyclass] #[pyclass]
@ -145,6 +146,10 @@ impl SendMessagePy {
pub fn get_content(&self) -> String { self.msg.content.clone() } pub fn get_content(&self) -> String { self.msg.content.clone() }
#[setter] #[setter]
pub fn set_content(&mut self, content: String) { self.msg.content = content; } 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) { pub fn set_img(&mut self, file: Vec<u8>, file_type: String, as_sticker: bool) {
self.msg.set_img(&file, &file_type, as_sticker); self.msg.set_img(&file, &file_type, as_sticker);
@ -250,6 +255,38 @@ impl IcaClientPy {
#[getter] #[getter]
pub fn get_startup_time(&self) -> SystemTime { crate::start_up_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) { pub fn debug(&self, content: String) {
event!(Level::DEBUG, "{}", content); event!(Level::DEBUG, "{}", content);
} }

View File

@ -3,10 +3,12 @@ use std::time::SystemTime;
use pyo3::prelude::*; use pyo3::prelude::*;
use rust_socketio::asynchronous::Client; use rust_socketio::asynchronous::Client;
use tokio::runtime::Runtime;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use crate::data_struct::tailchat::messages::{ReceiveMessage, SendingFile, SendingMessage}; use crate::data_struct::tailchat::messages::{ReceiveMessage, SendingFile, SendingMessage};
use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId}; use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};
use crate::py::PyStatus;
use crate::tailchat::client::send_message; use crate::tailchat::client::send_message;
#[pyclass] #[pyclass]
@ -73,6 +75,38 @@ impl TailchatClientPy {
#[getter] #[getter]
pub fn get_startup_time(&self) -> SystemTime { crate::start_up_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))] #[pyo3(signature = (content, converse_id, group_id = None))]
pub fn new_message( pub fn new_message(
&self, &self,

View File

@ -1,12 +1,10 @@
use std::{ use std::{path::Path, str::FromStr};
path::{Path, PathBuf},
str::FromStr,
};
use toml_edit::{value, DocumentMut, Key, Table, TomlError, Value}; use toml_edit::{value, DocumentMut, Key, Table, TomlError, Value};
use tracing::{event, Level}; use tracing::{event, Level};
use crate::py::PyStatus; use crate::py::PyStatus;
use crate::MainStatus;
/// ```toml /// ```toml
/// # 这个文件是由 shenbot 自动生成的, 请 **谨慎** 修改 /// # 这个文件是由 shenbot 自动生成的, 请 **谨慎** 修改
@ -31,14 +29,38 @@ pub const DEFAULT_CONFIG: &str = r#"
impl PluginConfigFile { impl PluginConfigFile {
pub fn from_str(data: &str) -> Result<Self, TomlError> { 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 }) 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> { pub fn from_config_path(path: &Path) -> anyhow::Result<Self> {
let config_path = path.join(CONFIG_FILE_NAME); let config_path = path.join(CONFIG_FILE_NAME);
if !config_path.exists() { if !config_path.exists() {
event!(Level::INFO, "插件配置文件不存在, 正在创建"); event!(Level::WARN, "插件配置文件不存在, 正在创建");
std::fs::write(&config_path, DEFAULT_CONFIG)?; std::fs::write(&config_path, DEFAULT_CONFIG)?;
Ok(Self::from_str(DEFAULT_CONFIG)?) Ok(Self::from_str(DEFAULT_CONFIG)?)
} else { } else {
@ -47,16 +69,6 @@ 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> { fn get_table(&self) -> Option<&Table> {
self.data.get(CONFIG_KEY).and_then(|item| item.as_table()) self.data.get(CONFIG_KEY).and_then(|item| item.as_table())
} }
@ -67,11 +79,10 @@ impl PluginConfigFile {
/// 获取插件状态 /// 获取插件状态
/// 默认为 true /// 默认为 true
pub fn get_status(&self, path: &Path) -> bool { pub fn get_status(&self, plugin_id: &str) -> bool {
let path_str = path.to_str().unwrap();
if let Some(item) = self.data.get(CONFIG_KEY) { if let Some(item) = self.data.get(CONFIG_KEY) {
if let Some(table) = item.as_table() { 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() { if let Some(bool) = item.as_bool() {
return bool; return bool;
} }
@ -100,7 +111,6 @@ impl PluginConfigFile {
/// 设置插件状态 /// 设置插件状态
pub fn set_status(&mut self, path: &Path, status: bool) { pub fn set_status(&mut self, path: &Path, status: bool) {
self.verify_and_init();
let path_str = path.to_str().unwrap(); let path_str = path.to_str().unwrap();
let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap(); let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap();
if table.contains_key(path_str) { if table.contains_key(path_str) {
@ -115,27 +125,47 @@ impl PluginConfigFile {
} }
} }
pub fn sync_status_from_config(&mut self) { /// 从默认文件读取状态
///
/// 返回是否成功
pub fn read_status_from_default(&mut self) -> bool {
if !self.reload_from_default() {
return false;
}
event!(Level::INFO, "同步插件状态");
let plugins = PyStatus::get_mut(); let plugins = PyStatus::get_mut();
self.verify_and_init();
plugins.files.iter_mut().for_each(|(path, status)| { plugins.files.iter_mut().for_each(|(path, status)| {
let config_status = self.get_status(path); let plugin_id = status.get_id();
event!(Level::INFO, "插件状态: {:?} {} -> {}", path, status.enabled, config_status); let config_status = self.get_status(&plugin_id);
event!(
Level::INFO,
"插件状态: {}({:?}) {} -> {}",
status.get_id(),
path,
status.enabled,
config_status
);
status.enabled = config_status; status.enabled = config_status;
}); });
true
} }
pub fn sync_status_to_config(&mut self) { pub fn sync_status_to_config(&mut self) {
let plugins = PyStatus::get(); let plugins = PyStatus::get();
self.verify_and_init();
let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap(); let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap();
table.clear(); table.clear();
plugins.files.iter().for_each(|(path, status)| { plugins.files.iter().for_each(|(_, status)| {
table.insert(path.to_str().unwrap(), value(status.enabled)); 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); let config_path = path.join(CONFIG_FILE_NAME);
std::fs::write(config_path, self.data.to_string())?; std::fs::write(config_path, self.data.to_string())?;
Ok(()) Ok(())

View File

@ -2,7 +2,8 @@ pub mod call;
pub mod class; pub mod class;
pub mod config; pub mod config;
use std::ffi::CString; use std::ffi::{CString, OsStr};
use std::fmt::Display;
use std::path::Path; use std::path::Path;
use std::sync::OnceLock; use std::sync::OnceLock;
use std::time::SystemTime; use std::time::SystemTime;
@ -11,7 +12,7 @@ use std::{collections::HashMap, path::PathBuf};
use colored::Colorize; use colored::Colorize;
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::PyTuple; use pyo3::types::PyTuple;
use tracing::{event, info, span, warn, Level}; use tracing::{event, span, warn, Level};
use crate::MainStatus; use crate::MainStatus;
@ -29,10 +30,8 @@ static mut PyPluginStatus: OnceLock<PyStatus> = OnceLock::new();
impl PyStatus { impl PyStatus {
pub fn init() { pub fn init() {
let plugin_path = MainStatus::global_config().py().plugin_path.clone(); let config =
let mut config = config::PluginConfigFile::default_init().expect("初始化 Python 插件配置文件失败");
config::PluginConfigFile::from_config_path(&PathBuf::from(plugin_path)).unwrap();
config.verify_and_init();
let status = PyStatus { let status = PyStatus {
files: HashMap::new(), files: HashMap::new(),
config, config,
@ -44,37 +43,57 @@ impl PyStatus {
pub fn get_mut() -> &'static mut PyStatus { unsafe { PyPluginStatus.get_mut().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 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 delete_file(&mut self, path: &PathBuf) -> Option<PyPlugin> { self.files.remove(path) } pub fn delete_file(&mut self, path: &PathBuf) -> Option<PyPlugin> { self.files.remove(path) }
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 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 verify_file(&self, path: &PathBuf) -> bool { pub fn verify_file(&self, path: &PathBuf) -> bool {
self.files.get(path).is_some_and(|plugin| plugin.verifiy()) self.files.get(path).is_some_and(|plugin| plugin.verifiy())
} }
/// 获取某个插件的状态
/// 以 config 优先
pub fn get_status(&self, path: &PathBuf) -> Option<bool> {
self.files.get(path).map(|plugin| plugin.enabled)
}
pub fn sync_status(&mut self) { self.config.sync_status_from_config(); }
pub fn set_status(&mut self, path: &Path, status: bool) {
self.config.set_status(path, status);
if let Some(plugin) = self.files.get_mut(path) {
plugin.enabled = status;
}
}
pub fn display() -> String { pub fn display() -> String {
format!( format!(
"Python 插件 {{ {} }}", "Python 插件 {{ {} }}",
Self::get() Self::get()
.files .files
.iter() .values()
.map(|(k, v)| format!("{:?}-{}", k, v.enabled)) .map(|v| v.to_string())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n") .join("\n")
) )
@ -126,15 +145,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); let raw_file = load_py_file(&self.file_path);
match raw_file { match raw_file {
Ok(raw_file) => match Self::try_from(raw_file) { Ok(raw_file) => match Self::try_from(raw_file) {
Ok(plugin) => { Ok(plugin) => {
self.py_module = plugin.py_module; self.py_module = plugin.py_module;
self.changed_time = plugin.changed_time; self.changed_time = plugin.changed_time;
self.enabled = PyStatus::get().config.get_status(self.file_path.as_path()); self.enabled = PyStatus::get().config.get_status(&self.get_id());
event!(Level::INFO, "更新 Python 插件文件 {:?} 完成", self.file_path); event!(Level::INFO, "更新 Python 插件文件 {:?} 完成", self.file_path);
true
} }
Err(e) => { Err(e) => {
warn!( warn!(
@ -143,10 +163,12 @@ impl PyPlugin {
e, e,
get_py_err_traceback(&e) get_py_err_traceback(&e)
); );
false
} }
}, },
Err(e) => { Err(e) => {
warn!("更新插件 {:?}: {:?} 失败", self.file_path, e); warn!("更新插件 {:?}: {:?} 失败", self.file_path, e);
false
} }
} }
} }
@ -164,6 +186,14 @@ 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"; pub const CONFIG_DATA_NAME: &str = "CONFIG_DATA";
@ -191,17 +221,26 @@ impl TryFrom<RawPyPlugin> for PyPlugin {
let mut base_path: PathBuf = PathBuf::from(base_path); let mut base_path: PathBuf = PathBuf::from(base_path);
if !base_path.exists() { if !base_path.exists() {
warn!("python 插件路径不存在, 创建: {:?}", base_path); event!(Level::WARN, "python 插件路径不存在, 创建: {:?}", base_path);
std::fs::create_dir_all(&base_path)?; std::fs::create_dir_all(&base_path)?;
} }
base_path.push(&config); base_path.push(&config);
let config_value = if base_path.exists() { let config_value = if base_path.exists() {
info!("加载 {:?} 的配置文件 {:?} 中", path, base_path); event!(
Level::INFO,
"加载 {:?} 的配置文件 {:?} 中",
path,
base_path
);
let content = std::fs::read_to_string(&base_path)?; let content = std::fs::read_to_string(&base_path)?;
toml::from_str(&content) toml::from_str(&content)
} else { } else {
warn!("配置文件 {:?} 不存在, 创建默认配置", base_path); event!(
Level::WARN,
"配置文件 {:?} 不存在, 创建默认配置",
base_path
);
// 写入默认配置 // 写入默认配置
std::fs::write(base_path, &default)?; std::fs::write(base_path, &default)?;
toml::from_str(&default) toml::from_str(&default)
@ -211,7 +250,7 @@ impl TryFrom<RawPyPlugin> for PyPlugin {
let py_config = let py_config =
Bound::new(py, class::ConfigDataPy::new(config)); Bound::new(py, class::ConfigDataPy::new(config));
if let Err(e) = py_config { if let Err(e) = py_config {
warn!("添加配置文件信息失败: {:?}", e); event!(Level::WARN, "添加配置文件信息失败: {:?}", e);
return Err(e); return Err(e);
} }
let py_config = py_config.unwrap(); let py_config = py_config.unwrap();
@ -304,6 +343,15 @@ impl TryFrom<RawPyPlugin> for PyPlugin {
} }
} }
/// 插件路径转换为 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) { pub fn load_py_plugins(path: &PathBuf) {
let plugins = PyStatus::get_mut(); let plugins = PyStatus::get_mut();
if path.exists() { if path.exists() {
@ -330,7 +378,8 @@ pub fn load_py_plugins(path: &PathBuf) {
} else { } else {
event!(Level::WARN, "插件加载目录不存在: {:?}", path); event!(Level::WARN, "插件加载目录不存在: {:?}", path);
} }
plugins.config.sync_status_from_config(); plugins.config.read_status_from_default();
plugins.config.sync_status_to_config();
event!( event!(
Level::INFO, Level::INFO,
"python 插件目录: {:?} 加载完成, 加载到 {} 个插件", "python 插件目录: {:?} 加载完成, 加载到 {} 个插件",
@ -365,29 +414,117 @@ pub fn load_py_file(path: &PathBuf) -> std::io::Result<RawPyPlugin> {
Ok((path.clone(), changed_time, content)) Ok((path.clone(), changed_time, content))
} }
fn init_py_with_env_path(path: &str) {
unsafe {
#[cfg(target_os = "linux")]
use std::os::unix::ffi::OsStrExt;
#[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);
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 侧初始化 /// Python 侧初始化
pub fn init_py() { pub fn init_py() {
// 从 全局配置中获取 python 插件路径 // 从 全局配置中获取 python 插件路径
let span = span!(Level::INFO, "初始化 python 及其插件.ing"); let span = span!(Level::INFO, "py init");
let _enter = span.enter(); let _enter = span.enter();
let plugin_path = MainStatus::global_config().py().plugin_path; let plugin_path = MainStatus::global_config().py().plugin_path;
event!(Level::INFO, "正在初始化 python"); let cli_args = std::env::args().collect::<Vec<String>>();
pyo3::prepare_freethreaded_python(); 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(); PyStatus::init();
let plugin_path = PathBuf::from(plugin_path); let plugin_path = PathBuf::from(plugin_path);
load_py_plugins(&plugin_path); load_py_plugins(&plugin_path);
event!(Level::DEBUG, "python 插件列表: {}", PyStatus::display()); event!(Level::DEBUG, "python 插件列表: {}", PyStatus::display());
info!("python inited") event!(Level::INFO, "python 初始化完成")
} }
pub fn post_py() -> anyhow::Result<()> { pub async fn post_py() -> anyhow::Result<()> {
PyStatus::get_mut().config.sync_status_to_config(); let status = PyStatus::get_mut();
PyStatus::get() status.config.sync_status_to_config();
.config status.config.write_to_default()?;
.write_to_file(&PathBuf::from(MainStatus::global_config().py().config_path))?;
stop_tasks().await;
unsafe {
if !pyo3::ffi::Py_FinalizeEx() == 0 {
event!(Level::ERROR, "Python 退出失败 (不过应该无所谓)");
}
}
Ok(()) Ok(())
} }
async fn stop_tasks() {
if call::PY_TASKS.lock().await.is_empty() {
return;
}
let waiter = tokio::spawn(async {
call::PY_TASKS.lock().await.join_all().await;
});
tokio::select! {
_ = waiter => {
event!(Level::INFO, "Python 任务完成");
}
_ = tokio::signal::ctrl_c() => {
call::PY_TASKS.lock().await.cancel_all();
event!(Level::INFO, "Python 任务被中断");
}
}
}

View File

@ -1,4 +1,3 @@
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use colored::Colorize; use colored::Colorize;
@ -11,7 +10,7 @@ use crate::data_struct::tailchat::status::{BotStatus, UpdateDMConverse};
use crate::py::call::tailchat_new_message_py; use crate::py::call::tailchat_new_message_py;
use crate::py::PyStatus; use crate::py::PyStatus;
use crate::tailchat::client::{emit_join_room, send_message}; use crate::tailchat::client::{emit_join_room, send_message};
use crate::{client_id, help_msg, start_up_time, version_str, MainStatus, VERSION}; use crate::{client_id, help_msg, version_str, MainStatus, VERSION};
/// 所有 /// 所有
pub async fn any_event(event: Event, payload: Payload, _client: Client, _status: Arc<BotStatus>) { pub async fn any_event(event: Event, payload: Payload, _client: Client, _status: Arc<BotStatus>) {
@ -98,16 +97,6 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
let reply = message.reply_with(&help_msg()); let reply = message.reply_with(&help_msg());
send_message(&client, &reply).await; 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().tailchat().admin_list.contains(&message.sender_id) { if MainStatus::global_config().tailchat().admin_list.contains(&message.sender_id) {
// admin 区 // admin 区
let client_id = client_id(); let client_id = client_id();
@ -115,8 +104,7 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
// 先判定是否为 admin // 先判定是否为 admin
// 尝试获取后面的信息 // 尝试获取后面的信息
if let Some((_, name)) = message.content.split_once(" ") { if let Some((_, name)) = message.content.split_once(" ") {
let path_name = PathBuf::from(name); match PyStatus::get().get_status(name) {
match PyStatus::get().get_status(&path_name) {
None => { None => {
let reply = message.reply_with("未找到插件"); let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await; send_message(&client, &reply).await;
@ -126,7 +114,7 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
send_message(&client, &reply).await; send_message(&client, &reply).await;
} }
Some(false) => { Some(false) => {
PyStatus::get_mut().set_status(&path_name, true); PyStatus::get_mut().set_status(name, true);
let reply = message.reply_with("启用插件完成"); let reply = message.reply_with("启用插件完成");
send_message(&client, &reply).await; send_message(&client, &reply).await;
} }
@ -134,8 +122,7 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
} }
} else if message.content.starts_with(&format!("/bot-disable-{}", client_id)) { } else if message.content.starts_with(&format!("/bot-disable-{}", client_id)) {
if let Some((_, name)) = message.content.split_once(" ") { if let Some((_, name)) = message.content.split_once(" ") {
let path_name = PathBuf::from(name); match PyStatus::get().get_status(name) {
match PyStatus::get().get_status(&path_name) {
None => { None => {
let reply = message.reply_with("未找到插件"); let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await; send_message(&client, &reply).await;
@ -145,7 +132,7 @@ pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus
send_message(&client, &reply).await; send_message(&client, &reply).await;
} }
Some(true) => { Some(true) => {
PyStatus::get_mut().set_status(&path_name, false); PyStatus::get_mut().set_status(name, false);
let reply = message.reply_with("禁用插件完成"); let reply = message.reply_with("禁用插件完成");
send_message(&client, &reply).await; send_message(&client, &reply).await;
} }

67
news.md
View File

@ -1,10 +1,51 @@
# 更新日志 # 更新日志
## 0.8.2
- ica 兼容版本号更新到 `2.12.28`
- 现在支持通过读取环境变量里的 `VIRTUAL_ENV` 来获取虚拟环境路径
- 用于在虚拟环境中运行插件
- 添加了 `-h` 参数
- 用于展示帮助信息
- 添加了 `-env` 参数
- 用于指定 python 插件的虚拟环境路径
- 会覆盖环境变量中的 `VIRTUAL_ENV`
- 现在加入了默认的配置文件路径 `./config.toml`
- 现在会记录所有的 python 运行中 task 了
- 也会在退出的时候等待所有的 task 结束
- 二次 ctrl-c 会立即退出
- 改进了一下 ica 的新消息显示
- 添加了 ica 链接用时的显示
### 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 ## 0.8.0
- ica 兼容版本号更新到 `2.12.24` - ica 兼容版本号更新到 ~~`2.12.24`~~ `2.12.26`
- 从 `py::PyStatus` 开始进行一个 `static mut` -> `static mut OnceLock` 的改造 - 从 `py::PyStatus` 开始进行一个 `static mut` -> `static mut OnceLock` 的改造
- 用于看着更舒服(逃) - 用于看着更舒服(逃)
- 部分重构了一下 读取插件启用状态 的配置文件的代码
- 现在 `/bot-help` 会直接输出实际的 client id, 而不是给你一个默认的 `<client-id>`
### ica 1.6.5 ### ica 1.6.5
@ -17,6 +58,29 @@
- 或者指定好友 - 或者指定好友
- 目前还是有点问题 - 目前还是有点问题
- socketio event: `sendGroupPoke` - 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 ## 0.7.4
@ -136,6 +200,7 @@
好耶! 好耶!
[!note] [!note]
```text ```text
notice_room = [] notice_room = []
notice_start = true notice_start = true