Compare commits

..

No commits in common. "9f5956e77aba839294d89da019b830a16e0c065c" and "3ed0f5af1ef000347835b17627ed66f4cdbb0b2b" have entirely different histories.

23 changed files with 345 additions and 511 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "ica-rs"
version = "0.4.10"
version = "0.4.9"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -94,11 +94,6 @@ class IcaClient:
"""
def send_message(self, message: SendMessage) -> bool:
...
@property
def status() -> IcaStatus:
...
def debug(self, message: str) -> None:
"""向日志中输出调试信息"""
def info(self, message: str) -> None:
@ -115,6 +110,4 @@ on_message = Callable[[NewMessage, IcaClient], None]
# def on_message(msg: NewMessage, client: IcaClient) -> None:
# ...
on_delete_message = Callable[[MessageId, IcaClient], None]
# def on_delete_message(msg_id: MessageId, client: IcaClient) -> None:
# ...
on_delete_message = Callable[[int, IcaClient], None]

View File

@ -44,25 +44,11 @@ def format_hit_count(count: int) -> str:
if count_len <= 4:
return count_str
else:
# 先倒序
# 再插入
# 最后再倒序
count_str = count_str[::-1]
count_str = "_".join([count_str[i:i+4] for i in range(0, count_len, 4)])
count_str = count_str[::-1]
return count_str
return "_".join(count_str[i:i + 4] for i in range(0, count_len, 4))
def wrap_request(url: str, msg: NewMessage, client: IcaClient) -> Optional[dict]:
try:
response = requests.get(url)
except requests.exceptions.RequestException as e:
client.warn(
f"数据请求失败, 请检查网络\n{e}"
)
reply = msg.reply_with(f"数据请求失败, 请检查网络\n{e}")
client.send_message(reply)
return None
def wrap_request(url: str, client: IcaClient) -> Optional[dict]:
response = requests.get(url)
if not response.status_code == 200 or response.reason != "OK":
client.warn(
f"数据请求失败, 请检查网络\n{response.status}"
@ -75,7 +61,7 @@ def wrap_request(url: str, msg: NewMessage, client: IcaClient) -> Optional[dict]
def bmcl_dashboard(msg: NewMessage, client: IcaClient) -> None:
req_time = time.time()
# 记录请求时间
data = wrap_request("https://bd.bangbang93.com/openbmclapi/metric/dashboard", msg, client)
data = wrap_request("https://bd.bangbang93.com/openbmclapi/metric/dashboard", client)
if data is None:
return
data_bytes: float = data["bytes"]
@ -118,7 +104,7 @@ def parse_rank(data: dict) -> dict:
def bmcl_rank(msg: NewMessage, client: IcaClient, name: Optional[str]) -> None:
req_time = time.time()
# 记录请求时间
rank_data = wrap_request("https://bd.bangbang93.com/openbmclapi/metric/rank", msg, client)
rank_data = wrap_request("https://bd.bangbang93.com/openbmclapi/metric/rank", client)
if rank_data is None:
return
ranks = [parse_rank(data) for data in rank_data]

View File

@ -1,23 +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"

View File

@ -47,14 +47,25 @@ impl IcalinguaStatus {
self.online_data = Some(online_data);
}
pub fn update_rooms(&mut self, rooms: Vec<Room>) { self.rooms = Some(rooms); }
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_login_status(&mut self, login: bool) {
self.login = login;
}
pub fn update_config(&mut self, config: IcaConfig) { self.config = Some(config); }
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") }
unsafe {
ClientStatus
.online_data
.as_ref()
.expect("online_data should be set")
}
}
pub fn get_config() -> &'static IcaConfig {
@ -88,5 +99,8 @@ pub async fn sign_callback(payload: Payload, client: Client) {
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");
client
.emit("auth", sign)
.await
.expect("Faild to send signin data");
}

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,3 +1,4 @@
use crate::client::IcalinguaStatus;
use crate::data_struct::files::MessageFile;
use crate::data_struct::{MessageId, RoomId, UserId};
@ -6,10 +7,6 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use tracing::warn;
pub mod msg_trait;
pub use msg_trait::MessageTrait;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum At {
All,
@ -18,7 +15,7 @@ pub enum At {
}
impl At {
#[inline]
/// new_from_json(&message["at"])
pub fn new_from_json(json: &JsonValue) -> Self {
match json {
JsonValue::Bool(b) => Self::Bool(*b),
@ -55,40 +52,11 @@ pub struct ReplyMessage {
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,
pub struct NewMessage {
/// 房间 id
pub room_id: RoomId,
/// 消息 id
pub msg_id: MessageId,
/// 发送者 id
@ -135,29 +103,33 @@ pub struct Message {
pub raw_msg: JsonValue,
}
impl Message {
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 = json["_id"].as_str().unwrap();
let msg_id = message["_id"].as_str().unwrap();
// 发送者 id (Optional)
let sender_id = json["senderId"].as_i64().unwrap_or(-1);
let sender_id = message["senderId"].as_i64().unwrap_or(-1);
// 发送者名字 必有
let sender_name = json["username"].as_str().unwrap();
let sender_name = message["username"].as_str().unwrap();
// 消息内容
let content = json["content"].as_str().unwrap();
let content = message["content"].as_str().unwrap();
// xml / json 内容
let code = json["code"].clone();
let code = message["code"].clone();
// 消息时间 (怎么这个也是可选啊(恼))
// 没有就取当前时间
let current = chrono::Utc::now().naive_utc();
let time = json["time"]
let time = message["time"]
.as_i64()
.map(|t| NaiveDateTime::from_timestamp_micros(t).unwrap_or(current))
.unwrap_or(current);
// 身份
let role = json["role"].as_str().unwrap_or("unknown");
let role = message["role"].as_str().unwrap_or("unknown");
// 文件
let value_files = json["files"].as_array().unwrap_or(&Vec::new()).to_vec();
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());
@ -166,49 +138,44 @@ impl Message {
}
}
// 回复的消息
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 {
let reply: Option<ReplyMessage> = match message.get("replyMessage") {
Some(value) => match serde_json::from_value::<ReplyMessage>(value.clone()) {
Ok(reply) => Some(reply),
Err(e) => {
warn!("Failed to parse reply message: {}", e);
None
}
}
},
None => None,
};
// At
let at = At::new_from_json(&json["at"]);
let at = At::new_from_json(&message["at"]);
// 是否已撤回
let deleted = json["deleted"].as_bool().unwrap_or(false);
let deleted = message["deleted"].as_bool().unwrap_or(false);
// 是否是系统消息
let system = json["system"].as_bool().unwrap_or(false);
let system = message["system"].as_bool().unwrap_or(false);
// mirai
let mirai = json["mirai"].clone();
let mirai = message["mirai"].clone();
// reveal
let reveal = json["reveal"].as_bool().unwrap_or(false);
let reveal = message["reveal"].as_bool().unwrap_or(false);
// flash
let flash = json["flash"].as_bool().unwrap_or(false);
let flash = message["flash"].as_bool().unwrap_or(false);
// "群主授予的头衔"
let title = json["title"].as_str().unwrap_or("");
let title = message["title"].as_str().unwrap_or("");
// anonymous id
let anonymous_id = json["anonymousId"].as_i64();
let anonymous_id = message["anonymousId"].as_i64();
// 是否已被隐藏
let hide = json["hide"].as_bool().unwrap_or(false);
let hide = message["hide"].as_bool().unwrap_or(false);
// 气泡 id
let bubble_id = json["bubble_id"].as_i64().unwrap_or(1);
let bubble_id = message["bubble_id"].as_i64().unwrap_or(1);
// 子? id
let subid = json["subid"].as_i64().unwrap_or(1);
let subid = message["subid"].as_i64().unwrap_or(1);
// 头像 img?
let head_img = json["head_img"].clone();
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(),
@ -237,9 +204,8 @@ impl Message {
pub fn output(&self) -> String {
format!(
// >10 >10 >15
// >10 >15
"{:>12}|{:<20}|{}",
self.sender_id, self.sender_name, self.content
"{:>10}|{:>12}|{:<20}|{}",
self.room_id, self.sender_id, self.sender_name, self.content
)
}
@ -255,27 +221,28 @@ impl Message {
}
}
/// 获取回复
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: &String) -> SendMessage {
SendMessage::new(content.clone(), self.room_id, Some(self.msg.as_reply()))
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()
}
}
@ -300,56 +267,58 @@ impl SendMessage {
}
}
pub fn as_value(&self) -> JsonValue { serde_json::to_value(self).unwrap() }
pub fn as_value(&self) -> JsonValue {
serde_json::to_value(self).unwrap()
}
}
// #[cfg(test)]
// mod test {
// use serde_json::json;
#[cfg(test)]
mod test {
use serde_json::json;
// use super::*;
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_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 = 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());
// }
// }
#[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,126 +0,0 @@
use std::fmt::Display;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::client::IcalinguaStatus;
use crate::data_struct::messages::{At, Message, NewMessage};
use crate::data_struct::{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))
}
}
pub trait MessageTrait {
fn is_reply(&self) -> bool;
fn is_from_self(&self) -> bool {
let qq_id = IcalinguaStatus::get_online_data().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) -> &NaiveDateTime;
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) -> &NaiveDateTime { &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 {
write!(f, "{}|{}|{}|{}", self.msg_id(), self.sender_id, self.sender_name, self.content)
}
}
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) -> &NaiveDateTime { &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 {
write!(
f,
"{}|{}|{}|{}|{}",
self.msg_id(),
self.room_id,
self.msg.sender_id,
self.msg.sender_name,
self.msg.content
)
}
}

View File

@ -4,22 +4,6 @@ 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;
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

@ -197,7 +197,10 @@ mod tests {
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");

View File

@ -5,7 +5,7 @@ use tracing::{info, warn};
use crate::client::{send_message, IcalinguaStatus};
use crate::data_struct::all_rooms::Room;
use crate::data_struct::messages::{Message, MessageTrait, NewMessage};
use crate::data_struct::messages::NewMessage;
use crate::data_struct::online_data::OnlineData;
use crate::{py, VERSION};
@ -26,47 +26,35 @@ pub async fn get_online_data(payload: Payload, _client: Client) {
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();
let message = NewMessage::new_from_json(value);
// 检测是否在过滤列表内
if IcalinguaStatus::get_config().filter_list.contains(&message.msg.sender_id) {
if IcalinguaStatus::get_config()
.filter_list
.contains(&message.sender_id)
{
return;
}
info!("add_message {}", message.to_string().cyan());
info!("add_message {}", message.output().cyan());
// info!("add_message {}", format!("{:#?}", message).cyan());
// 就在这里处理掉最基本的消息
// 之后的处理交给插件
if message.content().eq("/bot-rs") && !message.is_from_self() && !message.is_reply() {
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::call::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());
py::new_message_py(&message, &client).await;
}
}
}
/// 撤回消息
pub async fn delete_message(payload: Payload, client: Client) {
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() {
info!("delete_message {}", format!("{}", msg_id).yellow());
py::call::delete_message_py(msg_id.to_string(), &client).await;
}
}
}
@ -76,8 +64,10 @@ 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();
let rooms: Vec<Room> = raw_rooms
.iter()
.map(|room| Room::new_from_json(room))
.collect();
unsafe {
crate::ClientStatus.update_rooms(rooms.clone());
}
@ -87,22 +77,6 @@ pub async fn update_all_room(payload: Payload, _client: Client) {
}
}
pub async fn succes_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());
}
}
}
/// 所有
pub async fn any_event(event: Event, payload: Payload, _client: Client) {
let handled = vec![
@ -115,16 +89,11 @@ pub async fn any_event(event: Event, payload: Payload, _client: Client) {
"addMessage",
"deleteMessage",
"setAllRooms",
"setMessages",
// 也许以后会用到
"messageSuccess",
"messageFailed",
"setAllChatGroups",
// 忽略的
"notify",
"closeLoading", // 发送消息/加载新聊天 有一个 loading
"updateRoom",
// "syncRead",
"syncRead",
];
match &event {
Event::Custom(event_name) => {

View File

@ -35,7 +35,9 @@ macro_rules! wrap_any_callback {
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().with_max_level(tracing::Level::DEBUG).init();
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
info!("ica-async-rs v{}", VERSION);
// 从命令行获取 host 和 key
@ -53,11 +55,8 @@ async fn main() {
.on("message", wrap_callback!(events::connect_callback))
.on("authSucceed", wrap_callback!(events::connect_callback))
.on("authFailed", wrap_callback!(events::connect_callback))
.on("messageSuccess", wrap_callback!(events::succes_message))
.on("messageFailed", wrap_callback!(events::failed_message))
.on("onlineData", wrap_callback!(events::get_online_data))
.on("setAllRooms", wrap_callback!(events::update_all_room))
.on("setMessages", wrap_callback!(events::set_messages))
.on("addMessage", wrap_callback!(events::add_message))
.on("deleteMessage", wrap_callback!(events::delete_message))
.connect()
@ -75,8 +74,9 @@ async fn main() {
);
std::thread::sleep(Duration::from_secs(1));
info!("发送启动消息到房间: {}", room);
if let Err(e) =
socket.emit("sendMessage", serde_json::to_value(startup_msg).unwrap()).await
if let Err(e) = socket
.emit("sendMessage", serde_json::to_value(startup_msg).unwrap())
.await
{
info!("启动信息发送失败 房间:{}|e:{}", room, e);
}

View File

@ -1,85 +0,0 @@
use std::path::PathBuf;
use pyo3::prelude::*;
use rust_socketio::asynchronous::Client;
use tracing::{debug, warn};
use crate::data_struct::messages::NewMessage;
use crate::data_struct::MessageId;
use crate::py::{class, verify_plugins, PyStatus};
pub fn get_func<'py>(py_module: &'py PyAny, path: &PathBuf, name: &'py str) -> Option<&'py PyAny> {
// 要处理的情况:
// 1. 有这个函数
// 2. 没有这个函数
// 3. 函数不是 Callable
match py_module.hasattr(name) {
Ok(contain) => {
if contain {
match py_module.getattr(name) {
Ok(func) => {
if func.is_callable() {
Some(func)
} else {
warn!("function<{}>: {:#?} in {:?} is not callable", name, func, path);
None
}
}
Err(e) => {
warn!("failed to get function<{}> from {:?}: {:?}", name, path, e);
None
}
}
} else {
debug!("no function<{}> in module {:?}", name, path);
None
}
}
Err(e) => {
warn!("failed to check function<{}> from {:?}: {:?}", name, path, e);
None
}
}
}
/// 执行 new message 的 python 插件
pub async fn new_message_py(message: &NewMessage, client: &Client) {
// 验证插件是否改变
verify_plugins();
let plugins = PyStatus::get_files();
for (path, (_, py_module)) in plugins.iter() {
let msg = class::NewMessagePy::new(message);
let client = class::IcaClientPy::new(client);
// 甚至实际上压根不需要await这个spawn, 直接让他自己跑就好了(离谱)
tokio::spawn(async move {
Python::with_gil(|py| {
let args = (msg, client);
if let Some(py_func) = get_func(py_module.as_ref(py), &path, "on_message") {
if let Err(e) = py_func.call1(args) {
warn!("failed to call function<on_message>: {:?}", e);
}
}
})
});
}
}
pub async fn delete_message_py(msg_id: MessageId, client: &Client) {
verify_plugins();
let plugins = PyStatus::get_files();
for (path, (_, py_module)) in plugins.iter() {
let msg_id = msg_id.clone();
let client = class::IcaClientPy::new(client);
tokio::spawn(async move {
Python::with_gil(|py| {
let args = (msg_id.clone(), client);
if let Some(py_func) = get_func(py_module.as_ref(py), &path, "on_delete_message") {
if let Err(e) = py_func.call1(args) {
warn!("failed to call function<on_delete_message>: {:?}", e);
}
}
})
});
}
}

View File

@ -4,7 +4,7 @@ use tokio::runtime::Runtime;
use tracing::{debug, info, warn};
use crate::client::send_message;
use crate::data_struct::messages::{MessageTrait, NewMessage, ReplyMessage, SendMessage};
use crate::data_struct::messages::{NewMessage, ReplyMessage, SendMessage};
use crate::data_struct::MessageId;
use crate::ClientStatus;
@ -15,10 +15,14 @@ pub struct IcaStatusPy {}
#[pymethods]
impl IcaStatusPy {
#[new]
pub fn py_new() -> Self { Self {} }
pub fn py_new() -> Self {
Self {}
}
#[getter]
pub fn get_login(&self) -> bool { unsafe { ClientStatus.login } }
pub fn get_login(&self) -> bool {
unsafe { ClientStatus.login }
}
#[getter]
pub fn get_online(&self) -> bool {
@ -102,7 +106,9 @@ impl IcaStatusPy {
}
impl IcaStatusPy {
pub fn new() -> Self { Self {} }
pub fn new() -> Self {
Self {}
}
}
#[derive(Clone)]
@ -118,21 +124,35 @@ impl NewMessagePy {
SendMessagePy::new(self.msg.reply_with(&content))
}
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
pub fn __str__(&self) -> String {
format!("{:?}", self.msg)
}
#[getter]
pub fn get_id(&self) -> MessageId { self.msg.msg_id().clone() }
pub fn get_id(&self) -> MessageId {
self.msg.msg_id.clone()
}
#[getter]
pub fn get_content(&self) -> String { self.msg.content().clone() }
pub fn get_content(&self) -> String {
self.msg.content.clone()
}
#[getter]
pub fn get_sender_id(&self) -> i64 { self.msg.sender_id() }
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() }
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() }
pub fn get_is_reply(&self) -> bool {
self.msg.is_reply()
}
}
impl NewMessagePy {
pub fn new(msg: &NewMessage) -> Self { Self { msg: msg.clone() } }
pub fn new(msg: &NewMessage) -> Self {
Self { msg: msg.clone() }
}
}
#[pyclass]
@ -143,11 +163,15 @@ pub struct ReplyMessagePy {
#[pymethods]
impl ReplyMessagePy {
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
pub fn __str__(&self) -> String {
format!("{:?}", self.msg)
}
}
impl ReplyMessagePy {
pub fn new(msg: ReplyMessage) -> Self { Self { msg } }
pub fn new(msg: ReplyMessage) -> Self {
Self { msg }
}
}
#[derive(Clone)]
@ -159,21 +183,29 @@ pub struct SendMessagePy {
#[pymethods]
impl SendMessagePy {
pub fn __str__(&self) -> String { format!("{:?}", self.msg) }
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() }
pub fn get_content(&self) -> String {
self.msg.content.clone()
}
#[setter]
pub fn set_content(&mut self, content: String) { self.msg.content = content; }
pub fn set_content(&mut self, content: String) {
self.msg.content = content;
}
}
impl SendMessagePy {
pub fn new(msg: SendMessage) -> Self { Self { msg } }
pub fn new(msg: SendMessage) -> Self {
Self { msg }
}
}
#[derive(Clone)]
@ -206,9 +238,6 @@ impl IcaClientPy {
})
}
#[getter]
pub fn get_status(&self) -> IcaStatusPy { IcaStatusPy::new() }
pub fn debug(&self, content: String) {
debug!("{}", content);
}

View File

@ -1,14 +1,16 @@
pub mod call;
pub mod class;
use std::time::SystemTime;
use std::{collections::HashMap, path::PathBuf};
use futures_util::future::join_all;
use pyo3::prelude::*;
use rust_socketio::asynchronous::Client;
use tracing::{debug, info, warn};
use crate::client::IcalinguaStatus;
use crate::config::IcaConfig;
use crate::data_struct::messages::NewMessage;
#[derive(Debug, Clone)]
pub struct PyStatus {
@ -107,7 +109,11 @@ pub fn load_py_plugins(path: &PathBuf) {
pub fn verify_plugins() {
let mut need_reload_files: Vec<PathBuf> = Vec::new();
let plugin_path = IcalinguaStatus::get_config().py_plugin_path.as_ref().unwrap().to_owned();
let plugin_path = IcalinguaStatus::get_config()
.py_plugin_path
.as_ref()
.unwrap()
.to_owned();
for entry in std::fs::read_dir(&plugin_path).unwrap() {
if let Ok(entry) = entry {
let path = entry.path();
@ -188,3 +194,124 @@ pub fn init_py(config: &IcaConfig) {
info!("python inited")
}
/// 执行 new message 的 python 插件
pub async fn new_message_py(message: &NewMessage, client: &Client) {
// 验证插件是否改变
verify_plugins();
let plugins = PyStatus::get_files();
// let tasks: Vec<_> = plugins.iter().map(|(path, (_, py_module))| {
// let msg = class::NewMessagePy::new(message);
// let client = class::IcaClientPy::new(client);
// let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel();
// let task = tokio::spawn(async move {
// tokio::select! {
// _ = tokio::spawn(async move {Python::with_gil(|py| {
// let args = (msg, client);
// let async_py_func = py_module.getattr(py, "on_message");
// match async_py_func {
// Ok(async_py_func) => match async_py_func.as_ref(py).call1(args) {
// Err(e) => {
// warn!("get a PyErr when call on_message from {:?}: {:?}", path, e);
// }
// _ => (),
// },
// Err(e) => {
// warn!("failed to get on_message function: {:?}", e);
// }
// }
// })}) => (),
// _ = cancel_rx => (),
// }
// });
// (task, cancel_tx)
// }).collect();
// let timeout = tokio::time::sleep(std::time::Duration::from_secs(5));
// tokio::select! {
// _ = join_all(tasks.map(|(task, _)| task)) => (),
// _ = timeout => {
// warn!("timeout when join all tasks");
// for (_, cancel_tx) in &tasks {
// let _ = cancel_tx.send(());
// }
// }
// }
// for (path, (_, py_module)) in plugins.iter() {
// let msg = class::NewMessagePy::new(message);
// let client = class::IcaClientPy::new(client);
// let task = tokio::spawn(async move {
// Python::with_gil(|py| {
// let args = (msg, client);
// let async_py_func = py_module.getattr(py, "on_message");
// match async_py_func {
// Ok(async_py_func) => match async_py_func.as_ref(py).call1(args) {
// Err(e) => {
// warn!("get a PyErr when call on_message from {:?}: {:?}", path, e);
// }
// _ => (),
// },
// Err(e) => {
// warn!("failed to get on_message function: {:?}", e);
// }
// }
// })
// });
// tokio::select! {
// _ = task => (),
// _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {
// warn!("timeout when join all tasks");
// // task.abort();
// }
// }
// }
let mut tasks = Vec::with_capacity(plugins.len());
for (path, (_, py_module)) in plugins.iter() {
let msg = class::NewMessagePy::new(message);
let client = class::IcaClientPy::new(client);
let task = tokio::spawn(async move {
Python::with_gil(|py| {
let args = (msg, client);
let async_py_func = py_module.getattr(py, "on_message");
match async_py_func {
Ok(async_py_func) => match async_py_func.as_ref(py).call1(args) {
Err(e) => {
warn!("get a PyErr when call on_message from {:?}: {:?}", path, e);
}
_ => (),
},
Err(e) => {
warn!("failed to get on_message function: {:?}", e);
}
}
})
});
tasks.push(task);
}
// 等待所有的插件执行完毕
// 超时时间为 0.1 秒
// ~~ 超时则取消所有的任务 ~~
// 超时就超时了……, 就让他跑着了……
// 主要是, 这玩意是同步的 还没法取消
let wait_time = std::time::Duration::from_millis(100);
let awaits = join_all(tasks);
let timeout = tokio::time::sleep(wait_time.clone());
let await_task = tokio::time::timeout(wait_time.clone(), awaits);
tokio::select! {
_ = await_task => (),
_ = timeout => {
warn!("timeout when join all tasks");
// for task in tasks {
// task.abort();
// }
}
}
// match tokio::time::timeout(wait_time.clone(), awaits).await {
// Ok(_) => (),
// Err(e) => {
// warn!("timeout when join all tasks: {:?}", e);
// }
// }
}

10
news.md
View File

@ -1,15 +1,5 @@
# 更新日志
## 0.4.10
好家伙, 我感觉都快能叫 0.5 了
修改了一些内部数据结构, 使得插件更加稳定
添加了 `rustfmt.toml` 用于格式化代码
**注意**: 请在提交代码前使用 `cargo +nightly fmt` 格式化代码
修复了 `Message` 解析 `replyMessage` 字段是 如果是 null 则会解析失败的问题
## 0.4.9
修复了 Python 插件运行错误会导致整个程序崩溃的问题