Compare commits

..

No commits in common. "main" and "0.4.5" have entirely different histories.
main ... 0.4.5

60 changed files with 1962 additions and 8002 deletions

View File

@ -1,43 +0,0 @@
name: build and test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: 获取版本号
id: get_version
uses: sravinet/toml-select@v1.0.1
with:
file: ./ica-rs/Cargo.toml
field: "package.version"
- name: Setup Python
uses: actions/setup-python@v5.0.0
with:
# Version range or exact version of Python or PyPy to use, using SemVer's version range syntax. Reads from .python-version if unset.
python-version: 3.8
- name: Run tests
run: cargo test --verbose
- name: Build
run: cargo build --release
- name: 上传
uses: actions/upload-artifact@v4
with:
name: ica-rs-b${{ github.run_number }}-${{ steps.get_version.outputs.value }}-py38-win-x64
path: ./target/release/ica-rs.exe

View File

@ -1,45 +0,0 @@
name: publish
on:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
publish: # 全都要!
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
# python-version: ["3.8", "3.9", "3.10", "3.11",]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: 获取版本号
id: get_version
uses: sravinet/toml-select@v1.0.1
with:
file: ./ica-rs/Cargo.toml
field: "package.version"
- name: Setup Python
uses: actions/setup-python@v5.0.0
with:
# Version range or exact version of Python or PyPy to use, using SemVer's version range syntax. Reads from .python-version if unset.
python-version: 3.8
- name: Run tests
run: cargo test --verbose
- name: Build
run: cargo build --release
- name: 上传
uses: actions/upload-artifact@v4
with:
name: ica-rs-b${{ github.run_number }}-${{ steps.get_version.outputs.value }}-py38-win-x64
path: ./target/release/ica-rs.exe

9
.gitignore vendored
View File

@ -1,13 +1,8 @@
venv
env*
env
config*.toml
config.toml
.idea
*.pyc
*__pycache__/
make.*
# Added by cargo
/target

2626
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
[workspace]
members = [
"ica-rs"
]
resolver = "2"
[patch.crates-io]
rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "main" }
# rust_socketio = { path = "../../rust-socketio/socketio" }
# pyo3 = { git = "https://github.com/PyO3/pyo3.git", branch = "main" }

View File

@ -1,20 +1,4 @@
# 填写 [ica] 但不填写此项则不启用 ica
enable_ica = true # 是否启用 ica
# 填写 [matrix] 但不填写此项则不启用 matrix
enable_matrix = true # 是否启用 matrix
enable_py = true # 是否启用 python 插件
[py]
# python 插件路径
plugin_path = "/path/to/your/plugin"
config_path = "/path/to/your/config"
[ica]
private_key = "" # 与 icalingua 客户端使用的 private_key 一致
host = "" # docker 版 icalingua 服务的地址
self_id = 0 # 机器人的 qq 号
@ -26,15 +10,6 @@ notice_start = true # 是否在启动 bot 后通知
# 机器人的管理员
admin_list = [0] # 机器人的管理员
# 过滤的人
filter_list = [0]
[matrix]
home_server = "" # matrix 服务器地址
bot_id = "" # 机器人的 id
bot_password = "" # 机器人的密码
# 启动时通知的房间
notice_room = [""] # 启动 bot 后通知的房间
notice_start = true # 是否在启动 bot 后通知
# python 插件路径
py_plugin_path = "/path/to/your/plugin"

161
connect.py Normal file
View File

@ -0,0 +1,161 @@
import time
import json
import random
import asyncio
import argparse
import traceback
from typing import Dict, List, Tuple, Any
import socketio
from colorama import Fore
from nacl.signing import SigningKey
from lib_not_dr.loggers import config
from data_struct import SendMessage, ReplyMessage, get_config, BotConfig, BotStatus
from main import BOTCONFIG, _version_
from router import route
logger = config.get_logger("icalingua")
sio: socketio.AsyncClient = socketio.AsyncClient()
@sio.on("connect") # type: ignore
def connect():
logger.info(f"{Fore.GREEN}icalingua 已连接")
@sio.on("requireAuth") # type: ignore
async def require_auth(salt: str, versions: Dict[str, str]):
logger.info(f"{Fore.BLUE}versions: {versions}\n{type(salt)}|{salt=}")
# 准备数据
sign = SigningKey(bytes.fromhex(BOTCONFIG.private_key))
signature = sign.sign(bytes.fromhex(salt))
# 发送数据
await sio.emit("auth", signature.signature)
logger.info(f"{Fore.BLUE}send auth emit")
@sio.on("auth") # type: ignore
def auth(data: Dict[str, Any]):
logger.info(f"auth: {data}")
@sio.on("authFailed") # type: ignore
async def auth_failed():
logger.info(f"{Fore.RED}authFailed")
await sio.disconnect()
@sio.on("authSucceed") # type: ignore
def auth_succeed():
logger.info(f"{Fore.GREEN}authSucceed")
@sio.on("connect_error") # type: ignore
def connect_error(*args, **kwargs):
logger.info(f"连接错误 {args}, {kwargs}")
@sio.on("updateRoom") # type: ignore
def update_room(data: Dict[str, Any]):
logger.info(f"{Fore.CYAN}update_room: {data}")
@sio.on("addMessage") # type: ignore
async def add_message(data: Dict[str, Any]):
logger.info(f"{Fore.MAGENTA}add_message: {data}")
is_self = data["message"]["senderId"] == BOTCONFIG.self_id
if not is_self:
await route(data, sio)
@sio.on("deleteMessage") # type: ignore
def delete_message(message_id: str):
logger.info(f"{Fore.MAGENTA}delete_message: {message_id}")
@sio.on("setMessages") # type: ignore
def set_messages(data: Dict[str, Any]):
logger.info(
f"{Fore.YELLOW}set_messages: {data}\nmessage_len: {len(data['messages'])}"
)
async def notice_startup(room_list: List[int]):
for notice_room in BOTCONFIG.notice_room:
if notice_room in room_list:
notice_message = SendMessage(
content=f"ica bot v{_version_}", room_id=notice_room
)
await sio.emit("sendMessage", notice_message.to_json())
BotStatus.inited = True
logger.info("inited", tag="notice room")
else:
logger.warn(f"未找到通知房间: {notice_room}", tag="notice room")
await asyncio.sleep(random.randint(2, 5))
@sio.on("setAllRooms") # type: ignore
async def set_all_rooms(rooms: List[Dict[str, Any]]):
BotStatus.running = True
room_list: List[int] = [room.get("roomId") for room in rooms] # type: ignore
if not BotStatus.inited:
logger.info("initing...", tag="setAllRooms")
logger.debug(f"room_list: {room_list}", tag="setAllRooms")
if BOTCONFIG.notice_start:
await notice_startup(room_list)
if room_list != BotStatus.rooms:
logger.info(f"{Fore.YELLOW}set_all_rooms: {rooms}\nlen: {len(rooms)}\n")
BotStatus.rooms = room_list
logger.info(f"更新房间: {room_list}", tag="setAllRooms")
@sio.on("setAllChatGroups") # type: ignore
def set_all_chat_groups(groups: List[Dict[str, Any]]):
logger.info(f"{Fore.YELLOW}set_all_chat_groups: {groups}\nlen: {len(groups)}\n")
@sio.on("notify") # type: ignore
def notify(data: List[Tuple[str, Any]]):
logger.info(f"notify: {data}")
@sio.on("closeLoading") # type: ignore
def close_loading(_):
logger.info(f"{Fore.GREEN}close_loading")
@sio.on("onlineData") # type: ignore
def online_data(data: Dict[str, Any]):
logger.info(f"{Fore.GREEN}online_data: {data}")
@sio.on("*") # type: ignore
def catch_all(event, data):
logger.info(f"{Fore.RED}catch_all: {event}|{data}")
async def main():
"""
while True:
await self.eio.wait()
await self.sleep(1) # give the reconnect task time to start up
if not self._reconnect_task:
break
await self._reconnect_task
if self.eio.state != 'connected':
break
"""
try:
await sio.connect(BOTCONFIG.host)
await sio.wait()
except KeyboardInterrupt:
logger.info("KeyboardInterrupt")
except Exception:
logger.error(traceback.format_exc())

102
data_struct.py Normal file
View File

@ -0,0 +1,102 @@
from typing import List, Optional, Union, Literal, Tuple
import qtoml
from lib_not_dr.types import Options
class AtElement(Options):
text: str
id: Union[int, Literal['all']] = 'all'
class ReplyMessage(Options):
id: str
username: str = ''
content: str = ''
files: list = []
def to_json(self) -> dict:
return {
'_id': self.id,
'username': self.username,
'content': self.content,
'files': self.files
}
class SendMessage(Options):
content: str
room_id: Optional[int] = None
room: Optional[int] = None # room id 和 room 二选一 ( 实际上直接填 room id 就行了 )
file: None = None # TODO: 上传文件
reply_to: Optional[ReplyMessage] = None # 源码 给了一个 any TODO: 回复消息
b64_img: Optional[str] = None # TODO: 发送图片
at: Optional[List[AtElement]] = [] # TODO: @某人
sticker: Optional[None] = None # TODO: 发送表情
message_type: Optional[str] = None # TODO: 消息类型
def to_json(self) -> dict:
return {
'content': self.content,
'roomId': self.room_id,
'room': self.room,
'file': self.file,
'replyMessage': self.reply_to.to_json() if self.reply_to else None,
'b64img': self.b64_img,
'at': self.at,
'sticker': self.sticker,
'messageType': self.message_type
}
def to_content(self, content: str) -> "SendMessage":
self.content = content
return self
class NewMessage(Options):
sender_id: int
sender_name: str
room_id: int
content: str
msg_id: str
data: dict
def init(self, **kwargs) -> None:
data = kwargs.pop('data')
self.sender_name = data["message"]["username"]
self.sender_id = data["message"]["senderId"]
self.content = data["message"]["content"]
self.room_id = data["roomId"]
self.msg_id = data["message"]["_id"]
def is_self(self, self_id: int) -> bool:
return self.sender_id == self_id
class BotConfig(Options):
name = 'icalingua bot config'
# _check_filled = True
private_key: str
host: str
self_id: int
notice_room: List[int]
notice_start: bool = False
admin_list: List[int]
py_plugin_path: str
def init(self, **kwargs) -> None:
if self.notice_room is None:
self.notice_start = False
def get_config(config_path: str = 'config.toml') -> BotConfig:
with open(config_path, 'r', encoding='utf-8') as f:
config = qtoml.decoder.load(f)
return BotConfig(**config)
class BotStatus(Options):
inited: bool = False
running: bool = False
rooms: List[int] = []

1
ica-rs/.gitignore vendored
View File

@ -1,3 +1,2 @@
target
Cargo.lock
config/

View File

@ -1,54 +1,32 @@
[package]
name = "ica-rs"
version = "0.9.0"
edition = "2024"
version = "0.4.5"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["ica", "tailchat"]
ica = [
"dep:ed25519",
"dep:ed25519-dalek",
"dep:hex",
"dep:rust_socketio",
"dep:base64",
]
tailchat = ["dep:rust_socketio", "dep:md-5", "dep:reqwest"]
[dependencies]
ed25519 = "2.2.3"
ed25519-dalek = "2.1.1"
hex = "0.4.3"
blake3 = "1.5.0"
rust_socketio = { version = "0.4.4", features = ["async"]}
# ica
base64 = { version = "0.22", optional = true }
ed25519 = { version = "2.2", optional = true }
ed25519-dalek = { version = "2.1", optional = true }
hex = { version = "0.4", optional = true }
# tailchat
reqwest = { version = "0.12", optional = true, features = ["multipart"] }
md-5 = { version = "0.10", optional = true }
# ica & tailchat (socketio)
rust_socketio = { version = "0.6.0", features = ["async"], optional = true }
# data
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"
toml = "0.8"
toml_edit = "0.22"
colored = "3.0"
chrono = "0.4.34"
toml = "0.8.10"
colored = "2.1.0"
# runtime
tokio = { version = "1.43", features = ["rt-multi-thread", "time", "signal", "macros"] }
futures-util = "0.3"
pyo3 = { version = "0.24", features = ["experimental-async"] }
anyhow = { version = "1.0", features = ["backtrace"] }
# async 这玩意以后在搞
tokio = { version = "1.0", features = ["full"] }
futures-util = "0.3.30"
pyo3 = "0.20.2"
# pyo3-async = "0.3.2"
# pyo3-asyncio = { version = "0.20.0", features = ["attributes", "tokio-runtime"] }
pyo3-asyncio = { version = "0.20.0", features = ["attributes", "tokio-runtime"] }
# log
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["time"] }
foldhash = "0.1.4"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["time"] }
[patch.crates-io]
rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "mult_payload" }
# pyo3 = { git = "https://github.com/PyO3/pyo3.git", branch = "main" }

82
ica-rs/ica_typing.py Normal file
View File

@ -0,0 +1,82 @@
# Python 兼容版本 3.8+
from typing import Optional
class IcaStatus:
@property
def login(self) -> bool:
...
@property
def online(self) -> bool:
...
@property
def self_id(self) -> Optional[bool]:
...
@property
def nick_name(self) -> Optional[str]:
...
@property
def ica_version(self) -> Optional[str]:
...
@property
def os_info(self) -> Optional[str]:
...
@property
def resident_set_size(self) -> Optional[str]:
...
@property
def head_used(self) -> Optional[str]:
...
@property
def load_average(self) -> Optional[str]:
...
class ReplyMessage:
...
class SendMessage:
...
class NewMessage:
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:
...
@property
def is_reply(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 or msg.is_reply):
if msg.content == "/bmcl":
bmcl(msg, client)

View File

@ -1,25 +0,0 @@
# cargo fmt config
# 最大行长
max_width = 100
# 链式调用的最大长度
chain_width = 80
# 数组的最大长度
array_width = 70
# 函数参数的最大长度
attr_fn_like_width = 60
# 函数调用参数的最大长度
fn_call_width = 80
# 简单函数格式化为单行
fn_single_line = true
# 自动对齐最大长度
enum_discrim_align_threshold = 5
# 字段初始化使用简写
use_field_init_shorthand = true
# 是否使用彩色输出
color = "Always"
edition = "2021"
# 这样不用 nightly 也可以使用 unstable 特性
unstable_features = true

106
ica-rs/src/client.rs Normal file
View File

@ -0,0 +1,106 @@
use crate::config::IcaConfig;
use crate::data_struct::messages::SendMessage;
use crate::data_struct::{all_rooms::Room, online_data::OnlineData};
use crate::ClientStatus;
use colored::Colorize;
use ed25519_dalek::{Signature, Signer, SigningKey};
use rust_socketio::asynchronous::Client;
use rust_socketio::Payload;
use serde_json::Value;
use tracing::{debug, warn};
/// "安全" 的 发送一条消息
pub async fn send_message(client: &Client, message: &SendMessage) -> bool {
let value = message.as_value();
match client.emit("sendMessage", value).await {
Ok(_) => {
debug!("send_message {}", format!("{:#?}", message).cyan());
true
}
Err(e) => {
warn!("send_message faild:{}", format!("{:#?}", e).red());
false
}
}
}
#[derive(Debug, Clone)]
pub struct IcalinguaStatus {
pub login: bool,
pub online_data: Option<OnlineData>,
pub rooms: Option<Vec<Room>>,
pub config: Option<IcaConfig>,
}
impl IcalinguaStatus {
pub fn new() -> Self {
Self {
login: false,
online_data: None,
rooms: None,
config: Some(IcaConfig::new_from_cli()),
}
}
pub fn update_online_data(&mut self, online_data: OnlineData) {
self.online_data = Some(online_data);
}
pub fn update_rooms(&mut self, rooms: Vec<Room>) {
self.rooms = Some(rooms);
}
pub fn update_login_status(&mut self, login: bool) {
self.login = login;
}
pub fn update_config(&mut self, config: IcaConfig) {
self.config = Some(config);
}
pub fn get_online_data() -> &'static OnlineData {
unsafe {
ClientStatus
.online_data
.as_ref()
.expect("online_data should be set")
}
}
pub fn get_config() -> &'static IcaConfig {
unsafe { ClientStatus.config.as_ref().expect("config should be set") }
}
}
pub async fn sign_callback(payload: Payload, client: Client) {
// 获取数据
let require_data = match payload {
Payload::Text(json_value) => Some(json_value),
_ => None,
}
.expect("Payload should be Json data");
let (auth_key, version) = (&require_data[0], &require_data[1]);
debug!("auth_key: {:?}, version: {:?}", auth_key, version);
let auth_key = match &require_data.get(0) {
Some(Value::String(auth_key)) => Some(auth_key),
_ => None,
}
.expect("auth_key should be string");
let salt = hex::decode(auth_key).expect("Got an invalid salt from the server");
// 签名
let private_key = IcalinguaStatus::get_config().private_key.clone();
let array_key: [u8; 32] = hex::decode(private_key)
.expect("Not a vaild pub key")
.try_into()
.expect("Not a vaild pub key");
let signing_key: SigningKey = SigningKey::from_bytes(&array_key);
let signature: Signature = signing_key.sign(salt.as_slice());
let sign = signature.to_bytes().to_vec();
client
.emit("auth", sign)
.await
.expect("Faild to send signin data");
}

View File

