tailchat support p1

This commit is contained in:
shenjack 2024-03-30 12:52:49 +08:00
parent 6dfbc4e879
commit eaae60902d
Signed by: shenjack
GPG Key ID: 7B1134A979775551
11 changed files with 225 additions and 38 deletions

4
Cargo.lock generated
View File

@ -890,9 +890,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.101" version = "0.9.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",

View File

@ -26,9 +26,9 @@ rust_socketio = { version = "0.4.4", features = ["async"], optional = true }
# data # data
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
chrono = "0.4.37" chrono = "0.4"
toml = "0.8.12" toml = "0.8"
colored = "2.1.0" colored = "2.1"
# runtime # runtime
tokio = { version = "1.37", features = ["full"] } tokio = { version = "1.37", features = ["full"] }

View File

@ -24,7 +24,6 @@ pub struct IcaConfig {
pub filter_list: Vec<i64>, pub filter_list: Vec<i64>,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct PyConfig { pub struct PyConfig {
/// 插件路径 /// 插件路径

View File

@ -1 +1,2 @@
pub mod ica; pub mod ica;
pub mod tailchat;

View File

@ -0,0 +1,6 @@
pub mod messages;
pub type GroupId = String;
pub type ConverseId = String;
pub type UserId = String;
pub type MessageId = String;

View File

@ -0,0 +1,119 @@
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};
/*{'_id': '6606b3075163504389a6fc47',
'content': '(',
'author': '6602e20d7b8d10675758e36b',
'groupId': '6602e331d31fd31b04aa0693',
'converseId': '6602f785928c4254f17726b2',
'hasRecall': False,
'meta': {'mentions': []},
'reactions': [],
'createdAt': ExtType(code=0, data=b'\x00\x00\x01\x8e\x8a+TJ'),
'updatedAt': ExtType(code=0, data=b'\x00\x00\x01\x8e\x8a+TJ'),
'__v': 0} */
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ReciveMessage {
/// 消息ID
#[serde(rename = "_id")]
pub msg_id: MessageId,
/// 消息内容
pub content: String,
/// 发送者ID
#[serde(rename = "author")]
pub sender_id: UserId,
/// 服务器ID
#[serde(rename = "groupId")]
pub group_id: GroupId,
/// 会话ID
#[serde(rename = "converseId")]
pub converse_id: ConverseId,
/// 是否有回复?
#[serde(rename = "hasRecall")]
pub has_recall: bool,
/// 暂时懒得解析这玩意
pub meta: JsonValue,
/// 也懒得解析这玩意
pub reactions: Vec<JsonValue>,
/// 创建时间
#[serde(rename = "createdAt")]
pub created_at: JsonValue,
/// 更新时间
#[serde(rename = "updatedAt")]
pub updated_at: JsonValue,
/// 未知
#[serde(rename = "__v")]
pub v: JsonValue,
}
impl ReciveMessage {
pub fn as_reply(&self) -> ReplyMessage {
ReplyMessage {
content: self.content.clone(),
converse_id: self.converse_id.clone(),
group_id: self.group_id.clone(),
reply_id: self.msg_id.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendingMessage {
/// 消息内容
pub content: String,
/// 会话ID
#[serde(rename = "converseId")]
pub converse_id: ConverseId,
/// 服务器ID
#[serde(rename = "groupId")]
pub group_id: GroupId,
}
#[derive(Debug, Clone)]
pub struct ReplyMeta {
/// 被回复的人的ID (可以是多个?)
pub mentions: Vec<UserId>,
/// 被回复的消息ID
pub reply_id: MessageId,
/// 被回复的消息的发送者ID
pub reply_author: UserId,
/// 被回复的消息内容
pub reply_content: String,
}
impl Serialize for ReplyMeta {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let reply = json! {
{
"replyId": self.reply_id,
"replyAuthor": self.reply_author,
"replyContent": self.reply_content,
}
};
let mut map = serde_json::Map::new();
map.insert("mentions".to_string(), serde_json::to_value(&self.mentions).unwrap());
map.insert("reply".to_string(), reply);
map.serialize(serializer)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplyMessage {
/// 消息内容
pub content: String,
/// 会话ID
#[serde(rename = "converseId")]
pub converse_id: ConverseId,
/// 服务器ID
#[serde(rename = "groupId")]
pub group_id: GroupId,
/// 回复的消息ID
#[serde(rename = "replyId")]
pub reply_id: MessageId,
}

View File

@ -6,6 +6,21 @@ pub enum IcaError {
SocketIoError(rust_socketio::error::Error), SocketIoError(rust_socketio::error::Error),
} }
#[derive(Debug)]
pub enum PyPluginError {
/// 插件内未找到指定函数
/// 函数名, 模块名
FuncNotFound(String, String),
/// 插件内函数获取错误
/// pyerr, func_name, module_name
CouldNotGetFunc(pyo3::PyErr, String, String),
/// 插件内函数不可调用
FuncNotCallable(String, String),
/// 插件内函数调用错误
/// pyerr, func_name, module_name
FuncCallError(pyo3::PyErr, String, String),
}
impl From<rust_socketio::Error> for IcaError { impl From<rust_socketio::Error> for IcaError {
fn from(e: rust_socketio::Error) -> Self { IcaError::SocketIoError(e) } fn from(e: rust_socketio::Error) -> Self { IcaError::SocketIoError(e) }
} }
@ -18,6 +33,25 @@ impl std::fmt::Display for IcaError {
} }
} }
impl std::fmt::Display for PyPluginError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PyPluginError::FuncNotFound(name, module) => {
write!(f, "插件内未找到函数: {} in {}", name, module)
}
PyPluginError::CouldNotGetFunc(py_err, name, module) => {
write!(f, "插件内函数获取错误: {:#?}|{} in {}", py_err, name, module)
}
PyPluginError::FuncNotCallable(name, module) => {
write!(f, "插件内函数不可调用: {} in {}", name, module)
}
PyPluginError::FuncCallError(py_err, name, module) => {
write!(f, "插件内函数调用错误: {:#?}|{} in {}", py_err, name, module)
}
}
}
}
impl std::error::Error for IcaError { impl std::error::Error for IcaError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self { match self {
@ -25,3 +59,14 @@ impl std::error::Error for IcaError {
} }
} }
} }
impl std::error::Error for PyPluginError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
PyPluginError::FuncNotFound(_, _) => None,
PyPluginError::CouldNotGetFunc(e, _, _) => Some(e),
PyPluginError::FuncNotCallable(_, _) => None,
PyPluginError::FuncCallError(_, _) => None,
}
}
}

