Compare commits

..

5 Commits

Author SHA1 Message Date
df0355cad7
0.4.4 2024-02-22 14:10:11 +08:00
75e7e0085a
上rust! 2024-02-22 13:06:30 +08:00
bbaf8cf0c9
噫,好,成了 2024-02-22 12:47:20 +08:00
e5fde06837
test! passed! 2024-02-22 12:06:43 +08:00
974368166b
好歹过编译+test了…… 2024-02-22 02:40:39 +08:00
11 changed files with 424 additions and 65 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ica-rs" name = "ica-rs"
version = "0.4.2" version = "0.4.4"
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
@ -20,12 +20,13 @@ colored = "2.1.0"
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.0", features = ["full"] }
futures-util = "0.3.30" futures-util = "0.3.30"
pyo3 = "0.20.2"
# pyo3-async = "0.3.2"
pyo3-asyncio = { version = "0.20.0", features = ["attributes", "tokio-runtime"] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["time"] } tracing-subscriber = { version = "0.3.18", features = ["time"] }
[dependencies.pyo3]
version = "0.20.2"
[patch.crates-io] [patch.crates-io]
rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "mult_payload" } rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "mult_payload" }
# pyo3 = { git = "https://github.com/PyO3/pyo3.git", branch = "main" }

View File

@ -44,3 +44,36 @@ class SendMessage:
class NewMessage: class NewMessage:
def reply_with(self, message: str) -> SendMessage: def reply_with(self, message: str) -> SendMessage:
... ...
def __str__(self) -> str:
...
@property
def content(self) -> str:
...
@property
def sender_id(self) -> int:
...
@property
def is_from_self(self) -> bool:
...
class IcaClient:
@staticmethod
async def send_message_a(client: "IcaClient", message: SendMessage) -> bool:
"""
仅作占位
(因为目前来说, rust调用 Python端没法启动一个异步运行时
所以只能 tokio::task::block_in_place 转换成同步调用)
"""
def send_message(self, message: SendMessage) -> bool:
...
def debug(self, message: str) -> None:
...
def info(self, message: str) -> None:
...
def warn(self, message: str) -> None:
...
def on_message(msg: NewMessage, client: IcaClient) -> None:
...

15
ica-rs/plugins/base.py Normal file
View File

@ -0,0 +1,15 @@
from typing import TYPE_CHECKING, TypeVar
if TYPE_CHECKING:
from ica_typing import NewMessage, IcaClient
else:
NewMessage = TypeVar("NewMessage")
IcaClient = TypeVar("IcaClient")
_version_ = "1.0.0"
def on_message(msg: NewMessage, client: IcaClient) -> None:
if not msg.is_from_self:
if msg.content == "/bot-rs-py":
reply = msg.reply_with(f"ica-async-rs-sync-py {_version_}")
client.send_message(reply)

86
ica-rs/plugins/bmcl.py Normal file
View File

