mirror of
http://shenjack.top:5100/shenjack/icalingua-python-bot.git
synced 2025-04-20 10:09:54 +08:00
Compare commits
16 Commits
06ee237bf5
...
a5f916542e
Author | SHA1 | Date | |
---|---|---|---|
a5f916542e | |||
e0a7988c1a | |||
34ed6d1841 | |||
d9184925d2 | |||
00dc10f395 | |||
f2ea1764ae | |||
8aae68ae6b | |||
85f4c6088c | |||
4819880e2c | |||
198839c65f | |||
e5f0d1b60e | |||
c1608a2c84 | |||
774b5b1d1c | |||
1da32a4401 | |||
492800fed6 | |||
e6df56b04d |
|
@ -2,7 +2,14 @@
|
||||||
private_key = "" # 与 icalingua 客户端使用的 private_key 一致
|
private_key = "" # 与 icalingua 客户端使用的 private_key 一致
|
||||||
host = "" # docker 版 icalingua 服务的地址
|
host = "" # docker 版 icalingua 服务的地址
|
||||||
self_id = 0 # 机器人的 qq 号
|
self_id = 0 # 机器人的 qq 号
|
||||||
|
|
||||||
|
# 启动时通知的群号/人
|
||||||
notice_room = [-0] # 启动 bot 后通知的群号/人
|
notice_room = [-0] # 启动 bot 后通知的群号/人
|
||||||
# 群号请使用群号的负数
|
# 群号请使用群号的负数
|
||||||
|
|
||||||
notice_start = true # 是否在启动 bot 后通知
|
notice_start = true # 是否在启动 bot 后通知
|
||||||
|
|
||||||
|
# 机器人的管理员
|
||||||
|
admin_list = [0] # 机器人的管理员
|
||||||
|
|
||||||
|
# python 插件路径
|
||||||
|
py_plugin_path = "/path/to/your/plugin"
|
||||||
|
|
|
@ -77,11 +77,13 @@ class NewMessage(Options):
|
||||||
class BotConfig(Options):
|
class BotConfig(Options):
|
||||||
name = 'icalingua bot config'
|
name = 'icalingua bot config'
|
||||||
# _check_filled = True
|
# _check_filled = True
|
||||||
host: str
|
|
||||||
private_key: str
|
private_key: str
|
||||||
|
host: str
|
||||||
self_id: int
|
self_id: int
|
||||||
notice_room: List[int]
|
notice_room: List[int]
|
||||||
notice_start: bool = False
|
notice_start: bool = False
|
||||||
|
admin_list: List[int]
|
||||||
|
py_plugin_path: str
|
||||||
|
|
||||||
def init(self, **kwargs) -> None:
|
def init(self, **kwargs) -> None:
|
||||||
if self.notice_room is None:
|
if self.notice_room is None:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ica-rs"
|
name = "ica-rs"
|
||||||
version = "0.2.0"
|
version = "0.4.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
@ -9,6 +9,7 @@ edition = "2021"
|
||||||
ed25519 = "2.2.3"
|
ed25519 = "2.2.3"
|
||||||
ed25519-dalek = "2.1.1"
|
ed25519-dalek = "2.1.1"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
blake3 = "1.5.0"
|
||||||
rust_socketio = "0.4.4"
|
rust_socketio = "0.4.4"
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
45
ica-rs/ica_typing.py
Normal file
45
ica-rs/ica_typing.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# 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 NewMessage:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class ReplyMessage:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class SendMessage:
|
||||||
|
...
|
0
ica-rs/plugins/test.py
Normal file
0
ica-rs/plugins/test.py
Normal file
|
@ -1,9 +1,64 @@
|
||||||
use crate::config::IcaConfig;
|
use crate::config::IcaConfig;
|
||||||
|
use crate::data_struct::messages::SendMessage;
|
||||||
|
use crate::data_struct::{all_rooms::Room, online_data::OnlineData};
|
||||||
|
|
||||||
use ed25519_dalek::{Signature, Signer, SigningKey};
|
use ed25519_dalek::{Signature, Signer, SigningKey};
|
||||||
use rust_socketio::{Payload, RawClient};
|
use rust_socketio::{Payload, RawClient};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tracing::debug;
|
use colored::Colorize;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
/// "安全" 的 发送一条消息
|
||||||
|
pub fn send_message(client: RawClient, message: SendMessage) {
|
||||||
|
let value = message.as_value();
|
||||||
|
match client.emit("sendMessage", value) {
|
||||||
|
Ok(_) => debug!("send_message {}", format!("{:#?}", message).cyan()),
|
||||||
|
Err(e) => warn!("send_message faild:{}", format!("{:#?}", e).red()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(&self) -> &OnlineData {
|
||||||
|
self.online_data.as_ref().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config(&self) -> &IcaConfig {
|
||||||
|
self.config.as_ref().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct IcalinguaSinger {
|
pub struct IcalinguaSinger {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
|
@ -11,9 +66,9 @@ pub struct IcalinguaSinger {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IcalinguaSinger {
|
impl IcalinguaSinger {
|
||||||
pub fn new_from_config(config: IcaConfig) -> Self {
|
pub fn new_from_config(config: &IcaConfig) -> Self {
|
||||||
let host = config.host;
|
let host = config.host.clone();
|
||||||
let pub_key = config.private_key;
|
let pub_key = config.private_key.clone();
|
||||||
Self::new_from_raw(host, pub_key)
|
Self::new_from_raw(host, pub_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use toml;
|
use toml::from_str;
|
||||||
|
|
||||||
/// Icalingua bot 的配置
|
/// Icalingua bot 的配置
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct IcaConfig {
|
pub struct IcaConfig {
|
||||||
/// icalingua 私钥
|
/// icalingua 私钥
|
||||||
pub private_key: String,
|
pub private_key: String,
|
||||||
|
@ -16,7 +16,9 @@ pub struct IcaConfig {
|
||||||
/// 提醒的房间
|
/// 提醒的房间
|
||||||
pub notice_room: Vec<i64>,
|
pub notice_room: Vec<i64>,
|
||||||
/// 是否提醒
|
/// 是否提醒
|
||||||
pub notice_start: Option<bool>,
|
pub notice_start: bool,
|
||||||
|
/// 管理员列表
|
||||||
|
pub admin_list: Vec<i64>,
|
||||||
/// Python 插件路径
|
/// Python 插件路径
|
||||||
pub py_plugin_path: Option<String>,
|
pub py_plugin_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -25,7 +27,7 @@ impl IcaConfig {
|
||||||
pub fn new_from_path(config_file_path: String) -> Self {
|
pub fn new_from_path(config_file_path: String) -> Self {
|
||||||
// try read config from file
|
// try read config from file
|
||||||
let config = fs::read_to_string(&config_file_path).expect("Failed to read config file");
|
let config = fs::read_to_string(&config_file_path).expect("Failed to read config file");
|
||||||
let ret: Self = toml::from_str(&config)
|
let ret: Self = from_str(&config)
|
||||||
.expect(format!("Failed to parse config file {}", &config_file_path).as_str());
|
.expect(format!("Failed to parse config file {}", &config_file_path).as_str());
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
82
ica-rs/src/data_struct/all_rooms.rs
Normal file
82
ica-rs/src/data_struct/all_rooms.rs
Normal 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>,
|
||||||
|
}
|
28
ica-rs/src/data_struct/files.rs
Normal file
28
ica-rs/src/data_struct/files.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/*interface MessageFile {
|
||||||
|
type: string
|
||||||
|
url: string
|
||||||
|
size?: number
|
||||||
|
name?: string
|
||||||
|
fid?: string
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct MessageFile {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub file_type: String,
|
||||||
|
pub url: String,
|
||||||
|
pub size: Option<i32>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub fid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageFile {
|
||||||
|
pub fn get_name(&self) -> Option<&String> {
|
||||||
|
self.name.as_ref()
|
||||||
|
}
|
||||||
|
pub fn get_fid(&self) -> Option<&String> {
|
||||||
|
self.fid.as_ref()
|
||||||
|
}
|
||||||
|
}
|
311
ica-rs/src/data_struct/messages.rs
Normal file
311
ica-rs/src/data_struct/messages.rs
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
use crate::data_struct::files::MessageFile;
|
||||||
|
use crate::data_struct::{MessageId, RoomId, UserId};
|
||||||
|
use crate::ClientStatus;
|
||||||
|
|
||||||
|
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 = unsafe {
|
||||||
|
ClientStatus.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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1,9 @@
|
||||||
pub mod new_message;
|
pub mod files;
|
||||||
|
pub mod messages;
|
||||||
|
|
||||||
|
pub mod all_rooms;
|
||||||
pub mod online_data;
|
pub mod online_data;
|
||||||
|
|
||||||
|
pub type RoomId = i64;
|
||||||
|
pub type UserId = i64;
|
||||||
|
pub type MessageId = String;
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
/// {"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 msg_id: String,
|
|
||||||
/// 发送者 id
|
|
||||||
pub sender_id: i64,
|
|
||||||
/// 子? id
|
|
||||||
pub subid: i64,
|
|
||||||
/// 房间 id
|
|
||||||
pub room_id: i64,
|
|
||||||
/// 发送者名字
|
|
||||||
pub sender_name: String,
|
|
||||||
/// 消息时间
|
|
||||||
pub time: NaiveDateTime,
|
|
||||||
/// 身份
|
|
||||||
pub role: String,
|
|
||||||
/// "群主授予的头衔"
|
|
||||||
pub title: String,
|
|
||||||
/// 消息内容
|
|
||||||
pub content: String,
|
|
||||||
/// 气泡 id
|
|
||||||
pub bubble_id: i64,
|
|
||||||
/// 原始消息
|
|
||||||
pub raw: JsonValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NewMessage {
|
|
||||||
pub fn new_from_json(json: &JsonValue) -> Option<Self> {
|
|
||||||
let message = json["message"].as_object()?;
|
|
||||||
let room_id = json["roomId"].as_i64()?;
|
|
||||||
let sender_id = message.get("senderId")?.as_i64()?;
|
|
||||||
let subid = message.get("subid")?.as_i64()?;
|
|
||||||
let sender_name = message.get("username")?.as_str()?.to_string();
|
|
||||||
let msg_id = message.get("_id")?.as_str()?.to_string();
|
|
||||||
let time = message.get("time")?.as_i64()?;
|
|
||||||
let time = NaiveDateTime::from_timestamp_micros(time)?;
|
|
||||||
let role = message.get("role")?.as_str()?.to_string();
|
|
||||||
let content = message.get("content")?.as_str()?.to_string();
|
|
||||||
let title = message.get("title")?.as_str()?.to_string();
|
|
||||||
let bubble_id = message.get("bubble_id")?.as_i64()?;
|
|
||||||
Some(Self {
|
|
||||||
msg_id,
|
|
||||||
sender_id,
|
|
||||||
subid,
|
|
||||||
room_id,
|
|
||||||
sender_name,
|
|
||||||
time,
|
|
||||||
role,
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
bubble_id,
|
|
||||||
raw: json.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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).unwrap();
|
|
||||||
assert_eq!(new_message.sender_id, 123456);
|
|
||||||
assert_eq!(new_message.room_id, -123456);
|
|
||||||
assert_eq!(new_message.sender_name, "shenjack");
|
|
||||||
assert_eq!(new_message.msg_id, "idddddd");
|
|
||||||
assert_eq!(new_message.role, "admin");
|
|
||||||
assert_eq!(new_message.content, "test");
|
|
||||||
assert_eq!(new_message.title, "索引管理员");
|
|
||||||
assert_eq!(new_message.raw, value);
|
|
||||||
assert_eq!(
|
|
||||||
new_message.time,
|
|
||||||
NaiveDateTime::from_timestamp_micros(1708267062000_i64).unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(new_message.raw, value);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,10 +2,13 @@ use colored::Colorize;
|
||||||
use rust_socketio::{Event, Payload, RawClient};
|
use rust_socketio::{Event, Payload, RawClient};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::data_struct::new_message::NewMessage;
|
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::data_struct::online_data::OnlineData;
|
||||||
use crate::py;
|
use crate::{py, VERSION};
|
||||||
|
|
||||||
|
/// 获取在线数据
|
||||||
pub fn get_online_data(payload: Payload, _client: RawClient) {
|
pub fn get_online_data(payload: Payload, _client: RawClient) {
|
||||||
if let Payload::Text(values) = payload {
|
if let Payload::Text(values) = payload {
|
||||||
if let Some(value) = values.first() {
|
if let Some(value) = values.first() {
|
||||||
|
@ -14,19 +17,65 @@ pub fn get_online_data(payload: Payload, _client: RawClient) {
|
||||||
"update_online_data {}",
|
"update_online_data {}",
|
||||||
format!("{:#?}", online_data).cyan()
|
format!("{:#?}", online_data).cyan()
|
||||||
);
|
);
|
||||||
|
unsafe {
|
||||||
|
crate::ClientStatus.update_online_data(online_data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_message(payload: Payload, _client: RawClient) {
|
/// 接收消息
|
||||||
|
pub fn add_message(payload: Payload, client: RawClient) {
|
||||||
if let Payload::Text(values) = payload {
|
if let Payload::Text(values) = payload {
|
||||||
if let Some(value) = values.first() {
|
if let Some(value) = values.first() {
|
||||||
let message = NewMessage::new_from_json(value);
|
let message = NewMessage::new_from_json(value);
|
||||||
info!("add_message {}", format!("{:#?}", message).cyan());
|
info!("add_message {}", format!("{:#?}", message).cyan());
|
||||||
|
if message.is_reply() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if message.is_from_self() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 就在这里处理掉最基本的消息
|
||||||
|
// 之后的处理交给插件
|
||||||
|
if message.content.eq("/bot-rs") {
|
||||||
|
let reply = message.reply_with(&format!("ica-rs pong v{}", VERSION));
|
||||||
|
send_message(client, reply)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 撤回消息
|
||||||
|
pub fn delete_message(payload: Payload, _client: RawClient) {
|
||||||
|
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 fn update_all_room(payload: Payload, _client: RawClient) {
|
||||||
|
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 fn any_event(event: Event, payload: Payload, _client: RawClient) {
|
pub fn any_event(event: Event, payload: Payload, _client: RawClient) {
|
||||||
let handled = vec![
|
let handled = vec![
|
||||||
// 真正处理过的
|
// 真正处理过的
|
||||||
|
@ -36,8 +85,11 @@ pub fn any_event(event: Event, payload: Payload, _client: RawClient) {
|
||||||
"requireAuth",
|
"requireAuth",
|
||||||
"onlineData",
|
"onlineData",
|
||||||
"addMessage",
|
"addMessage",
|
||||||
|
"deleteMessage",
|
||||||
|
"setAllRooms",
|
||||||
// 忽略的
|
// 忽略的
|
||||||
"notify",
|
"notify",
|
||||||
|
"closeLoading", // 发送消息/加载新聊天 有一个 loading
|
||||||
"updateRoom",
|
"updateRoom",
|
||||||
];
|
];
|
||||||
match &event {
|
match &event {
|
||||||
|
@ -46,6 +98,20 @@ pub fn any_event(event: Event, payload: Payload, _client: RawClient) {
|
||||||
return;
|
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 {
|
match payload {
|
||||||
|
@ -78,7 +144,10 @@ pub fn connect_callback(payload: Payload, _client: RawClient) {
|
||||||
Some("authRequired") => {
|
Some("authRequired") => {
|
||||||
warn!("{}", "需要登录到 icalingua!".yellow())
|
warn!("{}", "需要登录到 icalingua!".yellow())
|
||||||
}
|
}
|
||||||
_ => (),
|
Some(msg) => {
|
||||||
|
warn!("未知消息 {}", msg.yellow())
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tracing::info;
|
|
||||||
use rust_socketio::ClientBuilder;
|
use rust_socketio::ClientBuilder;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
mod config;
|
mod config;
|
||||||
|
@ -9,25 +9,61 @@ mod data_struct;
|
||||||
mod events;
|
mod events;
|
||||||
mod py;
|
mod py;
|
||||||
|
|
||||||
fn ws_main() {
|
#[allow(non_upper_case_globals)]
|
||||||
|
pub static mut ClientStatus: client::IcalinguaStatus = client::IcalinguaStatus {
|
||||||
|
login: false,
|
||||||
|
online_data: None,
|
||||||
|
rooms: None,
|
||||||
|
config: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(tracing::Level::DEBUG)
|
||||||
|
.init();
|
||||||
|
|
||||||
// 从命令行获取 host 和 key
|
// 从命令行获取 host 和 key
|
||||||
// 从命令行获取配置文件路径
|
// 从命令行获取配置文件路径
|
||||||
let ica_config = config::IcaConfig::new_from_cli();
|
let ica_config = config::IcaConfig::new_from_cli();
|
||||||
let ica_singer = client::IcalinguaSinger::new_from_config(ica_config);
|
unsafe {
|
||||||
|
ClientStatus.update_config(ica_config.clone());
|
||||||
|
}
|
||||||
|
py::init_py(&ica_config);
|
||||||
|
let ica_singer = client::IcalinguaSinger::new_from_config(&ica_config);
|
||||||
|
|
||||||
let socket = ClientBuilder::new(ica_singer.host.clone())
|
let socket = ClientBuilder::new(ica_singer.host.clone())
|
||||||
.transport_type(rust_socketio::TransportType::Websocket)
|
.transport_type(rust_socketio::TransportType::Websocket)
|
||||||
.on_any(events::any_event)
|
.on_any(events::any_event)
|
||||||
.on("requireAuth", move |a, b| ica_singer.sign_callback(a, b))
|
.on("requireAuth", move |a, b| ica_singer.sign_callback(a, b))
|
||||||
.on("authRequired", events::connect_callback)
|
.on("message", events::connect_callback)
|
||||||
.on("authSucceed", events::connect_callback)
|
.on("authSucceed", events::connect_callback)
|
||||||
.on("authFailed", events::connect_callback)
|
.on("authFailed", events::connect_callback)
|
||||||
.on("onlineData", events::get_online_data)
|
.on("onlineData", events::get_online_data)
|
||||||
|
.on("setAllRooms", events::update_all_room)
|
||||||
.on("addMessage", events::add_message)
|
.on("addMessage", events::add_message)
|
||||||
|
.on("deleteMessage", events::delete_message)
|
||||||
.connect()
|
.connect()
|
||||||
.expect("Connection failed");
|
.expect("Connection failed");
|
||||||
|
|
||||||
info!("Connected");
|
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-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()) {
|
||||||
|
info!("启动信息发送失败 房间:{}|e:{}", room, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::thread::sleep(Duration::from_secs(3));
|
std::thread::sleep(Duration::from_secs(3));
|
||||||
// 等待一个输入
|
// 等待一个输入
|
||||||
info!("Press any key to exit");
|
info!("Press any key to exit");
|
||||||
|
@ -35,13 +71,4 @@ fn ws_main() {
|
||||||
std::io::stdin().read_line(&mut input).unwrap();
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
socket.disconnect().expect("Disconnect failed");
|
socket.disconnect().expect("Disconnect failed");
|
||||||
info!("Disconnected");
|
info!("Disconnected");
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_max_level(tracing::Level::DEBUG)
|
|
||||||
.init();
|
|
||||||
py::init_py();
|
|
||||||
ws_main();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
use pyo3::{prelude::*, types::IntoPyDict};
|
|
||||||
use tracing::{debug, info};
|
|
||||||
|
|
||||||
#[pyclass]
|
|
||||||
#[pyo3(name = "BotStatus")]
|
|
||||||
pub struct BotStatusPy {}
|
|
||||||
|
|
||||||
pub fn run() {
|
|
||||||
Python::with_gil(|py| {
|
|
||||||
let bot_status = BotStatusPy {};
|
|
||||||
let _bot_status = PyCell::new(py, bot_status).unwrap();
|
|
||||||
let locals = [("state", _bot_status)].into_py_dict(py);
|
|
||||||
py.run("print(state)", None, Some(locals)).unwrap();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_py() {
|
|
||||||
debug!("initing python threads");
|
|
||||||
pyo3::prepare_freethreaded_python();
|
|
||||||
info!("python inited")
|
|
||||||
}
|
|
143
ica-rs/src/py/class.rs
Normal file
143
ica-rs/src/py/class.rs
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use crate::data_struct::messages::{NewMessage, ReplyMessage, SendMessage};
|
||||||
|
use crate::ClientStatus;
|
||||||
|
|
||||||
|
#[pyclass]
|
||||||
|
#[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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyclass]
|
||||||
|
#[pyo3(name = "NewMessage")]
|
||||||
|
pub struct NewMessagePy {
|
||||||
|
pub msg: NewMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewMessagePy {
|
||||||
|
pub fn new(msg: &NewMessage) -> Self {
|
||||||
|
Self { msg: msg.clone() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyclass]
|
||||||
|
#[pyo3(name = "ReplyMessage")]
|
||||||
|
pub struct ReplyMessagePy {
|
||||||
|
pub msg: ReplyMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReplyMessagePy {
|
||||||
|
pub fn new(msg: ReplyMessage) -> Self {
|
||||||
|
Self { msg }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyclass]
|
||||||
|
#[pyo3(name = "SendMessage")]
|
||||||
|
pub struct SendMessagePy {
|
||||||
|
pub msg: SendMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendMessagePy {
|
||||||
|
pub fn new(msg: SendMessage) -> Self {
|
||||||
|
Self { msg }
|
||||||
|
}
|
||||||
|
}
|
111
ica-rs/src/py/mod.rs
Normal file
111
ica-rs/src/py/mod.rs
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
pub mod class;
|
||||||
|
|
||||||
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
use pyo3::{prelude::*, types::IntoPyDict};
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
use blake3::Hasher;
|
||||||
|
|
||||||
|
use crate::config::IcaConfig;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PyStatus {
|
||||||
|
pub files: Option<HashMap<PathBuf, (Vec<u8>, String)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PyStatus {
|
||||||
|
pub fn get_files() -> &'static HashMap<PathBuf, (Vec<u8>, String)> {
|
||||||
|
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 add_file(path: PathBuf, content: Vec<u8>, hash: String) {
|
||||||
|
unsafe {
|
||||||
|
match PYSTATUS.files.as_mut() {
|
||||||
|
Some(files) => {
|
||||||
|
files.insert(path, (content, hash));
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let mut files = HashMap::new();
|
||||||
|
files.insert(path, (content, hash));
|
||||||
|
PYSTATUS.files = Some(files);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_file(path: &PathBuf, hash: &String) -> bool {
|
||||||
|
unsafe {
|
||||||
|
match PYSTATUS.files.as_ref() {
|
||||||
|
Some(files) => {
|
||||||
|
match files.get(path) {
|
||||||
|
Some((_, file_hash)) => file_hash == hash,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static mut PYSTATUS: PyStatus = PyStatus { files: None };
|
||||||
|
|
||||||
|
pub fn run() {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
let bot_status = class::IcaStatusPy::new();
|
||||||
|
let _bot_status: &PyCell<_> = PyCell::new(py, bot_status).unwrap();
|
||||||
|
|
||||||
|
let locals = [("state", _bot_status)].into_py_dict(py);
|
||||||
|
py.run("from pathlib import Path\nprint(Path.cwd())\nprint(state)", None, Some(locals)).unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_py_file(path: &PathBuf) -> (Vec<u8>, String) {
|
||||||
|
let mut hasher = Hasher::new();
|
||||||
|
let content = std::fs::read(path).unwrap();
|
||||||
|
hasher.update(&content);
|
||||||
|
let hash = hasher.finalize().as_bytes().to_vec();
|
||||||
|
(content, hex::encode(hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if path.exists() {
|
||||||
|
info!("finding plugins in: {:?}", path);
|
||||||
|
// 搜索所有的 py 文件 和 文件夹单层下面的 py 文件
|
||||||
|
match path.read_dir() {
|
||||||
|
Err(e) => {
|
||||||
|
warn!("failed to read plugin path: {:?}", e);
|
||||||
|
}
|
||||||
|
Ok(dir) => {
|
||||||
|
for entry in dir {
|
||||||
|
if let Ok(entry) = entry {
|
||||||
|
let path = entry.path();
|
||||||
|
if let Some(ext) = path.extension() {
|
||||||
|
if ext == "py" {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("plugin path not exists: {:?}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("python inited")
|
||||||
|
}
|
2
main.py
2
main.py
|
@ -6,7 +6,7 @@ from lib_not_dr.loggers import config
|
||||||
|
|
||||||
from data_struct import get_config, BotConfig, BotStatus
|
from data_struct import get_config, BotConfig, BotStatus
|
||||||
|
|
||||||
_version_ = "0.3.1"
|
_version_ = "0.3.3"
|
||||||
|
|
||||||
logger = config.get_logger("bot")
|
logger = config.get_logger("bot")
|
||||||
|
|
||||||
|
|
13
news.md
13
news.md
|
@ -1,5 +1,18 @@
|
||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## 0.3.3
|
||||||
|
|
||||||
|
适配 Rust 端的配置文件修改
|
||||||
|
|
||||||
|
## 0.3.1/2
|
||||||
|
|
||||||
|
改进 `/bmcl` 的细节
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
合并了 dongdigua 的代码, 把消息处理部分分离
|
||||||
|
现在代码更阳间了(喜
|
||||||
|
|
||||||
## 0.2.3
|
## 0.2.3
|
||||||
|
|
||||||
添加了 `/bmcl` 请求 bmclapi 状态
|
添加了 `/bmcl` 请求 bmclapi 状态
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import socketio
|
|
||||||
|
|
||||||
from lib_not_dr.loggers import config
|
from lib_not_dr.loggers import config
|
||||||
|
|
||||||
|
@ -10,11 +8,47 @@ from data_struct import NewMessage, SendMessage
|
||||||
|
|
||||||
logger = config.get_logger("bmcl")
|
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):
|
async def bmcl(sio, reply_msg: SendMessage, msg: NewMessage):
|
||||||
await asyncio.sleep(0.1)
|
req_time = time.time()
|
||||||
await sio.emit(
|
# 记录请求时间
|
||||||
"sendMessage", reply_msg.to_content("请求数据中……").to_json()
|
|
||||||
)
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
"https://bd.bangbang93.com/openbmclapi/metric/dashboard"
|
"https://bd.bangbang93.com/openbmclapi/metric/dashboard"
|
||||||
|
@ -40,21 +74,16 @@ async def bmcl(sio, reply_msg: SendMessage, msg: NewMessage):
|
||||||
load_str: float = data["load"] * 100
|
load_str: float = data["load"] * 100
|
||||||
online_node: int = data["currentNodes"]
|
online_node: int = data["currentNodes"]
|
||||||
online_bandwidth: int = data["bandwidth"]
|
online_bandwidth: int = data["bandwidth"]
|
||||||
data_lens = ["B", "KB", "MB", "GB", "TB"]
|
data_len = format_data_size(data_bytes)
|
||||||
data_len = "0B"
|
hits_count = format_hit_count(data_hits)
|
||||||
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
|
|
||||||
|
|
||||||
report_msg = (
|
report_msg = (
|
||||||
"OpenBMCLAPI 状态:\n"
|
f"OpenBMCLAPI 状态面板v{_version_} :\n"
|
||||||
f"在线节点: {online_node} 带宽: {online_bandwidth}Mbps\n"
|
f"实时信息: {online_node} 带宽: {online_bandwidth}Mbps\n"
|
||||||
f"实时负载带宽: {data_bandwidth:.5f}Mbps 负载: {load_str:.3f}%\n"
|
f"负载: {load_str:.2f}% 带宽: {data_bandwidth:.2f}Mbps\n"
|
||||||
f"当日 总请求: {data_hits} 总数据量: {data_len}"
|
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(
|
await sio.emit(
|
||||||
"sendMessage",
|
"sendMessage",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user