@ -1,12 +1,9 @@
use std::env;
use std::fs;
use colored::Colorize;
use serde::Deserialize;
use toml::from_str;
use crate::data_struct::{ica, tailchat};
/// Icalingua bot 的配置
#[derive(Debug, Clone, Deserialize)]
pub struct IcaConfig {
@ -15,118 +12,27 @@ pub struct IcaConfig {
/// icalingua 服务器地址
pub host: String,
/// bot 的 qq
pub self_id: ica::UserId,
pub self_id: u64,
/// 提醒的房间
#[serde(default = "default_empty_i64_vec")]
pub notice_room: Vec<ica::RoomId>,
pub notice_room: Vec<i64>,
/// 是否提醒
#[serde(default = "default_false")]
pub notice_start: bool,
/// 管理员列表
#[serde(default = "default_empty_i64_vec")]
pub admin_list: Vec<ica::UserId>,
/// 过滤列表
#[serde(default = "default_empty_i64_vec")]
pub filter_list: Vec<ica::UserId>,
pub admin_list: Vec<i64>,
/// Python 插件路径
pub py_plugin_path: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TailchatConfig {
/// 服务器地址
pub host: String,
/// 机器人 App ID
pub app_id: String,
/// 机器人 App Secret
pub app_secret: String,
/// 提醒的房间
pub notice_room: Vec<(tailchat::GroupId, tailchat::ConverseId)>,
/// 是否提醒
#[serde(default = "default_false")]
pub notice_start: bool,
/// 管理员列表
#[serde(default = "default_empty_str_vec")]
pub admin_list: Vec<tailchat::UserId>,
/// 过滤列表
#[serde(default = "default_empty_str_vec")]
pub filter_list: Vec<tailchat::UserId>,
}
fn default_plugin_path() -> String { "./plugins".to_string() }
fn default_config_path() -> String { "./config".to_string() }
#[derive(Debug, Clone, Deserialize)]
pub struct PyConfig {
/// 插件路径
#[serde(default = "default_plugin_path")]
pub plugin_path: String,
/// 配置文件夹路径
#[serde(default = "default_config_path")]
pub config_path: String,
}
fn default_empty_i64_vec() -> Vec<i64> { Vec::new() }
fn default_empty_str_vec() -> Vec<String> { Vec::new() }
fn default_false() -> bool { false }
/// 主配置
#[derive(Debug, Clone, Deserialize)]
pub struct BotConfig {
/// 是否启用 icalingua
#[serde(default = "default_false")]
pub enable_ica: bool,
/// Ica 配置
pub ica: Option<IcaConfig>,
/// 是否启用 Tailchat
#[serde(default = "default_false")]
pub enable_tailchat: bool,
/// Tailchat 配置
pub tailchat: Option<TailchatConfig>,
/// 是否启用 Python 插件
#[serde(default = "default_false")]
pub enable_py: bool,
/// Python 插件配置
pub py: Option<PyConfig>,
}
impl BotConfig {
impl IcaConfig {
pub fn new_from_path(config_file_path: String) -> Self {
// try read config from file
let config = fs::read_to_string(&config_file_path).expect("Failed to read config file");
let ret: Self = from_str(&config).unwrap_or_else(|e| {
panic!("Failed to parse config file {}\ne:{:?}", &config_file_path, e)
});
let ret: Self = from_str(&config)
.expect(format!("Failed to parse config file {}", &config_file_path).as_str());
ret
}
pub fn new_from_cli() -> Self {
// let config_file_path = env::args().nth(1).expect("No config path given");
// -c <config_file_path>
let mut config_file_path = "./config.toml".to_string();
let mut args = env::args();
while let Some(arg) = args.next() {
if arg == "-c" {
config_file_path = args.next().unwrap_or_else(|| {
panic!("{}", "No config path given\nUsage: -c <config_file_path>".red())
});
break;
}
}
let config_file_path = env::args().nth(1).expect("No config path given");
Self::new_from_path(config_file_path)
}
/// 检查是否启用 ica
pub fn check_ica(&self) -> bool { self.enable_ica }
/// 检查是否启用 Tailchat
pub fn check_tailchat(&self) -> bool { self.enable_tailchat }
/// 检查是否启用 Python 插件
pub fn check_py(&self) -> bool { self.enable_py }
pub fn ica(&self) -> IcaConfig { self.ica.clone().expect("No ica config found") }
pub fn tailchat(&self) -> TailchatConfig {
self.tailchat.clone().expect("No tailchat config found")
}
pub fn py(&self) -> PyConfig { self.py.clone().expect("No py config found") }
}

View File

@ -0,0 +1,82 @@
use crate::data_struct::messages::{At, LastMessage};
use crate::data_struct::RoomId;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
/// export default interface Room {
/// roomId: number
/// roomName: string
/// index: number
/// unreadCount: number
/// priority: 1 | 2 | 3 | 4 | 5
/// utime: number
/// users:
/// | [{ _id: 1; username: '1' }, { _id: 2; username: '2' }]
/// | [{ _id: 1; username: '1' }, { _id: 2; username: '2' }, { _id: 3; username: '3' }]
/// at?: boolean | 'all'
/// lastMessage: LastMessage
/// autoDownload?: boolean
/// downloadPath?: string
/// }
#[derive(Debug, Clone)]
pub struct Room {
pub room_id: RoomId,
pub room_name: String,
pub index: i64,
pub unread_count: u64,
pub priority: u8,
pub utime: i64,
/// 我严重怀疑是脱裤子放屁
/// 历史遗留啊,那没事了()
// pub users: JsonValue,
pub at: At,
pub last_message: LastMessage,
pub auto_download: Option<String>,
pub download_path: Option<String>,
}
impl Room {
pub fn new_from_json(json: &JsonValue) -> Self {
let inner = serde_json::from_value::<InnerRoom>(json.clone()).unwrap();
let at = At::new_from_json(&json["at"]);
Self {
room_id: inner.room_id,
room_name: inner.room_name,
index: inner.index,
unread_count: inner.unread_count,
priority: inner.priority,
utime: inner.utime,
// users: inner.users,
at,
last_message: inner.last_message,
auto_download: inner.auto_download,
download_path: inner.download_path,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct InnerRoom {
#[serde(rename = "roomId")]
pub room_id: RoomId,
#[serde(rename = "roomName")]
pub room_name: String,
#[serde(rename = "index")]
pub index: i64,
#[serde(rename = "unreadCount")]
pub unread_count: u64,
#[serde(rename = "priority")]
pub priority: u8,
#[serde(rename = "utime")]
pub utime: i64,
#[serde(rename = "users")]
pub users: JsonValue,
// 忽略 at
#[serde(rename = "lastMessage")]
pub last_message: LastMessage,
#[serde(rename = "autoDownload")]
pub auto_download: Option<String>,
#[serde(rename = "downloadPath")]
pub download_path: Option<String>,
}

View File

@ -19,6 +19,10 @@ pub struct MessageFile {
}
impl MessageFile {
pub fn get_name(&self) -> Option<&String> { self.name.as_ref() }
pub fn get_fid(&self) -> Option<&String> { self.fid.as_ref() }
pub fn get_name(&self) -> Option<&String> {
self.name.as_ref()
}
pub fn get_fid(&self) -> Option<&String> {
self.fid.as_ref()
}
}

View File

@ -1,28 +0,0 @@
pub mod files;
pub mod messages;
pub mod all_rooms;
pub mod online_data;
/// 房间 id
/// 群聊 < 0
/// 私聊 > 0
pub type RoomId = i64;
pub type UserId = i64;
pub type MessageId = String;
#[allow(unused)]
pub trait RoomIdTrait {
/// 判断是否是群聊
fn is_room(&self) -> bool;
/// 判断是否是私聊
fn is_chat(&self) -> bool { !self.is_room() }
fn as_room_id(&self) -> RoomId;
fn as_chat_id(&self) -> RoomId;
}
impl RoomIdTrait for RoomId {
fn is_room(&self) -> bool { (*self).is_negative() }
fn as_room_id(&self) -> RoomId { -(*self).abs() }
fn as_chat_id(&self) -> RoomId { (*self).abs() }
}

View File

@ -1,137 +0,0 @@
use crate::data_struct::ica::messages::{At, LastMessage, SendMessage};
use crate::data_struct::ica::{RoomId, UserId};
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value as JsonValue};
/// export default interface Room {
/// roomId: number
/// roomName: string
/// index: number
/// unreadCount: number
/// priority: 1 | 2 | 3 | 4 | 5
/// utime: number
/// users:
/// | [{ _id: 1; username: '1' }, { _id: 2; username: '2' }]
/// | [{ _id: 1; username: '1' }, { _id: 2; username: '2' }, { _id: 3; username: '3' }]
/// at?: boolean | 'all'
/// lastMessage: LastMessage
/// autoDownload?: boolean
/// downloadPath?: string
/// }
#[derive(Debug, Clone)]
pub struct Room {
pub room_id: RoomId,
pub room_name: String,
pub index: i64,
pub unread_count: u64,
pub priority: u8,
pub utime: i64,
/// 我严重怀疑是脱裤子放屁
/// 历史遗留啊,那没事了()
// pub users: JsonValue,
pub at: At,
pub last_message: LastMessage,
// 这俩都没啥用
// pub auto_download: Option<String>,
// pub download_path: Option<String>,
}
impl Room {
pub fn new_from_json(raw_json: &JsonValue) -> Self {
let mut parse_json = raw_json.clone();
// 手动 patch 一下 roomId
// ica issue: https://github.com/Icalingua-plus-plus/Icalingua-plus-plus/issues/793
if parse_json.get("roomId").is_none_or(|id| id.is_null()) {
use tracing::warn;
warn!("Room::new_from_json roomId is None, patching it to -1, raw: {:?}", raw_json);
parse_json["roomId"] = JsonValue::Number(Number::from(-1));
}
// 现在 fix 了
let inner = match serde_json::from_value::<InnerRoom>(parse_json) {
Ok(data) => data,
Err(e) => {
panic!("Room::new_from_json error: {}, raw: {:#?}", e, raw_json);
}
};
let at = At::new_from_json(&raw_json["at"]);
Self {
room_id: inner.room_id,
room_name: inner.room_name,
index: inner.index,
unread_count: inner.unread_count,
priority: inner.priority,
utime: inner.utime,
// users: inner.users,
at,
last_message: inner.last_message,
// download_path: inner.download_path,
}
}
pub fn new_message_to(&self, content: String) -> SendMessage {
SendMessage::new(content, self.room_id, None)
}
}
fn room_id_default() -> RoomId { -1 }
#[derive(Debug, Clone, Deserialize, Serialize)]
struct InnerRoom {
#[serde(rename = "roomId", default = "room_id_default")]
pub room_id: RoomId,
#[serde(rename = "roomName")]
pub room_name: String,
#[serde(rename = "index")]
pub index: i64,
#[serde(rename = "unreadCount")]
pub unread_count: u64,
#[serde(rename = "priority")]
pub priority: u8,
#[serde(rename = "utime")]
pub utime: i64,
#[serde(rename = "users")]
pub users: JsonValue,
// 忽略 at
#[serde(rename = "lastMessage")]
pub last_message: LastMessage,
// 这俩都没啥用
// #[serde(rename = "autoDownload")]
// pub auto_download: Option<String>,
// #[serde(rename = "downloadPath")]
// pub download_path: Option<String>,
}
/// ```json
/// {
/// "comment": "问题:从哪里了解到的本群\n答案aaa",
/// "flag": "e4cd5a892ba34bed063196a0cc47a8",
/// "group_id": xxxxx,
/// "group_name": "Nuitka 和 Python 打包",
/// "nickname": "jashcken",
/// "post_type": "request",
/// "request_type": "group",
/// "self_id": 45620725,
/// "sub_type": "add",
/// "time": 1743372872,
/// "tips": "",
/// "user_id": 3838663305
/// }
/// ```
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JoinRequestRoom {
/// 问题+答案
pub comment: String,
pub group_id: RoomId,
pub group_name: String,
pub user_id: UserId,
pub nickname: String,
// 剩下的应该没用了……吧?
pub request_type: String,
pub post_type: String,
pub sub_type: String,
pub time: i64,
pub tips: String,
pub flag: String,
}

View File

@ -1,409 +0,0 @@
use crate::data_struct::ica::files::MessageFile;
use crate::data_struct::ica::{MessageId, RoomId, UserId};
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use tracing::warn;
pub mod msg_trait;
pub use msg_trait::MessageTrait;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum At {
All,
Bool(bool),
None,
}
impl At {
#[inline]
pub fn new_from_json(json: &JsonValue) -> Self {
match json {
JsonValue::Bool(b) => Self::Bool(*b),
#[allow(non_snake_case)]
JsonValue::String(_I_dont_Care) => Self::All,
_ => Self::None,
}
}
}
/*export default interface LastMessage {
content?: string
timestamp?: string
username?: string
userId?: number
}
*/
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LastMessage {
pub content: Option<String>,
pub timestamp: Option<String>,
pub username: Option<String>,
// 因为这玩意可能返回 raw buffer, 所以先不解析了
// #[serde(rename = "userId")]
// pub user_id: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ReplyMessage {
#[serde(rename = "_id")]
pub msg_id: String,
pub content: String,
pub files: JsonValue,
#[serde(rename = "username")]
pub sender_name: String,
}
/*
export default interface Message {
_id: string | number
senderId?: number
username: string
content: string
code?: string
timestamp?: string
date?: string
role?: string
file?: MessageFile
files: MessageFile[]
time?: number
replyMessage?: Message
at?: boolean | 'all'
deleted?: boolean
system?: boolean
mirai?: MessageMirai
reveal?: boolean
flash?: boolean
title?: string
anonymousId?: number
anonymousflag?: string
hide?: boolean
bubble_id?: number
subid?: number
head_img?: string
}*/
/// {"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack"},"roomId":-123456}
#[derive(Debug, Clone)]
pub struct Message {
// /// 房间 id
// pub room_id: RoomId,
/// 消息 id
pub msg_id: MessageId,
/// 发送者 id
pub sender_id: UserId,
/// 发送者名字
pub sender_name: String,
/// 消息内容
pub content: String,
/// xml / json 内容
pub code: JsonValue,
/// 消息时间
pub time: DateTime<chrono::Utc>,
/// 身份
pub role: String,
/// 文件
pub files: Vec<MessageFile>,
/// 回复的消息
pub reply: Option<ReplyMessage>,
/// At
pub at: At,
/// 是否已撤回
pub deleted: bool,
/// 是否是系统消息
pub system: bool,
/// mirai?
pub mirai: JsonValue,
/// reveal ?
pub reveal: bool,
/// flash
pub flash: bool,
/// "群主授予的头衔"
pub title: String,
/// anonymous id
pub anonymous_id: Option<i64>,
/// 是否已被隐藏
pub hide: bool,
/// 气泡 id
pub bubble_id: i64,
/// 子? id
pub subid: i64,
/// 头像 img?
pub head_img: JsonValue,
/// 原始消息 (准确来说是 json["message"])
pub raw_msg: JsonValue,
}
impl Message {
pub fn new_from_json(json: &JsonValue) -> Self {
// 消息 id
let msg_id = json["_id"].as_str().unwrap();
// 发送者 id (Optional)
let sender_id = json["senderId"].as_i64().unwrap_or(-1);
// 发送者名字 必有
let sender_name = json["username"].as_str().unwrap();
// 消息内容
let content = json["content"].as_str().unwrap();
// xml / json 内容
let code = json["code"].clone();
// 消息时间 (怎么这个也是可选啊(恼))
// 没有就取当前时间
let current = chrono::Utc::now();
let time = json["time"]
.as_i64()
.map(|t| DateTime::from_timestamp_micros(t).unwrap_or(current))
.unwrap_or(current);
// 身份
let role = json["role"].as_str().unwrap_or("unknown");
// 文件
let value_files = json["files"].as_array().unwrap_or(&Vec::new()).to_vec();
let mut files = Vec::with_capacity(value_files.len());
for file in &value_files {
let file = serde_json::from_value::<MessageFile>(file.clone());
if let Ok(file) = file {
files.push(file);
}
}
// 回复的消息
let reply: Option<ReplyMessage> = match json.get("replyMessage") {
Some(value) => {
if !value.is_null() {
match serde_json::from_value::<ReplyMessage>(value.clone()) {
Ok(reply) => Some(reply),
Err(e) => {
warn!("Failed to parse reply message: {}", e);
None
}
}
} else {
None
}
}
None => None,
};
// At
let at = At::new_from_json(&json["at"]);
// 是否已撤回
let deleted = json["deleted"].as_bool().unwrap_or(false);
// 是否是系统消息
let system = json["system"].as_bool().unwrap_or(false);
// mirai
let mirai = json["mirai"].clone();
// reveal
let reveal = json["reveal"].as_bool().unwrap_or(false);
// flash
let flash = json["flash"].as_bool().unwrap_or(false);
// "群主授予的头衔"
let title = json["title"].as_str().unwrap_or("");
// anonymous id
let anonymous_id = json["anonymousId"].as_i64();
// 是否已被隐藏
let hide = json["hide"].as_bool().unwrap_or(false);
// 气泡 id
let bubble_id = json["bubble_id"].as_i64().unwrap_or(1);
// 子? id
let subid = json["subid"].as_i64().unwrap_or(1);
// 头像 img?
let head_img = json["head_img"].clone();
// 原始消息
let raw_msg = json["message"].clone();
Self {
msg_id: msg_id.to_string(),
sender_id,
sender_name: sender_name.to_string(),
content: content.to_string(),
code,
time,
role: role.to_string(),
files,
reply,
at,
deleted,
system,
mirai,
reveal,
flash,
title: title.to_string(),
anonymous_id,
hide,
bubble_id,
subid,
head_img,
raw_msg,
}
}
pub fn output(&self) -> String {
format!(
// >10 >10 >15
// >10 >15
"{:>12}|{:<20}|{}",
self.sender_id, self.sender_name, self.content
)
}
/// 作为回复消息使用
pub fn as_reply(&self) -> ReplyMessage {
ReplyMessage {
// 虽然其实只要这一条就行
msg_id: self.msg_id.clone(),
// 但是懒得动上面的了, 就这样吧
content: self.content.clone(),
files: json!([]),
sender_name: self.sender_name.clone(),
}
}
/// 获取回复
pub fn get_reply(&self) -> Option<&ReplyMessage> { self.reply.as_ref() }
pub fn get_reply_mut(&mut self) -> Option<&mut ReplyMessage> { self.reply.as_mut() }
}
/// 这才是 NewMessage
#[derive(Debug, Clone, Deserialize)]
pub struct NewMessage {
#[serde(rename = "roomId")]
pub room_id: RoomId,
#[serde(rename = "message")]
pub msg: Message,
}
impl NewMessage {
pub fn new(room_id: RoomId, msg: Message) -> Self { Self { room_id, msg } }
/// 创建一条对这条消息的回复
pub fn reply_with(&self, content: &str) -> SendMessage {
SendMessage::new(content.to_string(), self.room_id, Some(self.msg.as_reply()))
}
/// 作为被删除的消息
pub fn as_deleted(&self) -> DeleteMessage {
DeleteMessage {
room_id: self.room_id,
message_id: self.msg.msg_id.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendMessage {
/// 就是消息内容
pub content: String,
/// 发送的房间 id
#[serde(rename = "roomId")]
pub room_id: RoomId,
/// 回复的消息
#[serde(rename = "replyMessage")]
pub reply_to: Option<ReplyMessage>,
/// @ 谁
#[serde(rename = "at")]
pub at: JsonValue,
/// base64 的图片
#[serde(rename = "b64img")]
file_data: Option<String>,
/// 是否当作表情发送
///
/// 默认 false
pub sticker: bool,
}
impl SendMessage {
pub fn new(content: String, room_id: RoomId, reply_to: Option<ReplyMessage>) -> Self {
Self {
content,
room_id,
reply_to,
at: json!([]),
file_data: None,
sticker: false,
}
}
pub fn as_value(&self) -> JsonValue { serde_json::to_value(self).unwrap() }
/// 设置消息的图片
///
/// as_sticker: 是否当作表情发送
/// file: 图片数据
/// file_type: 图片类型(MIME) (image/png; image/jpeg)
pub fn set_img(&mut self, file: &Vec<u8>, file_type: &str, as_sticker: bool) {
self.sticker = as_sticker;
use base64::{Engine as _, engine::general_purpose};
let base64_data = general_purpose::STANDARD.encode(file);
self.file_data = Some(format!("data:{};base64,{}", file_type, base64_data));
}
}
/// 被删除的消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteMessage {
#[serde(rename = "roomId")]
pub room_id: RoomId,
#[serde(rename = "messageId")]
pub message_id: MessageId,
}
impl DeleteMessage {
pub fn new(room_id: RoomId, message_id: MessageId) -> Self {
Self {
room_id,
message_id,
}
}
pub fn as_value(&self) -> JsonValue { serde_json::to_value(self).unwrap() }
}
// #[cfg(test)]
// mod test {
// use serde_json::json;
// use super::*;
// #[test]
// fn test_new_from_json() {
// let value = json!({"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack"},"roomId":-123456});
// let new_message = Message::new_from_json(&value);
// assert_eq!(new_message.msg_id, "idddddd");
// assert_eq!(new_message.sender_id, 123456);
// assert_eq!(new_message.sender_name, "shenjack");
// assert_eq!(new_message.content, "test");
// assert_eq!(new_message.role, "admin");
// assert_eq!(
// new_message.time,
// NaiveDateTime::from_timestamp_micros(1708267062000_i64).unwrap()
// );
// assert!(new_message.files.is_empty());
// assert!(new_message.get_reply().is_none());
// assert!(!new_message.is_reply());
// assert!(!new_message.deleted);
// assert!(!new_message.system);
// assert!(!new_message.reveal);
// assert!(!new_message.flash);
// assert_eq!(new_message.title, "索引管理员");
// assert!(new_message.anonymous_id.is_none());
// assert!(!new_message.hide);
// assert_eq!(new_message.bubble_id, 0);
// assert_eq!(new_message.subid, 1);
// assert!(new_message.head_img.is_null());
// }
// #[test]
// fn test_parse_reply() {
// let value = json!({"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack", "replyMessage": {"content": "test", "username": "jackyuanjie", "files": [], "_id": "adwadaw"}},"roomId":-123456});
// let new_message = Message::new_from_json(&value);
// assert_eq!(new_message.get_reply().unwrap().sender_name, "jackyuanjie");
// assert_eq!(new_message.get_reply().unwrap().content, "test");
// assert_eq!(new_message.get_reply().unwrap().msg_id, "adwadaw");
// assert!(new_message
// .get_reply()
// .unwrap()
// .files
// .as_array()
// .unwrap()
// .is_empty());
// }
// }

View File

@ -1,164 +0,0 @@
use std::fmt::Display;
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::MainStatus;
use crate::data_struct::ica::messages::{At, Message, NewMessage};
use crate::data_struct::ica::{MessageId, UserId};
impl Serialize for At {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
match self {
At::All => serializer.serialize_str("all"),
At::Bool(b) => serializer.serialize_bool(*b),
At::None => serializer.serialize_none(),
}
}
}
impl<'de> Deserialize<'de> for At {
fn deserialize<D>(deserializer: D) -> Result<At, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let value = JsonValue::deserialize(deserializer)?;
Ok(At::new_from_json(&value))
}
}
#[allow(unused)]
pub trait MessageTrait {
fn is_reply(&self) -> bool;
fn is_from_self(&self) -> bool {
let qq_id = MainStatus::global_ica_status().online_status.qqid;
self.sender_id() == qq_id
}
fn msg_id(&self) -> &MessageId;
fn sender_id(&self) -> UserId;
fn sender_name(&self) -> &String;
fn content(&self) -> &String;
fn time(&self) -> &DateTime<chrono::Utc>;
fn role(&self) -> &String;
fn has_files(&self) -> bool;
fn deleted(&self) -> bool;
fn system(&self) -> bool;
fn reveal(&self) -> bool;
fn flash(&self) -> bool;
fn title(&self) -> &String;
fn anonymous_id(&self) -> Option<i64>;
fn hide(&self) -> bool;
fn bubble_id(&self) -> i64;
fn subid(&self) -> i64;
}
impl MessageTrait for Message {
fn is_reply(&self) -> bool { self.reply.is_some() }
fn msg_id(&self) -> &MessageId { &self.msg_id }
fn sender_id(&self) -> UserId { self.sender_id }
fn sender_name(&self) -> &String { &self.sender_name }
fn content(&self) -> &String { &self.content }
fn time(&self) -> &DateTime<chrono::Utc> { &self.time }
fn role(&self) -> &String { &self.role }
fn has_files(&self) -> bool { !self.files.is_empty() }
fn deleted(&self) -> bool { self.deleted }
fn system(&self) -> bool { self.system }
fn reveal(&self) -> bool { self.reveal }
fn flash(&self) -> bool { self.flash }
fn title(&self) -> &String { &self.title }
fn anonymous_id(&self) -> Option<i64> { self.anonymous_id }
fn hide(&self) -> bool { self.hide }
fn bubble_id(&self) -> i64 { self.bubble_id }
fn subid(&self) -> i64 { self.subid }
}
impl<'de> Deserialize<'de> for Message {
fn deserialize<D>(deserializer: D) -> Result<Message, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let value = JsonValue::deserialize(deserializer)?;
Ok(Message::new_from_json(&value))
}
}
impl Display for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.content.is_empty() && !self.content.trim().is_empty() {
write!(f, "{}|{}|{}|{}", self.msg_id(), self.sender_id, self.sender_name, self.content)
} else if !self.files.is_empty() {
write!(
f,
"{}|{}|{}|{:?}",
self.msg_id(),
self.sender_id,
self.sender_name,
self.files[0].name
)
} else {
write!(
f,
"{}|{}|{}|empty content & empty files",
self.msg_id(),
self.sender_id,
self.sender_name
)
}
}
}
impl MessageTrait for NewMessage {
fn is_reply(&self) -> bool { self.msg.reply.is_some() }
fn msg_id(&self) -> &MessageId { &self.msg.msg_id }
fn sender_id(&self) -> UserId { self.msg.sender_id }
fn sender_name(&self) -> &String { &self.msg.sender_name }
fn content(&self) -> &String { &self.msg.content }
fn time(&self) -> &DateTime<chrono::Utc> { &self.msg.time }
fn role(&self) -> &String { &self.msg.role }
fn has_files(&self) -> bool { !self.msg.files.is_empty() }
fn deleted(&self) -> bool { self.msg.deleted }
fn system(&self) -> bool { self.msg.system }
fn reveal(&self) -> bool { self.msg.reveal }
fn flash(&self) -> bool { self.msg.flash }
fn title(&self) -> &String { &self.msg.title }
fn anonymous_id(&self) -> Option<i64> { self.msg.anonymous_id }
fn hide(&self) -> bool { self.msg.hide }
fn bubble_id(&self) -> i64 { self.msg.bubble_id }
fn subid(&self) -> i64 { self.msg.subid }
}
impl Display for NewMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.msg.content.trim().is_empty() {
write!(
f,
"{}|{}|{}|{}|{}",
self.msg.msg_id,
self.room_id,
self.msg.sender_id,
self.msg.sender_name,
self.msg.content
)
} else if !self.msg.files.is_empty() {
write!(
f,
"{}|{}|{}|{}|{:?}",
self.msg.msg_id,
self.room_id,
self.msg.sender_id,
self.msg.sender_name,
self.msg.files[0]
)
} else {
write!(
f,
"{}|{}|{}|{}|empty content & empty files",
self.msg.msg_id, self.room_id, self.msg.sender_id, self.msg.sender_name
)
}
}
}

View File

