diff --git a/ica-rs/ica_typing.py b/ica-rs/ica_typing.py index a6f75a5..2cc4ed0 100644 --- a/ica-rs/ica_typing.py +++ b/ica-rs/ica_typing.py @@ -105,6 +105,10 @@ class IcaClient: """ def send_message(self, message: SendMessage) -> bool: ... + def send_and_warn(self, message: SendMessage) -> bool: + """发送消息, 并在日志中输出警告信息""" + self.warn(message.content) + return self.send_message(message) def delete_message(self, message: DeleteMessage) -> bool: ... diff --git a/ica-rs/plugins/bmcl.py b/ica-rs/plugins/bmcl.py index 081f880..d131c73 100644 --- a/ica-rs/plugins/bmcl.py +++ b/ica-rs/plugins/bmcl.py @@ -1,6 +1,7 @@ import re import time import requests +import traceback from typing import TYPE_CHECKING, TypeVar, Optional, Tuple @@ -56,19 +57,27 @@ def format_hit_count(count: int) -> str: def wrap_request(url: str, msg: NewMessage, client: IcaClient) -> Optional[dict]: + # if CONFIG_DATA try: - response = requests.get(url) - except requests.exceptions.RequestException as e: - client.warn( - f"数据请求失败, 请检查网络\n{e}" - ) - reply = msg.reply_with(f"数据请求失败, 请检查网络\n{e}") - client.send_message(reply) + cookie = CONFIG_DATA["cookie"] + if cookie == "填写你的 cookie" or cookie is None: + response = requests.get(url) + else: + response = requests.get(url, cookies={"openbmclapi-jwt": cookie}) + except requests.exceptions.RequestException: + warn_msg = f"数据请求失败, 请检查网络\n{traceback.format_exc()}" + reply = msg.reply_with(warn_msg) + client.send_and_warn(reply) + return None + except Exception as _: + warn_msg = f"数据请求中发生未知错误, 请呼叫 shenjack\n{traceback.format_exc()}" + reply = msg.reply_with(warn_msg) + client.send_and_warn(reply) return None if not response.status_code == 200 or response.reason != "OK": - client.warn( - f"数据请求失败, 请检查网络\n{response.status}" - ) + warn_msg = f"请求失败, 请检查网络\n{response.status_code} {response.reason}" + reply = msg.reply_with(warn_msg) + client.send_and_warn(reply) return None return response.json() @@ -209,7 +218,6 @@ help = """/bmcl -> dashboard def on_message(msg: NewMessage, client: IcaClient) -> None: - print(CONFIG_DATA) if not (msg.is_from_self or msg.is_reply): if msg.content.startswith("/bmcl"): if msg.content == "/bmcl": @@ -236,5 +244,5 @@ def on_message(msg: NewMessage, client: IcaClient) -> None: def on_config() -> Tuple[str, str]: return ( "bmcl.toml", - "" + """cookie = \"填写你的 cookie\"""" ) diff --git a/ica-rs/src/config.rs b/ica-rs/src/config.rs index 37d2e16..e580791 100644 --- a/ica-rs/src/config.rs +++ b/ica-rs/src/config.rs @@ -23,6 +23,8 @@ pub struct IcaConfig { pub filter_list: Vec, /// Python 插件路径 pub py_plugin_path: Option, + /// Python 配置文件路径 + pub py_config_path: Option, } impl IcaConfig { diff --git a/ica-rs/src/py/call.rs b/ica-rs/src/py/call.rs index 549f089..50d135a 100644 --- a/ica-rs/src/py/call.rs +++ b/ica-rs/src/py/call.rs @@ -2,11 +2,12 @@ use std::path::PathBuf; use pyo3::prelude::*; use rust_socketio::asynchronous::Client; -use tracing::{debug, warn}; +use tracing::{debug, info, warn}; +use crate::client::IcalinguaStatus; use crate::data_struct::messages::NewMessage; use crate::data_struct::MessageId; -use crate::py::{class, verify_plugins, PyStatus}; +use crate::py::{class, PyPlugin, PyStatus}; pub fn get_func<'py>(py_module: &'py PyAny, path: &PathBuf, name: &'py str) -> Option<&'py PyAny> { // 要处理的情况: @@ -42,20 +43,58 @@ pub fn get_func<'py>(py_module: &'py PyAny, path: &PathBuf, name: &'py str) -> O } } +pub fn verify_plugins() { + let mut need_reload_files: Vec = Vec::new(); + let plugin_path = IcalinguaStatus::get_config().py_plugin_path.as_ref(); + if let None = plugin_path { + warn!("未配置 Python 插件路径"); + return; + } + let plugin_path = plugin_path.unwrap(); + for entry in std::fs::read_dir(&plugin_path).unwrap() { + if let Ok(entry) = entry { + let path = entry.path(); + if let Some(ext) = path.extension() { + if ext == "py" { + if !PyStatus::verify_file(&path) { + need_reload_files.push(path); + } + } + } + } + } + + if need_reload_files.is_empty() { + return; + } + info!("file change list: {:?}", need_reload_files); + for reload_file in need_reload_files { + match PyPlugin::new_from_path(&reload_file) { + Some(plugin) => { + PyStatus::add_file(reload_file.clone(), plugin); + info!("重载 Python 插件: {:?}", reload_file); + } + None => { + warn!("重载 Python 插件: {:?} 失败", reload_file); + } + } + } +} + /// 执行 new message 的 python 插件 pub async fn new_message_py(message: &NewMessage, client: &Client) { // 验证插件是否改变 verify_plugins(); let plugins = PyStatus::get_files(); - for (path, (_, py_module)) in plugins.iter() { + for (path, plugin) in plugins.iter() { let msg = class::NewMessagePy::new(message); let client = class::IcaClientPy::new(client); + let args = (msg, client); // 甚至实际上压根不需要await这个spawn, 直接让他自己跑就好了(离谱) tokio::spawn(async move { Python::with_gil(|py| { - let args = (msg, client); - if let Some(py_func) = get_func(py_module.as_ref(py), &path, "on_message") { + if let Some(py_func) = get_func(plugin.py_module.as_ref(py), &path, "on_message") { if let Err(e) = py_func.call1(args) { warn!("failed to call function: {:?}", e); } @@ -67,14 +106,17 @@ pub async fn new_message_py(message: &NewMessage, client: &Client) { pub async fn delete_message_py(msg_id: MessageId, client: &Client) { verify_plugins(); + let plugins = PyStatus::get_files(); - for (path, (_, py_module)) in plugins.iter() { + for (path, plugin) in plugins.iter() { let msg_id = msg_id.clone(); let client = class::IcaClientPy::new(client); + let args = (msg_id.clone(), client); tokio::spawn(async move { Python::with_gil(|py| { - let args = (msg_id.clone(), client); - if let Some(py_func) = get_func(py_module.as_ref(py), &path, "on_delete_message") { + if let Some(py_func) = + get_func(plugin.py_module.as_ref(py), &path, "on_delete_message") + { if let Err(e) = py_func.call1(args) { warn!("failed to call function: {:?}", e); } diff --git a/ica-rs/src/py/class.rs b/ica-rs/src/py/class.rs index 6cc2c87..59659fa 100644 --- a/ica-rs/src/py/class.rs +++ b/ica-rs/src/py/class.rs @@ -162,6 +162,14 @@ impl IcaClientPy { }) } + pub fn send_and_warn(&self, message: SendMessagePy) -> bool { + warn!(message.msg.content); + tokio::task::block_in_place(|| { + let rt = Runtime::new().unwrap(); + rt.block_on(send_message(&self.client, &message.msg)) + }) + } + pub fn delete_message(&self, message: DeleteMessagePy) -> bool { tokio::task::block_in_place(|| { let rt = Runtime::new().unwrap(); diff --git a/ica-rs/src/py/mod.rs b/ica-rs/src/py/mod.rs index 3e858b4..f57b47c 100644 --- a/ica-rs/src/py/mod.rs +++ b/ica-rs/src/py/mod.rs @@ -5,6 +5,7 @@ use std::time::SystemTime; use std::{collections::HashMap, path::PathBuf}; use pyo3::prelude::*; +use pyo3::types::PyTuple; use tracing::{debug, info, warn}; use crate::client::IcalinguaStatus; @@ -12,11 +13,138 @@ use crate::config::IcaConfig; #[derive(Debug, Clone)] pub struct PyStatus { - pub files: Option, Py)>>, + pub files: Option>, +} + +pub type PyPluginData = HashMap; +pub type RawPyPlugin = (PathBuf, Option, String); + +#[derive(Debug, Clone)] +pub struct PyPlugin { + pub file_path: PathBuf, + pub changed_time: Option, + pub py_module: Py, +} + +impl PyPlugin { + pub fn new_from_path(path: &PathBuf) -> Option { + let raw_file = load_py_file(&path); + match raw_file { + Ok(raw_file) => match Self::try_from(raw_file) { + Ok(plugin) => Some(plugin), + Err(e) => { + warn!("加载 Python 插件文件{:?}: {:?} 失败", path, e); + None + } + }, + Err(e) => { + warn!("加载插件 {:?}: {:?} 失败", path, e); + None + } + } + } + pub fn verifiy(&self) -> bool { + match get_change_time(&self.file_path) { + None => false, + Some(time) => { + if let Some(changed_time) = self.changed_time { + time.eq(&changed_time) + } else { + true + } + } + } + } +} + +impl TryFrom for PyPlugin { + type Error = PyErr; + fn try_from(value: RawPyPlugin) -> Result { + let (path, changed_time, content) = value; + let py_module = py_module_from_code(&content, &path); + if let Err(e) = py_module { + warn!("加载 Python 插件: {:?} 失败", e); + return Err(e); + }; + let py_module = py_module.unwrap(); + Python::with_gil(|py| { + let module = py_module.as_ref(py); + if let Some(config_func) = call::get_func(module, &path, "on_config") { + match config_func.call0() { + Ok(config) => { + if config.is_instance_of::() { + let (config, default) = config.extract::<(String, String)>().unwrap(); + let base_path = + IcalinguaStatus::get_config().py_config_path.as_ref().unwrap(); + + 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) + } else { + warn!("配置文件 {:?} 不存在, 创建默认配置", base_path); + // 写入默认配置 + std::fs::write(base_path, &default)?; + toml::from_str(&default) + }; + match config_value { + Ok(config) => { + let py_config = class::ConfigDataPy::new(config); + let py_config = PyCell::new(py, py_config).unwrap(); + module.setattr("CONFIG_DATA", py_config).unwrap(); + Ok(PyPlugin { + file_path: path, + changed_time, + py_module: module.into_py(py), + }) + } + Err(e) => { + warn!( + "加载 Python 插件 {:?} 的配置文件信息时失败:{:?}", + path, e + ); + Err(PyErr::new::(format!( + "加载 Python 插件 {:?} 的配置文件信息时失败:{:?}", + path, e + ))) + } + } + } else { + warn!( + "加载 Python 插件 {:?} 的配置文件信息时失败:返回的不是 [str, str]", + path + ); + Err(PyErr::new::(format!( + "返回的不是 [str, str]" + ))) + } + } + Err(e) => { + warn!("加载 Python 插件 {:?} 的配置文件信息时失败:{:?}", path, e); + Err(e) + } + } + } else { + Ok(PyPlugin { + file_path: path, + changed_time, + py_module: module.into_py(py), + }) + } + }) + } } impl PyStatus { - pub fn get_files() -> &'static HashMap, Py)> { + pub fn get_files() -> &'static PyPluginData { unsafe { match PYSTATUS.files.as_ref() { Some(files) => files, @@ -28,15 +156,15 @@ impl PyStatus { } } - pub fn add_file(path: PathBuf, changed_time: Option, py_module: Py) { + pub fn add_file(path: PathBuf, plugin: PyPlugin) { unsafe { match PYSTATUS.files.as_mut() { Some(files) => { - files.insert(path, (changed_time, py_module)); + files.insert(path, plugin); } None => { - let mut files = HashMap::new(); - files.insert(path, (changed_time, py_module)); + let mut files: PyPluginData = HashMap::new(); + files.insert(path, plugin); PYSTATUS.files = Some(files); } } @@ -47,16 +175,7 @@ impl PyStatus { unsafe { match PYSTATUS.files.as_ref() { Some(files) => match files.get(path) { - Some((changed_time, _)) => { - if let Some(changed_time) = changed_time { - if let Some(new_changed_time) = get_change_time(path) { - if new_changed_time != *changed_time { - return false; - } - } - } - true - } + Some(plugin) => plugin.verifiy(), None => false, }, None => false, @@ -67,7 +186,7 @@ impl PyStatus { pub static mut PYSTATUS: PyStatus = PyStatus { files: None }; -pub fn load_py_plugins(path: &PathBuf) { +pub fn load_py_plugins(path: &PathBuf) -> () { if path.exists() { info!("finding plugins in: {:?}", path); // 搜索所有的 py 文件 和 文件夹单层下面的 py 文件 @@ -77,18 +196,12 @@ pub fn load_py_plugins(path: &PathBuf) { } Ok(dir) => { for entry in dir { - if let Ok(entry) = entry { - let path = entry.path(); - if let Some(ext) = path.extension() { - if ext == "py" { - match load_py_module(&path) { - Some((changed_time, py_module)) => { - PyStatus::add_file(path.clone(), changed_time, py_module); - } - None => { - warn!("加载 Python 插件: {:?} 失败", path); - } - } + let entry = entry.unwrap(); + let path = entry.path(); + if let Some(ext) = path.extension() { + if ext == "py" { + if let Some(plugin) = PyPlugin::new_from_path(&path) { + PyStatus::add_file(path, plugin); } } } @@ -105,49 +218,17 @@ pub fn load_py_plugins(path: &PathBuf) { ); } -pub fn verify_plugins() { - let mut need_reload_files: Vec = Vec::new(); - let plugin_path = IcalinguaStatus::get_config().py_plugin_path.as_ref().unwrap().to_owned(); - for entry in std::fs::read_dir(&plugin_path).unwrap() { - if let Ok(entry) = entry { - let path = entry.path(); - if let Some(ext) = path.extension() { - if ext == "py" { - if !PyStatus::verify_file(&path) { - need_reload_files.push(path); - } - } - } - } - } - - if need_reload_files.is_empty() { - return; - } - info!("file change list: {:?}", need_reload_files); - for reload_file in need_reload_files { - match load_py_module(&reload_file) { - Some((changed_time, py_module)) => { - PyStatus::add_file(reload_file.clone(), changed_time, py_module); - } - None => { - warn!("重载 Python 插件: {:?} 失败", reload_file); - } - } - } -} - pub fn get_change_time(path: &PathBuf) -> Option { path.metadata().ok()?.modified().ok() } -pub fn py_module_from_code(content: &str, path: &str) -> PyResult> { +pub fn py_module_from_code(content: &str, path: &PathBuf) -> PyResult> { Python::with_gil(|py| -> PyResult> { let module: PyResult> = PyModule::from_code( py, &content, - &path, - &path, + &path.to_string_lossy(), + &path.file_name().unwrap().to_string_lossy(), // !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的 ) .map(|module| module.into()); @@ -157,38 +238,10 @@ pub fn py_module_from_code(content: &str, path: &str) -> PyResult> { /// 传入文件路径 /// 返回 hash 和 文件内容 -pub fn load_py_file(path: &PathBuf) -> std::io::Result<(Option, String)> { +pub fn load_py_file(path: &PathBuf) -> std::io::Result { let changed_time = get_change_time(&path); let content = std::fs::read_to_string(path)?; - Ok((changed_time, content)) -} - -pub fn load_py_module(path: &PathBuf) -> Option<(Option, Py)> { - let (changed_time, content) = match load_py_file(&path) { - Ok((changed_time, content)) => (changed_time, content), - Err(e) => { - warn!("failed to load file: {:?} | e: {:?}", path, e); - return None; - } - }; - let py_module: PyResult> = Python::with_gil(|py| -> PyResult> { - let module: PyResult> = PyModule::from_code( - py, - &content, - &path.to_string_lossy(), - &path.to_string_lossy(), - // !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的 - ) - .map(|module| module.into()); - module - }); - match py_module { - Ok(py_module) => Some((changed_time, py_module)), - Err(e) => { - warn!("failed to load file: {:?} | e: {:?}", path, e); - None - } - } + Ok((path.clone(), changed_time, content)) } pub fn init_py(config: &IcaConfig) {