@ -0,0 +1,86 @@
import time
import requests
from typing import TYPE_CHECKING, TypeVar
if TYPE_CHECKING:
from ica_typing import NewMessage, IcaClient
else:
NewMessage = TypeVar("NewMessage")
IcaClient = TypeVar("IcaClient")
_version_ = "2.0.0-rs"
def format_data_size(data_bytes: float) -> str:
data_lens = ["B", "KB", "MB", "GB", "TB"]
data_len = "0B"
for i in range(5):
if data_bytes < 1024:
data_bytes = round(data_bytes, 5)
data_len = f"{data_bytes}{data_lens[i]}"
break
else:
data_bytes /= 1024
return data_len
def format_hit_count(count: int) -> str:
"""数据分段, 四位一个下划线
Args:
count (int): 数据
Returns:
str: 格式化后的数据
1 -> 1
1000 -> 1000
10000 -> 1_0000
100000 -> 10_0000
1000000 -> 100_0000
"""
count_str = str(count)
count_len = len(count_str)
if count_len <= 4:
return count_str
else:
return "_".join(count_str[i:i + 4] for i in range(0, count_len, 4))
def bmcl(msg: NewMessage, client: IcaClient) -> None:
req_time = time.time()
# 记录请求时间
response = requests.get("https://bd.bangbang93.com/openbmclapi/metric/dashboard")
if not response.status_code == 200 or response.reason != "OK":
reply = msg.reply_with(f"请求数据失败\n{response.status_code}")
client.warn(
f"数据请求失败, 请检查网络\n{response.status}"
)
client.send_message(reply)
return
data = response.json()
data_bytes: float = data["bytes"]
data_hits: int = data["hits"]
data_bandwidth: float = data["currentBandwidth"]
load_str: float = data["load"] * 100
online_node: int = data["currentNodes"]
online_bandwidth: int = data["bandwidth"]
data_len = format_data_size(data_bytes)
hits_count = format_hit_count(data_hits)
report_msg = (
f"OpenBMCLAPI 状态面板v{_version_} :\n"
f"实时信息: {online_node} 带宽: {online_bandwidth}Mbps\n"
f"负载: {load_str:.2f}% 带宽: {data_bandwidth:.2f}Mbps\n"
f"当日请求: {hits_count} 数据量: {data_len}\n"
f"请求时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(req_time))}\n"
"数据源: https://bd.bangbang93.com/pages/dashboard"
)
client.info(report_msg)
reply = msg.reply_with(report_msg)
client.send_message(reply)
def on_message(msg: NewMessage, client: IcaClient) -> None:
if not msg.is_from_self:
if msg.content == "/bmcl-rs":
bmcl(msg, client)

View File

View File

@ -11,11 +11,17 @@ use serde_json::Value;
use tracing::{debug, warn}; use tracing::{debug, warn};
/// "安全" 的 发送一条消息 /// "安全" 的 发送一条消息
pub async fn send_message(client: Client, message: SendMessage) { pub async fn send_message(client: &Client, message: &SendMessage) -> bool {
let value = message.as_value(); let value = message.as_value();
match client.emit("sendMessage", value).await { match client.emit("sendMessage", value).await {
Ok(_) => debug!("send_message {}", format!("{:#?}", message).cyan()), Ok(_) => {
Err(e) => warn!("send_message faild:{}", format!("{:#?}", e).red()), debug!("send_message {}", format!("{:#?}", message).cyan());
true
}
Err(e) => {
warn!("send_message faild:{}", format!("{:#?}", e).red());
false
}
} }
} }

View File