@ -0,0 +1,309 @@
use crate::client::IcalinguaStatus;
use crate::data_struct::files::MessageFile;
use crate::data_struct::{MessageId, RoomId, UserId};
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum At {
All,
Bool(bool),
None,
}
impl At {
/// new_from_json(&message["at"])
pub fn new_from_json(json: &JsonValue) -> Self {
match json {
JsonValue::Bool(b) => Self::Bool(*b),
#[allow(non_snake_case)]
JsonValue::String(_I_dont_Care) => Self::All,
_ => Self::None,
}
}
}
/*export default interface LastMessage {
content?: string
timestamp?: string
username?: string
userId?: number
}
*/
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LastMessage {
pub content: Option<String>,
pub timestamp: Option<String>,
pub username: Option<String>,
#[serde(rename = "userId")]
pub user_id: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ReplyMessage {
#[serde(rename = "_id")]
pub msg_id: String,
pub content: String,
pub files: JsonValue,
#[serde(rename = "username")]
pub sender_name: String,
}
/// {"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack"},"roomId":-123456}
#[derive(Debug, Clone)]
pub struct NewMessage {
/// 房间 id
pub room_id: RoomId,
/// 消息 id
pub msg_id: MessageId,
/// 发送者 id
pub sender_id: UserId,
/// 发送者名字
pub sender_name: String,
/// 消息内容
pub content: String,
/// xml / json 内容
pub code: JsonValue,
/// 消息时间
pub time: NaiveDateTime,
/// 身份
pub role: String,
/// 文件
pub files: Vec<MessageFile>,
/// 回复的消息
pub reply: Option<ReplyMessage>,
/// At
pub at: At,
/// 是否已撤回
pub deleted: bool,
/// 是否是系统消息
pub system: bool,
/// mirai?
pub mirai: JsonValue,
/// reveal ?
pub reveal: bool,
/// flash
pub flash: bool,
/// "群主授予的头衔"
pub title: String,
/// anonymous id
pub anonymous_id: Option<i64>,
/// 是否已被隐藏
pub hide: bool,
/// 气泡 id
pub bubble_id: i64,
/// 子? id
pub subid: i64,
/// 头像 img?
pub head_img: JsonValue,
/// 原始消息 (准确来说是 json["message"])
pub raw_msg: JsonValue,
}
impl NewMessage {
pub fn new_from_json(json: &JsonValue) -> Self {
// room id 还是必定有的
let room_id = json["roomId"].as_i64().unwrap();
// message 本体也是
let message = json.get("message").unwrap();
// 消息 id
let msg_id = message["_id"].as_str().unwrap();
// 发送者 id (Optional)
let sender_id = message["senderId"].as_i64().unwrap_or(-1);
// 发送者名字 必有
let sender_name = message["username"].as_str().unwrap();
// 消息内容
let content = message["content"].as_str().unwrap();
// xml / json 内容
let code = message["code"].clone();
// 消息时间 (怎么这个也是可选啊(恼))
// 没有就取当前时间
let current = chrono::Utc::now().naive_utc();
let time = message["time"]
.as_i64()
.map(|t| NaiveDateTime::from_timestamp_micros(t).unwrap_or(current))
.unwrap_or(current);
// 身份
let role = message["role"].as_str().unwrap_or("unknown");
// 文件
let value_files = message["files"].as_array().unwrap_or(&Vec::new()).to_vec();
let mut files = Vec::with_capacity(value_files.len());
for file in &value_files {
let file = serde_json::from_value::<MessageFile>(file.clone());
if let Ok(file) = file {
files.push(file);
}
}
// 回复的消息
let reply: Option<ReplyMessage> = match message.get("replyMessage") {
Some(value) => serde_json::from_value::<ReplyMessage>(value.clone()).ok(),
None => None,
};
// At
let at = At::new_from_json(&message["at"]);
// 是否已撤回
let deleted = message["deleted"].as_bool().unwrap_or(false);
// 是否是系统消息
let system = message["system"].as_bool().unwrap_or(false);
// mirai
let mirai = message["mirai"].clone();
// reveal
let reveal = message["reveal"].as_bool().unwrap_or(false);
// flash
let flash = message["flash"].as_bool().unwrap_or(false);
// "群主授予的头衔"
let title = message["title"].as_str().unwrap_or("");
// anonymous id
let anonymous_id = message["anonymousId"].as_i64();
// 是否已被隐藏
let hide = message["hide"].as_bool().unwrap_or(false);
// 气泡 id
let bubble_id = message["bubble_id"].as_i64().unwrap_or(1);
// 子? id
let subid = message["subid"].as_i64().unwrap_or(1);
// 头像 img?
let head_img = message["head_img"].clone();
// 原始消息
let raw_msg = json["message"].clone();
Self {
room_id,
msg_id: msg_id.to_string(),
sender_id,
sender_name: sender_name.to_string(),
content: content.to_string(),
code,
time,
role: role.to_string(),
files,
reply,
at,
deleted,
system,
mirai,
reveal,
flash,
title: title.to_string(),
anonymous_id,
hide,
bubble_id,
subid,
head_img,
raw_msg,
}
}
/// 作为回复消息使用
pub fn as_reply(&self) -> ReplyMessage {
ReplyMessage {
// 虽然其实只要这一条就行
msg_id: self.msg_id.clone(),
// 但是懒得动上面的了, 就这样吧
content: self.content.clone(),
files: json!([]),
sender_name: self.sender_name.clone(),
}
}
/// 创建一条对这条消息的回复
pub fn reply_with(&self, content: &String) -> SendMessage {
SendMessage::new(content.clone(), self.room_id, Some(self.as_reply()))
}
/// 是否是回复
pub fn is_reply(&self) -> bool {
self.reply.is_some()
}
pub fn is_from_self(&self) -> bool {
let qq_id = IcalinguaStatus::get_online_data().qqid;
self.sender_id == qq_id
}
/// 获取回复
pub fn get_reply(&self) -> Option<&ReplyMessage> {
self.reply.as_ref()
}
pub fn get_reply_mut(&mut self) -> Option<&mut ReplyMessage> {
self.reply.as_mut()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendMessage {
pub content: String,
#[serde(rename = "roomId")]
pub room_id: RoomId,
#[serde(rename = "replyMessage")]
pub reply_to: Option<ReplyMessage>,
#[serde(rename = "at")]
pub at: JsonValue,
}
impl SendMessage {
pub fn new(content: String, room_id: RoomId, reply_to: Option<ReplyMessage>) -> Self {
Self {
content,
room_id,
reply_to,
at: json!([]),
}
}
pub fn as_value(&self) -> JsonValue {
serde_json::to_value(self).unwrap()
}
}
#[cfg(test)]
mod test {
use serde_json::json;
use super::*;
#[test]
fn test_new_from_json() {
let value = json!({"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack"},"roomId":-123456});
let new_message = NewMessage::new_from_json(&value);
assert_eq!(new_message.msg_id, "idddddd");
assert_eq!(new_message.sender_id, 123456);
assert_eq!(new_message.sender_name, "shenjack");
assert_eq!(new_message.content, "test");
assert_eq!(new_message.role, "admin");
assert_eq!(
new_message.time,
NaiveDateTime::from_timestamp_micros(1708267062000_i64).unwrap()
);
assert!(new_message.files.is_empty());
assert!(new_message.get_reply().is_none());
assert!(!new_message.is_reply());
assert!(!new_message.deleted);
assert!(!new_message.system);
assert!(!new_message.reveal);
assert!(!new_message.flash);
assert_eq!(new_message.title, "索引管理员");
assert!(new_message.anonymous_id.is_none());
assert!(!new_message.hide);
assert_eq!(new_message.bubble_id, 0);
assert_eq!(new_message.subid, 1);
assert!(new_message.head_img.is_null());
}
#[test]
fn test_parse_reply() {
let value = json!({"message": {"_id":"idddddd","anonymousId":null,"anonymousflag":null,"bubble_id":0,"content":"test","date":"2024/02/18","files":[],"role":"admin","senderId":123456,"subid":1,"time":1708267062000_i64,"timestamp":"22:37:42","title":"索引管理员","username":"shenjack", "replyMessage": {"content": "test", "username": "jackyuanjie", "files": [], "_id": "adwadaw"}},"roomId":-123456});
let new_message = NewMessage::new_from_json(&value);
assert_eq!(new_message.get_reply().unwrap().sender_name, "jackyuanjie");
assert_eq!(new_message.get_reply().unwrap().content, "test");
assert_eq!(new_message.get_reply().unwrap().msg_id, "adwadaw");
assert!(new_message
.get_reply()
.unwrap()
.files
.as_array()
.unwrap()
.is_empty());
}
}

View File

@ -1,2 +1,9 @@
pub mod ica;
pub mod tailchat;
pub mod files;
pub mod messages;
pub mod all_rooms;
pub mod online_data;
pub type RoomId = i64;
pub type UserId = i64;
pub type MessageId = String;

View File

@ -24,7 +24,7 @@ impl IcalinguaInfo {
let mut load = None;
let mut server_node = None;
let mut client_count = None;
let info_list = s.split('\n').collect::<Vec<&str>>();
let info_list = s.split("\n").collect::<Vec<&str>>();
for info in info_list {
if info.starts_with("icalingua-bridge-oicq") {
ica_version = Some(info.split_at(22).1.to_string());
@ -40,9 +40,9 @@ impl IcalinguaInfo {
server_node = Some(info.split_at(12).1.to_string());
} else if info.ends_with("clients connected") {
client_count = Some(
info.split(' ')
info.split(" ")
.collect::<Vec<&str>>()
.first()
.get(0)
.unwrap_or(&"1")
.parse::<u16>()
.unwrap_or_else(|e| {
@ -141,26 +141,6 @@ impl OnlineData {
}
}
impl Default for OnlineData {
fn default() -> Self {
OnlineData {
bkn: -1,
nick: "UNKNOWN".to_string(),
online: false,
qqid: -1,
icalingua_info: IcalinguaInfo {
ica_version: "UNKNOWN".to_string(),
os_info: "UNKNOWN".to_string(),
resident_set_size: "UNKNOWN".to_string(),
heap_used: "UNKNOWN".to_string(),
load: "UNKNOWN".to_string(),
server_node: "UNKNOWN".to_string(),
client_count: 1,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -214,10 +194,13 @@ mod tests {
}));
assert_eq!(online_data.bkn, 123);
assert_eq!(online_data.nick, "test");
assert!(online_data.online);
assert_eq!(online_data.online, true);
assert_eq!(online_data.qqid, 123456);
assert_eq!(online_data.icalingua_info.ica_version, "2.11.1");
assert_eq!(online_data.icalingua_info.os_info, "Linux c038fad79f13 4.4.302+");
assert_eq!(
online_data.icalingua_info.os_info,
"Linux c038fad79f13 4.4.302+"
);
assert_eq!(online_data.icalingua_info.resident_set_size, "95.43MB");
assert_eq!(online_data.icalingua_info.heap_used, "37.31MB");
assert_eq!(online_data.icalingua_info.load, "4.23 2.15 1.59");
@ -230,7 +213,7 @@ mod tests {
let online_data = OnlineData::new_from_json(&serde_json::json!({}));
assert_eq!(online_data.bkn, -1);
assert_eq!(online_data.nick, "UNKNOWN");
assert!(!online_data.online);
assert_eq!(online_data.online, false);
assert_eq!(online_data.qqid, -1);
assert_eq!(online_data.icalingua_info.ica_version, "UNKNOWN");
assert_eq!(online_data.icalingua_info.os_info, "UNKNOWN");

View File

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

View File

@ -1,8 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct FileUpload {
pub etag: String,
pub path: String,
pub url: String,
}

View File

@ -1,238 +0,0 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ReceiveMessage {
/// 消息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: Option<GroupId>,
/// 会话ID
#[serde(rename = "converseId")]
pub converse_id: ConverseId,
/// 是否有回复?
#[serde(rename = "hasRecall")]
pub has_recall: bool,
/// 暂时懒得解析这玩意
/// 准确来说是不确定内容, 毕竟没细看 API
pub meta: Option<JsonValue>,
/// 也懒得解析这玩意
pub reactions: Vec<JsonValue>,
/// 创建时间
#[serde(rename = "createdAt")]
pub created_at: String,
/// 更新时间
#[serde(rename = "updatedAt")]
pub updated_at: String,
}
impl ReceiveMessage {
pub fn is_reply(&self) -> bool {
if let Some(meta) = &self.meta {
meta.get("reply").is_some()
} else {
false
}
}
pub fn is_from_self(&self) -> bool {
crate::MainStatus::global_tailchat_status().user_id == self.sender_id
}
/// 创建一个对这条消息的回复
pub fn as_reply(&self) -> SendingMessage {
SendingMessage::new(
"".to_string(),
self.converse_id.clone(),
self.group_id.clone(),
Some(ReplyMeta::from_receive_message(self)),
)
}
/// 回复这条消息
pub fn reply_with(&self, content: &str) -> SendingMessage {
SendingMessage::new(
content.to_string(),
self.converse_id.clone(),
self.group_id.clone(),
Some(ReplyMeta::from_receive_message(self)),
)
}
}
impl Display for ReceiveMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// msgid|groupid-converseid|senderid|content
write!(
f,
"{}|{:?}-{}|{}|{}",
self.msg_id, self.group_id, self.converse_id, self.sender_id, self.content
)
}
}
#[derive(Debug, Clone, Default)]
pub enum SendingFile {
#[default]
None,
/// 需要生成
/// [img height=1329 width=1918]{BACKEND}/static/files/6602e20d7b8d10675758e36b/8db505b87bdf9fb309467abcec4d8e2a.png[/img]
Image { file: Vec<u8>, name: String },
/// [card type=file url={BACKEND}/static/files/6602e20d7b8d10675758e36b/9df28943d17b9713cb0ea9625f37d015.wav]Engine.wav[/card]
File { file: Vec<u8>, name: String },
}
impl SendingFile {
pub fn is_some(&self) -> bool { !matches!(self, Self::None) }
pub fn is_image(&self) -> bool { matches!(self, Self::Image { .. }) }
pub fn is_file(&self) -> bool { matches!(self, Self::File { .. }) }
pub fn file_data(&self) -> Vec<u8> {
match self {
Self::Image { file, .. } => file.clone(),
Self::File { file, .. } => file.clone(),
_ => vec![],
}
}
pub fn file_name(&self) -> String {
match self {
Self::Image { name, .. } => name.clone(),
Self::File { name, .. } => name.clone(),
_ => "".to_string(),
}
}
pub fn gen_markdown(&self, backend_path: &str) -> String {
match self {
Self::Image { .. } => {
format!("[img]{}[/img]", backend_path)
}
Self::File { name, .. } => {
format!("[card type=file url={}]{}[/card]", backend_path, name)
}
_ => unreachable!(),
}
}
}
#[derive(Debug, Clone, Serialize)]
/// 将要发送的消息
///
/// 发送时:
/// - `content`: 回复的消息内容
/// - `converseId`: 会话ID
/// - `groupId`: 服务器ID
/// - `meta`: 回复的消息的元数据 可能为空
/// - `mentions`: 被回复的人的ID (可以是多个)
/// - `reply`: 被回复的消息
/// - `_id`: 被回复的消息ID
/// - `author`: 被回复的消息的发送者ID
/// - `content`: 被回复的消息内容
pub struct SendingMessage {
/// 消息内容
///
/// 其实还有个 plain, 就是不知道干啥的
pub content: String,
/// 会话ID
#[serde(rename = "converseId")]
pub converse_id: ConverseId,
/// 服务器ID
#[serde(rename = "groupId")]
pub group_id: Option<GroupId>,
/// 消息的元数据
pub meta: Option<ReplyMeta>,
/// 额外携带的文件
#[serde(skip)]
pub file: SendingFile,
}
impl SendingMessage {
pub fn new(
content: String,
converse_id: ConverseId,
group_id: Option<GroupId>,
meta: Option<ReplyMeta>,
) -> Self {
Self {
content,
converse_id,
group_id,
meta,
file: SendingFile::None,
}
}
pub fn new_without_meta(
content: String,
converse_id: ConverseId,
group_id: Option<GroupId>,
) -> Self {
Self {
content,
converse_id,
group_id,
meta: None,
file: SendingFile::None,
}
}
pub fn contain_file(&self) -> bool { self.file.is_some() }
pub fn add_img(&mut self, file: SendingFile) { self.file = file; }
pub fn as_value(&self) -> JsonValue { serde_json::to_value(self).unwrap() }
}
#[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 ReplyMeta {
pub fn from_receive_message(msg: &ReceiveMessage) -> Self {
Self {
mentions: vec![msg.sender_id.clone()],
reply_id: msg.msg_id.clone(),
reply_author: msg.sender_id.clone(),
reply_content: msg.content.clone(),
}
}
pub fn add_mention(&mut self, user_id: UserId) { self.mentions.push(user_id); }
pub fn replace_content(&mut self, content: String) { self.reply_content = content; }
}
impl Serialize for ReplyMeta {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let reply = json! {
{
"_id": self.reply_id,
"author": self.reply_author,
"content": 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)
}
}

View File

@ -1,65 +0,0 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use crate::data_struct::tailchat::UserId;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoginData {
pub jwt: String,
#[serde(rename = "userId")]
pub user_id: UserId,
pub email: String,
pub nickname: String,
pub avatar: String,
}
impl LoginData {
pub fn update_to_global(&self) {
let status = crate::status::tailchat::MainStatus {
enable: true,
login: true,
user_id: self.user_id.clone(),
nick_name: self.nickname.clone(),
email: self.email.clone(),
jwt_token: self.jwt.clone(),
avatar: self.avatar.clone(),
};
crate::MainStatus::update_tailchat_status(status);
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UpdateDMConverse {
/// 会话ID
#[serde(rename = "_id")]
pub id: String,
/// 创建时间
#[serde(rename = "createdAt")]
pub created_at: String,
/// 成员
pub members: Vec<UserId>,
/// 类型
#[serde(rename = "type")]
pub converse_type: String,
/// 更新时间
#[serde(rename = "updatedAt")]
pub updated_at: String,
}
#[allow(unused)]
pub type Writeable<T> = Arc<RwLock<T>>;
#[allow(unused)]
#[derive(Debug, Clone)]
pub struct BotStatus {
user_id: UserId,
}
#[allow(unused)]
impl BotStatus {
pub fn new(user_id: UserId) -> Self { Self { user_id } }
pub fn get_user_id(&self) -> UserId { self.user_id.clone() }
}

View File

@ -1,122 +0,0 @@
// use thiserror::Error;
pub type ClientResult<T, E> = Result<T, E>;
#[derive(Debug)]
pub enum IcaError {
/// Socket IO 链接错误
SocketIoError(rust_socketio::error::Error),
/// 登录失败
LoginFailed(String),
}
#[derive(Debug)]
pub enum TailchatError {
/// Socket IO 链接错误
SocketIoError(rust_socketio::error::Error),
/// reqwest 相关错误
ReqwestError(reqwest::Error),
/// 登录失败
LoginFailed(String),
}
#[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),
/// 插件停不下来!
PluginNotStopped,
}
impl From<rust_socketio::Error> for IcaError {
fn from(e: rust_socketio::Error) -> Self { IcaError::SocketIoError(e) }
}
impl From<rust_socketio::Error> for TailchatError {
fn from(e: rust_socketio::Error) -> Self { TailchatError::SocketIoError(e) }
}
impl From<reqwest::Error> for TailchatError {
fn from(e: reqwest::Error) -> Self { TailchatError::ReqwestError(e) }
}
impl std::fmt::Display for IcaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IcaError::SocketIoError(e) => write!(f, "Socket IO 链接错误: {}", e),
IcaError::LoginFailed(e) => write!(f, "登录失败: {}", e),
}
}
}
impl std::fmt::Display for TailchatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TailchatError::SocketIoError(e) => write!(f, "Socket IO 链接错误: {}", e),
TailchatError::ReqwestError(e) => write!(f, "Reqwest 错误: {}", e),
TailchatError::LoginFailed(e) => write!(f, "登录失败: {}", e),
}
}
}
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)
}
PyPluginError::PluginNotStopped => {
write!(f, "插件未停止")
}
}
}
}
impl std::error::Error for IcaError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
IcaError::SocketIoError(e) => Some(e),
IcaError::LoginFailed(_) => None,
}
}
}
impl std::error::Error for TailchatError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
TailchatError::SocketIoError(e) => Some(e),
TailchatError::ReqwestError(e) => Some(e),
TailchatError::LoginFailed(_) => None,
}
}
}
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(e, _, _) => Some(e),
PyPluginError::PluginNotStopped => None,
}
}
}

158
ica-rs/src/events.rs Normal file
View File

@ -0,0 +1,158 @@
use colored::Colorize;
use rust_socketio::asynchronous::Client;
use rust_socketio::{Event, Payload};
use tracing::{info, warn};
use crate::client::send_message;
use crate::data_struct::all_rooms::Room;
use crate::data_struct::messages::NewMessage;
use crate::data_struct::online_data::OnlineData;
use crate::{py, VERSION};
/// 获取在线数据
pub async fn get_online_data(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let online_data = OnlineData::new_from_json(value);
info!(
"update_online_data {}",
format!("{:#?}", online_data).cyan()
);
unsafe {
crate::ClientStatus.update_online_data(online_data);
}
}
}
}
/// 接收消息
pub async fn add_message(payload: Payload, client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let message = NewMessage::new_from_json(value);
info!("add_message {}", format!("{:#?}", message).cyan());
// if message.is_reply() {
// return;
// }
// if message.is_from_self() {
// return;
// }
// 就在这里处理掉最基本的消息
// 之后的处理交给插件
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));
send_message(&client, &reply).await;
}
// python 插件
py::new_message_py(&message, &client).await;
}
}
}
/// 撤回消息
pub async fn delete_message(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
// 消息 id
if let Some(value) = values.first() {
if let Some(msg_id) = value.as_str() {
warn!("delete_message {}", format!("{}", msg_id).yellow());
}
}
}
}
pub async fn update_all_room(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
if let Some(raw_rooms) = value.as_array() {
let rooms: Vec<Room> = raw_rooms
.iter()
.map(|room| Room::new_from_json(room))
.collect();
unsafe {
crate::ClientStatus.update_rooms(rooms.clone());
}
info!("update_all_room {}", rooms.len());
}
}
}
}
/// 所有
pub async fn any_event(event: Event, payload: Payload, _client: Client) {
let handled = vec![
// 真正处理过的
"authSucceed",
"authFailed",
"authRequired",
"requireAuth",
"onlineData",
"addMessage",
"deleteMessage",
"setAllRooms",
// 忽略的
"notify",
"closeLoading", // 发送消息/加载新聊天 有一个 loading
"updateRoom",
];
match &event {
Event::Custom(event_name) => {
if handled.contains(&event_name.as_str()) {
return;
}
}
Event::Message => {
match payload {
Payload::Text(values) => {
if let Some(value) = values.first() {
if handled.contains(&value.as_str().unwrap()) {
return;
}
info!("收到消息 {}", value.to_string().yellow());
}
}
_ => (),
}
return;
}
_ => (),
}
match payload {
Payload::Binary(ref data) => {
println!("event: {} |{:?}", event, data)
}
Payload::Text(ref data) => {
print!("event: {}", event.as_str().purple());
for value in data {
println!("|{}", value.to_string());
}
}
_ => (),
}
}
pub async fn connect_callback(payload: Payload, _client: Client) {
match payload {
Payload::Text(values) => {
if let Some(value) = values.first() {
match value.as_str() {
Some("authSucceed") => {
info!("{}", "已经登录到 icalingua!".green())
}
Some("authFailed") => {
info!("{}", "登录到 icalingua 失败!".red());
panic!("登录失败")
}
Some("authRequired") => {
warn!("{}", "需要登录到 icalingua!".yellow())
}
Some(msg) => {
warn!("未知消息 {}", msg.yellow())
}
None => (),
}
}
}
_ => (),
}
}

View File

@ -1,133 +0,0 @@
pub mod client;
pub mod events;
// use std::sync::OnceLock;
use colored::Colorize;
use rust_socketio::asynchronous::{Client, ClientBuilder};
use rust_socketio::{Event, Payload, TransportType};
use rust_socketio::{async_any_callback, async_callback};
use tracing::{Level, event, span};
use crate::config::IcaConfig;
use crate::error::{ClientResult, IcaError};
use crate::{StopGetter, version_str};
/// icalingua 客户端的兼容版本号
pub const ICA_PROTOCOL_VERSION: &str = "2.12.28";
// mod status {
// use crate::data_struct::ica::all_rooms::Room;
// pub use crate::data_struct::ica::online_data::OnlineData;
// #[derive(Debug, Clone)]
// pub struct MainStatus {
// /// 是否启用 ica
// pub enable: bool,
// /// qq 是否登录
// pub qq_login: bool,
// /// 当前已加载的消息数量
// pub current_loaded_messages_count: u64,
// /// 房间数据
// pub rooms: Vec<Room>,
// /// 在线数据 (Icalingua 信息)
// pub online_status: OnlineData,
// }
// impl MainStatus {
// pub fn update_rooms(&mut self, room: Vec<Room>) { self.rooms = room; }
// pub fn update_online_status(&mut self, status: OnlineData) { self.online_status = status; }
// }
// }
// static ICA_STATUS: OnceLock<status::MainStatus> = OnceLock::new();
pub async fn start_ica(config: &IcaConfig, stop_reciver: StopGetter) -> ClientResult<(), IcaError> {
let span = span!(Level::INFO, "Icalingua Client");
let _enter = span.enter();
event!(Level::INFO, "ica-async-rs v{} initing", crate::ICA_VERSION);
let start_connect_time = std::time::Instant::now();
let socket = match ClientBuilder::new(config.host.clone())
.transport_type(TransportType::Websocket)
.on_any(async_any_callback!(events::any_event))
.on("requireAuth", async_callback!(client::sign_callback))
.on("message", async_callback!(events::connect_callback))
.on("authSucceed", async_callback!(events::connect_callback))
.on("authFailed", async_callback!(events::connect_callback))
.on("messageSuccess", async_callback!(events::success_message))
.on("messageFailed", async_callback!(events::failed_message))
.on("onlineData", async_callback!(events::get_online_data))
.on("setAllRooms", async_callback!(events::update_all_room))
.on("setMessages", async_callback!(events::set_messages))
.on("addMessage", async_callback!(events::add_message))
.on("deleteMessage", async_callback!(events::delete_message))
.on("handleRequest", async_callback!(events::join_request))
.connect()
.await
{
Ok(client) => {
event!(
Level::INFO,
"{}",
format!("socketio connected time: {:?}", start_connect_time.elapsed()).on_cyan()
);
client
}
Err(e) => {
event!(Level::ERROR, "socketio connect failed: {}", e);
return Err(IcaError::SocketIoError(e));
}
};
if config.notice_start {
for room in config.notice_room.iter() {
let startup_msg = crate::data_struct::ica::messages::SendMessage::new(
format!("{}\n启动成功", version_str()),
*room,
None,
);
// 这可是 qq, 要保命
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
event!(Level::INFO, "发送启动消息到房间: {}", room);
if let Err(e) =
socket.emit("sendMessage", serde_json::to_value(startup_msg).unwrap()).await
{
event!(Level::INFO, "启动信息发送失败 房间:{}|e:{}", room, e);
}
}
}
// 等待停止信号
event!(Level::INFO, "{}", "ica client waiting for stop signal".purple());
stop_reciver.await.ok();
event!(Level::INFO, "{}", "socketio client stopping".yellow());
match socket.disconnect().await {
Ok(_) => {
event!(Level::INFO, "{}", "socketio client stopped".green());
Ok(())
}
Err(e) => {
// 单独处理 SocketIoError(IncompleteResponseFromEngineIo(WebsocketError(AlreadyClosed)))
match e {
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e) => {
if inner_e.to_string().contains("AlreadyClosed") {
event!(Level::INFO, "{}", "socketio client stopped".green());
Ok(())
} else {
event!(Level::ERROR, "socketio 客户端出现了 Error: {:?}", inner_e);
Err(IcaError::SocketIoError(
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e),
))
}
}
e => {
event!(Level::ERROR, "socketio client stopped with error: {}", e);
Err(IcaError::SocketIoError(e))
}
}
}
}
}

View File

