mirror of
http://shenjack.top:5100/shenjack/icalingua-python-bot.git
synced 2024-11-23 12:41:05 +08:00
大的来了
This commit is contained in:
parent
d362e7c155
commit
e3708dc41a
|
@ -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:
|
||||
...
|
||||
|
||||
|
|
|
@ -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:
|
||||
cookie = CONFIG_DATA["cookie"]
|
||||
if cookie == "填写你的 cookie" or cookie is None:
|
||||
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)
|
||||
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\""""
|
||||
)
|
||||
|
|
|
@ -23,6 +23,8 @@ pub struct IcaConfig {
|
|||
pub filter_list: Vec<i64>,
|
||||
/// Python 插件路径
|
||||
pub py_plugin_path: Option<String>,
|
||||
/// Python 配置文件路径
|
||||
pub py_config_path: Option<String>,
|
||||
}
|
||||
|
||||
impl IcaConfig {
|
||||
|
|
|
@ -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<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 插件
|
||||
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<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) {
|
||||
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<on_delete_message>: {:?}", e);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<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 {
|
||||
pub fn get_files() -> &'static HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)> {
|
||||
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<SystemTime>, py_module: Py<PyAny>) {
|
||||
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 entry = entry.unwrap();
|
||||
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);
|
||||
}
|
||||
}
|
||||
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<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> {
|
||||
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>> {
|
||||
let module: PyResult<Py<PyAny>> = 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<Py<PyAny>> {
|
|||
|
||||
/// 传入文件路径
|
||||
/// 返回 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 content = std::fs::read_to_string(path)?;
|
||||
Ok((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
|
||||
}
|
||||
}
|
||||
Ok((path.clone(), changed_time, content))
|
||||
}
|
||||
|
||||
pub fn init_py(config: &IcaConfig) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user