View File

@ -3,13 +3,14 @@ use std::time::Duration;
mod config; mod config;
mod data_struct; mod data_struct;
mod error; mod error;
#[cfg(feature = "ica")]
mod ica;
// #[cfg(feature = "tailchat")]
// mod tailchat;
mod py; mod py;
mod status; mod status;
#[cfg(feature = "ica")]
mod ica;
#[cfg(feature = "tailchat")]
mod tailchat;
use config::BotConfig; use config::BotConfig;
use tracing::{event, info, span, Level}; use tracing::{event, info, span, Level};

View File

@ -4,12 +4,15 @@ use pyo3::prelude::*;
use rust_socketio::asynchronous::Client; use rust_socketio::asynchronous::Client;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use crate::data_struct::ica::messages::NewMessage; use crate::data_struct::{ica, tailchat};
use crate::data_struct::ica::MessageId; use crate::error::PyPluginError;
use crate::py::{class, PyPlugin, PyStatus}; use crate::py::{class, PyPlugin, PyStatus};
use crate::MainStatus; use crate::MainStatus;
pub fn get_func<'py>(py_module: &'py PyAny, path: &PathBuf, name: &'py str) -> Option<&'py PyAny> { pub fn get_func<'py>(
py_module: &Bound<'py, PyAny>,
name: &'py str,
) -> Result<Bound<'py, PyAny>, PyPluginError> {
// 要处理的情况: // 要处理的情况:
// 1. 有这个函数 // 1. 有这个函数
// 2. 没有这个函数 // 2. 没有这个函数
@ -20,25 +23,39 @@ pub fn get_func<'py>(py_module: &'py PyAny, path: &PathBuf, name: &'py str) -> O
match py_module.getattr(name) { match py_module.getattr(name) {
Ok(func) => { Ok(func) => {
if func.is_callable() { if func.is_callable() {
Some(func) Ok(func)
} else { } else {
warn!("function<{}>: {:#?} in {:?} is not callable", name, func, path); // warn!("function<{}>: {:#?} in {:?} is not callable", name, func, path);
None Err(PyPluginError::FuncNotCallable(
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
} }
} }
Err(e) => { Err(e) => {
warn!("failed to get function<{}> from {:?}: {:?}", name, path, e); // warn!("failed to get function<{}> from {:?}: {:?}", name, path, e);
None Err(PyPluginError::CouldNotGetFunc(
e,
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
} }
} }
} else { } else {
debug!("no function<{}> in module {:?}", name, path); // debug!("no function<{}> in module {:?}", name, path);
None Err(PyPluginError::FuncNotFound(
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
} }
} }
Err(e) => { Err(e) => {
warn!("failed to check function<{}> from {:?}: {:?}", name, path, e); // warn!("failed to check function<{}> from {:?}: {:?}", name, path, e);
None Err(PyPluginError::CouldNotGetFunc(
e,
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
} }
} }
} }
@ -79,23 +96,22 @@ pub const ICA_DELETE_MESSAGE_FUNC: &str = "on_ica_delete_message";
pub const TAILCHAT_NEW_MESSAGE_FUNC: &str = "on_tailchat_message"; pub const TAILCHAT_NEW_MESSAGE_FUNC: &str = "on_tailchat_message";
/// 执行 new message 的 python 插件 /// 执行 new message 的 python 插件
pub async fn ica_new_message_py(message: &NewMessage, client: &Client) { pub async fn ica_new_message_py(message: &ica::messages::NewMessage, client: &Client) {
// 验证插件是否改变 // 验证插件是否改变
verify_plugins(); verify_plugins();
let plugins = PyStatus::get_files(); let plugins = PyStatus::get_files();
for (path, plugin) in plugins.iter() { for (_path, plugin) in plugins.iter() {
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, 直接让他自己跑就好了(离谱) // 甚至实际上压根不需要await这个spawn, 直接让他自己跑就好了(离谱)
tokio::spawn(async move { tokio::spawn(async move {
Python::with_gil(|py| { Python::with_gil(|py| {
if let Some(py_func) = if let Ok(py_func) = get_func(plugin.py_module.bind(py), ICA_NEW_MESSAGE_FUNC) {
get_func(plugin.py_module.as_ref(py), path, ICA_NEW_MESSAGE_FUNC)
{
if let Err(e) = py_func.call1(args) { if let Err(e) = py_func.call1(args) {
warn!("failed to call function<{}>: {:?}", ICA_NEW_MESSAGE_FUNC, e); let e = PyPluginError::FuncCallError(e, ICA_NEW_MESSAGE_FUNC.to_string());
// warn!("failed to call function<{}>: {:?}", ICA_NEW_MESSAGE_FUNC, e);
} }
} }
}) })
@ -103,19 +119,17 @@ pub async fn ica_new_message_py(message: &NewMessage, client: &Client) {
} }
} }
pub async fn ica_delete_message_py(msg_id: MessageId, client: &Client) { pub async fn ica_delete_message_py(msg_id: ica::MessageId, client: &Client) {
verify_plugins(); verify_plugins();
let plugins = PyStatus::get_files(); let plugins = PyStatus::get_files();
for (path, plugin) in plugins.iter() { for (_path, plugin) in plugins.iter() {
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);
tokio::spawn(async move { tokio::spawn(async move {
Python::with_gil(|py| { Python::with_gil(|py| {
if let Some(py_func) = if let Ok(py_func) = get_func(plugin.py_module.bind(py), ICA_DELETE_MESSAGE_FUNC) {
get_func(plugin.py_module.as_ref(py), path, ICA_DELETE_MESSAGE_FUNC)
{
if let Err(e) = py_func.call1(args) { if let Err(e) = py_func.call1(args) {
warn!("failed to call function<{}>: {:?}", ICA_DELETE_MESSAGE_FUNC, e); warn!("failed to call function<{}>: {:?}", ICA_DELETE_MESSAGE_FUNC, e);
} }
@ -124,3 +138,5 @@ pub async fn ica_delete_message_py(msg_id: MessageId, client: &Client) {
}); });
} }
} }
// pub async fn tailchat_new_message_py(message: )

View File

@ -68,8 +68,8 @@ impl TryFrom<RawPyPlugin> for PyPlugin {
}; };
let py_module = py_module.unwrap(); let py_module = py_module.unwrap();
Python::with_gil(|py| { Python::with_gil(|py| {
let module = py_module.as_ref(py); let module = py_module.bind(py);
if let Some(config_func) = call::get_func(module, &path, "on_config") { if let Ok(config_func) = call::get_func(module, "on_config") {
match config_func.call0() { match config_func.call0() {
Ok(config) => { Ok(config) => {
if config.is_instance_of::<PyTuple>() { if config.is_instance_of::<PyTuple>() {
@ -96,8 +96,7 @@ impl TryFrom<RawPyPlugin> for PyPlugin {
}; };
match config_value { match config_value {
Ok(config) => { Ok(config) => {
let py_config = class::ConfigDataPy::new(config); let py_config = Bound::new(py, class::ConfigDataPy::new(config)).unwrap();
let py_config = PyCell::new(py, py_config).unwrap();
module.setattr("CONFIG_DATA", py_config).unwrap(); module.setattr("CONFIG_DATA", py_config).unwrap();
Ok(PyPlugin { Ok(PyPlugin {
file_path: path, file_path: path,
@ -228,7 +227,7 @@ pub fn get_change_time(path: &Path) -> Option<SystemTime> { path.metadata().ok()
pub fn py_module_from_code(content: &str, path: &Path) -> PyResult<Py<PyAny>> { pub fn py_module_from_code(content: &str, path: &Path) -> 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_bound(
py, py,
content, content,
&path.to_string_lossy(), &path.to_string_lossy(),

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

@ -0,0 +1 @@