@ -1,144 +0,0 @@
use crate::MainStatus;
use crate::data_struct::ica::messages::{DeleteMessage, SendMessage};
use crate::data_struct::ica::{RoomId, RoomIdTrait, UserId};
use crate::error::{ClientResult, IcaError};
use colored::Colorize;
use ed25519_dalek::{Signature, Signer, SigningKey};
use rust_socketio::Payload;
use rust_socketio::asynchronous::Client;
use serde_json::{Value, json};
use tracing::{Level, event, span};
/// "安全" 的 发送一条消息
pub async fn send_message(client: &Client, message: &SendMessage) -> bool {
let value = message.as_value();
match client.emit("sendMessage", value).await {
Ok(_) => {
event!(Level::INFO, "send_message {}", format!("{:#?}", message).cyan());
true
}
Err(e) => {
event!(Level::WARN, "send_message faild:{}", format!("{:#?}", e).red());
false
}
}
}
/// "安全" 的 删除一条消息
pub async fn delete_message(client: &Client, message: &DeleteMessage) -> bool {
let value = message.as_value();
match client.emit("deleteMessage", value).await {
Ok(_) => {
event!(Level::DEBUG, "delete_message {}", format!("{:#?}", message).yellow());
true
}
Err(e) => {
event!(Level::WARN, "delete_message faild:{}", format!("{:#?}", e).red());
false
}
}
}
/// "安全" 的 获取历史消息
/// ```typescript
/// async fetchHistory(messageId: string, roomId: number, currentLoadedMessagesCount: number)
/// ```
// #[allow(dead_code)]
// pub async fn fetch_history(client: &Client, roomd_id: RoomId) -> bool { false }
async fn inner_sign(payload: Payload, client: &Client) -> ClientResult<(), IcaError> {
let span = span!(Level::INFO, "signing icalingua");
let _guard = span.enter();
// 获取数据
let require_data = match payload {
Payload::Text(json_value) => Ok(json_value),
_ => Err(IcaError::LoginFailed("Got a invalid payload".to_string())),
}?;
let (auth_key, version) = (&require_data[0], &require_data[1]);
event!(
Level::INFO,
"服务器发过来的待签名key: {:?}, 服务端版本号: {:?}",
auth_key,
version
);
// 判定和自己的兼容版本号是否 一致
let server_protocol_version = version
.get("protocolVersion")
.unwrap_or(&Value::Null)
.as_str()
.unwrap_or("unknow");
if server_protocol_version != crate::ica::ICA_PROTOCOL_VERSION {
event!(
Level::WARN,
"服务器版本与兼容版本不一致\n服务器协议版本:{:?}\n兼容版本:{}",
version.get("protocolVersion"),
crate::ica::ICA_PROTOCOL_VERSION
);
}
let auth_key = match &require_data.first() {
Some(Value::String(auth_key)) => Ok(auth_key),
_ => Err(IcaError::LoginFailed("Got a invalid auth_key".to_string())),
}?;
let salt = hex::decode(auth_key).expect("Got an invalid salt from the server");
// 签名
let private_key = MainStatus::global_config().ica().private_key.clone();
let array_key: [u8; 32] = hex::decode(private_key)
.expect("配置文件设置的私钥不是一个有效的私钥, 无法使用hex解析")
.try_into()
.expect("配置文件设置的私钥不是一个有效的私钥, 无法转换为[u8; 32]数组");
let signing_key: SigningKey = SigningKey::from_bytes(&array_key);
let signature: Signature = signing_key.sign(salt.as_slice());
// 发送签名
let sign = signature.to_bytes().to_vec();
client.emit("auth", sign).await.expect("发送签名信息失败");
Ok(())
}
/// 签名回调
/// 失败的时候得 panic
pub async fn sign_callback(payload: Payload, client: Client) {
inner_sign(payload, &client).await.expect("Faild to sign");
}
/// 向指定群发送签到信息
///
/// 只能是群啊, 不能是私聊
pub async fn send_room_sign_in(client: &Client, room_id: RoomId) -> bool {
if room_id.is_chat() {
event!(Level::WARN, "不能向私聊发送签到信息");
return false;
}
let data = json!(room_id.abs());
match client.emit("sendGroupSign", data).await {
Ok(_) => {
event!(Level::INFO, "已向群 {} 发送签到信息", room_id);
true
}
Err(e) => {
event!(Level::ERROR, "向群 {} 发送签到信息失败: {}", room_id, e);
false
}
}
}
/// 向某个群/私聊的某个人发送戳一戳
pub async fn send_poke(client: &Client, room_id: RoomId, target: UserId) -> bool {
let data = vec![json!(room_id), json!(target)];
match client.emit("sendGroupPoke", data).await {
Ok(_) => {
event!(Level::INFO, "已向 {} 的 {} 发送戳一戳", room_id, target);
true
}
Err(e) => {
event!(Level::ERROR, "向 {} 的 {} 发送戳一戳失败: {}", room_id, target, e);
false
}
}
}

View File

@ -1,296 +0,0 @@
use colored::Colorize;
use rust_socketio::asynchronous::Client;
use rust_socketio::{Event, Payload};
use serde_json::json;
use tracing::{Level, event, info, span, warn};
use crate::data_struct::ica::RoomId;
use crate::data_struct::ica::all_rooms::{JoinRequestRoom, Room};
use crate::data_struct::ica::messages::{Message, MessageTrait, NewMessage};
use crate::data_struct::ica::online_data::OnlineData;
use crate::ica::client::send_message;
use crate::{MainStatus, VERSION, client_id, help_msg, py, version_str};
/// 获取在线数据
pub async fn get_online_data(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let online_data = OnlineData::new_from_json(value);
event!(Level::DEBUG, "update_online_data {}", format!("{:?}", online_data).cyan());
MainStatus::global_ica_status_mut().update_online_status(online_data);
}
}
}
/// 接收消息
pub async fn add_message(payload: Payload, client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let message: NewMessage = serde_json::from_value(value.clone()).unwrap();
// 检测是否在过滤列表内
if MainStatus::global_config().ica().filter_list.contains(&message.msg.sender_id) {
return;
}
println!("new_msg {}", message.to_string().cyan());
// 就在这里处理掉最基本的消息
// 之后的处理交给插件
if !message.is_from_self() && !message.is_reply() {
if message.content() == "/bot-rs" {
let reply = message.reply_with(&version_str());
send_message(&client, &reply).await;
} else if message.content() == "/bot-ls" {
let reply = message.reply_with(&format!(
"shenbot-py v{}-{}\n{}",
VERSION,
client_id(),
if MainStatus::global_config().check_py() {
py::PyStatus::display()
} else {
"未启用 Python 插件".to_string()
}
));
send_message(&client, &reply).await;
} else if message.content() == "/bot-help" {
let reply = message.reply_with(&help_msg());
send_message(&client, &reply).await;
}
// else if message.content() == "/bot-uptime" {
// let duration = match start_up_time().elapsed() {
// Ok(d) => format!("{:?}", d),
// Err(e) => format!("出问题啦 {:?}", e),
// };
// let reply = message.reply_with(&format!(
// "shenbot 已运行: {}", duration
// ));
// send_message(&client, &reply).await;
// }
if MainStatus::global_config().ica().admin_list.contains(&message.sender_id()) {
// admin 区
// 先判定是否为 admin
let client_id = client_id();
if message.content().starts_with(&format!("/bot-enable-{}", client_id)) {
// 尝试获取后面的信息
if let Some((_, name)) = message.content().split_once(" ") {
match py::PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
}
Some(true) => {
let reply = message.reply_with("无变化, 插件已经启用");
send_message(&client, &reply).await;
}
Some(false) => {
py::PyStatus::get_mut().set_status(name, true);
let reply = message.reply_with("启用插件完成");
send_message(&client, &reply).await;
}
}
}
} else if message.content().starts_with(&format!("/bot-disable-{}", client_id))
{
if let Some((_, name)) = message.content().split_once(" ") {
match py::PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
}
Some(false) => {
let reply = message.reply_with("无变化, 插件已经禁用");
send_message(&client, &reply).await;
}
Some(true) => {
py::PyStatus::get_mut().set_status(name, false);
let reply = message.reply_with("禁用插件完成");
send_message(&client, &reply).await;
}
}
}
} else if message.content() == "/bot-fetch" {
let reply = message.reply_with("正在更新当前群消息");
send_message(&client, &reply).await;
fetch_messages(&client, message.room_id).await;
}
}
}
// python 插件
py::call::ica_new_message_py(&message, &client).await;
}
}
}
/// 理论上不会用到 (因为依赖一个客户端去请求)
/// 但反正实际上还是我去请求, 所以只是暂时
/// 加载一个房间的所有消息
pub async fn set_messages(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let messages: Vec<Message> = serde_json::from_value(value["messages"].clone()).unwrap();
let room_id = value["roomId"].as_i64().unwrap();
info!("set_messages {} len: {}", room_id.to_string().cyan(), messages.len());
}
}
}
/// 撤回消息
pub async fn delete_message(payload: Payload, client: Client) {
if let Payload::Text(values) = payload {
// 消息 id
if let Some(value) = values.first() {
if let Some(msg_id) = value.as_str() {
event!(Level::INFO, "delete_message {}", msg_id.to_string().yellow());
py::call::ica_delete_message_py(msg_id.to_string(), &client).await;
}
}
}
}
pub async fn update_all_room(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
if let Some(raw_rooms) = value.as_array() {
let rooms: Vec<Room> = raw_rooms.iter().map(Room::new_from_json).collect();
event!(Level::DEBUG, "update_all_room {}", rooms.len());
MainStatus::global_ica_status_mut().update_rooms(rooms);
}
}
}
}
pub async fn success_message(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
info!("messageSuccess {}", value.to_string().green());
}
}
}
pub async fn failed_message(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
warn!("messageFailed {}", value.to_string().red());
}
}
}
/// 处理加群申请
///
/// add: 2.0.1
pub async fn join_request(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
match serde_json::from_value::<JoinRequestRoom>(value.clone()) {
Ok(join_room) => {
event!(Level::INFO, "{}", format!("收到加群申请 {:?}", join_room).on_blue());
}
Err(e) => {
event!(
Level::WARN,
"呼叫 shenjack! JoinRequestRoom 的 serde 没写好! {}\nraw: {:#?}",
e,
value
)
}
}
}
}
}
pub async fn fetch_history(client: Client, room: RoomId) { let mut request_body = json!(room); }
pub async fn fetch_messages(client: &Client, room: RoomId) {
let mut request_body = json!(room);
match client.emit("fetchMessages", request_body).await {
Ok(_) => {}
Err(e) => {
event!(Level::WARN, "fetch_messages {}", e);
}
}
}
/// 所有
pub async fn any_event(event: Event, payload: Payload, _client: Client) {
let handled = vec![
// 真正处理过的
"authSucceed",
"authFailed",
"authRequired",
"requireAuth",
"onlineData",
"addMessage",
"deleteMessage",
"setAllRooms",
"setMessages",
"handleRequest", // 处理验证消息 (加入请求之类的)
// 也许以后会用到
"messageSuccess",
"messageFailed",
"setAllChatGroups",
// 忽略的
"notify",
"setShutUp", // 禁言
"syncRead", // 同步已读
"closeLoading", // 发送消息/加载新聊天 有一个 loading
"renewMessage", // 我也不确定到底是啥事件
"requestSetup", // 需要登录
"updateRoom", // 更新房间
];
match &event {
Event::Custom(event_name) => {
if handled.contains(&event_name.as_str()) {
return;
}
}
Event::Message => {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
if handled.contains(&value.as_str().unwrap()) {
return;
}
info!("收到消息 {}", value.to_string().yellow());
}
}
return;
}
_ => (),
}
match payload {
Payload::Binary(ref data) => {
println!("event: {} |{:?}", event, data)
}
Payload::Text(ref data) => {
print!("event: {}", event.as_str().purple());
for value in data {
println!("|{}", value);
}
}
_ => (),
}
}
pub async fn connect_callback(payload: Payload, _client: Client) {
let span = span!(Level::INFO, "ica connect_callback");
let _enter = span.enter();
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
match value.as_str() {
Some("authSucceed") => {
event!(Level::INFO, "{}", "已经登录到 icalingua!".green())
}
Some("authFailed") => {
event!(Level::ERROR, "{}", "登录到 icalingua 失败!".red());
panic!("登录失败")
}
Some("authRequired") => {
event!(Level::INFO, "{}", "需要登录到 icalingua!".yellow())
}
Some(msg) => {
event!(Level::INFO, "{}{}", "未知消息".yellow(), msg);
}
_ => (),
}
}
}
}

View File

