大的来了

This commit is contained in:
shenjack 2024-02-25 18:49:39 +08:00
parent d362e7c155
commit e3708dc41a
Signed by: shenjack
GPG Key ID: 7B1134A979775551
6 changed files with 231 additions and 114 deletions

View File

@ -105,6 +105,10 @@ class IcaClient:
""" """
def send_message(self, message: SendMessage) -> bool: 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: def delete_message(self, message: DeleteMessage) -> bool:
... ...

View File

@ -1,6 +1,7 @@
import re import re
import time import time
import requests import requests
import traceback
from typing import TYPE_CHECKING, TypeVar, Optional, Tuple 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]: def wrap_request(url: str, msg: NewMessage, client: IcaClient) -> Optional[dict]:
# if CONFIG_DATA
try: try:
response = requests.get(url) cookie = CONFIG_DATA["cookie"]
except requests.exceptions.RequestException as e: if cookie == "填写你的 cookie" or cookie is None:
client.warn( response = requests.get(url)
f"数据请求失败, 请检查网络\n{e}" else:
) response = requests.get(url, cookies={"openbmclapi-jwt": cookie})
reply = msg.reply_with(f"数据请求失败, 请检查网络\n{e}") except requests.exceptions.RequestException:
client.send_message(reply) 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 return None
if not response.status_code == 200 or response.reason != "OK": if not response.status_code == 200 or response.reason != "OK":
client.warn( warn_msg = f"请求失败, 请检查网络\n{response.status_code} {response.reason}"
f"数据请求失败, 请检查网络\n{response.status}" reply = msg.reply_with(warn_msg)
) client.send_and_warn(reply)
return None return None
return response.json() return response.json()
@ -209,7 +218,6 @@ help = """/bmcl -> dashboard
def on_message(msg: NewMessage, client: IcaClient) -> None: def on_message(msg: NewMessage, client: IcaClient) -> None:
print(CONFIG_DATA)
if not (msg.is_from_self or msg.is_reply): if not (msg.is_from_self or msg.is_reply):
if msg.content.startswith("/bmcl"): if msg.content.startswith("/bmcl"):
if msg.content == "/bmcl": if msg.content == "/bmcl":
@ -236,5 +244,5 @@ def on_message(msg: NewMessage, client: IcaClient) -> None:
def on_config() -> Tuple[str, str]: def on_config() -> Tuple[str, str]:
return ( return (
"bmcl.toml", "bmcl.toml",
"" """cookie = \"填写你的 cookie\""""
) )

View File