@ -31,18 +31,20 @@ pub async fn add_message(payload: Payload, client: Client) {
if let Some(value) = values.first() { if let Some(value) = values.first() {
let message = NewMessage::new_from_json(value); let message = NewMessage::new_from_json(value);
info!("add_message {}", format!("{:#?}", message).cyan()); info!("add_message {}", format!("{:#?}", message).cyan());
if message.is_reply() { // if message.is_reply() {
return; // return;
} // }
if message.is_from_self() { // if message.is_from_self() {
return; // return;
} // }
// 就在这里处理掉最基本的消息 // 就在这里处理掉最基本的消息
// 之后的处理交给插件 // 之后的处理交给插件
if message.content.eq("/bot-rs") { if message.content.eq("/bot-rs") && !message.is_from_self() && !message.is_reply() {
let reply = message.reply_with(&format!("ica-async-rs pong v{}", VERSION)); let reply = message.reply_with(&format!("ica-async-rs pong v{}", VERSION));
send_message(client, reply).await; send_message(&client, &reply).await;
} }
// python 插件
py::new_message_py(&message, &client).await;
} }
} }
} }
@ -135,7 +137,6 @@ pub async fn connect_callback(payload: Payload, _client: Client) {
if let Some(value) = values.first() { if let Some(value) = values.first() {
match value.as_str() { match value.as_str() {
Some("authSucceed") => { Some("authSucceed") => {
py::run();
info!("{}", "已经登录到 icalingua!".green()) info!("{}", "已经登录到 icalingua!".green())
} }
Some("authFailed") => { Some("authFailed") => {

View File

@ -1,5 +1,9 @@
use pyo3::prelude::*; use pyo3::prelude::*;
use tracing::{debug, info, warn};
use rust_socketio::asynchronous::Client;
use tokio::runtime::Runtime;
use crate::client::send_message;
use crate::data_struct::messages::{NewMessage, ReplyMessage, SendMessage}; use crate::data_struct::messages::{NewMessage, ReplyMessage, SendMessage};
use crate::ClientStatus; use crate::ClientStatus;
@ -106,6 +110,7 @@ impl IcaStatusPy {
} }
} }
#[derive(Clone)]
#[pyclass] #[pyclass]
#[pyo3(name = "NewMessage")] #[pyo3(name = "NewMessage")]
pub struct NewMessagePy { pub struct NewMessagePy {
@ -117,6 +122,23 @@ impl NewMessagePy {
pub fn reply_with(&self, content: String) -> SendMessagePy { pub fn reply_with(&self, content: String) -> SendMessagePy {
SendMessagePy::new(self.msg.reply_with(&content)) SendMessagePy::new(self.msg.reply_with(&content))
} }
pub fn __str__(&self) -> String {
format!("{:?}", self.msg)
}
#[getter]
pub fn get_content(&self) -> String {
self.msg.content.clone()
}
#[getter]
pub fn get_sender_id(&self) -> i64 {
self.msg.sender_id
}
#[getter]
pub fn get_is_from_self(&self) -> bool {
self.msg.is_from_self()
}
} }
impl NewMessagePy { impl NewMessagePy {
@ -131,20 +153,86 @@ pub struct ReplyMessagePy {
pub msg: ReplyMessage, pub msg: ReplyMessage,
} }
#[pymethods]
impl ReplyMessagePy {
pub fn __str__(&self) -> String {
format!("{:?}", self.msg)
}
}
impl ReplyMessagePy { impl ReplyMessagePy {
pub fn new(msg: ReplyMessage) -> Self { pub fn new(msg: ReplyMessage) -> Self {
Self { msg } Self { msg }
} }
} }
#[derive(Clone)]
#[pyclass] #[pyclass]
#[pyo3(name = "SendMessage")] #[pyo3(name = "SendMessage")]
pub struct SendMessagePy { pub struct SendMessagePy {
pub msg: SendMessage, pub msg: SendMessage,
} }
#[pymethods]
impl SendMessagePy {
pub fn __str__(&self) -> String {
format!("{:?}", self.msg)
}
}
impl SendMessagePy { impl SendMessagePy {
pub fn new(msg: SendMessage) -> Self { pub fn new(msg: SendMessage) -> Self {
Self { msg } Self { msg }
} }
} }
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "IcaClient")]
pub struct IcaClientPy {
pub client: Client,
}
#[pymethods]
impl IcaClientPy {
pub fn send_message(&self, message: SendMessagePy) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(send_message(&self.client, &message.msg))
})
}
/// 仅作占位
/// (因为目前来说, rust调用 Python端没法启动一个异步运行时
/// 所以只能 tokio::task::block_in_place 转换成同步调用)
#[staticmethod]
pub fn send_message_a(
py: Python,
client: IcaClientPy,
message: SendMessagePy,
) -> PyResult<&PyAny> {
pyo3_asyncio::tokio::future_into_py(py, async move {
Ok(send_message(&client.client, &message.msg).await)
})
}
pub fn debug(&self, content: String) {
debug!("{}", content);
}
pub fn info(&self, content: String) {
info!("{}", content);
}
pub fn warn(&self, content: String) {
warn!("{}", content);
}
}
impl IcaClientPy {
pub fn new(client: &Client) -> Self {
Self {
client: client.clone(),
}
}
}

View File

@ -1,20 +1,22 @@
pub mod class; pub mod class;
use std::time::SystemTime;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use blake3::Hasher;
use pyo3::{prelude::*, types::IntoPyDict}; use pyo3::{prelude::*, types::IntoPyDict};
use rust_socketio::asynchronous::Client;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use crate::config::IcaConfig; use crate::config::IcaConfig;
use crate::data_struct::messages::NewMessage;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PyStatus { pub struct PyStatus {
pub files: Option<HashMap<PathBuf, (Vec<u8>, String)>>, pub files: Option<HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)>>,
} }
impl PyStatus { impl PyStatus {
pub fn get_files() -> &'static HashMap<PathBuf, (Vec<u8>, String)> { pub fn get_files() -> &'static HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)> {
unsafe { unsafe {
match PYSTATUS.files.as_ref() { match PYSTATUS.files.as_ref() {
Some(files) => files, Some(files) => files,
@ -27,26 +29,37 @@ impl PyStatus {
} }
} }
pub fn add_file(path: PathBuf, content: Vec<u8>, hash: String) { pub fn add_file(path: PathBuf, changed_time: Option<SystemTime>, py_module: Py<PyAny>) {
unsafe { unsafe {
match PYSTATUS.files.as_mut() { match PYSTATUS.files.as_mut() {
Some(files) => { Some(files) => {
files.insert(path, (content, hash)); files.insert(path, (changed_time, py_module));
debug!("Added file to py status, {:?}", files);
} }
None => { None => {
warn!("No files in py status, creating new");
let mut files = HashMap::new(); let mut files = HashMap::new();
files.insert(path, (content, hash)); files.insert(path, (changed_time, py_module));
PYSTATUS.files = Some(files); PYSTATUS.files = Some(files);
} }
} }
} }
} }
pub fn verify_file(path: &PathBuf, hash: &String) -> bool { pub fn verify_file(path: &PathBuf) -> bool {
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((_, file_hash)) => file_hash == hash, 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
},
None => false, None => false,
}, },
None => false, None => false,
@ -57,27 +70,97 @@ impl PyStatus {
pub static mut PYSTATUS: PyStatus = PyStatus { files: None }; pub static mut PYSTATUS: PyStatus = PyStatus { files: None };
pub fn run() { pub fn load_py_plugins(path: &PathBuf) {
Python::with_gil(|py| { if path.exists() {
let bot_status = class::IcaStatusPy::new(); info!("finding plugins in: {:?}", path);
let _bot_status: &PyCell<_> = PyCell::new(py, bot_status).unwrap(); // 搜索所有的 py 文件 和 文件夹单层下面的 py 文件
match path.read_dir() {
let locals = [("state", _bot_status)].into_py_dict(py); Err(e) => {
py.run( warn!("failed to read plugin path: {:?}", e);
"from pathlib import Path\nprint(Path.cwd())\nprint(state)", }
None, Ok(dir) => {
Some(locals), for entry in dir {
) if let Ok(entry) = entry {
.unwrap(); let path = entry.path();
}); if let Some(ext) = path.extension() {
if ext == "py" {
match load_py_file(&path) {
Ok((changed_time, content)) => {
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()
)
.map(|module| module.into());
module
});
match py_module {
Ok(py_module) => {
info!("加载到插件: {:?}", path);
PyStatus::add_file(path, changed_time, py_module);
}
Err(e) => {
warn!("failed to load file: {:?} | e: {:?}", path, e);
}
}
}
Err(e) => {
warn!("failed to load file: {:?} | e: {:?}", path, e);
}
}
}
}
}
}
}
}
} else {
warn!("plugin path not exists: {:?}", path);
}
info!(
"python 插件目录: {:?} 加载完成, 加载到 {} 个插件",
path,
PyStatus::get_files().len()
);
} }
pub fn load_py_file(path: &PathBuf) -> (Vec<u8>, String) { pub fn verify_plugins() {
let mut hasher = Hasher::new(); let plugins = PyStatus::get_files();
let content = std::fs::read(path).unwrap(); for (path, _) in plugins.iter() {
hasher.update(&content); if !PyStatus::verify_file(path) {
let hash = hasher.finalize().as_bytes().to_vec(); info!("file changed: {:?}", path);
(content, hex::encode(hash)) if let Ok((changed_time, content)) = load_py_file(path) {
let py_module = Python::with_gil(|py| -> Py<PyAny> {
let module: Py<PyAny> = PyModule::from_code(
py,
&content,
&path.to_string_lossy(),
&path.to_string_lossy(),
// !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的
)
.unwrap()
.into();
module
});
PyStatus::add_file(path.clone(), changed_time, py_module);
}
}
}
}
pub fn get_change_time(path: &PathBuf) -> Option<SystemTime> {
path.metadata().ok()?.modified().ok()
}
/// 传入文件路径
/// 返回 hash 和 文件内容
pub fn load_py_file(path: &PathBuf) -> std::io::Result<(Option<SystemTime>, String)> {
let changed_time = get_change_time(&path);
let content = std::fs::read_to_string(path)?;
Ok((changed_time, content))
} }
pub fn init_py(config: &IcaConfig) { pub fn init_py(config: &IcaConfig) {
@ -85,28 +168,47 @@ pub fn init_py(config: &IcaConfig) {
pyo3::prepare_freethreaded_python(); pyo3::prepare_freethreaded_python();
if let Some(plugin_path) = &config.py_plugin_path { if let Some(plugin_path) = &config.py_plugin_path {
let path = PathBuf::from(plugin_path); let path = PathBuf::from(plugin_path);
if path.exists() { load_py_plugins(&path);
info!("finding plugins in: {:?}", path); debug!("python 插件列表: {:#?}", PyStatus::get_files());
// 搜索所有的 py 文件 和 文件夹单层下面的 py 文件
match path.read_dir() {
Err(e) => {
warn!("failed to read plugin path: {:?}", e);
}
Ok(dir) => {
for entry in dir {
if let Ok(entry) = entry {
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "py" {}
}
}
}
}
}
} else {
warn!("plugin path not exists: {:?}", path);
}
} }
info!("python inited") info!("python inited")
} }
/// 执行 new message 的 python 插件
pub async fn new_message_py(message: &NewMessage, client: &Client) {
// 验证插件是否改变
verify_plugins();
let cwd = std::env::current_dir().unwrap();
let plugins = PyStatus::get_files();
for (path, (_, py_module)) in plugins.iter() {
// 切换工作目录到运行的插件的位置
let mut goto = cwd.clone();
goto.push(path.parent().unwrap());
if let Err(e) = std::env::set_current_dir(&goto) {
warn!("移动工作目录到 {:?} 失败 {:?} cwd: {:?}", goto, e, cwd);
}
Python::with_gil(|py| {
let msg = class::NewMessagePy::new(message);
let client = class::IcaClientPy::new(client);
let args = (msg, client);
let async_py_func = py_module.getattr(py, "on_message");
match async_py_func {
Ok(async_py_func) => {
async_py_func.as_ref(py).call1(args).unwrap();
}
Err(e) => {
warn!("failed to get on_message function: {:?}", e);
}
}
});
}
// 最后切换回来
if let Err(e) = std::env::set_current_dir(&cwd) {
warn!("设置工作目录{:?} 失败:{:?}", cwd, e);
}
}

View File

@ -1,5 +1,14 @@
# 更新日志 # 更新日志
## 0.4.4
现在正式支持 Python 插件了
`/bmcl` 也迁移到了 Python 插件版本
## 0.4.3
噫! 好! 我成了!
## 0.4.2 ## 0.4.2
现在是 async 版本啦! 现在是 async 版本啦!

View File

@ -5,7 +5,7 @@
> 出于某个企鹅, 和保护 作者 和 原项目 ( ica ) 的原因 \ > 出于某个企鹅, 和保护 作者 和 原项目 ( ica ) 的原因 \
> 功能请自行理解 > 功能请自行理解
## 使用方法 ## 使用方法 ( Python 版 )
- 安装依赖 - 安装依赖
@ -40,3 +40,21 @@ docker up -d
```powershell ```powershell
python connect.py python connect.py
``` ```
## 使用方法 ( Rust 版 )
- 准备一个 Python 环境
- 修改好配置文件
```powershell
Copy-Item config-temp.toml config.toml
```
- 编译
```powershell
cargo build --release
```
运行