@ -1,275 +1,93 @@
use std::{
hash::{DefaultHasher, Hash, Hasher},
sync::OnceLock,
time::{Duration, SystemTime},
};
use std::time::Duration;
use futures_util::FutureExt;
use rust_socketio::asynchronous::{Client, ClientBuilder};
use rust_socketio::{Event, Payload, TransportType};
use tracing::info;
mod client;
mod config;
mod data_struct;
mod error;
mod events;
mod py;
mod status;
mod wasms;
#[cfg(feature = "ica")]
mod ica;
#[cfg(feature = "tailchat")]
mod tailchat;
use colored::Colorize;
use config::BotConfig;
use error::PyPluginError;
use tracing::{Level, event, span};
pub static mut MAIN_STATUS: status::BotStatus = status::BotStatus {
#[allow(non_upper_case_globals)]
pub static mut ClientStatus: client::IcalinguaStatus = client::IcalinguaStatus {
login: false,
online_data: None,
rooms: None,
config: None,
ica_status: None,
tailchat_status: None,
};
pub type MainStatus = status::BotStatus;
pub type StopGetter = tokio::sync::oneshot::Receiver<()>;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const ICA_VERSION: &str = "2.0.1";
pub const TAILCHAT_VERSION: &str = "2.0.0";
const HELP_MSG: &str = r#"/bot-rs
rust
/bot-py
python (python插件启用了的话)
/bot-ls
/bot-enable-<client-id> <plugin>
()
/bot-disable-<client-id> <plugin>
()
by shenjackyuanjie"#;
/// 获取帮助信息
pub fn help_msg() -> String {
format!("{}\n{}", version_str(), HELP_MSG).replace("<client-id>", client_id().as_str())
}
static STARTUP_TIME: OnceLock<SystemTime> = OnceLock::new();
pub fn start_up_time() -> SystemTime { *STARTUP_TIME.get().expect("WTF, why did you panic?") }
/// 获得当前客户端的 id
/// 防止串号
pub fn client_id() -> String {
let mut hasher = DefaultHasher::new();
start_up_time().hash(&mut hasher);
let data = hasher.finish();
// 取后6位
format!("{:06}", data % 1_000_000)
}
/// 获取版本信息
pub fn version_str() -> String {
format!(
"shenbot-rs v{}{}-[{}] ica v{}({}) tailchat v{}",
VERSION,
if STABLE { "" } else { "-开发版" },
client_id(),
ICA_VERSION,
ica::ICA_PROTOCOL_VERSION,
TAILCHAT_VERSION,
)
}
/// 是否为稳定版本
/// 会在 release 的时候设置为 true
pub const STABLE: bool = false;
#[macro_export]
macro_rules! async_callback_with_state {
($f:expr, $state:expr) => {{
use futures_util::FutureExt;
let state = $state.clone();
move |payload: Payload, client: Client| $f(payload, client, state.clone()).boxed()
}};
}
#[macro_export]
macro_rules! async_any_callback_with_state {
($f:expr, $state:expr) => {{
use futures_util::FutureExt;
let state = $state.clone();
move |event: Event, payload: Payload, client: Client| {
$f(event, payload, client, state.clone()).boxed()
}
}};
}
const CLI_HELP_MSG: &str = r#"{VERSION}
-d
debug
-t
trace
-h
-env <env>
-c <config_file_path>
"#;
fn main() -> anyhow::Result<()> {
let start_up_time = SystemTime::now();
STARTUP_TIME.set(start_up_time).expect("WTF, why did you panic?");
// -d -> debug
// none -> info
let args = std::env::args();
let args = args.collect::<Vec<String>>();
if args.contains(&"-h".to_string()) {
println!("{}", CLI_HELP_MSG.replace("{VERSION}", version_str().as_str()));
return Ok(());
}
let level = {
if args.contains(&"-d".to_string()) {
Level::DEBUG
} else if args.contains(&"-t".to_string()) {
Level::TRACE
} else {
Level::INFO
}
macro_rules! wrap_callback {
($f:expr) => {
|payload: Payload, client: Client| $f(payload, client).boxed()
};
tracing_subscriber::fmt().with_max_level(level).init();
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_name("shenbot-rs")
.worker_threads(10)
.build()
.unwrap();
let result = rt.block_on(inner_main());
event!(Level::INFO, "shenbot-rs v{} exiting", VERSION);
match result {
Ok(_) => {}
Err(e) => {
if let Some(PyPluginError::PluginNotStopped) = e.downcast_ref::<PyPluginError>() {
event!(Level::WARN, "Python 插件停不下来, 3s 后终止 tokio rt");
rt.shutdown_timeout(Duration::from_secs(3));
} else {
event!(Level::ERROR, "shenbot-rs v{} exiting with error: {}", VERSION, e);
}
}
}
Ok(())
}
async fn inner_main() -> anyhow::Result<()> {
let span = span!(Level::INFO, "bot-main");
let _enter = span.enter();
event!(Level::INFO, "shenbot-rs v{} starting", VERSION);
if !STABLE {
event!(Level::WARN, "这是一个开发版本, 有问题记得找 shenjack");
}
let bot_config = BotConfig::new_from_cli();
MainStatus::static_init(bot_config);
let bot_config = MainStatus::global_config();
if bot_config.check_py() {
py::init_py();
}
// 准备一个用于停止 socket 的变量
let (ica_send, ica_recv) = tokio::sync::oneshot::channel::<()>();
if bot_config.check_ica() {
event!(Level::INFO, "{}", "开始启动 ICA".green());
let config = bot_config.ica();
tokio::spawn(async move {
ica::start_ica(&config, ica_recv).await.unwrap();
});
} else {
event!(Level::INFO, "{}", "ica 未启用, 不管他".cyan());
}
let (tailchat_send, tailchat_recv) = tokio::sync::oneshot::channel::<()>();
if bot_config.check_tailchat() {
event!(Level::INFO, "{}", "开始启动 tailchat".green());
let config = bot_config.tailchat();
tokio::spawn(async move {
tailchat::start_tailchat(config, tailchat_recv).await.unwrap();
});
} else {
event!(Level::INFO, "{}", "tailchat 未启用, 不管他".bright_magenta());
}
tokio::time::sleep(Duration::from_secs(1)).await;
// 等待一个输入
event!(Level::INFO, "Press ctrl+c to exit, second ctrl+c to force exit");
tokio::signal::ctrl_c().await.ok();
ica_send.send(()).ok();
tailchat_send.send(()).ok();
event!(Level::INFO, "Disconnected");
py::post_py().await?;
event!(Level::INFO, "Shenbot-rs exiting");
Ok(())
macro_rules! wrap_any_callback {
($f:expr) => {
|event: Event, payload: Payload, client: Client| $f(event, payload, client).boxed()
};
}
#[allow(dead_code, unused_variables)]
#[cfg(test)]
#[tokio::test]
async fn test_macro() {
use std::sync::Arc;
use tokio::sync::RwLock;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
use rust_socketio::Payload;
use rust_socketio::asynchronous::{Client, ClientBuilder};
/// 一个简单的例子
#[derive(Clone)]
struct BotState(String);
/// 一个复杂一些的例子
#[derive(Clone)]
struct BotState2 {
pub name: Arc<RwLock<String>>,
// 从命令行获取 host 和 key
// 从命令行获取配置文件路径
let ica_config = config::IcaConfig::new_from_cli();
unsafe {
ClientStatus.update_config(ica_config.clone());
}
py::init_py(&ica_config);
async fn some_event_with_state(payload: Payload, client: Client, state: Arc<BotState>) {
// do something with your state
}
async fn some_state_change_event(payload: Payload, client: Client, state: Arc<BotState2>) {
if let Payload::Text(text) = payload {
if let Some(first_one) = text.first() {
let new_name = first_one.as_str().unwrap_or_default();
let old_name = state.name.read().await;
if new_name != *old_name {
// update your name here
*state.name.write().await = new_name.to_string();
}
}
}
}
let state = Arc::new(BotState("hello".to_string()));
let state2 = Arc::new(BotState2 {
name: Arc::new(RwLock::new("hello".to_string())),
});
let socket = ClientBuilder::new("http://example.com")
.on("message", async_callback_with_state!(some_event_with_state, state))
.on("update_status", async_callback_with_state!(some_state_change_event, state2))
let socket = ClientBuilder::new(ica_config.host.clone())
.transport_type(TransportType::Websocket)
.on_any(wrap_any_callback!(events::any_event))
.on("requireAuth", wrap_callback!(client::sign_callback))
.on("message", wrap_callback!(events::connect_callback))
.on("authSucceed", wrap_callback!(events::connect_callback))
.on("authFailed", wrap_callback!(events::connect_callback))
.on("onlineData", wrap_callback!(events::get_online_data))
.on("setAllRooms", wrap_callback!(events::update_all_room))
.on("addMessage", wrap_callback!(events::add_message))
.on("deleteMessage", wrap_callback!(events::delete_message))
.connect()
.await;
.await
.expect("Connection failed");
info!("Connected");
if ica_config.notice_start {
for room in ica_config.notice_room.iter() {
let startup_msg = crate::data_struct::messages::SendMessage::new(
format!("ica-async-rs bot v{}", VERSION),
room.clone(),
None,
);
std::thread::sleep(Duration::from_secs(1));
info!("发送启动消息到房间: {}", room);
if let Err(e) = socket
.emit("sendMessage", serde_json::to_value(startup_msg).unwrap())
.await
{
info!("启动信息发送失败 房间:{}|e:{}", room, e);
}
}
}
std::thread::sleep(Duration::from_secs(3));
// 等待一个输入
info!("Press any key to exit");
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
socket.disconnect().await.expect("Disconnect failed");
info!("Disconnected");
}

View File

@ -1,255 +0,0 @@
use std::path::PathBuf;
use std::sync::LazyLock;
use pyo3::prelude::*;
use rust_socketio::asynchronous::Client;
use tokio::sync::Mutex;
use tracing::{Level, event, info, warn};
use crate::MainStatus;
use crate::data_struct::{ica, tailchat};
use crate::error::PyPluginError;
use crate::py::consts::events_func;
use crate::py::{PyPlugin, PyStatus, class};
pub struct PyTasks {
pub ica_new_message: Vec<tokio::task::JoinHandle<()>>,
pub ica_delete_message: Vec<tokio::task::JoinHandle<()>>,
pub tailchat_new_message: Vec<tokio::task::JoinHandle<()>>,
}
impl PyTasks {
pub fn push_ica_new_message(&mut self, handle: tokio::task::JoinHandle<()>) {
self.ica_new_message.push(handle);
self.ica_new_message.retain(|handle| !handle.is_finished());
}
pub fn push_ica_delete_message(&mut self, handle: tokio::task::JoinHandle<()>) {
self.ica_delete_message.push(handle);
self.ica_delete_message.retain(|handle| !handle.is_finished());
}
pub fn push_tailchat_new_message(&mut self, handle: tokio::task::JoinHandle<()>) {
self.tailchat_new_message.push(handle);
self.tailchat_new_message.retain(|handle| !handle.is_finished());
}
pub async fn join_all(&mut self) {
for handle in self.ica_new_message.drain(..) {
let _ = handle.await;
}
for handle in self.ica_delete_message.drain(..) {
let _ = handle.await;
}
for handle in self.tailchat_new_message.drain(..) {
let _ = handle.await;
}
}
pub fn len_check(&mut self) -> usize {
self.ica_delete_message.retain(|handle| !handle.is_finished());
self.ica_new_message.retain(|handle| !handle.is_finished());
self.tailchat_new_message.retain(|handle| !handle.is_finished());
self.ica_new_message.len() + self.ica_delete_message.len() + self.tailchat_new_message.len()
}
pub fn len(&self) -> usize {
self.ica_new_message.len() + self.ica_delete_message.len() + self.tailchat_new_message.len()
}
pub fn is_empty(&self) -> bool { self.len() == 0 }
pub fn cancel_all(&mut self) {
for handle in self.ica_new_message.drain(..) {
handle.abort();
}
for handle in self.ica_delete_message.drain(..) {
handle.abort();
}
for handle in self.tailchat_new_message.drain(..) {
handle.abort();
}
}
}
pub static PY_TASKS: LazyLock<Mutex<PyTasks>> = LazyLock::new(|| {
Mutex::new(PyTasks {
ica_new_message: Vec::new(),
ica_delete_message: Vec::new(),
tailchat_new_message: Vec::new(),
})
});
pub fn get_func<'py>(
py_module: &Bound<'py, PyAny>,
name: &'py str,
) -> Result<Bound<'py, PyAny>, PyPluginError> {
// 要处理的情况:
// 1. 有这个函数
// 2. 没有这个函数
// 3. 函数不是 Callable
match py_module.hasattr(name) {
Ok(contain) => {
if contain {
match py_module.getattr(name) {
Ok(func) => {
if func.is_callable() {
Ok(func)
} else {
// warn!("function<{}>: {:#?} in {:?} is not callable", name, func, path);
Err(PyPluginError::FuncNotCallable(
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
}
}
Err(e) => {
// warn!("failed to get function<{}> from {:?}: {:?}", name, path, e);
Err(PyPluginError::CouldNotGetFunc(
e,
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
}
}
} else {
// debug!("no function<{}> in module {:?}", name, path);
Err(PyPluginError::FuncNotFound(
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
}
}
Err(e) => {
// warn!("failed to check function<{}> from {:?}: {:?}", name, path, e);
Err(PyPluginError::CouldNotGetFunc(
e,
name.to_string(),
py_module.getattr("__name__").unwrap().extract::<String>().unwrap(),
))
}
}
}
pub fn verify_and_reload_plugins() {
let mut need_reload_files: Vec<PathBuf> = Vec::new();
let plugin_path = MainStatus::global_config().py().plugin_path.clone();
// 先检查是否有插件被删除
for path in PyStatus::get().files.keys() {
if !path.exists() {
event!(Level::INFO, "Python 插件: {:?} 已被删除", path);
PyStatus::get_mut().delete_file(path);
}
}
for entry in std::fs::read_dir(plugin_path).unwrap().flatten() {
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "py" && !PyStatus::get().verify_file(&path) {
need_reload_files.push(path);
}
}
}
if need_reload_files.is_empty() {
return;
}
event!(Level::INFO, "更改列表: {:?}", need_reload_files);
let plugins = PyStatus::get_mut();
for reload_file in need_reload_files {
if let Some(plugin) = plugins.files.get_mut(&reload_file) {
plugin.reload_from_file();
event!(Level::INFO, "重载 Python 插件: {:?} 完成", reload_file);
} else {
match PyPlugin::new_from_path(&reload_file) {
Some(plugin) => {
plugins.add_file(reload_file.clone(), plugin);
info!("加载 Python 插件: {:?} 完成", reload_file);
}
None => {
warn!("加载 Python 插件: {:?} 失败", reload_file);
}
}
}
}
}
macro_rules! call_py_func {
($args:expr, $plugin:expr, $plugin_path:expr, $func_name:expr, $client:expr) => {
tokio::spawn(async move {
Python::with_gil(|py| {
if let Ok(py_func) = get_func($plugin.py_module.bind(py), $func_name) {
if let Err(py_err) = py_func.call1($args) {
let e = PyPluginError::FuncCallError(
py_err,
$func_name.to_string(),
$plugin_path.to_string_lossy().to_string(),
);
event!(
Level::WARN,
"failed to call function<{}>: {}\ntraceback: {}",
$func_name,
e,
// 获取 traceback
match &e {
PyPluginError::FuncCallError(py_err, _, _) => match py_err.traceback(py) {
Some(traceback) => match traceback.format() {
Ok(trace) => trace,
Err(trace_e) => format!("failed to format traceback: {:?}", trace_e),
},
None => "no traceback".to_string(),
},
_ => unreachable!(),
}
);
}
}
})
})
};
}
/// 执行 new message 的 python 插件
pub async fn ica_new_message_py(message: &ica::messages::NewMessage, client: &Client) {
// 验证插件是否改变
verify_and_reload_plugins();
let plugins = PyStatus::get();
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
let msg = class::ica::NewMessagePy::new(message);
let client = class::ica::IcaClientPy::new(client);
let args = (msg, client);
let task = call_py_func!(args, plugin, path, events_func::ICA_NEW_MESSAGE, client);
PY_TASKS.lock().await.push_ica_new_message(task);
}
}
pub async fn ica_delete_message_py(msg_id: ica::MessageId, client: &Client) {
verify_and_reload_plugins();
let plugins = PyStatus::get();
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
let msg_id = msg_id.clone();
let client = class::ica::IcaClientPy::new(client);
let args = (msg_id.clone(), client);
let task = call_py_func!(args, plugin, path, events_func::ICA_DELETE_MESSAGE, client);
PY_TASKS.lock().await.push_ica_delete_message(task);
}
}
pub async fn tailchat_new_message_py(
message: &tailchat::messages::ReceiveMessage,
client: &Client,
) {
verify_and_reload_plugins();
let plugins = PyStatus::get();
for (path, plugin) in plugins.files.iter().filter(|(_, plugin)| plugin.enabled) {
let msg = class::tailchat::TailchatReceiveMessagePy::from_recive_message(message);
let client = class::tailchat::TailchatClientPy::new(client);
let args = (msg, client);
let task = call_py_func!(args, plugin, path, events_func::TAILCHAT_NEW_MESSAGE, client);
PY_TASKS.lock().await.push_tailchat_new_message(task);
}
}

View File

@ -1,85 +1,242 @@
pub mod commander;
pub mod config;
pub mod ica;
pub mod schdule;
pub mod tailchat;
use pyo3::prelude::*;
use tracing::{debug, info, warn};
use rust_socketio::asynchronous::Client;
use tokio::runtime::Runtime;
use pyo3::{
Bound, IntoPyObject, PyAny, PyRef, PyResult, pyclass, pymethods, pymodule,
types::{PyBool, PyModule, PyModuleMethods, PyString},
};
use toml::Value as TomlValue;
use tracing::{Level, event};
use crate::client::send_message;
use crate::data_struct::messages::{NewMessage, ReplyMessage, SendMessage};
use crate::ClientStatus;
// #[derive(Clone)]
#[pyclass]
#[pyo3(name = "ConfigData")]
pub struct ConfigDataPy {
pub data: TomlValue,
#[pyo3(name = "IcaStatus")]
pub struct IcaStatusPy {}
#[pymethods]
impl IcaStatusPy {
#[new]
pub fn py_new() -> Self {
Self {}
}
#[getter]
pub fn get_login(&self) -> bool {
unsafe { ClientStatus.login }
}
#[getter]
pub fn get_online(&self) -> bool {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => data.online,
None => false,
}
}
}
#[getter]
pub fn get_self_id(&self) -> Option<i64> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.qqid),
None => None,
}
}
}
#[getter]
pub fn get_nick_name(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.nick.clone()),
None => None,
}
}
}
#[getter]
pub fn get_ica_version(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.icalingua_info.ica_version.clone()),
None => None,
}
}
}
#[getter]
pub fn get_os_info(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.icalingua_info.os_info.clone()),
None => None,
}
}
}
#[getter]
pub fn get_resident_set_size(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.icalingua_info.resident_set_size.clone()),
None => None,
}
}
}
#[getter]
pub fn get_heap_used(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.icalingua_info.heap_used.clone()),
None => None,
}
}
}
#[getter]
pub fn get_load(&self) -> Option<String> {
unsafe {
match ClientStatus.online_data.as_ref() {
Some(data) => Some(data.icalingua_info.load.clone()),
None => None,
}
}
}
}
impl IcaStatusPy {
pub fn new() -> Self {
Self {}
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "NewMessage")]
pub struct NewMessagePy {
pub msg: NewMessage,
}
#[pymethods]
impl ConfigDataPy {
pub fn __getitem__(self_: PyRef<'_, Self>, key: String) -> Option<Bound<PyAny>> {
match self_.data.get(&key) {
Some(value) => match value {
TomlValue::String(s) => Some(PyString::new(self_.py(), s).into_any()),
TomlValue::Integer(i) => Some(i.into_pyobject(self_.py()).unwrap().into_any()),
TomlValue::Float(f) => Some(f.into_pyobject(self_.py()).unwrap().into_any()),
TomlValue::Boolean(b) => {
let py_value = PyBool::new(self_.py(), *b);
Some(py_value.as_any().clone())
}
TomlValue::Array(a) => {
let new_self = Self::new(TomlValue::Array(a.clone()));
let py_value = new_self.into_pyobject(self_.py()).unwrap().into_any();
Some(py_value)
}
TomlValue::Table(t) => {
let new_self = Self::new(TomlValue::Table(t.clone()));
let py_value = new_self.into_pyobject(self_.py()).unwrap().into_any();
Some(py_value)
}
_ => None,
},
None => None,
impl NewMessagePy {
pub fn reply_with(&self, content: String) -> SendMessagePy {
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()
}
#[getter]
pub fn get_is_reply(&self) -> bool {
self.msg.is_reply()
}
}
impl NewMessagePy {
pub fn new(msg: &NewMessage) -> Self {
Self { msg: msg.clone() }
}
}
#[pyclass]
#[pyo3(name = "ReplyMessage")]
pub struct ReplyMessagePy {
pub msg: ReplyMessage,
}
#[pymethods]
impl ReplyMessagePy {
pub fn __str__(&self) -> String {
format!("{:?}", self.msg)
}
}
impl ReplyMessagePy {
pub fn new(msg: ReplyMessage) -> Self {
Self { msg }
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "SendMessage")]
pub struct SendMessagePy {
pub msg: SendMessage,
}
#[pymethods]
impl SendMessagePy {
pub fn __str__(&self) -> String {
format!("{:?}", self.msg)
}
}
impl SendMessagePy {
pub fn new(msg: SendMessage) -> Self {
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(),
}
}
pub fn have_key(&self, key: String) -> bool { self.data.get(&key).is_some() }
}
impl ConfigDataPy {
pub fn new(data: TomlValue) -> Self { Self { data } }
}
/// Rust 侧向 Python 侧提供的 api
#[pymodule]
#[pyo3(name = "shenbot_api")]
fn rs_api_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add("__version__", crate::VERSION)?;
m.add("_version_", crate::VERSION)?;
m.add("_ica_version_", crate::ICA_VERSION)?;
m.add("_tailchat_version_", crate::TAILCHAT_VERSION)?;
m.add_class::<ConfigDataPy>()?;
m.add_class::<config::ConfigStoragePy>()?;
m.add_class::<schdule::SchedulerPy>()?;
Ok(())
}
/// 在 python 初始化之前注册所有需要的类
///
/// WARNING: 这个函数需要在 Python 初始化之前调用,否则会导致报错
///
/// (pyo3 提供的宏会检查一遍, 不过我这里就直接用原始形式了)
pub fn regist_class() {
event!(Level::INFO, "向 Python 注册 Rust 侧模块/函数");
unsafe {
// 单纯没用 macro 而已
pyo3::ffi::PyImport_AppendInittab(
rs_api_module::__PYO3_NAME.as_ptr(),
Some(rs_api_module::__pyo3_init),
);
}
event!(Level::INFO, "注册完成");
}

View File

@ -1 +0,0 @@

View File

@ -1,343 +0,0 @@
use std::collections::HashMap;
use pyo3::{
Bound, PyAny, PyResult, pyclass, pymethods,
types::{
PyAnyMethods, PyBool, PyBoolMethods, PyDict, PyDictMethods, PyFloat, PyInt, PyList,
PyListMethods, PyString, PyStringMethods, PyTypeMethods,
},
};
use tracing::{Level, event};
#[derive(Debug, Clone)]
pub enum ConfigItem {
None,
String(String),
Int(i64),
Float(f64),
Bool(bool),
Array(Vec<ConfigItemPy>),
Table(HashMap<String, ConfigItemPy>),
}
#[derive(Clone, Debug)]
#[pyclass]
#[pyo3(name = "ConfigItem")]
pub struct ConfigItemPy {
pub item: ConfigItem,
pub default_value: ConfigItem,
}
impl ConfigItemPy {
pub fn new(item: ConfigItem, default_value: ConfigItem) -> Self {
Self {
item,
default_value,
}
}
pub fn new_uninit(default_value: ConfigItem) -> Self {
Self {
item: ConfigItem::None,
default_value,
}
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "ConfigStorage")]
pub struct ConfigStoragePy {
pub keys: HashMap<String, ConfigItemPy>,
}
/// Storage 里允许的最大层级深度
///
/// 我也不知道为啥就突然有这玩意了(
pub const MAX_CFG_DEPTH: usize = 10;
fn parse_py_string(obj: &Bound<'_, PyAny>) -> PyResult<String> {
let py_str = obj.downcast::<PyString>()?;
let value = py_str.to_str()?;
Ok(value.to_string())
}
fn parse_py_bool(obj: &Bound<'_, PyAny>) -> PyResult<bool> {
let py_bool = obj.downcast::<PyBool>()?;
Ok(py_bool.is_true())
}
fn parse_py_int(obj: &Bound<'_, PyAny>) -> PyResult<i64> {
let py_int = obj.downcast::<PyInt>()?;
py_int.extract::<i64>()
}
fn parse_py_float(obj: &Bound<'_, PyAny>) -> PyResult<f64> {
let py_float = obj.downcast::<PyFloat>()?;
py_float.extract::<f64>()
}
impl ConfigStoragePy {
/// 递归 list 解析配置
///
/// 用个 Result 来标记递归过深
fn parse_py_list(
args: &Bound<'_, PyList>,
list: &mut Vec<ConfigItemPy>,
current_deepth: usize,
) -> Result<(), usize> {
if current_deepth > MAX_CFG_DEPTH {
return Err(current_deepth);
} else {
for value in args.iter() {
// 匹配 item
let value_type = value.get_type();
if value_type.is_instance_of::<PyDict>() {
let py_dict = value.downcast::<PyDict>().unwrap();
let mut new_map = HashMap::new();
match Self::parse_py_dict(py_dict, &mut new_map, current_deepth + 1) {
Ok(_) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Table(new_map)));
}
Err(e) => {
event!(
Level::WARN,
"value(dict) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyList>() {
let py_list = value.downcast::<PyList>().unwrap();
let mut new_list = Vec::new();
match Self::parse_py_list(py_list, &mut new_list, current_deepth + 1) {
Ok(_) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Array(new_list)));
}
Err(e) => {
event!(
Level::WARN,
"value(list) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyString>() {
match parse_py_string(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::String(value)));
}
Err(e) => {
event!(
Level::WARN,
"value(string) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyBool>() {
match parse_py_bool(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Bool(value)));
}
Err(e) => {
event!(
Level::WARN,
"value(bool) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else if value_type.is_instance_of::<PyInt>() {
match parse_py_int(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Int(value)));
}
Err(e) => {
event!(Level::WARN, "value(int) 解析时出现错误: {}\nraw: {}", e, value);
}
}
} else if value_type.is_instance_of::<PyFloat>() {
match parse_py_float(&value) {
Ok(value) => {
list.push(ConfigItemPy::new_uninit(ConfigItem::Float(value)));
}
Err(e) => {
event!(
Level::WARN,
"value(float) 解析时出现错误: {}\nraw: {}",
e,
value
);
}
}
} else {
// 先丢个 warning 出去
match value_type.name() {
Ok(type_name) => {
event!(
Level::WARN,
"value 为不支持的 {} 类型\nraw: {}",
type_name,
value
)
}
Err(e) => {
event!(
Level::WARN,
"value 为不支持的类型 (获取类型名失败: {})\nraw: {}",
e,
value
)
}
}
}
}
}
Ok(())
}
/// 递归 dict 解析配置
///
/// 用个 Result 来标记递归过深
fn parse_py_dict(
kwargs: &Bound<'_, PyDict>,
map: &mut HashMap<String, ConfigItemPy>,
current_deepth: usize,
) -> Result<(), usize> {
if current_deepth > MAX_CFG_DEPTH {
Err(current_deepth)
} else {
for (key, value) in kwargs.iter() {
if let Ok(name) = key.downcast::<PyString>() {
let name = name.to_string();
// 匹配 item
let value_type = value.get_type();
if value_type.is_instance_of::<PyDict>() {
let py_dict = value.downcast::<PyDict>().unwrap();
let mut new_map = HashMap::new();
match Self::parse_py_dict(py_dict, &mut new_map, current_deepth + 1) {
Ok(_) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Table(new_map)),
);
}
Err(e) => {
event!(Level::WARN, "value(dict) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyList>() {
let py_list = value.downcast::<PyList>().unwrap();
let mut new_list = Vec::new();
match Self::parse_py_list(py_list, &mut new_list, current_deepth + 1) {
Ok(_) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Array(new_list)),
);
}
Err(e) => {
event!(Level::WARN, "value(list) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyString>() {
match parse_py_string(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::String(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(string) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyBool>() {
match parse_py_bool(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Bool(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(bool) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyInt>() {
match parse_py_int(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Int(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(int) {} 解析时出现错误: {}", name, e);
}
}
} else if value_type.is_instance_of::<PyFloat>() {
match parse_py_float(&value) {
Ok(value) => {
map.insert(
name.clone(),
ConfigItemPy::new_uninit(ConfigItem::Float(value)),
);
}
Err(e) => {
event!(Level::WARN, "value(float) {} 解析时出现错误: {}", name, e);
}
}
} else {
// 先丢个 warning 出去
match value_type.name() {
Ok(type_name) => {
event!(Level::WARN, "value {} 为不支持的 {} 类型", name, type_name)
}
Err(e) => event!(
Level::WARN,
"value {} 为不支持的类型 (获取类型名失败: {})",
name,
e
),
}
continue;
}
}
}
Ok(())
}
}
}
#[pymethods]
impl ConfigStoragePy {
#[new]
#[pyo3(signature = (**kwargs))]
pub fn new(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
match kwargs {
Some(kwargs) => {
let mut keys = HashMap::new();
// 解析 kwargs
Self::parse_py_dict(kwargs, &mut keys, 0).map_err(|e| {
event!(Level::ERROR, "配置解析过深: {}", e);
pyo3::exceptions::PyValueError::new_err(format!("配置解析过深: {}", e))
})?;
// 解析完成
Ok(Self { keys })
}
None => Ok(Self {
keys: HashMap::new(),
}),
}
}
#[getter]
/// 获取最大允许的层级深度
pub fn get_max_allowed_depth(&self) -> usize { MAX_CFG_DEPTH }
}

View File

@ -1,369 +0,0 @@
use std::time::SystemTime;
use pyo3::{pyclass, pymethods};
use rust_socketio::asynchronous::Client;
use tokio::runtime::Runtime;
use tracing::{Level, event};
use crate::MainStatus;
use crate::data_struct::ica::messages::{
DeleteMessage, MessageTrait, NewMessage, ReplyMessage, SendMessage,
};
use crate::data_struct::ica::{MessageId, RoomId, RoomIdTrait, UserId};
use crate::ica::client::{delete_message, send_message, send_poke, send_room_sign_in};
use crate::py::PyStatus;
#[pyclass]
#[pyo3(name = "IcaStatus")]
pub struct IcaStatusPy {}
#[pymethods]
impl IcaStatusPy {
#[new]
pub fn py_new() -> Self { Self {} }
#[getter]
pub fn get_qq_login(&self) -> bool { MainStatus::global_ica_status().qq_login }
#[getter]
pub fn get_online(&self) -> bool { MainStatus::global_ica_status().online_status.online }
#[getter]
pub fn get_self_id(&self) -> i64 { MainStatus::global_ica_status().online_status.qqid }
#[getter]
pub fn get_nick_name(&self) -> String {
MainStatus::global_ica_status().online_status.nick.clone()
}
#[getter]
pub fn get_loaded_messages_count(&self) -> u64 {
MainStatus::global_ica_status().current_loaded_messages_count
}
#[getter]
pub fn get_ica_version(&self) -> String {
MainStatus::global_ica_status().online_status.icalingua_info.ica_version.clone()
}
#[getter]
pub fn get_os_info(&self) -> String {
MainStatus::global_ica_status().online_status.icalingua_info.os_info.clone()
}
#[getter]
pub fn get_resident_set_size(&self) -> String {
MainStatus::global_ica_status()
.online_status
.icalingua_info
.resident_set_size
.clone()
}
#[getter]
pub fn get_heap_used(&self) -> String {
MainStatus::global_ica_status().online_status.icalingua_info.heap_used.clone()
}
#[getter]
pub fn get_load(&self) -> String {
MainStatus::global_ica_status().online_status.icalingua_info.load.clone()
}
#[getter]
/// 获取当前用户加入的所有房间
///
/// 添加自 2.0.1
pub fn get_rooms(&self) -> Vec<IcaRoomPy> {
MainStatus::global_ica_status().rooms.iter().map(|r| r.into()).collect()
}
#[getter]
/// 获取所有管理员
///
/// 添加自 2.0.1
pub fn get_admins(&self) -> Vec<UserId> { MainStatus::global_config().ica().admin_list.clone() }
#[getter]
/// 获取所有被屏蔽的人
///
/// (好像没啥用就是了, 反正被过滤的不会给到插件)
///
/// 添加自 2.0.1
pub fn get_filtered(&self) -> Vec<UserId> {
MainStatus::global_config().ica().filter_list.clone()
}
}
impl Default for IcaStatusPy {
fn default() -> Self { Self::new() }
}
impl IcaStatusPy {
pub fn new() -> Self { Self {} }
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "IcaRoom")]
/// Room api
///
/// 添加自 2.0.1
pub struct IcaRoomPy {
pub inner: crate::data_struct::ica::all_rooms::Room,
}
impl From<crate::data_struct::ica::all_rooms::Room> for IcaRoomPy {
fn from(inner: crate::data_struct::ica::all_rooms::Room) -> Self { Self { inner } }
}
impl From<&crate::data_struct::ica::all_rooms::Room> for IcaRoomPy {
fn from(inner: &crate::data_struct::ica::all_rooms::Room) -> Self {
Self {
inner: inner.clone(),
}
}
}
#[pymethods]
impl IcaRoomPy {
#[getter]
pub fn get_room_id(&self) -> i64 { self.inner.room_id }
#[getter]
pub fn get_room_name(&self) -> String { self.inner.room_name.clone() }
#[getter]
pub fn get_unread_count(&self) -> u64 { self.inner.unread_count }
#[getter]
pub fn get_priority(&self) -> u8 { self.inner.priority }
#[getter]
pub fn get_utime(&self) -> i64 { self.inner.utime }
pub fn is_group(&self) -> bool { self.inner.room_id.is_room() }
pub fn is_chat(&self) -> bool { self.inner.room_id.is_chat() }
pub fn new_message_to(&self, content: String) -> SendMessagePy {
SendMessagePy::new(self.inner.new_message_to(content))
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "NewMessage")]
pub struct NewMessagePy {
pub msg: NewMessage,
}
#[pymethods]
impl NewMessagePy {
pub fn reply_with(&self, content: String) -> SendMessagePy {
SendMessagePy::new(self.msg.reply_with(&content))
}
pub fn as_deleted(&self) -> DeleteMessagePy { DeleteMessagePy::new(self.msg.as_deleted()) }
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
#[getter]
pub fn get_id(&self) -> MessageId { self.msg.msg_id().clone() }
#[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_sender_name(&self) -> String { self.msg.sender_name().clone() }
#[getter]
pub fn get_is_from_self(&self) -> bool { self.msg.is_from_self() }
#[getter]
pub fn get_is_reply(&self) -> bool { self.msg.is_reply() }
#[getter]
pub fn get_is_room_msg(&self) -> bool { self.msg.room_id.is_room() }
#[getter]
pub fn get_is_chat_msg(&self) -> bool { self.msg.room_id.is_chat() }
#[getter]
pub fn get_room_id(&self) -> RoomId { self.msg.room_id }
}
impl NewMessagePy {
pub fn new(msg: &NewMessage) -> Self { Self { msg: msg.clone() } }
}
#[pyclass]
#[pyo3(name = "ReplyMessage")]
pub struct ReplyMessagePy {
pub msg: ReplyMessage,
}
#[pymethods]
impl ReplyMessagePy {
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
}
impl ReplyMessagePy {
pub fn new(msg: ReplyMessage) -> Self { Self { msg } }
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "SendMessage")]
pub struct SendMessagePy {
pub msg: SendMessage,
}
#[pymethods]
impl SendMessagePy {
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
/// 设置消息内容
/// 用于链式调用
pub fn with_content(&mut self, content: String) -> Self {
self.msg.content = content;
self.clone()
}
#[getter]
pub fn get_content(&self) -> String { self.msg.content.clone() }
#[setter]
pub fn set_content(&mut self, content: String) { self.msg.content = content; }
#[getter]
pub fn get_room_id(&self) -> RoomId { self.msg.room_id }
#[setter]
pub fn set_room_id(&mut self, room_id: RoomId) { self.msg.room_id = room_id; }
/// 设置消息图片
pub fn set_img(&mut self, file: Vec<u8>, file_type: String, as_sticker: bool) {
self.msg.set_img(&file, &file_type, as_sticker);
}
pub fn remove_reply(&mut self) -> Self {
self.msg.reply_to = None;
self.clone()
}
}
impl SendMessagePy {
pub fn new(msg: SendMessage) -> Self { Self { msg } }
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "DeleteMessage")]
pub struct DeleteMessagePy {
pub msg: DeleteMessage,
}
#[pymethods]
impl DeleteMessagePy {
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
}
impl DeleteMessagePy {
pub fn new(msg: DeleteMessage) -> Self { Self { msg } }
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "IcaClient")]
pub struct IcaClientPy {
pub client: Client,
}
#[pymethods]
impl IcaClientPy {
/// 签到
///
/// 添加自 1.6.5 版本
pub fn send_room_sign_in(&self, room_id: RoomId) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(send_room_sign_in(&self.client, room_id))
})
}
/// 戳一戳
///
/// 添加自 1.6.5 版本
pub fn send_poke(&self, room_id: RoomId, user_id: UserId) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(send_poke(&self.client, room_id, user_id))
})
}
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))
})
}
pub fn send_and_warn(&self, message: SendMessagePy) -> bool {
event!(Level::WARN, message.msg.content);
self.send_message(message)
}
pub fn delete_message(&self, message: DeleteMessagePy) -> bool {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(delete_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)
// })
// }
#[getter]
pub fn get_status(&self) -> IcaStatusPy { IcaStatusPy::new() }
#[getter]
pub fn get_version(&self) -> String { crate::VERSION.to_string() }
#[getter]
pub fn get_version_str(&self) -> String { crate::version_str() }
#[getter]
pub fn get_client_id(&self) -> String { crate::client_id() }
#[getter]
pub fn get_ica_version(&self) -> String { crate::ICA_VERSION.to_string() }
#[getter]
pub fn get_startup_time(&self) -> SystemTime { crate::start_up_time() }
#[getter]
pub fn get_py_tasks_count(&self) -> usize {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(async { crate::py::call::PY_TASKS.lock().await.len_check() })
})
}
/// 重新加载插件状态
/// 返回是否成功
pub fn reload_plugin_status(&self) -> bool { PyStatus::get_mut().config.reload_from_default() }
/// 设置某个插件的状态
pub fn set_plugin_status(&self, plugin_name: String, status: bool) {
PyStatus::get_mut().set_status(&plugin_name, status);
}
pub fn get_plugin_status(&self, plugin_name: String) -> Option<bool> {
PyStatus::get().get_status(&plugin_name)
}
/// 同步状态到配置文件
/// 这样关闭的时候就会保存状态
pub fn sync_status_to_config(&self) { PyStatus::get_mut().config.sync_status_to_config(); }
/// 重新加载插件
///
/// 返回是否成功
pub fn reload_plugin(&self, plugin_name: String) -> bool {
PyStatus::get_mut().reload_plugin(&plugin_name)
}
pub fn debug(&self, content: String) {
event!(Level::DEBUG, "{}", content);
}
pub fn info(&self, content: String) {
event!(Level::INFO, "{}", content);
}
pub fn warn(&self, content: String) {
event!(Level::WARN, "{}", content);
}
}
impl IcaClientPy {
pub fn new(client: &Client) -> Self {
Self {
client: client.clone(),
}
}
}