@ -23,6 +23,8 @@ pub struct IcaConfig {
pub filter_list: Vec<i64>, pub filter_list: Vec<i64>,
/// Python 插件路径 /// Python 插件路径
pub py_plugin_path: Option<String>, pub py_plugin_path: Option<String>,
/// Python 配置文件路径
pub py_config_path: Option<String>,
} }
impl IcaConfig { impl IcaConfig {

View File

@ -2,11 +2,12 @@ use std::path::PathBuf;
use pyo3::prelude::*; use pyo3::prelude::*;
use rust_socketio::asynchronous::Client; 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::messages::NewMessage;
use crate::data_struct::MessageId; 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> { 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<PathBuf> = 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 插件 /// 执行 new message 的 python 插件
pub async fn new_message_py(message: &NewMessage, client: &Client) { pub async fn new_message_py(message: &NewMessage, client: &Client) {
// 验证插件是否改变 // 验证插件是否改变
verify_plugins(); verify_plugins();
let plugins = PyStatus::get_files(); 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 msg = class::NewMessagePy::new(message);
let client = class::IcaClientPy::new(client); let client = class::IcaClientPy::new(client);
let args = (msg, client);
// 甚至实际上压根不需要await这个spawn, 直接让他自己跑就好了(离谱) // 甚至实际上压根不需要await这个spawn, 直接让他自己跑就好了(离谱)
tokio::spawn(async move { tokio::spawn(async move {
Python::with_gil(|py| { Python::with_gil(|py| {
let args = (msg, client); if let Some(py_func) = get_func(plugin.py_module.as_ref(py), &path, "on_message") {
if let Some(py_func) = get_func(py_module.as_ref(py), &path, "on_message") {
if let Err(e) = py_func.call1(args) { if let Err(e) = py_func.call1(args) {
warn!("failed to call function<on_message>: {:?}", e); warn!("failed to call function<on_message>: {:?}", 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) { pub async fn delete_message_py(msg_id: MessageId, client: &Client) {
verify_plugins(); verify_plugins();
let plugins = PyStatus::get_files(); 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 msg_id = msg_id.clone();
let client = class::IcaClientPy::new(client); let client = class::IcaClientPy::new(client);
let args = (msg_id.clone(), client);
tokio::spawn(async move { tokio::spawn(async move {
Python::with_gil(|py| { Python::with_gil(|py| {
let args = (msg_id.clone(), client); if let Some(py_func) =
if let Some(py_func) = get_func(py_module.as_ref(py), &path, "on_delete_message") { get_func(plugin.py_module.as_ref(py), &path, "on_delete_message")
{
if let Err(e) = py_func.call1(args) { if let Err(e) = py_func.call1(args) {
warn!("failed to call function<on_delete_message>: {:?}", e); warn!("failed to call function<on_delete_message>: {:?}", e);
} }

View File

@ -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 { pub fn delete_message(&self, message: DeleteMessagePy) -> bool {
tokio::task::block_in_place(|| { tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();

View File

@ -5,6 +5,7 @@ use std::time::SystemTime;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::PyTuple;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use crate::client::IcalinguaStatus; use crate::client::IcalinguaStatus;
@ -12,11 +13,138 @@ use crate::config::IcaConfig;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PyStatus { pub struct PyStatus {
pub files: Option<HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)>>, pub files: Option<HashMap<PathBuf, PyPlugin>>,
}
pub type PyPluginData = HashMap<PathBuf, PyPlugin>;
pub type RawPyPlugin = (PathBuf, Option<SystemTime>, String);
#[derive(Debug, Clone)]
pub struct PyPlugin {
pub file_path: PathBuf,
pub changed_time: Option<SystemTime>,
pub py_module: Py<PyAny>,
}
impl PyPlugin {
pub fn new_from_path(path: &PathBuf) -> Option<Self> {
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<RawPyPlugin> for PyPlugin {
type Error = PyErr;
fn try_from(value: RawPyPlugin) -> Result<Self, Self::Error> {
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::<PyTuple>() {
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::<pyo3::exceptions::PyTypeError, _>(format!(
"加载 Python 插件 {:?} 的配置文件信息时失败:{:?}",
path, e
)))
}
}
} else {
warn!(
"加载 Python 插件 {:?} 的配置文件信息时失败:返回的不是 [str, str]",
path
);
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(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 { impl PyStatus {
pub fn get_files() -> &'static HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)> { pub fn get_files() -> &'static PyPluginData {
unsafe { unsafe {
match PYSTATUS.files.as_ref() { match PYSTATUS.files.as_ref() {
Some(files) => files, Some(files) => files,
@ -28,15 +156,15 @@ impl PyStatus {
} }
} }
pub fn add_file(path: PathBuf, changed_time: Option<SystemTime>, py_module: Py<PyAny>) { pub fn add_file(path: PathBuf, plugin: PyPlugin) {
unsafe { unsafe {
match PYSTATUS.files.as_mut() { match PYSTATUS.files.as_mut() {
Some(files) => { Some(files) => {
files.insert(path, (changed_time, py_module)); files.insert(path, plugin);
} }
None => { None => {
let mut files = HashMap::new(); let mut files: PyPluginData = HashMap::new();
files.insert(path, (changed_time, py_module)); files.insert(path, plugin);
PYSTATUS.files = Some(files); PYSTATUS.files = Some(files);
} }
} }
@ -47,16 +175,7 @@ impl PyStatus {
unsafe { unsafe {
match PYSTATUS.files.as_ref() { match PYSTATUS.files.as_ref() {
Some(files) => match files.get(path) { Some(files) => match files.get(path) {
Some((changed_time, _)) => { Some(plugin) => plugin.verifiy(),
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
}
None => false, None => false,
}, },
None => false, None => false,
@ -67,7 +186,7 @@ impl PyStatus {
pub static mut PYSTATUS: PyStatus = PyStatus { files: None }; 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() { if path.exists() {
info!("finding plugins in: {:?}", path); info!("finding plugins in: {:?}", path);
// 搜索所有的 py 文件 和 文件夹单层下面的 py 文件 // 搜索所有的 py 文件 和 文件夹单层下面的 py 文件
@ -77,18 +196,12 @@ pub fn load_py_plugins(path: &PathBuf) {
} }
Ok(dir) => { Ok(dir) => {
for entry in dir { for entry in dir {
if let Ok(entry) = entry { let entry = entry.unwrap();
let path = entry.path(); let path = entry.path();
if let Some(ext) = path.extension() { if let Some(ext) = path.extension() {
if ext == "py" { if ext == "py" {
match load_py_module(&path) { if let Some(plugin) = PyPlugin::new_from_path(&path) {
Some((changed_time, py_module)) => { PyStatus::add_file(path, plugin);
PyStatus::add_file(path.clone(), changed_time, py_module);
}
None => {
warn!("加载 Python 插件: {:?} 失败", path);
}
}
} }
} }
} }
@ -105,49 +218,17 @@ pub fn load_py_plugins(path: &PathBuf) {
); );
} }
pub fn verify_plugins() {
let mut need_reload_files: Vec<PathBuf> = 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<SystemTime> { pub fn get_change_time(path: &PathBuf) -> Option<SystemTime> {
path.metadata().ok()?.modified().ok() path.metadata().ok()?.modified().ok()
} }
pub fn py_module_from_code(content: &str, path: &str) -> PyResult<Py<PyAny>> { pub fn py_module_from_code(content: &str, path: &PathBuf) -> PyResult<Py<PyAny>> {
Python::with_gil(|py| -> PyResult<Py<PyAny>> { Python::with_gil(|py| -> PyResult<Py<PyAny>> {
let module: PyResult<Py<PyAny>> = PyModule::from_code( let module: PyResult<Py<PyAny>> = PyModule::from_code(
py, py,
&content, &content,
&path, &path.to_string_lossy(),
&path, &path.file_name().unwrap().to_string_lossy(),
// !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的 // !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的
) )
.map(|module| module.into()); .map(|module| module.into());
@ -157,38 +238,10 @@ pub fn py_module_from_code(content: &str, path: &str) -> PyResult<Py<PyAny>> {
/// 传入文件路径 /// 传入文件路径
/// 返回 hash 和 文件内容 /// 返回 hash 和 文件内容
pub fn load_py_file(path: &PathBuf) -> std::io::Result<(Option<SystemTime>, String)> { pub fn load_py_file(path: &PathBuf) -> std::io::Result<RawPyPlugin> {
let changed_time = get_change_time(&path); let changed_time = get_change_time(&path);
let content = std::fs::read_to_string(path)?; let content = std::fs::read_to_string(path)?;
Ok((changed_time, content)) Ok((path.clone(), changed_time, content))
}
pub fn load_py_module(path: &PathBuf) -> Option<(Option<SystemTime>, Py<PyAny>)> {
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<Py<PyAny>> = Python::with_gil(|py| -> PyResult<Py<PyAny>> {
let module: PyResult<Py<PyAny>> = 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
}
}
} }
pub fn init_py(config: &IcaConfig) { pub fn init_py(config: &IcaConfig) {