View File

@ -1,59 +0,0 @@
use std::time::Duration;
use pyo3::{Bound, Py, PyTraverseError, PyVisit, Python, pyclass, pymethods, types::PyFunction};
use tracing::{Level, event};
#[derive(Debug)]
#[pyclass]
#[pyo3(name = "Scheduler")]
/// 用于计划任务的类
///
/// 给 Python 侧使用
///
/// add: 0.9.0
pub struct SchedulerPy {
/// 回调函数
///
/// 你最好不要把他清理掉
pub callback: Py<PyFunction>,
/// 预计等待时间
pub schdule_time: Duration,
}
#[pymethods]
impl SchedulerPy {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self.callback)?;
Ok(())
}
#[new]
pub fn new(func: Bound<'_, PyFunction>, schdule_time: Duration) -> Self {
Self {
callback: func.unbind(),
schdule_time,
}
}
/// 开始
pub fn start(&self, py: Python<'_>) {
let wait = self.schdule_time;
let cb = self.callback.clone_ref(py);
tokio::spawn(async move {
let second = Duration::from_secs(1);
if wait > second {
let big_sleep = wait.checked_sub(second).unwrap();
tokio::time::sleep(big_sleep).await;
tokio::time::sleep(second).await;
} else {
tokio::time::sleep(wait).await;
}
Python::with_gil(|py| {
event!(Level::INFO, "正在调用计划 {:?}", wait);
if let Err(e) = cb.call0(py) {
event!(Level::WARN, "调用时出现错误 {}", e);
}
});
});
}
}

View File

@ -1,192 +0,0 @@
use std::time::SystemTime;
use pyo3::prelude::*;
use rust_socketio::asynchronous::Client;
use tokio::runtime::Runtime;
use tracing::{debug, info, warn};
use crate::data_struct::tailchat::messages::{ReceiveMessage, SendingFile, SendingMessage};
use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};
use crate::py::PyStatus;
use crate::tailchat::client::send_message;
#[pyclass]
#[pyo3(name = "TailchatClient")]
pub struct TailchatClientPy {
pub client: Client,
}
impl TailchatClientPy {
pub fn new(client: &Client) -> Self {
Self {
client: client.clone(),
}
}
}
#[pyclass]
#[pyo3(name = "TailchatStatus")]
/// 预留?
pub struct TailchatStatusPy {}
#[pyclass]
#[pyo3(name = "TailchatReceiveMessage")]
pub struct TailchatReceiveMessagePy {
pub message: ReceiveMessage,
}
impl TailchatReceiveMessagePy {
pub fn from_recive_message(msg: &ReceiveMessage) -> Self {
Self {
message: msg.clone(),
}
}
}
#[derive(Clone)]
#[pyclass]
#[pyo3(name = "TailchatSendingMessage")]
pub struct TailchatSendingMessagePy {
pub message: SendingMessage,
}
#[pymethods]
impl TailchatClientPy {
pub fn send_message(&self, message: TailchatSendingMessagePy) -> bool {
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(send_message(&self.client, &message.message))
})
}
pub fn send_and_warn(&self, message: TailchatSendingMessagePy) -> bool {
warn!("{}", message.message.content);
self.send_message(message)
}
#[getter]
pub fn get_version(&self) -> String { crate::VERSION.to_string() }
#[getter]
pub fn get_version_str(&self) -> String { crate::version_str() }
#[getter]
pub fn get_client_id(&self) -> String { crate::client_id() }
#[getter]
pub fn get_tailchat_version(&self) -> String { crate::TAILCHAT_VERSION.to_string() }
#[getter]
pub fn get_startup_time(&self) -> SystemTime { crate::start_up_time() }
#[getter]
pub fn get_py_tasks_count(&self) -> usize {
tokio::task::block_in_place(|| {
let rt = Runtime::new().unwrap();
rt.block_on(async { crate::py::call::PY_TASKS.lock().await.len_check() })
})
}
/// 重新加载插件状态
/// 返回是否成功
pub fn reload_plugin_status(&self) -> bool { PyStatus::get_mut().config.reload_from_default() }
/// 设置某个插件的状态
pub fn set_plugin_status(&self, plugin_name: String, status: bool) {
PyStatus::get_mut().set_status(&plugin_name, status);
}
pub fn get_plugin_status(&self, plugin_name: String) -> Option<bool> {
PyStatus::get().get_status(&plugin_name)
}
/// 同步状态到配置文件
/// 这样关闭的时候就会保存状态
pub fn sync_status_to_config(&self) { PyStatus::get_mut().config.sync_status_to_config(); }
/// 重新加载插件
///
/// 返回是否成功
pub fn reload_plugin(&self, plugin_name: String) -> bool {
PyStatus::get_mut().reload_plugin(&plugin_name)
}
#[pyo3(signature = (content, converse_id, group_id = None))]
pub fn new_message(
&self,
content: String,
converse_id: ConverseId,
group_id: Option<GroupId>,
) -> TailchatSendingMessagePy {
TailchatSendingMessagePy {
message: SendingMessage::new(content, converse_id, group_id, None),
}
}
pub fn debug(&self, content: String) {
debug!("{}", content);
}
pub fn info(&self, content: String) {
info!("{}", content);
}
pub fn warn(&self, content: String) {
warn!("{}", content);
}
}
#[pymethods]
impl TailchatReceiveMessagePy {
#[getter]
pub fn get_is_reply(&self) -> bool { self.message.is_reply() }
#[getter]
pub fn get_is_from_self(&self) -> bool { self.message.is_from_self() }
#[getter]
pub fn get_msg_id(&self) -> MessageId { self.message.msg_id.clone() }
#[getter]
pub fn get_content(&self) -> String { self.message.content.clone() }
#[getter]
pub fn get_sender_id(&self) -> UserId { self.message.sender_id.clone() }
#[getter]
pub fn get_group_id(&self) -> Option<GroupId> { self.message.group_id.clone() }
#[getter]
pub fn get_converse_id(&self) -> ConverseId { self.message.converse_id.clone() }
/// 作为回复
pub fn as_reply(&self) -> TailchatSendingMessagePy {
TailchatSendingMessagePy {
message: self.message.as_reply(),
}
}
pub fn reply_with(&self, content: String) -> TailchatSendingMessagePy {
TailchatSendingMessagePy {
message: self.message.reply_with(&content),
}
}
}
#[pymethods]
impl TailchatSendingMessagePy {
#[getter]
pub fn get_content(&self) -> String { self.message.content.clone() }
#[setter]
pub fn set_content(&mut self, content: String) { self.message.content = content; }
#[getter]
pub fn get_converse_id(&self) -> ConverseId { self.message.converse_id.clone() }
#[setter]
pub fn set_converse_id(&mut self, converse_id: ConverseId) {
self.message.converse_id = converse_id;
}
#[getter]
pub fn get_group_id(&self) -> Option<GroupId> { self.message.group_id.clone() }
#[setter]
pub fn set_group_id(&mut self, group_id: Option<GroupId>) { self.message.group_id = group_id; }
pub fn with_content(&mut self, content: String) -> Self {
self.message.content = content;
self.clone()
}
pub fn clear_meta(&mut self) -> Self {
self.message.meta = None;
self.clone()
}
pub fn set_img(&mut self, file: Vec<u8>, file_name: String) {
let file = SendingFile::Image {
file,
name: file_name,
};
self.message.add_img(file);
}
}

View File

@ -1,192 +0,0 @@
use std::{path::Path, str::FromStr};
use colored::Colorize;
use toml_edit::{DocumentMut, Key, Table, TomlError, Value, value};
use tracing::{Level, event};
use crate::MainStatus;
use crate::py::PyStatus;
/// ```toml
/// # 这个文件是由 shenbot 自动生成的, 请 **谨慎** 修改
/// # 请不要修改这个文件, 除非你知道你在做什么
///
/// [plugins]
/// "xxxxxxx" = false # 被禁用的插件
/// "xxxxxxx" = true # 被启用的插件
/// ```
#[derive(Debug, Clone)]
pub struct PluginConfigFile {
pub data: DocumentMut,
}
const CONFIG_KEY: &str = "plugins";
pub const CONFIG_FILE_NAME: &str = "plugins.toml";
pub const DEFAULT_CONFIG: &str = r#"
# shenbot , ****
# ,
[plugins]
"#;
#[allow(unused)]
impl PluginConfigFile {
pub fn from_str(data: &str) -> Result<Self, TomlError> {
let mut data = DocumentMut::from_str(data)?;
if data.get(CONFIG_KEY).is_none() {
event!(Level::WARN, "插件配置文件缺少 plugins 字段, 正在初始化");
data.insert_formatted(
&Key::from_str(CONFIG_KEY).unwrap(),
toml_edit::Item::Table(Table::new()),
);
}
Ok(Self { data })
}
pub fn default_init() -> anyhow::Result<Self> {
let config_path = MainStatus::global_config().py().config_path.clone();
let path = Path::new(&config_path);
Self::from_config_path(path)
}
pub fn reload_from_default(&mut self) -> bool {
let new_config = Self::default_init();
if let Err(e) = new_config {
event!(Level::ERROR, "从配置文件重加载时遇到错误: {}", e);
return false;
}
let new_config = new_config.unwrap();
self.data = new_config.data;
true
}
pub fn from_config_path(path: &Path) -> anyhow::Result<Self> {
let config_path = path.join(CONFIG_FILE_NAME);
if !config_path.exists() {
event!(Level::WARN, "插件配置文件不存在, 正在创建");
std::fs::write(&config_path, DEFAULT_CONFIG)?;
Ok(Self::from_str(DEFAULT_CONFIG)?)
} else {
let data = std::fs::read_to_string(&config_path)?;
Ok(Self::from_str(&data)?)
}
}
fn get_table(&self) -> Option<&Table> {
self.data.get(CONFIG_KEY).and_then(|item| item.as_table())
}
fn get_table_mut(&mut self) -> Option<&mut Table> {
self.data.get_mut(CONFIG_KEY).and_then(|item| item.as_table_mut())
}
/// 获取插件状态
/// 默认为 true
pub fn get_status(&self, plugin_id: &str) -> bool {
if let Some(item) = self.data.get(CONFIG_KEY) {
if let Some(table) = item.as_table() {
if let Some(item) = table.get(plugin_id) {
if let Some(bool) = item.as_bool() {
return bool;
}
}
}
}
true
}
/// 删掉一个状态
pub fn remove_status(&mut self, path: &Path) -> Option<bool> {
let path_str = path.to_str().unwrap();
if let Some(table) = self.get_table_mut() {
if let Some(item) = table.get_mut(path_str) {
if let Some(bool) = item.as_bool() {
table.remove(path_str);
return Some(bool);
} else {
table.remove(path_str);
return Some(false);
}
}
}
None
}
/// 设置插件状态
pub fn set_status(&mut self, path: &Path, status: bool) {
let path_str = path.to_str().unwrap();
let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap();
if table.contains_key(path_str) {
match table.get_mut(path_str).unwrap().as_value_mut() {
Some(value) => *value = Value::from(status),
None => {
table.insert(path_str, value(status));
}
}
} else {
table.insert(path_str, value(status));
}
}
/// 从默认文件读取状态
///
/// 返回是否成功
pub fn read_status_from_default(&mut self) -> bool {
if !self.reload_from_default() {
return false;
}
event!(Level::INFO, "同步插件状态");
let plugins = PyStatus::get_mut();
fn fmt_bool(b: bool) -> String {
if b {
"启用".green().to_string()
} else {
"禁用".red().to_string()
}
}
plugins.files.iter_mut().for_each(|(path, status)| {
let plugin_id = status.get_id();
let config_status = self.get_status(&plugin_id);
if config_status != status.enabled {
event!(
Level::INFO,
"插件状态: {} {} -> {}",
status.get_id(),
fmt_bool(status.enabled),
fmt_bool(config_status)
);
status.enabled = config_status;
} else {
event!(
Level::INFO,
"插件状态: {} {} (没变)",
status.get_id(),
fmt_bool(status.enabled)
);
}
});
true
}
pub fn sync_status_to_config(&mut self) {
let plugins = PyStatus::get();
let table = self.data.get_mut(CONFIG_KEY).unwrap().as_table_mut().unwrap();
table.clear();
plugins.files.iter().for_each(|(_, status)| {
table.insert(&status.get_id(), value(status.enabled));
});
}
pub fn write_to_default(&self) -> Result<(), std::io::Error> {
let config_path = MainStatus::global_config().py().config_path.clone();
let config_path = Path::new(&config_path);
self.write_to_file(config_path)
}
pub fn write_to_file(&self, path: &Path) -> Result<(), std::io::Error> {
let config_path = path.join(CONFIG_FILE_NAME);
std::fs::write(config_path, self.data.to_string())?;
Ok(())
}
}

View File

@ -1,21 +0,0 @@
pub mod events_func {
/// icalingua 的 加群请求
///
/// added: 2.0.1
pub const ICA_JOIN_REQUEST: &str = "on_ica_join_request";
/// icalingua 的 新消息
pub const ICA_NEW_MESSAGE: &str = "on_ica_message";
/// icalingua 的 消息撤回
pub const ICA_DELETE_MESSAGE: &str = "on_ica_delete_message";
/// tailchat 的 新消息
pub const TAILCHAT_NEW_MESSAGE: &str = "on_tailchat_message";
}
pub mod config_func {
/// 请求配置用的函数
pub const REQUIRE_CONFIG: &str = "require_config";
/// 接受配置用的函数
pub const ON_CONFIG: &str = "on_config";
}

View File

@ -1,441 +1,115 @@
pub mod call;
pub mod class;
pub mod config;
pub mod consts;
use std::ffi::CString;
use std::fmt::Display;
use std::path::Path;
use std::sync::OnceLock;
use std::time::SystemTime;
use std::{collections::HashMap, path::PathBuf};
use colored::Colorize;
use pyo3::{
Bound, Py, PyErr, PyResult, Python,
exceptions::PyTypeError,
intern,
types::{PyAnyMethods, PyModule, PyTracebackMethods, PyTuple},
};
use tracing::{Level, event, span, warn};
use pyo3::prelude::*;
use rust_socketio::asynchronous::Client;
use tracing::{debug, info, warn};
use crate::MainStatus;
use crate::error::PyPluginError;
use crate::config::IcaConfig;
use crate::data_struct::messages::NewMessage;
use consts::config_func;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct PyStatus {
pub files: PyPlugins,
pub config: config::PluginConfigFile,
pub files: Option<HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)>>,
}
pub type PyPlugins = HashMap<PathBuf, PyPlugin>;
pub type RawPyPlugin = (PathBuf, Option<SystemTime>, String);
#[allow(non_upper_case_globals)]
static mut PyPluginStatus: OnceLock<PyStatus> = OnceLock::new();
#[allow(static_mut_refs)]
impl PyStatus {
pub fn init() {
let config =
config::PluginConfigFile::default_init().expect("初始化 Python 插件配置文件失败");
let status = PyStatus {
files: HashMap::new(),
config,
};
let _ = unsafe { PyPluginStatus.get_or_init(|| status) };
}
pub fn get() -> &'static PyStatus { unsafe { PyPluginStatus.get().unwrap() } }
pub fn get_mut() -> &'static mut PyStatus { unsafe { PyPluginStatus.get_mut().unwrap() } }
/// 添加一个插件
pub fn add_file(&mut self, path: PathBuf, plugin: PyPlugin) { self.files.insert(path, plugin); }
/// 重新加载一个插件
pub fn reload_plugin(&mut self, plugin_name: &str) -> bool {
let plugin = self.files.iter_mut().find_map(|(_, plugin)| {
if plugin.get_id() == plugin_name {
Some(plugin)
} else {
None
}
});
if let Some(plugin) = plugin {
plugin.reload_from_file()
} else {
event!(Level::WARN, "没有找到插件: {}", plugin_name);
false
}
}
/// 删除一个插件
pub fn delete_file(&mut self, path: &PathBuf) -> Option<PyPlugin> { self.files.remove(path) }
pub fn get_status(&self, pluging_id: &str) -> Option<bool> {
self.files.iter().find_map(|(_, plugin)| {
if plugin.get_id() == pluging_id {
return Some(plugin.enabled);
}
None
})
}
pub fn set_status(&mut self, pluging_id: &str, status: bool) {
self.files.iter_mut().for_each(|(_, plugin)| {
if plugin.get_id() == pluging_id {
plugin.enabled = status;
}
});
}
pub fn verify_file(&self, path: &PathBuf) -> bool {
self.files.get(path).is_some_and(|plugin| plugin.verifiy())
}
pub fn display() -> String {
format!(
"Python 插件 {{ {} }}",
Self::get()
.files
.values()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join("\n")
)
}
}
pub fn get_py_err_traceback(py_err: &PyErr) -> String {
Python::with_gil(|py| match py_err.traceback(py) {
Some(traceback) => match traceback.format() {
Ok(trace) => trace,
Err(e) => format!("{:?}", e),
},
None => "".to_string(),
})
.red()
.to_string()
}
#[derive(Debug)]
pub struct PyPlugin {
pub file_path: PathBuf,
pub modify_time: Option<SystemTime>,
pub py_module: Py<PyModule>,
pub enabled: bool,
}
impl PyPlugin {
pub fn new(path: PathBuf, modify_time: Option<SystemTime>, module: Py<PyModule>) -> Self {
PyPlugin {
file_path: path.clone(),
modify_time,
py_module: module,
enabled: false,
}
}
/// 从文件创建一个新的
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 插件文件{:?}: {:?} 失败\n{}",
path,
e,
get_py_err_traceback(&e)
);
None
}
},
Err(e) => {
warn!("加载插件 {:?}: {:?} 失败", path, e);
None
}
}
}
/// 从文件更新
pub fn reload_from_file(&mut self) -> bool {
let raw_file = load_py_file(&self.file_path);
match raw_file {
Ok(raw_file) => match Self::try_from(raw_file) {
Ok(plugin) => {
self.py_module = plugin.py_module;
self.modify_time = plugin.modify_time;
self.enabled = PyStatus::get().config.get_status(&self.get_id());
event!(Level::INFO, "更新 Python 插件文件 {:?} 完成", self.file_path);
true
}
Err(e) => {
warn!(
"更新 Python 插件文件{:?}: {:?} 失败\n{}",
self.file_path,
e,
get_py_err_traceback(&e)
);
false
}
},
Err(e) => {
warn!("更新插件 {:?}: {:?} 失败", self.file_path, e);
false
}
}
}
/// 检查文件是否被修改
pub fn verifiy(&self) -> bool {
match get_change_time(&self.file_path) {
None => false,
Some(time) => {
if let Some(changed_time) = self.modify_time {
time.eq(&changed_time)
} else {
true
pub fn get_files() -> &'static HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)> {
unsafe {
match PYSTATUS.files.as_ref() {
Some(files) => files,
None => {
debug!("No files in py status");
PYSTATUS.files = Some(HashMap::new());
PYSTATUS.files.as_ref().unwrap()
}
}
}
}
pub fn get_id(&self) -> String { plugin_path_as_id(&self.file_path) }
}
impl Display for PyPlugin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}({:?})-{}", self.get_id(), self.file_path, self.enabled)
}
}
pub const CONFIG_DATA_NAME: &str = "CONFIG_DATA";
fn set_str_cfg_default_plugin(
module: &Bound<'_, PyModule>,
default: String,
path: String,
) -> PyResult<()> {
let base_path = MainStatus::global_config().py().config_path;
let mut base_path: PathBuf = PathBuf::from(base_path);
if !base_path.exists() {
event!(Level::WARN, "python 插件路径不存在, 创建: {:?}", base_path);
std::fs::create_dir_all(&base_path)?;
}
base_path.push(&path);
let config_str: String = if base_path.exists() {
event!(Level::INFO, "加载 {:?} 的配置文件 {:?} 中", path, base_path);
match std::fs::read_to_string(&base_path) {
Ok(v) => v,
Err(e) => {
event!(Level::WARN, "配置文件 {:?} 读取失败 {}, 创建默认配置", base_path, e);
// 写入默认配置
std::fs::write(&base_path, &default)?;
default
pub fn add_file(path: PathBuf, changed_time: Option<SystemTime>, py_module: Py<PyAny>) {
unsafe {
match PYSTATUS.files.as_mut() {
Some(files) => {
files.insert(path, (changed_time, py_module));
debug!("Added file to py status, {:?}", files);
}
None => {
warn!("No files in py status, creating new");
let mut files = HashMap::new();
files.insert(path, (changed_time, py_module));
PYSTATUS.files = Some(files);
}
}
}
} else {
event!(Level::WARN, "配置文件 {:?} 不存在, 创建默认配置", base_path);
// 写入默认配置
std::fs::write(base_path, &default)?;
default
};
if let Err(e) = module.setattr(intern!(module.py(), CONFIG_DATA_NAME), &config_str) {
event!(Level::WARN, "Python 插件 {:?} 的配置文件信息设置失败:{:?}", path, e);
return Err(PyTypeError::new_err(format!(
"Python 插件 {:?} 的配置文件信息设置失败:{:?}",
path, e
)));
}
// 给到 on config
if let Ok(attr) = module.getattr(intern!(module.py(), config_func::ON_CONFIG)) {
if !attr.is_callable() {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数不是 Callable",
path,
config_func::ON_CONFIG
);
return Ok(());
}
let args = (config_str.as_bytes(),);
if let Err(e) = attr.call1(args) {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数返回了一个报错 {}",
path,
config_func::ON_CONFIG,
e
);
}
}
Ok(())
}
fn set_bytes_cfg_default_plugin(
module: &Bound<'_, PyModule>,
default: Vec<u8>,
path: String,
) -> PyResult<()> {
let base_path = MainStatus::global_config().py().config_path;
let mut base_path: PathBuf = PathBuf::from(base_path);
if !base_path.exists() {
event!(Level::WARN, "python 插件路径不存在, 创建: {:?}", base_path);
std::fs::create_dir_all(&base_path)?;
}
base_path.push(&path);
let config_vec: Vec<u8> = if base_path.exists() {
event!(Level::INFO, "加载 {:?} 的配置文件 {:?} 中", path, base_path);
match std::fs::read(&base_path) {
Ok(v) => v,
Err(e) => {
event!(Level::WARN, "配置文件 {:?} 读取失败 {}, 创建默认配置", base_path, e);
// 写入默认配置
std::fs::write(&base_path, &default)?;
default
}
}
} else {
event!(Level::WARN, "配置文件 {:?} 不存在, 创建默认配置", base_path);
// 写入默认配置
std::fs::write(base_path, &default)?;
default
};
match module.setattr(intern!(module.py(), CONFIG_DATA_NAME), &config_vec) {
Ok(()) => (),
Err(e) => {
warn!("Python 插件 {:?} 的配置文件信息设置失败:{:?}", path, e);
return Err(PyTypeError::new_err(format!(
"Python 插件 {:?} 的配置文件信息设置失败:{:?}",
path, e
)));
}
}
// 给到 on config
if let Ok(attr) = module.getattr(intern!(module.py(), config_func::ON_CONFIG)) {
if !attr.is_callable() {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数不是 Callable",
path,
config_func::ON_CONFIG
);
return Ok(());
}
let args = (&config_vec,);
if let Err(e) = attr.call1(args) {
event!(
Level::WARN,
"Python 插件 {:?} 的 {} 函数返回了一个报错 {}",
path,
config_func::ON_CONFIG,
e
);
}
}
Ok(())
}
impl TryFrom<RawPyPlugin> for PyPlugin {
type Error = PyErr;
fn try_from(value: RawPyPlugin) -> Result<Self, Self::Error> {
let (path, modify_time, content) = value;
let py_module: Py<PyModule> = match py_module_from_code(&content, &path) {
Ok(module) => module,
Err(e) => {
warn!("加载 Python 插件: {:?} 失败", e);
return Err(e);
}
};
Python::with_gil(|py| {
let module = py_module.bind(py);
if let Ok(config_func) = call::get_func(module, config_func::REQUIRE_CONFIG) {
match config_func.call0() {
Ok(config) => {
if config.is_instance_of::<PyTuple>() {
// let (config, default) = config.extract::<(String, Vec<u8>)>().unwrap();
// let (config, default) = config.extract::<(String, String)>().unwrap();
if let Ok((config, default)) = config.extract::<(String, String)>() {
set_str_cfg_default_plugin(module, default, config)?;
} else if let Ok((config, default)) =
config.extract::<(String, Vec<u8>)>()
{
set_bytes_cfg_default_plugin(module, default, config)?;
} else {
warn!(
"加载 Python 插件 {:?} 的配置文件信息时失败:返回的不是 [str, bytes | str]",
path
);
return Err(PyTypeError::new_err(
"返回的不是 [str, bytes | str]".to_string(),
));
pub fn verify_file(path: &PathBuf) -> bool {
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;
}
}
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
} else if config.is_none() {
// 没有配置文件
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
} else {
warn!(
"加载 Python 插件 {:?} 的配置文件信息时失败:返回的不是 [str, str]",
path
);
Err(PyTypeError::new_err("返回的不是 [str, str]".to_string()))
}
}
Err(e) => {
warn!("加载 Python 插件 {:?} 的配置文件信息时失败:{:?}", path, e);
Err(e)
}
}
} else {
Ok(PyPlugin::new(path, modify_time, module.clone().unbind()))
true
},
None => false,
},
None => false,
}
})
}
}
}
/// 插件路径转换为 id
pub fn plugin_path_as_id(path: &Path) -> String {
path.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or("decode-failed")
.to_string()
}
pub static mut PYSTATUS: PyStatus = PyStatus { files: None };
pub fn load_py_plugins(path: &PathBuf) {
let plugins = PyStatus::get_mut();
if path.exists() {
event!(Level::INFO, "找到位于 {:?} 的插件", path);
info!("finding plugins in: {:?}", path);
// 搜索所有的 py 文件 和 文件夹单层下面的 py 文件
match path.read_dir() {
Err(e) => {
event!(Level::WARN, "读取插件路径失败 {:?}", e);
warn!("failed to read plugin path: {:?}", e);
}
Ok(dir) => {
for entry in dir {
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) {
plugins.add_file(path, plugin);
if let Ok(entry) = entry {
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);
}
}
}
}
}
@ -443,159 +117,98 @@ pub fn load_py_plugins(path: &PathBuf) {
}
}
} else {
event!(Level::WARN, "插件加载目录不存在: {:?}", path);
warn!("plugin path not exists: {:?}", path);
}
plugins.config.read_status_from_default();
plugins.config.sync_status_to_config();
event!(
Level::INFO,
info!(
"python 插件目录: {:?} 加载完成, 加载到 {} 个插件",
path,
plugins.files.len()
PyStatus::get_files().len()
);
}
pub fn get_change_time(path: &Path) -> Option<SystemTime> { path.metadata().ok()?.modified().ok() }
pub fn verify_plugins() {
let plugins = PyStatus::get_files();
for (path, _) in plugins.iter() {
if !PyStatus::verify_file(path) {
info!("file changed: {:?}", path);
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 py_module_from_code(content: &str, path: &Path) -> PyResult<Py<PyModule>> {
Python::with_gil(|py| -> PyResult<Py<PyModule>> {
let module = PyModule::from_code(
py,
CString::new(content).unwrap().as_c_str(),
CString::new(path.to_string_lossy().as_bytes()).unwrap().as_c_str(),
CString::new(path.file_name().unwrap().to_string_lossy().as_bytes())
.unwrap()
.as_c_str(),
// !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的
)?;
Ok(module.unbind())
})
}
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<RawPyPlugin> {
let changed_time = get_change_time(path);
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((path.clone(), changed_time, content))
Ok((changed_time, content))
}
fn init_py_with_env_path(path: &str) {
unsafe {
#[cfg(target_os = "windows")]
use std::ffi::OsStr;
#[cfg(target_os = "windows")]
use std::os::windows::ffi::OsStrExt;
let mut config = std::mem::zeroed::<pyo3::ffi::PyConfig>();
let config_ptr = &mut config as *mut pyo3::ffi::PyConfig;
// 初始化配置
// pyo3::ffi::PyConfig_InitIsolatedConfig(config_ptr);
pyo3::ffi::PyConfig_InitPythonConfig(config_ptr);
#[cfg(target_os = "linux")]
let wide_path = path.as_bytes().iter().map(|i| *i as i32).collect::<Vec<i32>>();
#[cfg(target_os = "windows")]
let wide_path = OsStr::new(path).encode_wide().chain(Some(0)).collect::<Vec<u16>>();
// 设置 prefix 和 exec_prefix
pyo3::ffi::PyConfig_SetString(config_ptr, &mut config.prefix as *mut _, wide_path.as_ptr());
pyo3::ffi::PyConfig_SetString(
config_ptr,
&mut config.exec_prefix as *mut _,
wide_path.as_ptr(),
);
// 使用 Py_InitializeFromConfig 初始化 python
let status = pyo3::ffi::Py_InitializeFromConfig(&config as *const _);
pyo3::ffi::PyEval_SaveThread();
// 清理配置
pyo3::ffi::PyConfig_Clear(config_ptr);
match status._type {
pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_OK => {
event!(Level::INFO, "根据配置初始化 python 完成");
}
pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_EXIT => {
event!(Level::ERROR, "不对啊, 怎么刚刚初始化 Python 就 EXIT 了");
}
pyo3::ffi::_PyStatus_TYPE::_PyStatus_TYPE_ERROR => {
event!(Level::ERROR, "初始化 python 时发生错误: ERROR");
pyo3::ffi::Py_ExitStatusException(status);
}
}
}
}
/// Python 侧初始化
pub fn init_py() {
// 从 全局配置中获取 python 插件路径
let span = span!(Level::INFO, "py init");
let _enter = span.enter();
event!(Level::INFO, "开始初始化 python");
// 注册东西
class::regist_class();
let plugin_path = MainStatus::global_config().py().plugin_path;
let cli_args = std::env::args().collect::<Vec<String>>();
if cli_args.contains(&"-env".to_string()) {
let env_path = cli_args.iter().find(|&arg| arg != "-env").expect("未找到 -env 参数的值");
event!(Level::INFO, "找到 -env 参数: {} 正在初始化", env_path);
// 判断一下是否有 VIRTUAL_ENV 环境变量
if let Ok(virtual_env) = std::env::var("VIRTUAL_ENV") {
event!(Level::WARN, "找到 VIRTUAL_ENV 环境变量: {} 将会被 -env 参数覆盖", virtual_env);
}
init_py_with_env_path(env_path);
} else {
// 根据 VIRTUAL_ENV 环境变量 进行一些处理
match std::env::var("VIRTUAL_ENV") {
Ok(virtual_env) => {
event!(Level::INFO, "找到 VIRTUAL_ENV 环境变量: {} 正在初始化", virtual_env);
init_py_with_env_path(&virtual_env);
}
Err(_) => {
event!(Level::INFO, "未找到 VIRTUAL_ENV 环境变量, 正常初始化");
pyo3::prepare_freethreaded_python();
event!(Level::INFO, "prepare_freethreaded_python 完成");
}
}
pub fn init_py(config: &IcaConfig) {
debug!("initing python threads");
pyo3::prepare_freethreaded_python();
if let Some(plugin_path) = &config.py_plugin_path {
let path = PathBuf::from(plugin_path);
load_py_plugins(&path);
debug!("python 插件列表: {:#?}", PyStatus::get_files());
}
PyStatus::init();
let plugin_path = PathBuf::from(plugin_path);
load_py_plugins(&plugin_path);
event!(Level::DEBUG, "python 插件列表: {}", PyStatus::display());
event!(Level::INFO, "python 初始化完成")
info!("python inited")
}
pub async fn post_py() -> anyhow::Result<()> {
let status = PyStatus::get_mut();
status.config.sync_status_to_config();
status.config.write_to_default()?;
/// 执行 new message 的 python 插件
pub async fn new_message_py(message: &NewMessage, client: &Client) {
// 验证插件是否改变
verify_plugins();
let cwd = std::env::current_dir().unwrap();
stop_tasks().await?;
Ok(())
}
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);
}
async fn stop_tasks() -> Result<(), PyPluginError> {
if call::PY_TASKS.lock().await.is_empty() {
return Ok(());
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);
}
}
});
}
let waiter = tokio::spawn(async {
call::PY_TASKS.lock().await.join_all().await;
});
tokio::select! {
_ = waiter => {
event!(Level::INFO, "Python 任务完成");
Ok(())
}
_ = tokio::signal::ctrl_c() => {
event!(Level::WARN, "正在强制结束 Python 任务");
Err(PyPluginError::PluginNotStopped)
}
// 最后切换回来
if let Err(e) = std::env::set_current_dir(&cwd) {
warn!("设置工作目录{:?} 失败:{:?}", cwd, e);
}
}

View File

@ -1,127 +0,0 @@
use crate::MAIN_STATUS;
use crate::config::BotConfig;
#[derive(Debug, Clone)]
pub struct BotStatus {
pub config: Option<BotConfig>,
pub ica_status: Option<ica::MainStatus>,
pub tailchat_status: Option<tailchat::MainStatus>,
}
impl BotStatus {
pub fn update_static_config(config: BotConfig) {
unsafe {
MAIN_STATUS.config = Some(config);
}
}
pub fn update_ica_status(status: ica::MainStatus) {
unsafe {
MAIN_STATUS.ica_status = Some(status);
}
}
pub fn update_tailchat_status(status: tailchat::MainStatus) {
unsafe {
MAIN_STATUS.tailchat_status = Some(status);
}
}
pub fn static_init(config: BotConfig) {
unsafe {
MAIN_STATUS.ica_status = Some(ica::MainStatus {
enable: config.check_ica(),
qq_login: false,
current_loaded_messages_count: 0,
rooms: Vec::new(),
online_status: ica::OnlineData::default(),
});
MAIN_STATUS.config = Some(config);
}
}
pub fn global_config() -> &'static BotConfig {
unsafe {
let ptr = &raw const MAIN_STATUS.config;
(*ptr).as_ref().unwrap()
}
}
pub fn global_ica_status() -> &'static ica::MainStatus {
unsafe {
let ptr = &raw const MAIN_STATUS.ica_status;
(*ptr).as_ref().unwrap()
}
}
pub fn global_tailchat_status() -> &'static tailchat::MainStatus {
unsafe {
let ptr = &raw const MAIN_STATUS.tailchat_status;
(*ptr).as_ref().unwrap()
}
}
pub fn global_ica_status_mut() -> &'static mut ica::MainStatus {
unsafe {
let ptr = &raw mut MAIN_STATUS.ica_status;
(*ptr).as_mut().unwrap()
}
}
pub fn global_tailchat_status_mut() -> &'static mut tailchat::MainStatus {
unsafe {
let ptr = &raw mut MAIN_STATUS.tailchat_status;
(*ptr).as_mut().unwrap()
}
}
}
pub mod ica {
use crate::data_struct::ica::all_rooms::Room;
pub use crate::data_struct::ica::online_data::OnlineData;
#[derive(Debug, Clone)]
pub struct MainStatus {
/// 是否启用 ica
pub enable: bool,
/// qq 是否登录
pub qq_login: bool,
/// 当前已加载的消息数量
pub current_loaded_messages_count: u64,
/// 房间数据
pub rooms: Vec<Room>,
/// 在线数据 (Icalingua 信息)
pub online_status: OnlineData,
}
impl MainStatus {
pub fn update_rooms(&mut self, room: Vec<Room>) { self.rooms = room; }
pub fn update_online_status(&mut self, status: OnlineData) { self.online_status = status; }
}
}
pub mod tailchat {
use crate::data_struct::tailchat::UserId;
#[derive(Debug, Clone)]
pub struct MainStatus {
/// 是否启用 tailchat
pub enable: bool,
/// 是否登录
pub login: bool,
/// 用户 ID
pub user_id: UserId,
/// 昵称
pub nick_name: String,
/// 邮箱
pub email: String,
/// JWT Token
pub jwt_token: String,
/// avatar
pub avatar: String,
}
impl MainStatus {
pub fn update_user_id(&mut self, user_id: UserId) { self.user_id = user_id; }
pub fn update_nick_name(&mut self, nick_name: String) { self.nick_name = nick_name; }
pub fn update_email(&mut self, email: String) { self.email = email; }
pub fn update_jwt_token(&mut self, jwt_token: String) { self.jwt_token = jwt_token; }
pub fn update_avatar(&mut self, avatar: String) { self.avatar = avatar; }
}
}

View File

@ -1,143 +0,0 @@
pub mod client;
pub mod events;
use std::sync::Arc;
use colored::Colorize;
use md5::{Digest, Md5};
use reqwest::ClientBuilder as reqwest_ClientBuilder;
use rust_socketio::async_callback;
use rust_socketio::asynchronous::{Client, ClientBuilder};
use rust_socketio::{Event, Payload, TransportType};
use serde_json::{Value, json};
use tracing::{Level, event, span};
use crate::config::TailchatConfig;
use crate::data_struct::tailchat::status::{BotStatus, LoginData};
use crate::error::{ClientResult, TailchatError};
use crate::{StopGetter, async_any_callback_with_state, async_callback_with_state, version_str};
pub async fn start_tailchat(
config: TailchatConfig,
stop_reciver: StopGetter,
) -> ClientResult<(), TailchatError> {
let span = span!(Level::INFO, "Tailchat Client");
let _enter = span.enter();
event!(Level::INFO, "tailchat-async-rs v{} initing", crate::TAILCHAT_VERSION);
let mut hasher = Md5::new();
hasher.update(config.app_id.as_bytes());
hasher.update(config.app_secret.as_bytes());
let token = format!("{:x}", hasher.finalize());
let mut header_map = reqwest::header::HeaderMap::new();
header_map.append("Content-Type", "application/json".parse().unwrap());
let client = reqwest_ClientBuilder::new().default_headers(header_map.clone()).build()?;
let status = match client
.post(format!("{}/api/openapi/bot/login", config.host))
.body(json! {{"appId": config.app_id, "token": token}}.to_string())
.send()
.await
{
Ok(resp) => {
if resp.status().is_success() {
let raw_data = resp.text().await?;
let json_data = serde_json::from_str::<Value>(&raw_data).unwrap();
let login_data = serde_json::from_value::<LoginData>(json_data["data"].clone());
match login_data {
Ok(data) => data,
Err(e) => {
event!(Level::ERROR, "login failed: {}|{}", e, raw_data);
return Err(TailchatError::LoginFailed(e.to_string()));
}
}
} else {
return Err(TailchatError::LoginFailed(resp.text().await?));
}
}
Err(e) => return Err(TailchatError::LoginFailed(e.to_string())),
};
status.update_to_global();
let sharded_status = BotStatus::new(status.user_id.clone());
let sharded_status = Arc::new(sharded_status);
let socket = ClientBuilder::new(config.host)
.auth(json!({"token": status.jwt.clone()}))
.transport_type(TransportType::Websocket)
.on_any(async_any_callback_with_state!(events::any_event, sharded_status.clone()))
.on(
"notify:chat.message.add",
async_callback_with_state!(events::on_message, sharded_status.clone()),
)
.on("notify:chat.message.delete", async_callback!(events::on_msg_delete))
.on(
"notify:chat.converse.updateDMConverse",
async_callback!(events::on_converse_update),
)
// .on("notify:chat.message.update", wrap_callback!(events::on_message))
// .on("notify:chat.message.addReaction", wrap_callback!(events::on_msg_update))
.connect()
.await
.unwrap();
event!(Level::INFO, "{}", "已经连接到 tailchat!".green());
// sleep for 500ms to wait for the connection to be established
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
socket.emit("chat.converse.findAndJoinRoom", json!([])).await.unwrap();
event!(Level::INFO, "{}", "tailchat 已经加入房间".green());
if config.notice_start {
event!(Level::INFO, "正在发送启动消息");
for (group, con) in config.notice_room {
event!(Level::INFO, "发送启动消息到: {}|{}", con, group);
let startup_msg =
crate::data_struct::tailchat::messages::SendingMessage::new_without_meta(
format!("{}\n启动成功", version_str()),
con.clone(),
Some(group.clone()),
);
// 反正是 tailchat, 不需要等, 直接发
if let Err(e) = socket.emit("chat.message.sendMessage", startup_msg.as_value()).await {
event!(Level::ERROR, "发送启动消息失败: {}", e);
}
}
}
stop_reciver.await.ok();
event!(Level::INFO, "socketio client stopping");
match socket.disconnect().await {
Ok(_) => {
event!(Level::INFO, "socketio client stopped");
Ok(())
}
Err(e) => {
// 单独处理 SocketIoError(IncompleteResponseFromEngineIo(WebsocketError(AlreadyClosed)))
match e {
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e) => {
if inner_e.to_string().contains("AlreadyClosed") {
event!(Level::INFO, "socketio client stopped");
Ok(())
} else {
event!(Level::ERROR, "socketio client stopped with error: {:?}", inner_e);
Err(TailchatError::SocketIoError(
rust_socketio::Error::IncompleteResponseFromEngineIo(inner_e),
))
}
}
e => {
event!(Level::ERROR, "socketio client stopped with error: {}", e);
Err(TailchatError::SocketIoError(e))
}
}
}
}
}

View File

@ -1,106 +0,0 @@
use crate::data_struct::tailchat::messages::SendingMessage;
// use crate::data_struct::tailchat::{ConverseId, GroupId, MessageId, UserId};
use colored::Colorize;
use reqwest::multipart;
use rust_socketio::asynchronous::Client;
use serde_json::{Value, json};
use tracing::{Level, event, span};
pub async fn send_message(client: &Client, message: &SendingMessage) -> bool {
let span = span!(Level::INFO, "tailchat send message");
let _enter = span.enter();
let mut value: Value = message.as_value();
if message.contain_file() {
// 处理文件
let mut header = reqwest::header::HeaderMap::new();
header.append(
"X-Token",
crate::MainStatus::global_tailchat_status().jwt_token.clone().parse().unwrap(),
);
let file_client = match reqwest::ClientBuilder::new().default_headers(header).build() {
Ok(client) => client,
Err(e) => {
event!(Level::ERROR, "file_client build failed:{}", format!("{:#?}", e).red());
return false;
}
};
// 感谢 https://stackoverflow.com/questions/65814450/how-to-post-a-file-using-reqwest
let upload_url =
format!("{}/upload", crate::MainStatus::global_config().tailchat().host.clone());
let file_body =
multipart::Part::stream(message.file.file_data()).file_name(message.file.file_name());
let form_data = multipart::Form::new().part("file", file_body);
event!(Level::INFO, "sending file message");
let data = match file_client.post(&upload_url).multipart(form_data).send().await {
Ok(resp) => {
if resp.status().is_success() {
match resp.text().await {
Ok(text) => match serde_json::from_str::<Value>(&text) {
Ok(json) => json,
Err(e) => {
event!(
Level::ERROR,
"file uploaded, but response parse failed:{}",
format!("{:#?}", e).red()
);
return false;
}
},
Err(e) => {
event!(
Level::ERROR,
"file uploaded, but failed to get response:{}",
format!("{:#?}", e).red()
);
return false;
}
}
} else {
event!(Level::ERROR, "file upload faild:{}", format!("{:#?}", resp).red());
return false;
}
}
Err(e) => {
event!(
Level::ERROR,
"file upload failed while posting data:{}",
format!("{:#?}", e).red()
);
return false;
}
};
let content = format!(
"{}{}",
message.content,
message.file.gen_markdown(data["url"].as_str().unwrap())
);
value["content"] = json!(content);
}
match client.emit("chat.message.sendMessage", value).await {
Ok(_) => {
event!(Level::DEBUG, "send message {}", format!("{:#?}", message).cyan());
true
}
Err(e) => {
event!(Level::WARN, "send message failed:{}", format!("{:#?}", e).red());
false
}
}
}
pub async fn emit_join_room(client: &Client) -> bool {
let span = span!(Level::INFO, "tailchat findAndJoinRoom");
let _enter = span.enter();
match client.emit("chat.converse.findAndJoinRoom", json!([])).await {
Ok(_) => {
event!(Level::INFO, "emiting join room");
true
}
Err(e) => {
event!(Level::WARN, "emit_join_room faild:{}", format!("{:#?}", e).red());
false
}
}
}

View File

@ -1,171 +0,0 @@
use std::sync::Arc;
use colored::Colorize;
use rust_socketio::asynchronous::Client;
use rust_socketio::{Event, Payload};
use tracing::{Level, event, info};
use crate::data_struct::tailchat::messages::ReceiveMessage;
use crate::data_struct::tailchat::status::{BotStatus, UpdateDMConverse};
use crate::py::PyStatus;
use crate::py::call::tailchat_new_message_py;
use crate::tailchat::client::{emit_join_room, send_message};
use crate::{MainStatus, VERSION, client_id, help_msg, version_str};
/// 所有
pub async fn any_event(event: Event, payload: Payload, _client: Client, _status: Arc<BotStatus>) {
let handled = [
// 真正处理过的
"notify:chat.message.add",
"notify:chat.message.delete",
"notify:chat.converse.updateDMConverse",
// 也许以后会用到
"notify:chat.message.update",
"notify:chat.message.addReaction",
"notify:chat.message.removeReaction",
// 忽略的
"notify:chat.inbox.append", // 被 @ 之类的事件
];
match &event {
Event::Custom(event_name) => {
if handled.contains(&event_name.as_str()) {
return;
}
}
Event::Message => {
match payload {
Payload::Text(values) => {
if let Some(value) = values.first() {
if handled.contains(&value.as_str().unwrap()) {
return;
}
info!("收到消息 {}", value.to_string().yellow());
}
}
_ => {
return;
}
}
return;
}
_ => (),
}
match payload {
Payload::Binary(ref data) => {
println!("event: {} |{:?}", event, data)
}
Payload::Text(ref data) => {
print!("event: {}", event.as_str().purple());
for value in data {
println!("|{}", value);
}
}
_ => (),
}
}
pub async fn on_message(payload: Payload, client: Client, _status: Arc<BotStatus>) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
let message: ReceiveMessage = match serde_json::from_value(value.clone()) {
Ok(v) => v,
Err(e) => {
event!(Level::WARN, "tailchat_msg {}", value.to_string().red());
event!(Level::WARN, "tailchat_msg {}", format!("{:?}", e).red());
return;
}
};
event!(Level::INFO, "tailchat_msg {}", message.to_string().yellow());
if !message.is_reply() {
if message.content == "/bot-rs" {
let reply = message.reply_with(&version_str());
send_message(&client, &reply).await;
} else if message.content == "/bot-ls" {
let reply = message.reply_with(&format!(
"shenbot-py v{}-{}\n{}",
VERSION,
client_id(),
if MainStatus::global_config().check_py() {
PyStatus::display()
} else {
"未启用 Python 插件".to_string()
}
));
send_message(&client, &reply).await;
} else if message.content == "/bot-help" {
let reply = message.reply_with(&help_msg());
send_message(&client, &reply).await;
}
if MainStatus::global_config().tailchat().admin_list.contains(&message.sender_id) {
// admin 区
let client_id = client_id();
if message.content.starts_with(&format!("/bot-enable-{}", client_id)) {
// 先判定是否为 admin
// 尝试获取后面的信息
if let Some((_, name)) = message.content.split_once(" ") {
match PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
}
Some(true) => {
let reply = message.reply_with("无变化, 插件已经启用");
send_message(&client, &reply).await;
}
Some(false) => {
PyStatus::get_mut().set_status(name, true);
let reply = message.reply_with("启用插件完成");
send_message(&client, &reply).await;
}
}
}
} else if message.content.starts_with(&format!("/bot-disable-{}", client_id)) {
if let Some((_, name)) = message.content.split_once(" ") {
match PyStatus::get().get_status(name) {
None => {
let reply = message.reply_with("未找到插件");
send_message(&client, &reply).await;
}
Some(false) => {
let reply = message.reply_with("无变化, 插件已经禁用");
send_message(&client, &reply).await;
}
Some(true) => {
PyStatus::get_mut().set_status(name, false);
let reply = message.reply_with("禁用插件完成");
send_message(&client, &reply).await;
}
}
}
}
}
}
tailchat_new_message_py(&message, &client).await;
}
}
}
pub async fn on_msg_delete(payload: Payload, _client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
info!("删除消息 {}", value.to_string().red());
}
}
}
pub async fn on_converse_update(payload: Payload, client: Client) {
if let Payload::Text(values) = payload {
if let Some(value) = values.first() {
emit_join_room(&client).await;
let update_info: UpdateDMConverse = match serde_json::from_value(value.clone()) {
Ok(value) => value,
Err(e) => {
event!(Level::WARN, "tailchat updateDMConverse {}", value.to_string().red());
event!(Level::WARN, "tailchat updateDMConverse {}", format!("{:?}", e).red());
return;
}
};
info!("更新会话 {}", format!("{:?}", update_info).cyan());
}
}
}

View File

@ -1 +0,0 @@

36
main.py Normal file
View File

@ -0,0 +1,36 @@
import asyncio
import argparse
# from lib_not_dr.types import Options
from lib_not_dr.loggers import config
from data_struct import get_config, BotConfig, BotStatus
_version_ = "0.3.3"
logger = config.get_logger("bot")
BOTCONFIG: BotConfig = get_config()
BotStatus = BotStatus()
if __name__ == "__main__":
# --debug
# --config=config.toml
# -n --no-notice
parser = argparse.ArgumentParser(description=f"icalingua bot v{_version_}")
parser.add_argument("-d", "--debug", action="store_true")
parser.add_argument("-n", "--no-notice", action="store_true")
parser.add_argument("-c", "--config", type=str)
args = parser.parse_args()
if args.debug:
logger.global_level = 0
if args.config:
# global BOTCONFIG
BOTCONFIG: BotConfig = get_config(args.config)
if args.no_notice:
BOTCONFIG.notice_start = False
from connect import main
asyncio.run(main())

47
news.md
View File

@ -1,5 +1,48 @@
# 更新日志
## [0.9](./news/0-9.md)
## 0.4.5
## [0.2 ~ 0.8](./news/old.md)
添加 `is_reply` api 到 `NewMessagePy`
## 0.4.4
现在正式支持 Python 插件了
`/bmcl` 也迁移到了 Python 插件版本
## 0.4.3
噫! 好! 我成了!
## 0.4.2
现在是 async 版本啦!
## 0.4.1
现在能发送登录信息啦
## 0.4.0
使用 Rust 从头实现一遍
\能登录啦/
## 0.3.3
适配 Rust 端的配置文件修改
## 0.3.1/2
改进 `/bmcl` 的细节
## 0.3.0
合并了 dongdigua 的代码, 把消息处理部分分离
现在代码更阳间了(喜
## 0.2.3
添加了 `/bmcl` 请求 bmclapi 状态
## 0.2.2
重构了一波整体代码

View File

@ -1,38 +0,0 @@
# 0.9 更新日志
## 0.9.0
- 修复了 Python 插件停不下来就真的停不下来的问题
- 让初始化的时候 插件启/禁状态显示更明显了
- 有颜色啦!
- 加了不少颜色
### ica 2.0.1
> 添加了 `Room` 相关的 api
- `IcaStatus` 添加了 `rooms(self) -> list[IcaRoom]` 方法
- 用于获取当前所有的房间
- 添加了 `IcaRoom`
- 用于表示一个房间
- `room_id -> int` 群号 (i64)
- `def is_group(self) -> bool` 是否为群聊
- `def is_chat(self) -> bool` 是否为私聊
- `room_name -> int` 群名 (String)
- `unread_count -> int` 未读消息数 (u64)
- `priority -> int` 优先级 (u8)
- `utime -> int` 最后活跃时间 (unix sec * 1000)
- `def new_message_to(self, content: str) -> IcaSendMessage`
- 用于创建一条指向这个房间的消息
> 添加了 Ica 侧的相关配置获取
- `IcaStatus` 添加了 `admins(self) -> list[UserId]` 方法
- 用于获取当前所有的管理员
- `IcaStatus` 添加了 `blocked(self) -> list[UserId]` 方法
- 用于获取当前所有的被屏蔽的人

View File

@ -1,383 +0,0 @@
# 更新日志 (0.2 ~ 0.8)
## 0.8.2
- ica 兼容版本号更新到 `2.12.28`
- 现在支持通过读取环境变量里的 `VIRTUAL_ENV` 来获取虚拟环境路径
- 用于在虚拟环境中运行插件
- 添加了 `-h` 参数
- 用于展示帮助信息
- 添加了 `-env` 参数
- 用于指定 python 插件的虚拟环境路径
- 会覆盖环境变量中的 `VIRTUAL_ENV`
- 现在加入了默认的配置文件路径 `./config.toml`
- 现在会记录所有的 python 运行中 task 了
- 也会在退出的时候等待所有的 task 结束
- 二次 ctrl-c 会立即退出
- 改进了一下 ica 的新消息显示
- 添加了 ica 链接用时的显示
### ica&tailchat 2.0.0
- BREAKING CHANGE
- 现在 `CONFIG_DATA` 为一个 `str | bytes`
- 用于存储插件的配置信息
- 需要自行解析
- 现在 `on_config` 函数签名为 `on_config = Callable[[bytes], None]`
- 用于接收配置信息
- 现在使用 `require_config = Callable[[None], str, bytes | str]` 函数来请求配置信息
- 用于请求配置信息
- `str` 为配置文件名
- `bytes | str` 为配置文件默认内容
- 以 `bytes` 形式或者 `str` 形式均可
### ica 1.6.7
- 为 `IcaClinet` 添加了 `py_tasks_count -> int` 属性
- 用于获取当前运行中的 python task 数量
### tailchat 1.2.6
- 为 `TailchatClient` 添加了 `py_tasks_count -> int` 属性
- 用于获取当前运行中的 python task 数量
## 0.8.1
- 修复了 Python 插件状态写入的时候写入路径错误的问题
- `ica-typing` 加入了 `from __future__ import annotations`
- 这样就可以随便用 typing 了
- 把 NewType 都扬了
### ica 1.6.6
- 修复了 `send_poke` api 的问题
- 现在可以正常使用了
## 0.8.0
- ica 兼容版本号更新到 ~~`2.12.24`~~ `2.12.26`
- 从 `py::PyStatus` 开始进行一个 `static mut` -> `static mut OnceLock` 的改造
- 用于看着更舒服(逃)
- 部分重构了一下 读取插件启用状态 的配置文件的代码
- 现在 `/bot-help` 会直接输出实际的 client id, 而不是给你一个默认的 `<client-id>`
### ica 1.6.5
- 添加了 `send_room_sign_in` api
- 用于发送群签到信息
- socketio event: `sendGroupSign`
- 添加了 `send_poke` api
- 用于发送戳一戳
- 可以指定群的某个人
- 或者指定好友
- 目前还是有点问题
- socketio event: `sendGroupPoke`
- 添加了 `reload_plugin_status` api
- 用于重新加载插件状态
- 添加了 `reload_plugin(plugin_name: str)` api
- 用于重新加载指定插件
- 添加了 `set_plugin_status(plugin_name: str, status: bool)` api
- 用于设置插件的启用状态
- 添加了 `get_plugin_status(plugin_name: str) -> bool` api
- 用于获取插件的启用状态
- 添加了 `sync_status_to_config` api
- 用于将内存中的插件状态同步到配置文件中
### tailchat 1.2.5
- 添加了 `reload_plugin_status` api
- 用于重新加载插件状态
- 添加了 `reload_plugin(plugin_name: str)` api
- 用于重新加载指定插件
- 添加了 `set_plugin_status(plugin_name: str, status: bool)` api
- 用于设置插件的启用状态
- 添加了 `get_plugin_status(plugin_name: str) -> bool` api
- 用于获取插件的启用状态
- 添加了 `sync_status_to_config` api
- 用于将内存中的插件状态同步到配置文件中
## 0.7.4
- ica 兼容版本号更新到 `2.12.23`
- 通过一个手动 json patch 修复了因为 icalingua 的奇怪类型问题导致的 bug
- [icalingua issue](https://github.com/Icalingua-plus-plus/Icalingua-plus-plus/issues/793)
## 0.7.3
- 也许修复了删除插件不会立即生效的问题
- ica 兼容版本号更新到 `2.12.21`
添加了一些新的 api
### ica 1.6.4
- 给 `SendMessagePy`
- 添加了 `remove_reply` 方法
- 用于取消回复状态
- 删除了 `Room``auto_download``download_path` 字段
- 因为这两个字段也没啥用
### tailcaht 1.2.4
- 给 `TailchatClientPy`
- 添加了 `new_message` 方法
- 用于创建新的消息
- 给 `TailchatSendingMessagePy`
- 添加了 `clear_meta` 功能
- 用于清除 `meta` 字段
- 可以用来取消回复状态
## 0.7.2
- 修复了一些 ica 和 tailchat 表现不一致的问题(捂脸)
## 0.7.1
- 两个 api 版本号分别升级到 `1.6.3(ica)``1.2.3(tailchat)`
- 加入了 `client_id`
- 用的 startup time hash 一遍取后六位
- 以及也有 python 侧的 `client_id` api
- 修复了上个版本其实没有写 python 侧 `version_str` api 的问题
## 0.7.0
> 我决定叫他 0.7.0
> 因为修改太多了.png
- 加入了 禁用/启用 插件功能
- 现在会在插件加载时警告你的插件原来定义了 `CONFIG_DATA` 这一项
- `IcaNewMessage` 添加了新的 api
- `get_sender_name` 获取发送人昵称
- `ica` 兼容版本号 `2.12.11` -> `2.12.12`
- 加入了 `STABLE` 信息, 用于标记稳定版本
- 不少配置文件项加上了默认值
- 添加了 `version_str() -> String` 用于方便的获取版本信息
- 同样在 `py` 侧也有 `version_str` 方法
- 加入了 `/help` 命令
- 用于获取帮助信息
- 加入了 `/bot-ls`
- 用于展示所有插件的信息
- 加入了 `/bot-enable``/bot-disable`
- 用于启用/禁用插件
## 0.6.10
- 加了点东西 (?)
## 0.6.9
我决定立即发布 0.6.9
- 添加了 `Client.startup_time() -> datetime` 方法
- 用于获取 bot 启动时间
- 这样就可以经常吹我 bot 跑了多久了 ( ˘•ω•˘ )
- 顺手加上了 `/bot-uptime` 命令
- 可以获取 bot 运行时间
- 谢谢 GitHub Copilot 的帮助
## 0.6.8
- 修复了一堆拼写错误
- 太难绷了
- `TailchatReciveMessagePy` -> `TailchatReceiveMessagePy`
- `ReciveMessage` -> `ReceiveMessage`
- `ReceiveMessage::meta`
- 从 `JsonValue` 改成 `Option<JsonValue>`
- 用来解决发图片的时候没有 `meta` 字段的问题
- 去除了自带的两个 macro
- `wrap_callback``wrap_any_callback`
- 因为现在他俩已经进到 `rust_socketio` 里啦
- 添加了新的 macro
- 支持了 `TailchatReceiveMessagePy``is_from_self` 方法
- 用于判断是否是自己发的消息
## 0.6.7
游学回来啦
- 处理了一些 tailchat 的特殊情况
- 比如 message 里面的 `GroupId` 实际上是可选的, 在私聊中没有这一项
- 忽略了所有的 `__v` (用于数据库记录信息的, bot不需要管)
- 作者原话 `不用管。数据库记录版本`
- 修复了如果没法解析新的信息, 会 panic 的问题
- `ica_typing.py`
- 补充了 `TailchatSendingMessage``group_id``converse_id` 字段
- 把 `group_id` 的设置和返回都改成了 `Optional[GroupId]`
- tailchat 的 API 也差点意思就是了(逃)
- 处理了 icalingua 的 `renewMessage` 事件 (其实就是直接忽略掉了)
## 0.6.6
游学之前最后一次更新
其实也就五天
正式支持了 tailchat 端
好耶!
[!note]
```text
notice_room = []
notice_start = true
admin_list = []
filter_list = []
```
的功能暂时不支持
## 0.6.5
怎么就突然 0.6.5 了
我也不造啊
- 反正支持了 tailchat 的信息接受
- 但是需要你在对面服务端打开 `DISABLE_MESSAGEPACK` 环境变量
- 能用就行
- 现在 `update_online_data` 不会再以 INFO 级别显示了
- `update_all_room` 同上
## 0.6.2
- 添加 API
- `NewMessage.set_img` 用于设置消息的图片
- `IcaSendMessage.set_img` 用于设置消息的图片 (python)
## 0.6.1
还是没写完 tailchat 支持
因为 rust_socketio 还是没写好 serdelizer 的支持
- 正在添加发送图片的 api
## 0.6.0-dev
- 去除了 matrix 的支持
- 淦哦
- 去除了相应代码和依赖
- 去除了 Python 侧代码
- 向 tailchat (typescript 低头)
- 修复了没法编译的问题(
## 0.5.3
修复了 Icalingua 断开时 如果 socketio 已经断开会导致程序 返回 Error 的问题
以及还有一些别的修复就是了
- Python 端修改
- `on_message` -> `on_ica_message`
- `on_delete_message` -> `on_ica_delete_message`
- 添加 `on_matrix_message`
## 0.5.1/2
重构了一整波, 还没改 `ica-typing.py` 的代码
但至少能用了
- Ica 版本号 `1.4.0`
- Matrix 版本号 `0.1.0`
## 0.5.0
准备接入 `Matrix`
去掉 `pyo3-async` 的依赖
## 0.4.12
把 0.4.11 的遗留问题修完了
## 0.4.11
这几天就是在刷版本号的感觉
- 添加
- `DeleteMessage` 用于删除消息
- `NewMessage.as_delete` 用于将消息转换为删除消息
- `client::delete_message` 用于删除消息
- `client::fetch_history` 用于获取历史消息 TODO
- `py::class::DeleteMessagePy` 用于删除消息 的 Python 侧 API
- `py::class::IcaClientPy.delete_message` 用于删除消息 的 Python 侧 API
- `IcalinguaStatus.current_loaded_messages_count`
- 用于以后加载信息计数
- 修改
- `py::class::IcaStatusPy`
- 大部分方法从手动 `unsafe` + `Option`
- 改成直接调用 `IcalinguaStatus` 的方法
- `IcalinguaStatus`
- 所有方法均改成 直接对着 `IcalinguaStatus` 的方法调用
- 补全没有的方法
## 0.4.10
好家伙, 我感觉都快能叫 0.5 了
修改了一些内部数据结构, 使得插件更加稳定
添加了 `rustfmt.toml` 用于格式化代码
**注意**: 请在提交代码前使用 `cargo +nightly fmt` 格式化代码
修复了 `Message` 解析 `replyMessage` 字段是 如果是 null 则会解析失败的问题
## 0.4.9
修复了 Python 插件运行错误会导致整个程序崩溃的问题
## 0.4.8
添加了 `filter_list` 用于过滤特定人的消息
## 0.4.7
修复了重载时如果代码有问题会直接 panic 的问题
## 0.4.6
现在更适合部署了
## 0.4.5
添加 `is_reply` api 到 `NewMessagePy`
## 0.4.4
现在正式支持 Python 插件了
`/bmcl` 也迁移到了 Python 插件版本
## 0.4.3
噫! 好! 我成了!
## 0.4.2
现在是 async 版本啦!
## 0.4.1
现在能发送登录信息啦
## 0.4.0
使用 Rust 从头实现一遍
\能登录啦/
## 0.3.3
适配 Rust 端的配置文件修改
## 0.3.1/2
改进 `/bmcl` 的细节
## 0.3.0
合并了 dongdigua 的代码, 把消息处理部分分离
现在代码更阳间了(喜
## 0.2.3
添加了 `/bmcl` 请求 bmclapi 状态
## 0.2.2
重构了一波整体代码

98
plugins/bmcl.py Normal file
View File

@ -0,0 +1,98 @@
import time
import json
import aiohttp
from lib_not_dr.loggers import config
from data_struct import NewMessage, SendMessage
logger = config.get_logger("bmcl")
_version_ = "1.1.1"
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))
async def bmcl(sio, reply_msg: SendMessage, msg: NewMessage):
req_time = time.time()
# 记录请求时间
async with aiohttp.ClientSession() as session:
async with session.get(
"https://bd.bangbang93.com/openbmclapi/metric/dashboard"
) as response:
if not response.status == 200 or response.reason != "OK":
await sio.emit(
"sendMessage",
reply_msg.to_content(
f"请求数据失败\n{response.status}"
).to_json(),
)
logger.warn(
f"数据请求失败, 请检查网络\n{response.status}",
tag="bmclapi_dashboard",
)
return
raw_data = await response.text()
try:
data = json.loads(raw_data)
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"
)
await sio.emit(
"sendMessage",
reply_msg.to_content(report_msg).to_json()
)
except (json.JSONDecodeError, AttributeError, ValueError) as e:
await sio.emit(
"sendMessage",
reply_msg.to_content(f"返回数据解析错误\n{e}").to_json(),
)
logger.warn(f"返回数据解析错误\n{e}", tag="bmclapi_dashboard")

72
plugins/safe_eval.py Normal file
View File

@ -0,0 +1,72 @@
import time
import random
import traceback
from main import BOTCONFIG
from colorama import Fore
from lib_not_dr.loggers import config
logger = config.get_logger("safe_eval")
def safe_eval(code: str) -> str:
try:
# code = code.replace('help', '坏东西!\n')
# code = code.replace('bytes', '坏东西!\n')
# code = code.replace('encode', '坏东西!\n')
# code = code.replace('decode', '坏东西!\n')
# code = code.replace('compile', '屑的!\n')
# code = code.replace('globals', '拿不到!\n')
code = code.replace("os", "坏东西!\n")
code = code.replace("sys", "坏东西!\n")
# code = code.replace('input', '坏东西!\n')
# code = code.replace('__', '啊哈!\n')
# code = code.replace('import', '很坏!\n')
code = code.replace(" kill", "别跑!\n")
code = code.replace(" rm ", "别跑!\n")
code = code.replace("exit", "好坏!\n")
code = code.replace("eval", "啊哈!\n")
code = code.replace("exec", "抓住!\n")
start_time = time.time()
try:
import os
import math
import decimal
global_val = {
"time": time,
"math": math,
"decimal": decimal,
"random": random,
"__import__": "<built-in function __import__>",
"globals": "<built-in function globals>",
"compile": "<built-in function compile>",
"help": "<built-in function help>",
"exit": "<built-in function exit>",
"input": "<built-in function input>",
"return": "别惦记你那个 return 了",
"getattr": "<built-in function getattr>",
"setattr": "<built-in function setattr>",
}
os.system = "不许"
result = str(eval(code, global_val, {}))
limit = 500
if len(result) > limit:
result = result[:limit]
except:
result = traceback.format_exc()
end_time = time.time()
result = result.replace(BOTCONFIG.private_key, "***")
result = result.replace(BOTCONFIG.host, "***")
logger.info(f"{Fore.MAGENTA}safe_eval: {result}")
if result == "6" or result == 6:
result = "他确实等于 6"
result = f"{code}\neval result:\n{result}\n耗时: {end_time - start_time} s"
return result
except:
error = traceback.format_exc()
result = f"error:\n{error}"
return result

View File

@ -1,33 +1,47 @@
# icalingua bot
这是一个基于 icalingua-bridge 的 bot
这是一个基于 icalingua docker 版的 bot
[插件市场(确信)](https://github.com/shenjackyuanjie/shenbot-plugins)
> 出于某个企鹅, 和保护 作者 和 原项目 ( ica ) 的原因 \
> 功能请自行理解
## 通用环境准备
## 使用方法 ( Python 版 )
- 安装 Python 3.8+
- 安装依赖
```powershell
# 你可以使用你自己的方法安装 Python
# 例如
choco install python
# 或者
scoop install python
# 又或者
uv venv
python -m pip install -r requirement.txt
```
- 启动 icalingua 后端
> 如果你想使用虚拟环境 \
> 可以使用 `python -m venv venv` 创建虚拟环境 \
> 然后使用 `venv\Scripts\activate` 激活虚拟环境 \
> 最后使用 `python -m pip install -r requirement.txt` 安装依赖
- 修改配置文件
```powershell
Copy-Item config-temp.toml config.toml
# 欸我就用 powershell
```
- icalingua 启动!
```bash
# 用你自己的方法启动你的 icalingua-bridge
# 用你自己的方法启动你的 icalingua 后端
# 例如
docker start icalingua
docker-compose up -d
# 或者
docker up -d
```
## 使用方法
- bot 启动!
```powershell
python connect.py
```
## 使用方法 ( Rust 版 )
- 准备一个 Python 环境
@ -44,7 +58,3 @@ cargo build --release
```
运行
```powershell
cargo run --release -- -c config.toml
```

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
lib-not-dr >= 0.3.13
colorama
qtoml
pynacl
python-socketio
aiohttp

62
router.py Normal file
View File

@ -0,0 +1,62 @@
import random
import asyncio
from lib_not_dr.loggers import config
from main import BOTCONFIG, _version_
from data_struct import SendMessage, ReplyMessage, NewMessage
from plugins import bmcl, safe_eval
logger = config.get_logger("router")
async def route(data, sio):
is_self = data["message"]["senderId"] == BOTCONFIG.self_id
sender_name = data["message"]["username"]
sender_id = data["message"]["senderId"]
content = data["message"]["content"]
room_id = data["roomId"]
msg_id = data["message"]["_id"]
msg = NewMessage(data=data)
reply_msg = SendMessage(content="", room_id=room_id, reply_to=ReplyMessage(id=msg_id))
if content == "/bot":
message = reply_msg.to_content(f"icalingua bot-python pong v{_version_}")
await sio.emit("sendMessage", message.to_json())
elif content.startswith("=="):
evals: str = content[2:]
result = safe_eval.safe_eval(evals)
# whitelist = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ', '.', '+', '-', '*', '/', '(', ')', '<',
# '>', '=']
# evals = evals.replace('**', '')
# express = ''
# for text in evals:
# if text in whitelist:
# express += text
# if express == '':
# result = '你在干嘛'
# else:
# result = str(eval(express))
message = reply_msg.to_content(result)
await asyncio.sleep(random.random() * 2)
await sio.emit("sendMessage", message.to_json())
elif content == "!!jrrp":
randomer = random.Random(
f'{sender_id}-{data["message"]["date"]}-jrrp-{_version_}'
)
result = randomer.randint(0, 50) + randomer.randint(0, 50)
logger.info(f"{sender_name} 今日人品值为 {result}")
message = reply_msg.to_content(f"{sender_name} 今日人品为 {result}")
await asyncio.sleep(0.5)
await sio.emit("sendMessage", message.to_json())
elif content == "/bmcl":
await bmcl.bmcl(sio, reply_msg, msg)

View File

@ -1,2 +0,0 @@
[toolchain]
channel = "stable"