├── src ├── data │ ├── config.rs │ └── mod.rs ├── message │ ├── macros.rs │ ├── at.rs │ ├── face.rs │ ├── image.rs │ ├── meta.rs │ ├── forward.rs │ ├── ffi.rs │ └── mod.rs ├── terminal │ ├── sys │ │ ├── windows.rs │ │ ├── unknown.rs │ │ ├── mod.rs │ │ └── unix.rs │ ├── buffer.rs │ └── mod.rs ├── event │ ├── custom.rs │ ├── listener.rs │ └── mod.rs ├── signal │ ├── sys │ │ ├── unknown.rs │ │ ├── mod.rs │ │ ├── macos.rs │ │ ├── windows.rs │ │ └── linux.rs │ └── mod.rs ├── macros.rs ├── config │ ├── mod.rs │ ├── plugin.rs │ ├── log.rs │ ├── login.rs │ └── service.rs ├── plugin │ ├── ffi │ │ ├── mod.rs │ │ ├── string.rs │ │ ├── log.rs │ │ ├── env.rs │ │ ├── message.rs │ │ ├── rt.rs │ │ ├── member.rs │ │ ├── event.rs │ │ ├── client.rs │ │ ├── listener.rs │ │ ├── friend.rs │ │ └── group.rs │ └── mod.rs ├── client │ ├── info.rs │ └── token.rs ├── contact │ ├── mod.rs │ ├── friend.rs │ └── member.rs ├── service │ ├── command │ │ ├── argument.rs │ │ ├── builtin.rs │ │ └── mod.rs │ ├── mod.rs │ ├── log.rs │ ├── listener.rs │ └── login.rs ├── error.rs ├── lib.rs ├── main.rs └── channel │ └── mod.rs ├── rust-toolchain.toml ├── statics └── atri.jpg ├── .gitmodules ├── default_config ├── plugin.toml ├── log.toml └── login.toml ├── .gitignore ├── resources └── welcome.txt ├── .github ├── dependabot.yml └── workflows │ ├── pr.yml │ ├── release.yml │ └── ci.yml ├── README.md ├── Cargo.toml └── Plugin.md /src/data/config.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/message/macros.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" -------------------------------------------------------------------------------- /statics/atri.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaoLittle/atri_bot/HEAD/statics/atri.jpg -------------------------------------------------------------------------------- /src/terminal/sys/windows.rs: -------------------------------------------------------------------------------- 1 | pub fn handle_standard_output() -> std::io::Result<()> { 2 | Ok(()) 3 | } 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "atri_rust"] 2 | path = atri_rust 3 | url = https://github.com/AtriKawaii/atri_rust.git 4 | -------------------------------------------------------------------------------- /src/terminal/sys/unknown.rs: -------------------------------------------------------------------------------- 1 | pub fn handle_standard_output() -> std::io::Result<()> { 2 | Err(std::io::ErrorKind::Unsupported.into()) 3 | } 4 | -------------------------------------------------------------------------------- /default_config/plugin.toml: -------------------------------------------------------------------------------- 1 | # 对插件异常的态度 2 | # FastFault: 立即结束程序, 记录堆栈 3 | # Ignore (实验性): 忽略错误, 关闭产生错误的监听器, 记录堆栈, 但可能导致内存泄露或其他问题 4 | fault_attitude = 'FastFault' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.DS_Store 3 | /target 4 | /config 5 | /data 6 | /example_plugins 7 | /log 8 | /plugins 9 | /service 10 | /clients 11 | /workspaces 12 | /src/.DS_Store -------------------------------------------------------------------------------- /src/event/custom.rs: -------------------------------------------------------------------------------- 1 | use crate::event::SharedEvent; 2 | 3 | pub struct CustomEvent(SharedEvent); 4 | 5 | impl CustomEvent { 6 | pub fn new(inner: T) -> Self { 7 | Self(SharedEvent::new(inner)) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /resources/welcome.txt: -------------------------------------------------------------------------------- 1 | ======================================= 2 | 欢迎使用AtriBot 3 | 本项目基于`MPL2.0`协议开源 4 | 你可以在Github找到本项目的源码: 5 | 6 | https://github.com/LaoLittle/atri_bot 7 | 8 | 当前版本: {version} 9 | ======================================= 10 | -------------------------------------------------------------------------------- /default_config/log.toml: -------------------------------------------------------------------------------- 1 | # 日志输出最大等级 2 | # 等级由低至高分别为: Error, Warn, Info, Debug, Trace 3 | max_level = 'Info' 4 | # 日志时间的输出格式 5 | # 详见'https://time-rs.github.io/book/api/format-description.html' 6 | time_format = '[year]-[month]-[day] [hour]:[minute]:[second]' -------------------------------------------------------------------------------- /src/signal/sys/unknown.rs: -------------------------------------------------------------------------------- 1 | pub fn init_crash_handler() { 2 | ::tracing::warn!("当前系统暂未支持处理插件异常"); 3 | } 4 | 5 | pub unsafe fn save_jmp() {} 6 | 7 | pub fn exception_jmp(status: std::ffi::c_int) -> ! { 8 | std::process::exit(status); 9 | } 10 | -------------------------------------------------------------------------------- /src/terminal/sys/mod.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | 3 | cfg_if! { 4 | if #[cfg(unix)] { 5 | mod unix; 6 | pub use unix::*; 7 | } else if #[cfg(windows)] { 8 | mod windows; 9 | pub use windows::*; 10 | } else { 11 | mod unknown; 12 | pub use unknown::*; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! channel_handle_result { 3 | ($($x:tt)+) => { 4 | match ($($x)+) { 5 | Ok(val) => val, 6 | Err(err) => { 7 | use tracing::error; 8 | error!("{:?}", err); 9 | return; 10 | }, 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /default_config/login.toml: -------------------------------------------------------------------------------- 1 | # 默认的'协议' 2 | # 可使用 IPAD/AndroidPhone/AndroidWatch/MacOS/QiDian 3 | default_protocol = 'IPAD' 4 | 5 | [[client]] 6 | account = 123456 7 | 8 | # 可配置多个Client 9 | [[client]] 10 | # Client的账号(必须) 11 | account = 114514 12 | # Client的密码, 此为可选, token登陆失败会自动尝试密码登陆 13 | password = '1919810' 14 | # Client的'协议' 15 | protocol = 'AndroidWatch' 16 | # 是否自动登陆(默认true) 17 | auto_login = true -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | pub mod log; 4 | pub mod login; 5 | pub mod plugin; 6 | pub mod service; 7 | 8 | pub fn service_config_dir_path() -> &'static Path { 9 | static SERVICE_CONFIG_PATH: &str = "service"; 10 | Path::new(SERVICE_CONFIG_PATH) 11 | } 12 | 13 | pub fn clients_dir_path() -> &'static Path { 14 | static CLIENTS_PATH: &str = "clients"; 15 | Path::new(CLIENTS_PATH) 16 | } 17 | -------------------------------------------------------------------------------- /src/config/plugin.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub const DEFAULT_CONFIG: &[u8] = include_bytes!("../../default_config/plugin.toml"); 4 | 5 | /// 插件服务配置 6 | #[derive(Serialize, Deserialize, Default)] 7 | pub struct PluginConfig { 8 | pub fault_attitude: FaultAttitude, 9 | } 10 | 11 | /// 对插件产生异常的态度 12 | #[derive(Serialize, Deserialize, Default)] 13 | pub enum FaultAttitude { 14 | #[default] 15 | /// 立即结束程序, 记录堆栈 16 | FastFault, 17 | /// 忽略错误, 关闭产生错误的监听器, 记录堆栈 18 | /// 19 | /// 可能导致内存泄露或其他问题 20 | Ignore, 21 | } 22 | -------------------------------------------------------------------------------- /src/signal/sys/mod.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | 3 | cfg_if! { 4 | if #[cfg(any( 5 | target_os = "macos", 6 | target_os = "ios", 7 | ))] { 8 | mod macos; 9 | pub use macos::*; 10 | } else if #[cfg(any( 11 | target_os = "linux", 12 | target_os = "android" 13 | ))] { 14 | mod linux; 15 | pub use linux::*; 16 | } else if #[cfg(windows)] { 17 | mod windows; 18 | pub use windows::*; 19 | } else { 20 | mod unknown; 21 | pub use unknown::*; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/plugin/ffi/mod.rs: -------------------------------------------------------------------------------- 1 | use atri_ffi::PHandle; 2 | 3 | pub mod client; 4 | pub mod env; 5 | pub mod event; 6 | pub mod friend; 7 | pub mod group; 8 | pub mod listener; 9 | pub mod log; 10 | pub mod member; 11 | pub mod message; 12 | pub mod rt; 13 | pub mod string; 14 | 15 | fn cast_ref<'a, T>(ptr: *const ()) -> &'a T { 16 | unsafe { &*(ptr as *const T) } 17 | } 18 | 19 | fn cast_ref_phandle<'a, T>(ptr: PHandle) -> &'a T { 20 | unsafe { &*(ptr as *const T) } 21 | } 22 | 23 | fn _cast_ref_mut<'a, T>(ptr: *mut ()) -> &'a mut T { 24 | unsafe { &mut *(ptr as *mut T) } 25 | } 26 | -------------------------------------------------------------------------------- /src/plugin/ffi/string.rs: -------------------------------------------------------------------------------- 1 | use atri_ffi::{RustStr, RustString}; 2 | use std::ffi::{c_char, CStr, CString}; 3 | use std::ptr::null_mut; 4 | 5 | pub extern "C" fn rust_str_cvt(str: RustStr) -> *mut c_char { 6 | let str = str.as_str(); 7 | CString::new(str) 8 | .map(CString::into_raw) 9 | .unwrap_or(null_mut()) 10 | } 11 | 12 | pub extern "C" fn c_str_cvt(ptr: *const c_char) -> RustString { 13 | let cstr = unsafe { CStr::from_ptr(ptr) }; 14 | 15 | cstr.to_string_lossy().to_string().into() 16 | } 17 | 18 | pub extern "C" fn rust_string_drop(str: RustString) { 19 | drop(String::from(str)); 20 | } 21 | -------------------------------------------------------------------------------- /src/client/info.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::AtomicU8; 2 | use std::sync::RwLock; 3 | 4 | pub struct AccountInfo { 5 | pub nickname: RwLock, 6 | pub age: AtomicU8, 7 | pub gender: AtomicU8, 8 | } 9 | 10 | impl From for AccountInfo { 11 | fn from( 12 | ricq::structs::AccountInfo { 13 | nickname, 14 | age, 15 | gender, 16 | }: ricq::structs::AccountInfo, 17 | ) -> Self { 18 | Self { 19 | nickname: nickname.into(), 20 | age: age.into(), 21 | gender: gender.into(), 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | ignore: 13 | - dependency-name: "prost" 14 | - dependency-name: "toml" 15 | -------------------------------------------------------------------------------- /src/plugin/ffi/log.rs: -------------------------------------------------------------------------------- 1 | use super::cast_ref; 2 | use crate::service::plugin::Plugin; 3 | use atri_ffi::RustStr; 4 | 5 | pub extern "C" fn log(handle: usize, _: *const (), level: u8, str: RustStr) { 6 | let str = str.as_ref(); 7 | let plugin: &Plugin = cast_ref(handle as _); 8 | match level { 9 | 0 => tracing::trace!("{}: {}", plugin, str), 10 | 1 => tracing::debug!("{}: {}", plugin, str), 11 | 2 => tracing::info!("{}: {}", plugin, str), 12 | 3 => tracing::warn!("{}: {}", plugin, str), 13 | 4 => tracing::error!("{}: {}", plugin, str), 14 | _ => tracing::info!("{}: {}", plugin, str), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PullRequest 2 | 3 | on: 4 | pull_request: 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | target: 11 | - { name: Linux-x64, os: ubuntu-latest } 12 | - { name: macOS-x64, os: macos-latest } 13 | - { name: Windows-x64, os: windows-latest } 14 | 15 | name: ${{ matrix.target.name }} 16 | runs-on: ${{ matrix.target.os }} 17 | 18 | env: 19 | CI: 1 20 | CARGO_INCREMENTAL: 0 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Setup rust toolchain 26 | run: rustup default nightly 27 | 28 | - name: Test 29 | run: cargo test -------------------------------------------------------------------------------- /src/plugin/ffi/env.rs: -------------------------------------------------------------------------------- 1 | use super::cast_ref; 2 | use crate::service::plugin::Plugin; 3 | use atri_ffi::RustString; 4 | use tracing::error; 5 | 6 | pub extern "C" fn env_get_workspace(handle: usize, _: *const ()) -> RustString { 7 | let p: &Plugin = cast_ref(handle as *const ()); 8 | 9 | let name = p.name(); 10 | let mut path = std::env::current_dir() 11 | .ok() 12 | .and_then(|p| { 13 | p.to_str().map(|str| { 14 | let mut p = String::from(str); 15 | p.push('/'); 16 | p 17 | }) 18 | }) 19 | .unwrap_or_default(); 20 | 21 | path.push_str("workspaces/"); 22 | path.push_str(name); 23 | 24 | if let Err(e) = std::fs::create_dir_all(&path) { 25 | error!("为{}创建Workspace失败: {}", p, e); 26 | } 27 | 28 | path.into() 29 | } 30 | -------------------------------------------------------------------------------- /src/contact/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::contact::friend::Friend; 2 | use crate::contact::group::Group; 3 | use crate::contact::member::Member; 4 | use crate::error::{AtriError, AtriResult}; 5 | use crate::message::meta::MessageReceipt; 6 | use crate::message::MessageChain; 7 | 8 | pub mod friend; 9 | pub mod group; 10 | pub mod member; 11 | 12 | pub enum Contact { 13 | Friend(Friend), 14 | Group(Group), 15 | Member(Member), 16 | Stranger, 17 | } 18 | 19 | impl Contact { 20 | pub async fn send_message>(&self, msg: M) -> AtriResult { 21 | match self { 22 | Self::Friend(f) => f.send_message(msg).await, 23 | Self::Group(g) => g.send_message(msg).await, 24 | Self::Member(m) => m.send_message(msg).await, 25 | Self::Stranger => Err(AtriError::NotSupported), // todo 26 | } 27 | } 28 | } 29 | 30 | pub trait ContactSubject { 31 | fn subject(&self) -> Contact; 32 | } 33 | -------------------------------------------------------------------------------- /src/message/at.rs: -------------------------------------------------------------------------------- 1 | use crate::message::MessageElement; 2 | use ricq::msg::{MessageElem, PushElem}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Clone)] 6 | pub struct At { 7 | pub target: i64, 8 | pub display: String, 9 | } 10 | 11 | impl At { 12 | pub const ALL: Self = Self { 13 | target: 0, 14 | display: String::new(), 15 | }; 16 | 17 | pub fn all() -> Self { 18 | Self::ALL 19 | } 20 | } 21 | 22 | impl From for ricq::msg::elem::At { 23 | fn from(At { target, display }: At) -> Self { 24 | Self { target, display } 25 | } 26 | } 27 | 28 | impl PushElem for At { 29 | fn push_to(elem: Self, vec: &mut Vec) { 30 | let At { target, display } = elem; 31 | 32 | let rq = ricq::msg::elem::At { target, display }; 33 | PushElem::push_to(rq, vec); 34 | } 35 | } 36 | 37 | impl From for MessageElement { 38 | fn from(at: At) -> Self { 39 | Self::At(at) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/plugin/ffi/message.rs: -------------------------------------------------------------------------------- 1 | use super::cast_ref; 2 | use crate::message::image::Image; 3 | use crate::message::MessageChain; 4 | use atri_ffi::error::FFIResult; 5 | use atri_ffi::ffi::ForFFI; 6 | use atri_ffi::message::FFIMessageChain; 7 | use atri_ffi::{Managed, RustStr, RustString}; 8 | 9 | pub extern "C" fn message_chain_to_json(chain: FFIMessageChain) -> RustString { 10 | let chain = MessageChain::from_ffi(chain); 11 | chain.to_json().into() 12 | } 13 | 14 | pub extern "C" fn message_chain_from_json(json: RustStr) -> FFIResult { 15 | MessageChain::from_json(json.as_ref()) 16 | .map(MessageChain::into_ffi) 17 | .into() 18 | } 19 | 20 | pub extern "C" fn image_get_id(img: *const ()) -> RustStr { 21 | let img: &Image = cast_ref(img); 22 | RustStr::from(img.id()) 23 | } 24 | 25 | pub extern "C" fn _image_to_flash(img: Managed) { 26 | let img: Image = unsafe { img.into_value() }; 27 | img.flash(); 28 | } 29 | 30 | pub extern "C" fn image_get_url(img: *const ()) -> RustString { 31 | let img: &Image = cast_ref(img); 32 | RustString::from(img.url()) 33 | } 34 | -------------------------------------------------------------------------------- /src/terminal/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, Ordering}; 2 | use std::sync::{Mutex, RwLock}; 3 | 4 | pub static TERMINAL_CLOSED: AtomicBool = AtomicBool::new(false); 5 | pub static INPUT_BUFFER: RwLock = RwLock::new(String::new()); 6 | 7 | pub fn is_terminal_closed() -> bool { 8 | TERMINAL_CLOSED.load(Ordering::Relaxed) 9 | } 10 | 11 | pub struct InputCache { 12 | pub caches: Vec, 13 | pub index: usize, 14 | pub last_input: String, 15 | } 16 | 17 | pub static INPUT_CACHE: Mutex = Mutex::new(InputCache { 18 | caches: vec![], 19 | index: 0, 20 | last_input: String::new(), 21 | }); 22 | 23 | pub static ALTERNATE_SCREEN: AtomicBool = AtomicBool::new(false); 24 | 25 | //pub static PLAYBACK_BUFFER: Mutex>> = Mutex::new(vec![]); 26 | 27 | pub fn enter_alternate_screen() { 28 | ALTERNATE_SCREEN.store(true, Ordering::Release); 29 | } 30 | 31 | pub fn exit_alternate_screen() { 32 | ALTERNATE_SCREEN.store(false, Ordering::Release); 33 | } 34 | 35 | pub fn is_alternate_screen_enabled() -> bool { 36 | ALTERNATE_SCREEN.load(Ordering::Relaxed) 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AtriBot 2 | ---- 3 |
4 | Atri 5 | 6 | QQ群怎能少得了高性能亚托莉的身影呢? 7 | 8 | 本项目致力于快速部署,简单使用。 9 | 10 | 项目Logo由[妮娅ko](https://space.bilibili.com/13347846)绘制 11 |
12 | 13 | ## 声明 14 | 本项目仅供学习参考,请勿用于非法或商业用途。 15 | 16 | 本项目形象均来自《[Atri-MyDearMoments](https://atri-mdm.com)》 17 | 18 | ## 特性 19 | - 使用Rust及[ricq](https://github.com/lz1998/ricq)构建 20 | > Rust: 一门赋予每个人的构建可靠且高效软件能力的语言。 21 | > 22 | > ricq: 使用Rust编写的qq协议 23 | 24 | - 支持加载原生动态库插件, 高性能低占用 25 | 26 | ## 部署 27 | 使用登陆帮助程序[atri_login](https://github.com/LaoLittle/atri_login)登陆后得到`device`和`token`, 28 | 放入`clients`文件夹内,然后配置登陆信息(位于`service/login.toml`)即可 29 | 30 | ## TODO 31 | - [ ] 完善事件 32 | - [ ] 完善消息类型 33 | - [ ] 完善插件管理 34 | 35 | 本Bot遵循[AtriPlugin](https://github.com/AtriKawaii/atri_plugin)原生插件加载标准, 36 | 若要使用Rust编写插件, AtriPlugin项目提供了友好的接口, 可以快速上手: 37 | [插件开发文档](https://atrikawaii.github.io/atri_doc/) 38 | 39 | 若需要使用其他的Native语言编写插件, 请参阅: 40 | [插件加载方式](https://github.com/AtriKawaii/atri_plugin/blob/main/Load.md) 41 | 42 | ## 二次开发 43 | 可直接基于本项目进行二次开发, 而不是作为插件加载 44 | 45 | 配置 Cargo.toml: 46 | ```toml 47 | [dependencies] 48 | atri_bot = "0.4.0" 49 | ``` 50 | 51 | ### 注意 52 | 目前处于开发阶段, 不保证插件接口稳定. 53 | 更推荐直接基于本项目进行二次开发 54 | 55 | #### *在0.2版本(及以后), 插件提供一定程度的跨版本兼容。 -------------------------------------------------------------------------------- /src/config/log.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub static DEFAULT_CONFIG: &[u8] = include_bytes!("../../default_config/log.toml"); 4 | 5 | /// 日志配置 6 | #[derive(Serialize, Deserialize, Debug)] 7 | pub struct LogConfig { 8 | #[serde(default = "default_level")] 9 | pub max_level: Level, 10 | #[serde(default = "default_time_format")] 11 | pub time_format: String, 12 | } 13 | 14 | impl Default for LogConfig { 15 | fn default() -> Self { 16 | Self { 17 | max_level: default_level(), 18 | time_format: default_time_format(), 19 | } 20 | } 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Debug)] 24 | pub enum Level { 25 | Trace, 26 | Debug, 27 | Info, 28 | Warn, 29 | Error, 30 | } 31 | 32 | impl Level { 33 | pub fn as_tracing_level(&self) -> tracing::Level { 34 | match self { 35 | Level::Trace => tracing::Level::TRACE, 36 | Level::Debug => tracing::Level::DEBUG, 37 | Level::Info => tracing::Level::INFO, 38 | Level::Warn => tracing::Level::WARN, 39 | Level::Error => tracing::Level::ERROR, 40 | } 41 | } 42 | } 43 | 44 | fn default_level() -> Level { 45 | Level::Info 46 | } 47 | 48 | fn default_time_format() -> String { 49 | "[year]-[month]-[day] [hour]:[minute]:[second]".into() 50 | } 51 | -------------------------------------------------------------------------------- /src/message/face.rs: -------------------------------------------------------------------------------- 1 | use ricq::msg::{MessageElem, PushElem}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize, Debug, Clone)] 5 | pub struct Face { 6 | pub index: i32, 7 | pub name: String, 8 | } 9 | 10 | impl From for Face { 11 | fn from(ricq::msg::elem::Face { index, name }: ricq::msg::elem::Face) -> Self { 12 | Self { index, name } 13 | } 14 | } 15 | 16 | impl From for ricq::msg::elem::Face { 17 | fn from(Face { index, name }: Face) -> Self { 18 | Self { index, name } 19 | } 20 | } 21 | 22 | impl PushElem for Face { 23 | fn push_to(elem: Self, vec: &mut Vec) { 24 | let rq: ricq::msg::elem::Face = elem.into(); 25 | PushElem::push_to(rq, vec); 26 | } 27 | } 28 | 29 | mod ffi { 30 | use crate::message::face::Face; 31 | use atri_ffi::ffi::ForFFI; 32 | use atri_ffi::message::FFIFace; 33 | 34 | impl ForFFI for Face { 35 | type FFIValue = FFIFace; 36 | 37 | fn into_ffi(self) -> Self::FFIValue { 38 | let Face { index, name } = self; 39 | 40 | FFIFace { 41 | index, 42 | name: name.into(), 43 | } 44 | } 45 | 46 | fn from_ffi(FFIFace { index, name }: Self::FFIValue) -> Self { 47 | Self { 48 | index, 49 | name: name.into(), 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/data/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{Read, Write}; 3 | use std::path::{Path, PathBuf}; 4 | 5 | pub mod config; 6 | 7 | pub static DATA_PATH: &str = "data"; 8 | 9 | pub struct Holder { 10 | path: PathBuf, 11 | data: T, 12 | ser: S, 13 | deser: D, 14 | } 15 | 16 | impl Holder 17 | where 18 | S: Fn(Option<&[u8]>) -> T, 19 | D: Fn(&T) -> &[u8], 20 | { 21 | pub fn new

(path: P, ser: S, deser: D) -> Self 22 | where 23 | P: AsRef, 24 | { 25 | let p = path.as_ref(); 26 | 27 | let result = std::fs::read(p); 28 | 29 | let data = if let Ok(bytes) = result { 30 | ser(Some(&bytes)) 31 | } else { 32 | ser(None) 33 | }; 34 | 35 | Self { 36 | path: p.to_path_buf(), 37 | data, 38 | ser, 39 | deser, 40 | } 41 | } 42 | 43 | pub fn reload T>(&mut self, data: F) -> std::io::Result<()> { 44 | let file = File::open(&self.path)?; 45 | let mut bytes = vec![]; 46 | 47 | (&file).read_to_end(&mut bytes)?; 48 | 49 | let data = data(&bytes); 50 | self.data = data; 51 | 52 | Ok(()) 53 | } 54 | 55 | pub fn store &[u8]>(&self, store: F) -> std::io::Result<()> { 56 | let bytes = store(&self.data); 57 | 58 | let f = File::create(&self.path)?; 59 | (&f).write_all(bytes)?; 60 | 61 | Ok(()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/message/image.rs: -------------------------------------------------------------------------------- 1 | use crate::message::MessageElement; 2 | use ricq::msg::elem::{FlashImage, FriendImage, GroupImage}; 3 | use ricq::msg::{MessageElem, PushElem}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize, Clone)] 7 | pub enum Image { 8 | Group(GroupImage), 9 | Friend(FriendImage), 10 | } 11 | 12 | impl Image { 13 | pub fn id(&self) -> &str { 14 | match self { 15 | Self::Group(g) => &g.file_path, 16 | Self::Friend(f) => &f.file_path, 17 | } 18 | } 19 | 20 | pub fn flash(self) -> FlashImage { 21 | match self { 22 | Self::Group(g) => g.flash(), 23 | Self::Friend(f) => f.flash(), 24 | } 25 | } 26 | 27 | pub fn url(&self) -> String { 28 | match self { 29 | Self::Group(g) => g.url(), 30 | Self::Friend(f) => f.url(), 31 | } 32 | } 33 | } 34 | 35 | impl PushElem for Image { 36 | fn push_to(elem: Self, vec: &mut Vec) { 37 | match elem { 38 | Self::Group(img) => PushElem::push_to(img, vec), 39 | Self::Friend(img) => PushElem::push_to(img, vec), 40 | } 41 | } 42 | } 43 | 44 | impl From for Image { 45 | fn from(g: GroupImage) -> Self { 46 | Self::Group(g) 47 | } 48 | } 49 | 50 | impl From for Image { 51 | fn from(f: FriendImage) -> Self { 52 | Self::Friend(f) 53 | } 54 | } 55 | 56 | impl From for MessageElement { 57 | fn from(img: Image) -> Self { 58 | Self::Image(img) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "atri_bot" 3 | version = "0.9.4" 4 | edition = "2021" 5 | authors = ["LaoLittle"] 6 | description = "A simple bot" 7 | readme = "README.md" 8 | keywords = [ 9 | "Atri", 10 | "plugin", 11 | "oicq" 12 | ] 13 | license = "MPL-2.0" 14 | homepage = "https://github.com/LaoLittle/atri_bot" 15 | repository = "https://github.com/LaoLittle/atri_bot" 16 | 17 | [dependencies] 18 | # serilize 19 | serde = "1" 20 | serde_json = "1" 21 | toml = "0.5" 22 | prost = { version = "0.9", default-features = false } 23 | 24 | bytes = "1" 25 | tracing = "0" 26 | tracing-subscriber = { version = "0", features = ["fmt", "local-time"] } 27 | tracing-appender = "0" 28 | async-trait = "0" 29 | time = { version = "0", features = ["macros", "local-offset", "formatting"] } 30 | regex = "1" 31 | dashmap = "5" 32 | rand = "0" 33 | futures = "0" 34 | 35 | # plugin 36 | libloading = "0" 37 | backtrace = "0" 38 | 39 | # terminal 40 | crossterm = "0" 41 | libc = "0" 42 | cfg-if = "1" 43 | 44 | [dependencies.tokio] 45 | version = "1" 46 | features = [ 47 | "rt-multi-thread", 48 | "sync", 49 | "mio", 50 | "io-std", 51 | "io-util", 52 | "fs", 53 | "signal", 54 | ] 55 | 56 | [dependencies.ricq] 57 | git = "https://github.com/AtriKawaii/ricq.git" 58 | #version = "0.1.19" 59 | 60 | #[dependencies.ricq-guild] 61 | #version = "0.1.0" 62 | 63 | [dependencies.atri_ffi] 64 | version = "0.9.0" 65 | 66 | [target."cfg(windows)".dependencies.winapi] 67 | version = "0" 68 | features = ["winnt"] 69 | 70 | [profile.release] 71 | lto = true 72 | strip = true 73 | codegen-units = 1 74 | 75 | [build-dependencies] 76 | serde = { version = "1", features = ["derive"] } 77 | toml = "0.5" -------------------------------------------------------------------------------- /src/plugin/ffi/rt.rs: -------------------------------------------------------------------------------- 1 | use super::cast_ref; 2 | use crate::service::plugin::PluginManager; 3 | use crate::signal::save_jmp; 4 | use atri_ffi::error::FFIResult; 5 | use atri_ffi::future::FFIFuture; 6 | use atri_ffi::Managed; 7 | use std::future::Future; 8 | 9 | pub extern "C" fn plugin_manager_spawn( 10 | manager: *const (), 11 | future: FFIFuture, 12 | ) -> FFIFuture> { 13 | let manager: &PluginManager = cast_ref(manager); 14 | let handle = manager.async_runtime().spawn(async move { 15 | if crate::service::plugin::is_rec_enabled() { 16 | unsafe { 17 | save_jmp(); 18 | } 19 | } 20 | 21 | future.await 22 | }); 23 | 24 | FFIFuture::from(async { FFIResult::from(handle.await) }) 25 | } 26 | 27 | pub extern "C" fn plugin_manager_block_on( 28 | manager: *const (), 29 | future: FFIFuture, 30 | ) -> Managed { 31 | let manager: &PluginManager = cast_ref(manager); 32 | manager.async_runtime().block_on(future) 33 | } 34 | 35 | pub fn future_block_on(manager: *const (), future: F) -> F::Output 36 | where 37 | F: Future, 38 | F: Send + 'static, 39 | F::Output: Send + 'static, 40 | { 41 | let manager: &PluginManager = cast_ref(manager); 42 | 43 | let (tx, rx) = std::sync::mpsc::channel(); 44 | 45 | manager.async_runtime().spawn(async move { 46 | let val = future.await; 47 | let _ = tx.send(val); 48 | }); 49 | 50 | let rx = || rx.recv().expect("Cannot recv"); 51 | // calling this outside a runtime normally calls the provided closure. 52 | // all runtime is multi-threaded 53 | tokio::task::block_in_place(rx) 54 | } 55 | -------------------------------------------------------------------------------- /src/config/login.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub const DEFAULT_CONFIG: &[u8] = include_bytes!("../../default_config/login.toml"); 4 | 5 | /// 登录配置 6 | #[derive(Deserialize, Serialize, Debug, Default)] 7 | pub struct LoginConfig { 8 | /// 默认登录协议 9 | pub default_protocol: Protocol, 10 | /// 是否自动重连 11 | #[serde(default = "true_bool")] 12 | pub auto_reconnect: bool, 13 | /// 所有配置进行登录的客户端 14 | #[serde(default, rename = "client")] 15 | pub clients: Vec, 16 | } 17 | 18 | /// 客户端配置 19 | #[derive(Deserialize, Serialize, Debug)] 20 | pub struct ClientConfig { 21 | /// 账号 22 | pub account: i64, 23 | /// 密码 24 | pub password: Option, 25 | /// 登录协议 26 | pub protocol: Option, 27 | /// 是否进行登录 28 | #[serde(default = "true_bool")] 29 | pub auto_login: bool, 30 | } 31 | 32 | #[derive(Deserialize, Serialize, Debug, Clone, Copy, Default)] 33 | pub enum Protocol { 34 | #[default] 35 | IPAD, 36 | AndroidPhone, 37 | AndroidWatch, 38 | MacOS, 39 | QiDian, 40 | } 41 | 42 | impl Protocol { 43 | pub fn as_rq_protocol(&self) -> ricq::version::Protocol { 44 | use ricq::version::Protocol; 45 | match self { 46 | Self::IPAD => Protocol::IPad, 47 | Self::AndroidPhone => Protocol::AndroidPhone, 48 | Self::AndroidWatch => Protocol::AndroidWatch, 49 | Self::MacOS => Protocol::MacOS, 50 | Self::QiDian => Protocol::QiDian, 51 | } 52 | } 53 | 54 | pub fn as_version(&self) -> ricq::version::Version { 55 | ricq::version::get_version(self.as_rq_protocol()) 56 | } 57 | } 58 | 59 | const fn true_bool() -> bool { 60 | true 61 | } 62 | -------------------------------------------------------------------------------- /src/plugin/ffi/member.rs: -------------------------------------------------------------------------------- 1 | use super::cast_ref; 2 | use super::rt::future_block_on; 3 | use crate::contact::member::NamedMember; 4 | use crate::plugin::ffi::group::group_to_handle; 5 | use atri_ffi::error::FFIResult; 6 | use atri_ffi::future::FFIFuture; 7 | use atri_ffi::{Handle, RustStr}; 8 | 9 | pub extern "C" fn named_member_get_id(named: *const ()) -> i64 { 10 | let named: &NamedMember = cast_ref(named); 11 | named.id() 12 | } 13 | 14 | pub extern "C" fn named_member_get_nickname(named: *const ()) -> RustStr { 15 | let named: &NamedMember = cast_ref(named); 16 | RustStr::from(named.nickname()) 17 | } 18 | 19 | pub extern "C" fn named_member_get_card_name(named: *const ()) -> RustStr { 20 | let named: &NamedMember = cast_ref(named); 21 | RustStr::from(named.card_name()) 22 | } 23 | 24 | pub extern "C" fn named_member_get_group(named: *const ()) -> Handle { 25 | let named: &NamedMember = cast_ref(named); 26 | unsafe { group_to_handle(named.group()) } 27 | } 28 | 29 | pub extern "C" fn named_member_change_card_name( 30 | named: *const (), 31 | card: RustStr, 32 | ) -> FFIFuture> { 33 | let card = card.as_ref().to_owned(); 34 | FFIFuture::from(async move { 35 | let named: &NamedMember = cast_ref(named); 36 | let result = named.change_card_name(card).await; 37 | FFIResult::from(result) 38 | }) 39 | } 40 | 41 | pub extern "C" fn named_member_change_card_name_blocking( 42 | manager: *const (), 43 | named: *const (), 44 | card: RustStr, 45 | ) -> FFIResult<()> { 46 | let named: &NamedMember = cast_ref(named); 47 | let card = card.as_ref().to_owned(); 48 | 49 | future_block_on(manager, async move { 50 | let result = named.change_card_name(card).await; 51 | FFIResult::from(result) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /src/service/command/argument.rs: -------------------------------------------------------------------------------- 1 | use crate::client::Client; 2 | use crate::contact::friend::Friend; 3 | use crate::contact::group::Group; 4 | use crate::service::command::{CommandError, CommandResult}; 5 | use std::str::FromStr; 6 | 7 | pub trait CommandArg: Sized { 8 | fn from_str(arg: &str) -> CommandResult; 9 | } 10 | 11 | impl CommandArg for T { 12 | fn from_str(arg: &str) -> CommandResult { 13 | ::from_str(arg).map_err(|_| CommandError::IllegalArgument) 14 | } 15 | } 16 | 17 | impl CommandArg for Client { 18 | fn from_str(arg: &str) -> CommandResult { 19 | let id = i64::from_str_radix(arg, 10)?; 20 | 21 | Client::find(id).ok_or_else(|| CommandError::execute_error(format!("无法找到客户端: {id}"))) 22 | } 23 | } 24 | 25 | impl CommandArg for Friend { 26 | fn from_str(arg: &str) -> CommandResult { 27 | let id = i64::from_str_radix(arg, 10)?; 28 | 29 | if let [client] = &Client::list()[..] { 30 | return client 31 | .find_friend(id) 32 | .ok_or_else(|| CommandError::execute_error(format!("无法找到好友: {id}"))); 33 | } 34 | 35 | Err(CommandError::execute_error( 36 | "无法从多个客户端中找到好友, 请指定客户端, 例 Client:Friend", 37 | )) 38 | } 39 | } 40 | 41 | impl CommandArg for Group { 42 | fn from_str(arg: &str) -> CommandResult { 43 | let id = i64::from_str_radix(arg, 10)?; 44 | 45 | if let [client] = &Client::list()[..] { 46 | return client 47 | .find_group(id) 48 | .ok_or_else(|| CommandError::execute_error(format!("无法找到群: {id}"))); 49 | } 50 | 51 | Err(CommandError::execute_error( 52 | "无法从多个客户端中找到群, 请指定客户端, 例 Client:Group", 53 | )) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/config/service.rs: -------------------------------------------------------------------------------- 1 | use crate::config::service_config_dir_path; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fs; 4 | use std::marker::PhantomData; 5 | use std::path::PathBuf; 6 | use tracing::error; 7 | 8 | pub struct ServiceConfig { 9 | path: PathBuf, 10 | service_name: &'static str, 11 | default_config: &'static [u8], 12 | _mark: PhantomData, 13 | } 14 | 15 | impl ServiceConfig 16 | where 17 | for<'a> T: Serialize + Deserialize<'a>, 18 | T: Default, 19 | { 20 | pub fn new(name: &'static str, default: &'static [u8]) -> Self { 21 | Self { 22 | path: service_config_dir_path().join(format!("{name}.toml")), 23 | default_config: default, 24 | service_name: name, 25 | _mark: PhantomData, 26 | } 27 | } 28 | 29 | pub fn read(&self) -> T { 30 | if self.path.is_file() { 31 | match fs::read(&self.path) { 32 | Ok(file) => toml::from_slice(&file).unwrap_or_else(|e| { 33 | error!("{e}"); 34 | let mut path = self.path.clone(); 35 | path.pop(); 36 | let mut name = self.service_name.to_owned(); 37 | name.push_str(".toml.bak"); 38 | path.push(name); 39 | let _ = fs::copy(&self.path, path); 40 | self.write_default() 41 | }), 42 | Err(e) => { 43 | error!("{e}"); 44 | self.write_default() 45 | } 46 | } 47 | } else { 48 | self.write_default() 49 | } 50 | } 51 | 52 | fn write_default(&self) -> T { 53 | let default = T::default(); 54 | let _ = fs::write(&self.path, self.default_config); 55 | default 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/plugin/ffi/event.rs: -------------------------------------------------------------------------------- 1 | use super::cast_ref; 2 | use crate::event::{FriendMessageEvent, GroupMessageEvent}; 3 | use atri_ffi::contact::FFIMember; 4 | use atri_ffi::ffi::ForFFI; 5 | 6 | use crate::contact::friend::Friend; 7 | use crate::contact::group::Group; 8 | use atri_ffi::message::FFIMessageChain; 9 | use atri_ffi::PHandle; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | 12 | pub extern "C" fn event_intercept(intercepted: *const ()) { 13 | let intercepted: &AtomicBool = cast_ref(intercepted); 14 | intercepted.store(true, Ordering::Relaxed); 15 | } 16 | 17 | pub extern "C" fn event_is_intercepted(intercepted: *const ()) -> bool { 18 | let intercepted: &AtomicBool = cast_ref(intercepted); 19 | intercepted.load(Ordering::Relaxed) 20 | } 21 | 22 | pub extern "C" fn group_message_event_get_group(event: *const ()) -> PHandle { 23 | let event: &GroupMessageEvent = cast_ref(event); 24 | event.group() as *const Group as PHandle 25 | } 26 | 27 | pub extern "C" fn group_message_event_get_message(event: *const ()) -> FFIMessageChain { 28 | let event: &GroupMessageEvent = cast_ref(event); 29 | let chain = event.message().to_owned(); 30 | chain.into_ffi() 31 | } 32 | 33 | pub extern "C" fn group_message_event_get_sender(event: *const ()) -> FFIMember { 34 | let event: &GroupMessageEvent = cast_ref(event); 35 | let sender = event.sender().to_owned(); 36 | sender.into_ffi() 37 | } 38 | 39 | pub extern "C" fn friend_message_event_get_friend(event: *const ()) -> PHandle { 40 | let event: &FriendMessageEvent = cast_ref(event); 41 | event.friend() as *const Friend as PHandle 42 | } 43 | 44 | pub extern "C" fn friend_message_event_get_message(event: *const ()) -> FFIMessageChain { 45 | let event: &FriendMessageEvent = cast_ref(event); 46 | let chain = event.message().to_owned(); 47 | chain.into_ffi() 48 | } 49 | -------------------------------------------------------------------------------- /Plugin.md: -------------------------------------------------------------------------------- 1 | # 插件的加载方式及接口描述 2 | 3 | 本文档适用于: 4 | - 想要理解插件工作方式的开发者 5 | - 希望移植到其他语言以便其他语言可以开发插件的开发者 6 | 7 | 如果您是开发插件的开发者, 请参阅[插件开发文档](atri_plugin/README.md) 8 | 9 | 10 | **本文档所提及的`插件`均为可以被AtriBot直接加载的原生动态库插件, 11 | 其余的一切不符合上述要求的`插件`均不在本文范畴内** 12 | 13 | ## 接口稳定性 14 | 插件均使用`C abi`保证插件的ABI层面接口稳定(如果您不知道何为C abi请移步[Bing](https://www.bing.com)) 15 | 16 | ## 加载 17 | 所有的`插件`都应暴露两个函数: 18 | `atri_manager_init`和`on_init` 19 | 20 | 插件位于加载目录下时会先通过系统加载动态库, 21 | 然后搜寻上述的两个函数。 22 | 23 | ### atri_manager_init 24 | 本函数为插件管理器的初始化函数, 25 | 插件加载会最先调用本函数, 26 | 函数接收一个结构体`AtriManager` 27 | (结构体定义位于[ffi.rs](atri_ffi/src/ffi.rs)) 28 | 29 | 函数定义如下 30 | #### Rust: 31 | ```rust 32 | #[no_mangle] 33 | unsafe extern "C" fn atri_manager_init(manager: AtriManager) { 34 | // 在此进行初始化操作 35 | } 36 | ``` 37 | #### C: 38 | ```c 39 | void atri_manager_init(AtriManager manager) { 40 | // 在此进行初始化操作 41 | } 42 | ``` 43 | 44 | ### on_init 45 | 本函数为插件实例初始化函数, 46 | 调用返回结构体`PluginInstance`作为插件的实例 47 | (结构体定义位于[plugin.rs](atri_ffi/src/plugin.rs)) 48 | 49 | 函数定义如下 50 | #### Rust: 51 | ```rust 52 | #[no_mangle] 53 | extern "C" fn on_init() -> PluginInstance { 54 | // 初始化插件实例 55 | } 56 | ``` 57 | #### C: 58 | ```c 59 | PluginInstance on_init() { 60 | // 初始化插件实例 61 | } 62 | ``` 63 | 64 | 此函数调用后, 插件加载基本完毕 65 | 66 | ## 启用 67 | 在上述加载过程执行完毕返回的插件实例内包含了插件的虚表, 68 | 插件启用前会调用插件的`new`函数指针得到插件实例指针 69 | 若`should_drop`为`true`, 70 | 则每次启用都会通过`new`构造一个实例 71 | (插件实例结构在每次调用`new`时都不应变更) 72 | 73 | 然后会使用该指针调用`enable`函数用于启用插件 74 | 75 | 注意: 重复启用一个插件是无效果的 76 | 77 | ## 禁用 78 | 禁用插件会调用`disable`函数, 79 | 若`should_drop`为`true`, 80 | 则会在`disable`执行完毕后调用`drop`函数销毁插件实例 81 | 82 | 注意: 重复禁用一个插件是无效果的 83 | 84 | ## 卸载 85 | 卸载插件会先禁用此插件, 86 | 然后调用他的`drop`函数销毁插件实例, 87 | 最后会释放动态库文件完成整个插件的生命周期 88 | 89 | ## 交互 90 | 插件与主程序可以通过加载阶段得到的`AtriManager`进行交互, 91 | 内部的函数指针`get_fun`传入一个`uint16`得到另一个函数指针, 92 | 所有的函数定义位于[plugin/ffi/mod.rs](src/plugin/ffi/mod.rs) 93 | 94 | 推荐在加载阶段将所需的全部函数保存为全局变量 95 | 96 | 传入未定义的`sig`会得到一个调用就会panic的函数 97 | 98 | ## 其他 99 | 插件的`new`和`drop`规则是迎合Rust设计得到的, 100 | 在其他语言可以适当调整其功能 101 | 102 | 原设计模式适用于: Rust, C/C++ 103 | 104 | 其他模式,如: 单例插件的`new`返回固定值, 105 | 固定`should_drop`标志为false, 106 | `drop`函数作为插件卸载行为函数。 107 | 108 | 此模式可能适用于: Go和Kotlin/Native -------------------------------------------------------------------------------- /src/client/token.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Serialize, Deserialize, prost::Message)] 4 | pub struct Token { 5 | #[prost(int64, tag = "1")] 6 | pub uin: i64, 7 | #[prost(bytes = "vec", tag = "2")] 8 | pub d2: Vec, 9 | #[prost(bytes = "vec", tag = "3")] 10 | pub d2key: Vec, 11 | #[prost(bytes = "vec", tag = "4")] 12 | pub tgt: Vec, 13 | #[prost(bytes = "vec", tag = "5")] 14 | pub srm_token: Vec, 15 | #[prost(bytes = "vec", tag = "6")] 16 | pub t133: Vec, 17 | #[prost(bytes = "vec", tag = "7")] 18 | pub encrypted_a1: Vec, 19 | #[prost(bytes = "vec", tag = "8")] 20 | pub out_packet_session_id: Vec, 21 | #[prost(bytes = "vec", tag = "9")] 22 | pub tgtgt_key: Vec, 23 | #[prost(bytes = "vec", tag = "10")] 24 | pub wt_session_ticket_key: Vec, 25 | } 26 | 27 | impl From for Token { 28 | fn from( 29 | ricq::client::Token { 30 | uin, 31 | d2, 32 | d2key, 33 | tgt, 34 | srm_token, 35 | t133, 36 | encrypted_a1, 37 | out_packet_session_id, 38 | tgtgt_key, 39 | wt_session_ticket_key, 40 | }: ricq::client::Token, 41 | ) -> Self { 42 | Self { 43 | uin, 44 | d2, 45 | d2key, 46 | tgt, 47 | srm_token, 48 | t133, 49 | encrypted_a1, 50 | out_packet_session_id, 51 | tgtgt_key, 52 | wt_session_ticket_key, 53 | } 54 | } 55 | } 56 | 57 | impl From for ricq::client::Token { 58 | fn from( 59 | Token { 60 | uin, 61 | d2, 62 | d2key, 63 | tgt, 64 | srm_token, 65 | t133, 66 | encrypted_a1, 67 | out_packet_session_id, 68 | tgtgt_key, 69 | wt_session_ticket_key, 70 | }: Token, 71 | ) -> Self { 72 | Self { 73 | uin, 74 | d2, 75 | d2key, 76 | tgt, 77 | srm_token, 78 | t133, 79 | encrypted_a1, 80 | out_packet_session_id, 81 | tgtgt_key, 82 | wt_session_ticket_key, 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/service/command/builtin.rs: -------------------------------------------------------------------------------- 1 | use crate::service::command::{CommandError, CommandResult, PLUGIN_COMMAND}; 2 | use crate::service::plugin::PluginManager; 3 | use std::collections::hash_map::Entry; 4 | use std::mem; 5 | use tracing::info; 6 | 7 | pub fn handle_plugin_command( 8 | plugin_command: &str, 9 | manager: &mut PluginManager, 10 | ) -> CommandResult<()> { 11 | let args: Vec<&str> = plugin_command[PLUGIN_COMMAND.len()..] 12 | .split(' ') 13 | .filter(|s| !s.is_empty()) 14 | .collect(); 15 | 16 | match *args.first().ok_or(CommandError::MissingArgument( 17 | "load unload enable disable list", 18 | ))? { 19 | "list" => { 20 | let mut s = String::from('\n'); 21 | for (i, plugin) in manager.plugins().into_iter().enumerate() { 22 | s.push_str(&format!("{} Plugin(handle={})", i + 1, plugin.handle())); 23 | s.push('\n'); 24 | } 25 | info!("已加载的插件: {}", s); 26 | } 27 | "load" => { 28 | let &name = args 29 | .get(1) 30 | .ok_or(CommandError::MissingArgument("Plugin name"))?; 31 | let path = manager.plugins_path().join(name); 32 | let plugin = manager 33 | .load_plugin(path) 34 | .map_err(|e| CommandError::ExecuteError(e.to_string().into()))?; 35 | match manager.plugins.entry(plugin.name().to_owned()) { 36 | Entry::Vacant(vac) => { 37 | vac.insert(plugin).enable(); 38 | } 39 | _ => return Err(CommandError::ExecuteError("插件不可重复加载".into())), 40 | } 41 | } 42 | "unload" => { 43 | let &id = args 44 | .get(1) 45 | .ok_or(CommandError::MissingArgument("Plugin name"))?; 46 | 47 | manager 48 | .plugins 49 | .remove(id) 50 | .ok_or_else(|| CommandError::ExecuteError("未找到插件".into()))?; 51 | info!("成功卸载插件"); 52 | } 53 | "reloadAll" => { 54 | drop(mem::take(&mut manager.plugins)); 55 | manager 56 | .load_plugins() 57 | .map_err(|e| CommandError::ExecuteError(e.to_string().into()))?; 58 | } 59 | _ => {} 60 | } 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter}; 2 | use std::io; 3 | 4 | pub type AtriResult = Result; 5 | 6 | #[derive(Debug)] 7 | pub enum AtriError { 8 | PluginError(PluginError), 9 | IO(io::Error), 10 | Protocol(ricq::RQError), 11 | Login(LoginError), 12 | NotSupported, 13 | } 14 | 15 | impl Display for AtriError { 16 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 17 | match self { 18 | Self::Login(e) => Display::fmt(e, f), 19 | Self::IO(e) => { 20 | f.write_str("io error: ")?; 21 | Display::fmt(e, f) 22 | } 23 | Self::PluginError(e) => Display::fmt(e, f), 24 | Self::Protocol(e) => Display::fmt(e, f), 25 | Self::NotSupported => f.write_str("operation not supported"), 26 | } 27 | } 28 | } 29 | 30 | impl std::error::Error for AtriError {} 31 | 32 | #[derive(Debug)] 33 | pub enum LoginError { 34 | TokenNotExist, 35 | WrongToken, 36 | TokenLoginFailed, 37 | } 38 | 39 | impl Display for LoginError { 40 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 41 | match self { 42 | Self::TokenNotExist => f.write_str("token not exist"), 43 | Self::WrongToken => f.write_str("wrong token"), 44 | Self::TokenLoginFailed => f.write_str("token login failed. maybe the token is expired"), 45 | } 46 | } 47 | } 48 | 49 | impl std::error::Error for LoginError {} 50 | 51 | #[derive(Debug)] 52 | pub enum PluginError { 53 | InitializeFail(&'static str), 54 | LoadFail(String), 55 | NameConflict, 56 | } 57 | 58 | impl Display for PluginError { 59 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 60 | f.write_str("plugin ")?; 61 | match self { 62 | Self::InitializeFail(s) => { 63 | f.write_str("initialize failed: ")?; 64 | f.write_str(s) 65 | } 66 | Self::LoadFail(s) => { 67 | f.write_str("load failed, cause: ")?; 68 | f.write_str(s) 69 | } 70 | Self::NameConflict => f.write_str("name conflicted"), 71 | } 72 | } 73 | } 74 | 75 | impl From for AtriError { 76 | fn from(err: io::Error) -> Self { 77 | Self::IO(err) 78 | } 79 | } 80 | 81 | impl From for AtriError { 82 | fn from(err: ricq::RQError) -> Self { 83 | Self::Protocol(err) 84 | } 85 | } 86 | 87 | impl From for AtriError { 88 | fn from(err: PluginError) -> Self { 89 | Self::PluginError(err) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/plugin/ffi/client.rs: -------------------------------------------------------------------------------- 1 | use crate::plugin::ffi::cast_ref_phandle; 2 | use crate::plugin::ffi::friend::{friend_to_ptr, friend_to_ptr_option}; 3 | use crate::plugin::ffi::group::{group_to_handle, group_to_ptr_option}; 4 | use crate::Client; 5 | use atri_ffi::{Handle, RustString, RustVec}; 6 | 7 | #[inline] 8 | pub unsafe fn client_to_handle(client: Client) -> Handle { 9 | unsafe { std::mem::transmute(client) } 10 | } 11 | 12 | pub unsafe fn client_to_handle_option(client: Option) -> Handle { 13 | client 14 | .map(|c| unsafe { client_to_handle(c) }) 15 | .unwrap_or_else(std::ptr::null) 16 | } 17 | 18 | pub extern "C" fn find_client(id: i64) -> Handle { 19 | unsafe { client_to_handle_option(Client::find(id)) } 20 | } 21 | 22 | pub extern "C" fn client_get_id(client: Handle) -> i64 { 23 | let b: &Client = cast_ref_phandle(&client); 24 | b.id() 25 | } 26 | 27 | pub extern "C" fn client_get_nickname(client: Handle) -> RustString { 28 | let b: &Client = cast_ref_phandle(&client); 29 | RustString::from(b.nickname()) 30 | } 31 | 32 | pub extern "C" fn client_get_list() -> RustVec { 33 | let clients: Vec = Client::list() 34 | .into_iter() 35 | .map(|c| unsafe { client_to_handle(c) }) 36 | .collect(); 37 | 38 | RustVec::from(clients) 39 | } 40 | 41 | pub extern "C" fn client_find_group(client: Handle, id: i64) -> Handle { 42 | let b: &Client = cast_ref_phandle(&client); 43 | 44 | unsafe { group_to_ptr_option(b.find_group(id)) } 45 | } 46 | 47 | pub extern "C" fn client_find_friend(client: Handle, id: i64) -> Handle { 48 | let b: &Client = cast_ref_phandle(&client); 49 | 50 | unsafe { friend_to_ptr_option(b.find_friend(id)) } 51 | } 52 | 53 | pub extern "C" fn client_get_groups(client: Handle) -> RustVec { 54 | let b: &Client = cast_ref_phandle(&client); 55 | let ma: Vec = b 56 | .groups() 57 | .into_iter() 58 | .map(|g| unsafe { group_to_handle(g) }) 59 | .collect(); 60 | 61 | RustVec::from(ma) 62 | } 63 | 64 | pub extern "C" fn client_get_friends(client: Handle) -> RustVec { 65 | let b: &Client = cast_ref_phandle(&client); 66 | let ma: Vec = b 67 | .friends() 68 | .into_iter() 69 | .map(|f| unsafe { friend_to_ptr(f) }) 70 | .collect(); 71 | 72 | RustVec::from(ma) 73 | } 74 | 75 | pub extern "C" fn client_clone(client: Handle) -> Handle { 76 | let b: &Client = cast_ref_phandle(&client); 77 | unsafe { client_to_handle(b.clone()) } 78 | } 79 | 80 | pub extern "C" fn client_drop(client: Handle) { 81 | drop::(unsafe { std::mem::transmute(client) }) 82 | } 83 | -------------------------------------------------------------------------------- /src/signal/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::fmt; 3 | use std::fmt::{Formatter, Write}; 4 | use std::path::Path; 5 | 6 | mod sys; 7 | pub use sys::init_crash_handler; 8 | pub(crate) use sys::save_jmp; 9 | 10 | struct DlBacktrace { 11 | pub inner: backtrace::Backtrace, 12 | pub fun: fn(*const std::ffi::c_void) -> String, 13 | } 14 | 15 | impl fmt::Display for DlBacktrace { 16 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 17 | let mut frame_back = HashSet::new(); 18 | for (frame_cnt, frame) in self.inner.frames().iter().enumerate() { 19 | let addr = frame.symbol_address(); 20 | let fname = (self.fun)(addr); 21 | 22 | write!(f, "{frame_cnt} File ")?; 23 | f.write_str(&fname)?; 24 | f.write_str(": \n")?; 25 | 26 | frame_back.insert(fname); 27 | 28 | let symbols = frame.symbols(); 29 | 30 | if symbols.len() == 0 { 31 | writeln!(f, " at {:p}", addr)?; 32 | } 33 | 34 | for symbol in symbols { 35 | writeln!( 36 | f, 37 | " {}", 38 | symbol.name().unwrap_or(backtrace::SymbolName::new(&[])), 39 | )?; 40 | 41 | if let Some(filename) = symbol.filename().and_then(Path::to_str) { 42 | write!(f, " at {}", filename)?; 43 | } 44 | 45 | match (symbol.lineno(), symbol.colno()) { 46 | (Some(line), Some(column)) => write!(f, ":{line}:{column}")?, 47 | (Some(line), None) => write!(f, ":{line}")?, 48 | (None, Some(column)) => write!(f, ":?:{column}")?, 49 | _ => {} 50 | } 51 | 52 | writeln!(f)?; 53 | } 54 | } 55 | 56 | f.write_str("--------Frames--------\n")?; 57 | for frame in frame_back { 58 | f.write_str(&frame)?; 59 | f.write_char('\n')?; 60 | } 61 | f.write_str("----------------------\n")?; 62 | 63 | writeln!(f, "\ncurrent thread: {:?}", std::thread::current())?; 64 | 65 | Ok(()) 66 | } 67 | } 68 | 69 | fn fatal_error_print() { 70 | eprintln!("An fatal error has been detected."); 71 | } 72 | 73 | fn pre_print_fatal() -> bool { 74 | let enabled = crossterm::terminal::is_raw_mode_enabled().unwrap_or(false); 75 | disable_raw_mode(); 76 | enabled 77 | } 78 | 79 | fn post_print_fatal(enabled: bool) { 80 | if enabled { 81 | disable_raw_mode(); 82 | } 83 | } 84 | 85 | fn disable_raw_mode() { 86 | let _ = crossterm::terminal::disable_raw_mode(); 87 | } 88 | -------------------------------------------------------------------------------- /src/terminal/sys/unix.rs: -------------------------------------------------------------------------------- 1 | use crossterm::cursor::MoveToColumn; 2 | use crossterm::execute; 3 | use crossterm::style::Print; 4 | use std::ffi::c_int; 5 | use std::io::Write; 6 | 7 | struct RawStdout { 8 | fd: c_int, 9 | } 10 | 11 | impl RawStdout { 12 | fn next_line(&mut self) -> Result<(), std::io::Error> { 13 | execute!(self, Print('\n'), MoveToColumn(0)) 14 | } 15 | } 16 | 17 | impl Write for RawStdout { 18 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 19 | let n = unsafe { libc::write(self.fd, buf.as_ptr() as _, buf.len() as _) }; 20 | 21 | if n > 0 { 22 | Ok(n as usize) 23 | } else { 24 | Err(std::io::Error::last_os_error()) 25 | } 26 | } 27 | 28 | fn flush(&mut self) -> std::io::Result<()> { 29 | Ok(()) 30 | } 31 | } 32 | 33 | const BUFFER_SIZE: usize = 4096; 34 | 35 | const STDOUT_FILENO: c_int = 1; 36 | 37 | pub fn handle_standard_output() -> std::io::Result<()> { 38 | let mut pipe = [0; 2]; 39 | 40 | let stdout_bak = unsafe { libc::dup(STDOUT_FILENO) }; 41 | 42 | let mut buf = [b'\0'; BUFFER_SIZE]; 43 | unsafe { 44 | libc::pipe(pipe.as_mut_ptr()); 45 | 46 | let stat = libc::dup2(pipe[1], STDOUT_FILENO); 47 | 48 | if stat == -1 { 49 | return Err(std::io::Error::last_os_error()); 50 | } 51 | 52 | let mut stdout_fd = RawStdout { fd: stdout_bak }; 53 | 54 | loop { 55 | let size = libc::read(pipe[0], buf.as_mut_ptr() as _, BUFFER_SIZE as _); 56 | 57 | if size == -1 { 58 | return Err(std::io::Error::last_os_error()); 59 | } 60 | 61 | if size == 1 && buf[0] == b'\n' { 62 | stdout_fd.next_line()?; 63 | continue; 64 | } 65 | 66 | let split: Vec<&[u8]> = buf[..size as usize].split(|&b| b == b'\n').collect(); 67 | let mut split = split.into_iter(); 68 | 69 | if split.len() == 1 { 70 | let slice = split.next().unwrap(); 71 | 72 | if slice.is_empty() { 73 | stdout_fd.next_line()?; 74 | } 75 | 76 | stdout_fd.write_all(slice)?; 77 | continue; 78 | } 79 | 80 | let last = split.len().saturating_sub(1); 81 | for (i, slice) in split.enumerate() { 82 | if i == last { 83 | if !slice.is_empty() { 84 | stdout_fd.write_all(slice)?; 85 | } 86 | 87 | continue; 88 | } 89 | 90 | if slice.is_empty() { 91 | stdout_fd.next_line()?; 92 | continue; 93 | } 94 | 95 | stdout_fd.write_all(slice)?; 96 | stdout_fd.next_line()?; 97 | } 98 | } 99 | 100 | //libc::dup2(stdout_bak, STDOUT_FILENO); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/plugin/ffi/listener.rs: -------------------------------------------------------------------------------- 1 | use super::rt::future_block_on; 2 | use crate::event::listener::{ListenerBuilder, Priority}; 3 | use crate::{Event, Listener}; 4 | use atri_ffi::closure::FFIFn; 5 | use atri_ffi::ffi::FFIEvent; 6 | use atri_ffi::future::FFIFuture; 7 | use atri_ffi::{FFIOption, Managed}; 8 | use futures::FutureExt; 9 | use std::sync::Arc; 10 | use std::time::Duration; 11 | 12 | pub extern "C" fn new_listener( 13 | concurrent: bool, 14 | f: FFIFn>, 15 | priority: u8, 16 | ) -> Managed { 17 | let guard = ListenerBuilder::listening_on(move |e: Event| f.invoke(e.into_ffi())) 18 | .concurrent(concurrent) 19 | .priority(Priority::from(priority)) 20 | .start(); 21 | 22 | Managed::from_value(guard) 23 | } 24 | 25 | pub extern "C" fn listener_next_event_with_priority( 26 | millis: u64, 27 | filter: FFIFn, 28 | priority: u8, 29 | ) -> FFIFuture> { 30 | FFIFuture::from(async move { 31 | let option = Listener::next_event_with_priority( 32 | Duration::from_millis(millis), 33 | move |e: &Event| { 34 | let ffi = e.clone().into_ffi(); 35 | 36 | filter.invoke(ffi) 37 | }, 38 | Priority::from(priority), 39 | ) 40 | .await 41 | .map(Event::into_ffi); 42 | 43 | FFIOption::from(option) 44 | }) 45 | } 46 | 47 | pub extern "C" fn listener_next_event_with_priority_blocking( 48 | manager: *const (), 49 | millis: u64, 50 | filter: FFIFn, 51 | priority: u8, 52 | ) -> FFIOption { 53 | future_block_on(manager, async move { 54 | let option = Listener::next_event_with_priority( 55 | Duration::from_millis(millis), 56 | move |e: &Event| { 57 | let ffi = e.clone().into_ffi(); 58 | 59 | filter.invoke(ffi) 60 | }, 61 | Priority::from(priority), 62 | ) 63 | .await 64 | .map(Event::into_ffi); 65 | 66 | FFIOption::from(option) 67 | }) 68 | } 69 | 70 | pub extern "C" fn new_listener_closure( 71 | concurrent: bool, 72 | f: FFIFn, 73 | priority: u8, 74 | ) -> Managed { 75 | let arc = Arc::new(f); 76 | let guard = ListenerBuilder::listening_on(move |e: Event| { 77 | let f = Arc::clone(&arc); 78 | tokio::task::spawn_blocking(move || f.invoke(e.into_ffi())).map(Result::unwrap) 79 | }) 80 | .concurrent(concurrent) 81 | .priority(Priority::from(priority)) 82 | .start(); 83 | 84 | Managed::from_value(guard) 85 | } 86 | 87 | pub extern "C" fn new_listener_c_func( 88 | concurrent: bool, 89 | f: extern "C" fn(FFIEvent) -> bool, 90 | priority: u8, 91 | ) -> Managed { 92 | let guard = ListenerBuilder::listening_on(move |e: Event| { 93 | tokio::task::spawn_blocking(move || f(e.into_ffi())).map(Result::unwrap) 94 | }) 95 | .concurrent(concurrent) 96 | .priority(Priority::from(priority)) 97 | .start(); 98 | 99 | Managed::from_value(guard) 100 | } 101 | -------------------------------------------------------------------------------- /src/service/command/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod argument; 2 | pub mod builtin; 3 | 4 | use std::borrow::Cow; 5 | use std::collections::HashMap; 6 | use std::error::Error; 7 | use std::fmt::{Display, Formatter}; 8 | use std::future::Future; 9 | use std::pin::Pin; 10 | 11 | pub enum CommandAction { 12 | Function(fn(raw: &str) -> CommandResult<()>), 13 | AsyncFunction( 14 | fn(raw: &str) -> Pin> + Send + 'static>>, 15 | ), 16 | Closure(Box CommandResult<()> + Send + Sync + 'static>), 17 | AsyncClosure(CommandHandlerAsync), 18 | ExternalCFunction(extern "C" fn(raw: atri_ffi::RustStr) -> atri_ffi::error::FFIResult<()>), 19 | ExternalClosure(atri_ffi::closure::FFIFn>), 20 | ExternalAsyncClosure( 21 | atri_ffi::closure::FFIFn< 22 | atri_ffi::RustStr, 23 | atri_ffi::future::FFIFuture>, 24 | >, 25 | ), 26 | Complex(HashMap), // cannot change after initialized, or just change the root command. 27 | } 28 | 29 | impl CommandAction { 30 | pub async fn action(&self, args: &str) -> CommandResult<()> { 31 | match self { 32 | Self::Function(f) => tokio::task::block_in_place(|| f(args))?, 33 | Self::AsyncFunction(f) => f(args).await?, 34 | Self::Closure(f) => tokio::task::block_in_place(|| f(args))?, 35 | Self::AsyncClosure(f) => f(args).await?, 36 | Self::ExternalCFunction(f) => Result::from(tokio::task::block_in_place(|| { 37 | f(atri_ffi::RustStr::from(args)) 38 | })) 39 | .map_err(CommandError::execute_error)?, 40 | Self::ExternalClosure(f) => Result::from(tokio::task::block_in_place(|| { 41 | f.invoke(atri_ffi::RustStr::from(args)) 42 | })) 43 | .map_err(CommandError::execute_error)?, 44 | Self::ExternalAsyncClosure(f) => { 45 | Result::from(f.invoke(atri_ffi::RustStr::from(args)).await) 46 | .map_err(CommandError::execute_error)? 47 | } 48 | Self::Complex(_commands) => return Err(CommandError::execute_error("")), 49 | } 50 | 51 | Ok(()) 52 | } 53 | } 54 | 55 | type CommandHandlerAsync = Box< 56 | dyn Fn(&str) -> Pin> + Send + 'static>> 57 | + Send 58 | + Sync 59 | + 'static, 60 | >; 61 | 62 | pub type CommandResult = Result; 63 | 64 | #[derive(Debug)] 65 | pub enum CommandError { 66 | MissingArgument(&'static str), 67 | ExecuteError(Cow<'static, str>), 68 | IllegalArgument, 69 | } 70 | 71 | impl CommandError { 72 | pub fn execute_error>>(str: S) -> Self { 73 | Self::ExecuteError(str.into()) 74 | } 75 | } 76 | 77 | impl Display for CommandError { 78 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 79 | write!(f, "{self:?}") 80 | } 81 | } 82 | 83 | impl Error for CommandError {} 84 | 85 | impl From for CommandError { 86 | fn from(_value: std::num::ParseIntError) -> Self { 87 | Self::IllegalArgument 88 | } 89 | } 90 | 91 | pub const PLUGIN_COMMAND: &str = "plugin"; 92 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(once_cell)] 2 | #![feature(string_leak)] 3 | #![feature(new_uninit)] 4 | 5 | use dashmap::DashMap; 6 | use ricq::msg::elem::Text; 7 | use ricq::structs::GroupMemberInfo; 8 | use std::sync::OnceLock; 9 | 10 | use crate::client::Client; 11 | use tokio::runtime; 12 | use tokio::runtime::Runtime; 13 | 14 | use crate::event::listener::Listener; 15 | use crate::event::Event; 16 | use crate::service::listener::ListenerWorker; 17 | use crate::service::plugin::PluginManager; 18 | 19 | pub mod channel; 20 | pub mod client; 21 | pub mod config; 22 | pub mod contact; 23 | //pub mod data; 24 | pub mod error; 25 | pub mod event; 26 | pub mod macros; 27 | pub mod message; 28 | pub mod plugin; 29 | pub mod service; 30 | pub mod signal; 31 | pub mod terminal; 32 | 33 | pub struct Atri { 34 | pub runtime: Runtime, 35 | //listener_runtime: Runtime, 36 | //listener_worker: ListenerWorker, 37 | pub plugin_manager: PluginManager, 38 | } 39 | 40 | impl Atri { 41 | pub fn new() -> Self { 42 | let mut builder = runtime::Builder::new_multi_thread(); 43 | 44 | let runtime = builder 45 | .thread_name("GlobalRuntime") 46 | .enable_all() 47 | .build() 48 | .expect("cannot create runtime"); 49 | 50 | Self { 51 | runtime, 52 | //listener_runtime, 53 | //listener_worker, 54 | plugin_manager: PluginManager::new(), 55 | } 56 | } 57 | } 58 | 59 | impl Default for Atri { 60 | fn default() -> Self { 61 | Self::new() 62 | } 63 | } 64 | 65 | pub struct AtriGlobalStatus { 66 | clients: DashMap, 67 | listener_worker: ListenerWorker, 68 | //commands: std::sync::RwLock>, 69 | } 70 | 71 | static ATRI_GLOBAL_STATUS: OnceLock = OnceLock::new(); 72 | 73 | pub fn global_status() -> &'static AtriGlobalStatus { 74 | ATRI_GLOBAL_STATUS.get_or_init(AtriGlobalStatus::new) 75 | } 76 | 77 | pub fn global_listener_runtime() -> &'static Runtime { 78 | global_status().listener_worker().runtime() 79 | } 80 | 81 | pub fn global_listener_worker() -> &'static ListenerWorker { 82 | global_status().listener_worker() 83 | } 84 | 85 | impl AtriGlobalStatus { 86 | pub fn new() -> Self { 87 | let listener_runtime = runtime::Builder::new_multi_thread() 88 | .worker_threads(8) 89 | .thread_name("GlobalListenerExecutor") 90 | .enable_all() 91 | .build() 92 | .unwrap(); 93 | 94 | Self { 95 | clients: DashMap::new(), 96 | listener_worker: ListenerWorker::new_with_runtime(listener_runtime), 97 | //commands: std::sync::RwLock::new(HashMap::new()), 98 | } 99 | } 100 | 101 | pub fn clients(&self) -> Vec { 102 | let mut clients = vec![]; 103 | for client in self.clients.iter() { 104 | let c = client.clone(); 105 | clients.push(c); 106 | } 107 | 108 | clients 109 | } 110 | 111 | pub fn listener_worker(&self) -> &ListenerWorker { 112 | &self.listener_worker 113 | } 114 | 115 | pub(crate) fn add_client(&self, client: Client) -> Option { 116 | self.clients.insert(client.id(), client) 117 | } 118 | 119 | pub(crate) fn remove_client(&self, client_id: i64) -> Option { 120 | self.clients.remove(&client_id).map(|(_, client)| client) 121 | } 122 | 123 | pub fn close_clients(&self) { 124 | for client in self.clients() { 125 | client.close(); 126 | self.remove_client(client.id()); 127 | } 128 | } 129 | } 130 | 131 | impl Default for AtriGlobalStatus { 132 | fn default() -> Self { 133 | Self::new() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/service/mod.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fs::File; 3 | 4 | use std::io::{Read, Write}; 5 | use std::path::{Path, PathBuf}; 6 | use std::sync::OnceLock; 7 | use std::{fs, io}; 8 | 9 | use serde::{Deserialize, Serialize}; 10 | use tracing::error; 11 | 12 | pub mod command; 13 | pub mod listener; 14 | pub mod log; 15 | pub mod login; 16 | pub mod plugin; 17 | 18 | fn get_service_path() -> &'static PathBuf { 19 | static PATH: OnceLock = OnceLock::new(); 20 | PATH.get_or_init(|| { 21 | let p = PathBuf::from("service"); 22 | let _ = fs::create_dir(&p); 23 | p 24 | }) 25 | } 26 | 27 | pub struct Service { 28 | name: String, 29 | path: PathBuf, 30 | } 31 | 32 | impl Service { 33 | pub fn new(name: S) -> Self { 34 | let name = name.to_string(); 35 | let mut p = get_service_path().clone(); 36 | let mut s = name.clone(); 37 | p.push(&s); 38 | s.push_str(".toml"); 39 | p.push(&s); 40 | Self { name, path: p } 41 | } 42 | 43 | pub fn with_path>(&mut self, path: P) { 44 | let path = path.as_ref(); 45 | if !path.is_dir() { 46 | fs::create_dir_all(path).unwrap(); 47 | } 48 | self.path = path.join(format!("{}.toml", self.name())); 49 | } 50 | 51 | pub fn name(&self) -> &str { 52 | &self.name 53 | } 54 | 55 | pub fn read_config(&self) -> T 56 | where 57 | for<'de> T: Serialize + Deserialize<'de> + Default, 58 | { 59 | fn _read_config(path: &Path) -> Result> 60 | where 61 | for<'de> T: Serialize + Deserialize<'de> + Default, 62 | { 63 | let exist = path.is_file(); 64 | 65 | let mut f = fs::OpenOptions::new() 66 | .create(true) 67 | .read(true) 68 | .write(true) 69 | .open(path)?; 70 | 71 | let data = if exist { 72 | let mut s = String::new(); 73 | f.read_to_string(&mut s)?; 74 | toml::from_str(&s)? 75 | } else { 76 | let dat = T::default(); 77 | let str = toml::to_string_pretty(&dat)?; 78 | let _ = f.write_all(str.as_bytes()); 79 | dat 80 | }; 81 | 82 | Ok(data) 83 | } 84 | 85 | match _read_config(&self.path) { 86 | Ok(data) => data, 87 | Err(e) => { 88 | error!("读取配置文件({:?})时发生意料之外的错误: {}", self.path, e); 89 | 90 | let mut bk = self.path.clone(); 91 | bk.pop(); 92 | bk.push(format!("{}.toml.bak", self.name())); 93 | let _ = fs::copy(&self.path, bk); 94 | 95 | let data = T::default(); 96 | if let Ok(mut f) = File::create(&self.path) { 97 | let s = toml::to_string_pretty(&data).unwrap_or_else(|e| { 98 | panic!("Cannot serialize service data: {}, {e}", self.name) 99 | }); 100 | let _ = f.write_all(s.as_bytes()); 101 | } 102 | 103 | data 104 | } 105 | } 106 | } 107 | 108 | pub fn write_config(&self, data: &T) 109 | where 110 | T: Serialize, 111 | { 112 | fn _write_config(path: &Path, data: &T) -> io::Result<()> 113 | where 114 | T: Serialize, 115 | { 116 | let mut f = File::create(path)?; 117 | let s = toml::to_string_pretty(data).expect("Cannot serialize data"); 118 | 119 | f.write_all(s.as_bytes())?; 120 | Ok(()) 121 | } 122 | 123 | if let Err(e) = _write_config(&self.path, data) { 124 | error!("写入配置文件({:?})时发生意料之外的错误: {}", self.path, e); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/service/log.rs: -------------------------------------------------------------------------------- 1 | use crate::config::log::LogConfig; 2 | use crate::config::service::ServiceConfig; 3 | use crate::terminal::buffer::{INPUT_BUFFER, TERMINAL_CLOSED}; 4 | use crate::terminal::PROMPT; 5 | use std::io; 6 | use std::io::Write; 7 | use std::sync::atomic::Ordering; 8 | use tracing::{warn, Level}; 9 | use tracing_appender::non_blocking::WorkerGuard; 10 | use tracing_subscriber::fmt::time::OffsetTime; 11 | use tracing_subscriber::fmt::writer::MakeWriterExt; 12 | use tracing_subscriber::layer::SubscriberExt; 13 | use tracing_subscriber::util::SubscriberInitExt; 14 | 15 | pub fn init_logger() -> [WorkerGuard; 3] { 16 | let config = ServiceConfig::::new("log", crate::config::log::DEFAULT_CONFIG).read(); 17 | 18 | let local_offset = time::UtcOffset::current_local_offset(); 19 | 20 | let mut errors = Vec::with_capacity(2); 21 | let time_format = time::format_description::parse(config.time_format.leak()) 22 | .or_else(|e| { 23 | errors.push(format!("日志时间格式错误: {e}, 将使用默认时间格式")); 24 | time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]") 25 | }) 26 | .unwrap(); 27 | 28 | let (s, s_guard) = tracing_appender::non_blocking(LogStdoutWriter); 29 | 30 | let stdout_layer = tracing_subscriber::fmt::layer() 31 | .with_target(false) 32 | .with_writer(s.with_max_level(config.max_level.as_tracing_level())); 33 | 34 | let file_writer = tracing_appender::rolling::daily("log", "atri_bot.log"); 35 | let (f, f_guard) = tracing_appender::non_blocking(file_writer); 36 | 37 | let file_layer = tracing_subscriber::fmt::layer() 38 | .with_target(false) 39 | .with_ansi(false) 40 | .with_writer(f.with_max_level(Level::INFO)); 41 | 42 | let file_error_writer = tracing_appender::rolling::daily("log/error", "atri_bot.err"); 43 | let (f_err, f_err_guard) = tracing_appender::non_blocking(file_error_writer); 44 | 45 | let file_error_layer = tracing_subscriber::fmt::layer() 46 | .with_target(false) 47 | .with_ansi(false) 48 | .with_writer(f_err.with_max_level(Level::ERROR)); 49 | 50 | let offset = match local_offset { 51 | Ok(ofs) => ofs, 52 | Err(e) => { 53 | errors.push(format!("初始化日志时间错误: {e}, 将使用默认时区UTC+8")); 54 | time::UtcOffset::from_hms(8, 0, 0).unwrap() 55 | } 56 | }; 57 | 58 | let timer = OffsetTime::new(offset, time_format); 59 | let (stdout_layer, file_layer, file_error_layer) = ( 60 | stdout_layer.with_timer(timer.clone()), 61 | file_layer.with_timer(timer.clone()), 62 | file_error_layer.with_timer(timer), 63 | ); 64 | 65 | tracing_subscriber::registry() 66 | .with(stdout_layer) 67 | .with(file_layer) 68 | .with(file_error_layer) 69 | .init(); 70 | 71 | for error in errors { 72 | warn!("{error}"); 73 | } 74 | 75 | [s_guard, f_guard, f_err_guard] 76 | } 77 | 78 | pub struct LogStdoutWriter; 79 | 80 | impl Default for LogStdoutWriter { 81 | fn default() -> Self { 82 | Self 83 | } 84 | } 85 | 86 | impl Write for LogStdoutWriter { 87 | fn write(&mut self, buf: &[u8]) -> io::Result { 88 | fn write_content(mut out: W, buf: &[u8]) -> io::Result { 89 | if TERMINAL_CLOSED.load(Ordering::Relaxed) { 90 | return Ok(buf.len()); 91 | } 92 | 93 | out.write_all(&[13])?; 94 | let size = out.write(buf)?; 95 | out.write_all(PROMPT)?; 96 | 97 | if let Ok(rw) = INPUT_BUFFER.try_read() { 98 | out.write_all(rw.as_bytes())?; 99 | } 100 | 101 | out.flush()?; 102 | 103 | Ok(size) 104 | } 105 | 106 | write_content(io::stdout().lock(), buf) 107 | } 108 | 109 | fn flush(&mut self) -> io::Result<()> { 110 | io::stdout().flush() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/plugin/ffi/friend.rs: -------------------------------------------------------------------------------- 1 | use super::rt::future_block_on; 2 | use crate::contact::friend::Friend; 3 | use crate::message::meta::MessageReceipt; 4 | use crate::message::MessageChain; 5 | use crate::plugin::ffi::cast_ref_phandle; 6 | use crate::plugin::ffi::client::client_to_handle; 7 | use atri_ffi::error::FFIResult; 8 | use atri_ffi::ffi::ForFFI; 9 | use atri_ffi::future::FFIFuture; 10 | use atri_ffi::message::{FFIMessageChain, FFIMessageReceipt}; 11 | use atri_ffi::{Handle, ManagedCloneable, RustStr, RustVec}; 12 | use std::slice; 13 | 14 | pub unsafe fn friend_to_ptr(friend: Friend) -> Handle { 15 | unsafe { std::mem::transmute(friend) } 16 | } 17 | 18 | pub unsafe fn friend_to_ptr_option(friend: Option) -> Handle { 19 | friend 20 | .map(|c| unsafe { friend_to_ptr(c) }) 21 | .unwrap_or_else(std::ptr::null) 22 | } 23 | 24 | pub extern "C" fn friend_get_id(friend: Handle) -> i64 { 25 | let f: &Friend = cast_ref_phandle(&friend); 26 | f.id() 27 | } 28 | 29 | pub extern "C" fn friend_get_nickname(friend: Handle) -> RustStr { 30 | let f: &Friend = cast_ref_phandle(&friend); 31 | let s = f.nickname(); 32 | RustStr::from(s) 33 | } 34 | 35 | pub extern "C" fn friend_get_client(friend: Handle) -> Handle { 36 | let f: &Friend = cast_ref_phandle(&friend); 37 | unsafe { client_to_handle(f.client()) } 38 | } 39 | 40 | pub extern "C" fn friend_send_message( 41 | friend: Handle, 42 | chain: FFIMessageChain, 43 | ) -> FFIFuture> { 44 | FFIFuture::from(async move { 45 | let f: &Friend = cast_ref_phandle(&friend); 46 | let chain = MessageChain::from_ffi(chain); 47 | let result = f.send_message(chain).await.map(MessageReceipt::into_ffi); 48 | 49 | FFIResult::from(result) 50 | }) 51 | } 52 | 53 | pub extern "C" fn friend_send_message_blocking( 54 | manager: Handle, 55 | friend: Handle, 56 | chain: FFIMessageChain, 57 | ) -> FFIResult { 58 | let friend: &Friend = cast_ref_phandle(&friend); 59 | let chain = MessageChain::from_ffi(chain); 60 | 61 | future_block_on(manager, async move { 62 | let result = friend 63 | .send_message(chain) 64 | .await 65 | .map(MessageReceipt::into_ffi); 66 | 67 | FFIResult::from(result) 68 | }) 69 | } 70 | 71 | pub extern "C" fn friend_upload_image( 72 | friend: Handle, 73 | img: RustVec, 74 | ) -> FFIFuture> { 75 | FFIFuture::from(async { 76 | let f: &Friend = cast_ref_phandle(&friend); 77 | let img = img.into_vec(); 78 | 79 | let result = f.upload_image(img).await.map(ManagedCloneable::from_value); 80 | FFIResult::from(result) 81 | }) 82 | } 83 | 84 | pub extern "C" fn friend_upload_image_blocking( 85 | manager: Handle, 86 | friend: Handle, 87 | data: RustVec, 88 | ) -> FFIResult { 89 | let friend: &Friend = cast_ref_phandle(&friend); 90 | let data = data.into_vec(); 91 | 92 | future_block_on(manager, async move { 93 | let result = friend 94 | .upload_image(data) 95 | .await 96 | .map(ManagedCloneable::from_value); 97 | 98 | FFIResult::from(result) 99 | }) 100 | } 101 | 102 | pub extern "C" fn friend_upload_image_ex( 103 | friend: Handle, 104 | ptr: *const u8, 105 | size: usize, 106 | ) -> FFIFuture> { 107 | let slice = unsafe { slice::from_raw_parts(ptr, size) }; 108 | FFIFuture::from(async { 109 | let f: &Friend = cast_ref_phandle(&friend); 110 | let result = f 111 | .upload_image(slice) 112 | .await 113 | .map(ManagedCloneable::from_value); 114 | FFIResult::from(result) 115 | }) 116 | } 117 | 118 | pub extern "C" fn friend_clone(friend: Handle) -> Handle { 119 | let f: &Friend = cast_ref_phandle(&friend); 120 | unsafe { friend_to_ptr(f.clone()) } 121 | } 122 | 123 | pub extern "C" fn friend_drop(friend: Handle) { 124 | drop::(unsafe { std::mem::transmute(friend) }) 125 | } 126 | -------------------------------------------------------------------------------- /src/contact/friend.rs: -------------------------------------------------------------------------------- 1 | use crate::client::WeakClient; 2 | use crate::error::{AtriError, AtriResult}; 3 | use crate::message::forward::ForwardMessage; 4 | use crate::message::image::Image; 5 | use crate::message::meta::{MessageReceipt, RecallMessage}; 6 | use crate::message::MessageChain; 7 | use crate::Client; 8 | use std::fmt; 9 | use std::sync::Arc; 10 | use tracing::error; 11 | 12 | #[derive(Clone)] 13 | pub struct Friend(Arc); 14 | 15 | impl Friend { 16 | pub fn id(&self) -> i64 { 17 | self.0.info.uin 18 | } 19 | 20 | pub fn nickname(&self) -> &str { 21 | &self.0.info.nick 22 | } 23 | 24 | pub fn remark(&self) -> &str { 25 | &self.0.info.remark 26 | } 27 | 28 | pub fn client(&self) -> Client { 29 | self.0.client.force_upgrade() 30 | } 31 | 32 | pub async fn delete(&self) -> bool { 33 | let result = self 34 | .client() 35 | .request_client() 36 | .delete_friend(self.id()) 37 | .await; 38 | 39 | if let Err(e) = result { 40 | error!( 41 | "尝试删除好友 {}({}) 时失败: {:?}", 42 | self.nickname(), 43 | self.id(), 44 | e 45 | ); 46 | return false; 47 | } 48 | 49 | let deleted = self.client().remove_friend_cache(self.id()); 50 | 51 | deleted.is_some() 52 | } 53 | 54 | async fn _send_message(&self, chain: MessageChain) -> AtriResult { 55 | let result = self 56 | .client() 57 | .request_client() 58 | .send_friend_message(self.id(), chain.into()) 59 | .await; 60 | 61 | if let Err(ref e) = result { 62 | error!( 63 | "{}发送消息失败, 目标好友: {}({}), {:?}", 64 | self.client(), 65 | self.nickname(), 66 | self.id(), 67 | e 68 | ); 69 | } 70 | 71 | result.map(MessageReceipt::from).map_err(AtriError::from) 72 | } 73 | 74 | pub async fn send_message>(&self, msg: M) -> AtriResult { 75 | self._send_message(msg.into()).await 76 | } 77 | 78 | async fn _send_forward_message(&self, _forward: ForwardMessage) -> AtriResult { 79 | Err(AtriError::NotSupported) 80 | } 81 | 82 | pub async fn _upload_image(&self, image: &[u8]) -> AtriResult { 83 | let f = self 84 | .client() 85 | .request_client() 86 | .upload_friend_image(self.id(), image) 87 | .await?; 88 | 89 | Ok(Image::Friend(f)) 90 | } 91 | 92 | pub async fn upload_image>(&self, image: B) -> AtriResult { 93 | self._upload_image(image.as_ref()).await 94 | } 95 | 96 | async fn _recall_message(&self, receipt: MessageReceipt) -> AtriResult<()> { 97 | self.client() 98 | .request_client() 99 | .recall_friend_message( 100 | self.id(), 101 | receipt.time, 102 | receipt.seqs.clone(), 103 | receipt.rands.clone(), 104 | ) 105 | .await 106 | .map_err(AtriError::from) 107 | } 108 | 109 | pub async fn recall_message(&self, msg: &M) -> AtriResult<()> { 110 | self._recall_message(msg.receipt()).await 111 | } 112 | } 113 | 114 | // internal impls 115 | impl Friend { 116 | pub(crate) fn from(client: &Client, info: ricq::structs::FriendInfo) -> Self { 117 | let f = imp::Friend { 118 | client: WeakClient::new(client), 119 | info, 120 | }; 121 | 122 | Self(Arc::new(f)) 123 | } 124 | } 125 | 126 | impl fmt::Debug for Friend { 127 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 128 | f.debug_tuple("Friend").field(&self.id()).finish() 129 | } 130 | } 131 | 132 | impl fmt::Display for Friend { 133 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 134 | write!(f, "好友[{}({})]", self.nickname(), self.id()) 135 | } 136 | } 137 | 138 | mod imp { 139 | use crate::client::WeakClient; 140 | use ricq::structs::FriendInfo; 141 | 142 | pub struct Friend { 143 | pub client: WeakClient, 144 | pub info: FriendInfo, 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::time::Duration; 3 | 4 | use atri_bot::service::command::{builtin::handle_plugin_command, PLUGIN_COMMAND}; 5 | use atri_bot::service::log::init_logger; 6 | use atri_bot::service::login::login_clients; 7 | use atri_bot::service::plugin::PluginManager; 8 | use atri_bot::{global_listener_runtime, global_listener_worker, global_status, terminal, Atri}; 9 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; 10 | use tokio::{io, signal}; 11 | use tracing::{error, info}; 12 | 13 | type MainResult = Result<(), Box>; 14 | 15 | fn main() -> MainResult { 16 | // pre-load 17 | print_welcome_info(); 18 | let _guards = init_logger(); 19 | atri_bot::signal::init_crash_handler(); 20 | atri_bot::service::plugin::init_plugin_service(); 21 | pre_create_dirs(); 22 | 23 | // start 24 | let mut atri = Atri::new(); 25 | 26 | global_listener_runtime().spawn(global_listener_worker().start()); 27 | 28 | atri.plugin_manager.load_plugins()?; 29 | 30 | let runtime = &atri.runtime; 31 | 32 | runtime.spawn(async { 33 | main0().await.expect("Error"); 34 | }); 35 | 36 | runtime.block_on(async { 37 | tokio::select! { 38 | result = loop_cli(&mut atri.plugin_manager) => { 39 | if let Err(e) = result { 40 | error!("{}", e); 41 | } 42 | } 43 | result = signal::ctrl_c() => { 44 | if let Err(e) = result { 45 | error!("{}", e); 46 | } 47 | } 48 | } 49 | 50 | Ok::<_, Box>(()) 51 | })?; 52 | 53 | global_status().close_clients(); 54 | 55 | atri.runtime.shutdown_timeout(Duration::from_millis(800)); 56 | 57 | println!("已成功停止服务"); 58 | 59 | Ok(()) 60 | } 61 | 62 | async fn main0() -> MainResult { 63 | login_clients().await?; 64 | 65 | Ok(()) 66 | } 67 | 68 | async fn loop_cli(manager: &mut PluginManager) -> MainResult { 69 | info!("已启动AtriBot"); 70 | 71 | let _out = tokio::task::spawn_blocking(|| { 72 | if let Err(e) = terminal::handle_standard_output() { 73 | error!("接管Stdout出现错误: {}", e); 74 | return false; 75 | } 76 | 77 | true 78 | }); 79 | 80 | let result = tokio::task::block_in_place(|| terminal::start_read_input(manager)); 81 | if let Err(e) = result { 82 | let _ = crossterm::terminal::disable_raw_mode(); 83 | error!("初始化命令行服务异常: {}, 命令行可能不会正常工作", e); 84 | 85 | let stdin = io::stdin(); 86 | let mut stdin = BufReader::new(stdin); 87 | let mut stdout = io::stdout(); 88 | 89 | let mut buf = String::with_capacity(terminal::BUFFER_SIZE); 90 | loop { 91 | buf.clear(); 92 | stdin.read_line(&mut buf).await?; 93 | let cmd = buf.trim_end(); 94 | 95 | match cmd { 96 | "" => { 97 | stdout.write_all(terminal::PROMPT).await?; 98 | stdout.flush().await?; 99 | } 100 | "help" | "?" | "h" => { 101 | static INFOS: &[&str] = &["help: 显示本帮助", "exit: 退出程序"]; 102 | 103 | let mut s = String::from('\n'); 104 | for &info in INFOS { 105 | s.push_str(info); 106 | s.push('\n'); 107 | } 108 | s.pop(); 109 | info!("{}", s); 110 | } 111 | "exit" | "quit" | "stop" | "q" => { 112 | terminal::stop_info(); 113 | break; 114 | } 115 | plugin if plugin.starts_with(PLUGIN_COMMAND) => { 116 | if let Err(e) = handle_plugin_command(plugin, manager) { 117 | error!("{}", e); 118 | } 119 | } 120 | _ => { 121 | info!("未知的命令 '{}', 使用 'help' 显示帮助信息", cmd); 122 | } 123 | } 124 | } 125 | } 126 | 127 | Ok(()) 128 | } 129 | 130 | fn print_welcome_info() { 131 | println!( 132 | "{}", 133 | include_str!(concat!(env!("OUT_DIR"), "/welcome_info")) 134 | ); 135 | } 136 | 137 | fn pre_create_dirs() { 138 | for name in ["workspaces"] { 139 | let _ = std::fs::create_dir(name); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/service/listener.rs: -------------------------------------------------------------------------------- 1 | use std::collections::LinkedList; 2 | use std::sync::atomic::{AtomicBool, Ordering}; 3 | use std::sync::Arc; 4 | 5 | use tokio::sync::{Mutex, RwLock}; 6 | 7 | use crate::{Event, Listener}; 8 | 9 | type LimitedListeners = LinkedList>>>>; 10 | 11 | pub struct ListenerWorker { 12 | listeners: [LimitedListeners; 5], 13 | listener_rx: Mutex>>, 14 | listener_tx: tokio::sync::mpsc::Sender>, 15 | closed: AtomicBool, 16 | runtime: tokio::runtime::Runtime, 17 | } 18 | 19 | impl ListenerWorker { 20 | #[inline] 21 | pub fn new() -> Self { 22 | Self::new_with_runtime(tokio::runtime::Runtime::new().unwrap()) 23 | } 24 | 25 | pub fn new_with_runtime(runtime: tokio::runtime::Runtime) -> Self { 26 | let listeners = std::array::from_fn(|_| LinkedList::new()); 27 | 28 | let (tx, rx) = tokio::sync::mpsc::channel(10); 29 | 30 | ListenerWorker { 31 | listeners, 32 | listener_rx: rx.into(), 33 | listener_tx: tx, 34 | closed: AtomicBool::new(false), 35 | runtime, 36 | } 37 | } 38 | 39 | pub fn runtime(&self) -> &tokio::runtime::Runtime { 40 | &self.runtime 41 | } 42 | 43 | pub fn closed(&self) -> bool { 44 | self.closed.load(Ordering::Relaxed) 45 | } 46 | 47 | pub async fn schedule(&self, listener: Listener) { 48 | let arc = Arc::new(listener); 49 | let _ = self.listener_tx.send(arc).await; 50 | } 51 | 52 | pub async fn handle(&self, event: &Event) { 53 | if self.closed() { 54 | return; 55 | } 56 | 57 | let mut handles = vec![]; 58 | for list in &self.listeners { 59 | handles.reserve(list.len()); 60 | for opt in list.iter().map(Arc::clone) { 61 | let event = event.clone(); 62 | let handle = tokio::spawn(async move { 63 | let listener = { 64 | let lock = opt.read().await; 65 | lock.as_ref().map(Arc::clone) 66 | }; 67 | 68 | if let Some(listener) = listener { 69 | let close_listener = async { 70 | let mut lock = opt.write().await; 71 | *lock = None; 72 | }; 73 | 74 | if let Some(ref mutex) = listener.concurrent_mutex { 75 | let _ = mutex.lock().await; 76 | } 77 | 78 | if listener.closed.load(Ordering::Relaxed) { 79 | close_listener.await; 80 | return; 81 | } 82 | 83 | let fu = (listener.handler)(event); 84 | 85 | let keep: bool = fu.await; 86 | if !keep { 87 | close_listener.await; 88 | }; 89 | } 90 | }); 91 | 92 | handles.push(handle); 93 | } 94 | 95 | while let Some(handle) = handles.pop() { 96 | let _ = handle.await; 97 | } 98 | 99 | if event.is_intercepted() { 100 | break; 101 | } 102 | } 103 | } 104 | 105 | pub async fn start(&self) { 106 | let mut lock = self 107 | .listener_rx 108 | .try_lock() 109 | .unwrap_or_else(|_| panic!("ListenerWorker只可开启一次")); 110 | 111 | 'add: while let Some(l) = lock.recv().await { 112 | let list = &self.listeners[l.priority as usize]; 113 | 114 | for opt in list { 115 | let node = { opt.read().await.clone() }; //限制生命周期 116 | 117 | if node.is_none() { 118 | let mut wl = opt.write().await; 119 | *wl = Some(l); 120 | continue 'add; 121 | } 122 | } 123 | 124 | // Safety: locked by the mpsc::channel, 125 | // and the node will not remove when listener close 126 | #[allow(clippy::cast_ref_to_mut)] 127 | let list = unsafe { &mut *(list as *const _ as *mut LimitedListeners) }; 128 | list.push_back(Arc::new(RwLock::new(Some(l)))); 129 | } 130 | } 131 | 132 | pub fn close(&self) { 133 | self.closed.store(true, Ordering::Release); 134 | } 135 | } 136 | 137 | impl Default for ListenerWorker { 138 | fn default() -> Self { 139 | Self::new() 140 | } 141 | } 142 | 143 | impl Drop for ListenerWorker { 144 | fn drop(&mut self) { 145 | self.close(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/message/meta.rs: -------------------------------------------------------------------------------- 1 | use crate::message::{MessageChain, MessageElement}; 2 | use ricq::msg::{MessageElem, PushElem}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, Default)] 6 | pub struct MessageReceipt { 7 | pub seqs: Vec, 8 | pub rands: Vec, 9 | pub time: i64, 10 | } 11 | 12 | impl From for MessageReceipt { 13 | fn from( 14 | ricq::structs::MessageReceipt { seqs, rands, time }: ricq::structs::MessageReceipt, 15 | ) -> Self { 16 | Self { seqs, rands, time } 17 | } 18 | } 19 | 20 | pub trait RecallMessage { 21 | fn receipt(&self) -> MessageReceipt; 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Clone, Default)] 25 | pub struct MessageMetadata { 26 | pub seqs: Vec, 27 | pub rands: Vec, 28 | pub time: i32, 29 | pub sender: i64, 30 | pub anonymous: Option, 31 | pub reply: Option, 32 | } 33 | 34 | impl RecallMessage for MessageMetadata { 35 | fn receipt(&self) -> MessageReceipt { 36 | MessageReceipt { 37 | seqs: self.seqs.clone(), 38 | rands: self.rands.clone(), 39 | time: self.time as i64, 40 | } 41 | } 42 | } 43 | 44 | #[derive(Serialize, Deserialize, Default, Debug, Clone)] 45 | pub struct Anonymous { 46 | pub anon_id: Vec, 47 | pub nick: String, 48 | pub portrait_index: i32, 49 | pub bubble_index: i32, 50 | pub expire_time: i32, 51 | pub color: String, 52 | } 53 | 54 | impl From for Anonymous { 55 | fn from( 56 | ricq::msg::elem::Anonymous { 57 | anon_id, 58 | nick, 59 | portrait_index, 60 | bubble_index, 61 | expire_time, 62 | color, 63 | }: ricq::msg::elem::Anonymous, 64 | ) -> Self { 65 | Self { 66 | anon_id, 67 | nick, 68 | portrait_index, 69 | bubble_index, 70 | expire_time, 71 | color, 72 | } 73 | } 74 | } 75 | 76 | impl From for ricq::msg::elem::Anonymous { 77 | fn from( 78 | Anonymous { 79 | anon_id, 80 | nick, 81 | portrait_index, 82 | bubble_index, 83 | expire_time, 84 | color, 85 | }: Anonymous, 86 | ) -> Self { 87 | Self { 88 | anon_id, 89 | nick, 90 | portrait_index, 91 | bubble_index, 92 | expire_time, 93 | color, 94 | } 95 | } 96 | } 97 | 98 | impl PushElem for Anonymous { 99 | fn push_to(elem: Self, vec: &mut Vec) { 100 | let rq = ricq::msg::elem::Anonymous::from(elem); 101 | 102 | vec.insert(0, rq.into()); 103 | } 104 | } 105 | 106 | #[derive(Serialize, Deserialize, Clone)] 107 | pub struct Reply { 108 | pub reply_seq: i32, 109 | pub sender: i64, 110 | pub time: i32, 111 | pub elements: Vec, 112 | } 113 | 114 | impl From for Reply { 115 | fn from( 116 | ricq::msg::elem::Reply { 117 | reply_seq, 118 | sender, 119 | time, 120 | elements, 121 | }: ricq::msg::elem::Reply, 122 | ) -> Self { 123 | Self { 124 | reply_seq, 125 | sender, 126 | time, 127 | elements: MessageChain::from(elements).elements, 128 | } 129 | } 130 | } 131 | 132 | impl From for ricq::msg::elem::Reply { 133 | fn from( 134 | Reply { 135 | reply_seq, 136 | sender, 137 | time, 138 | elements, 139 | }: Reply, 140 | ) -> Self { 141 | Self { 142 | reply_seq, 143 | sender, 144 | time, 145 | elements: ricq::msg::MessageChain::from(MessageChain::from(elements)), 146 | } 147 | } 148 | } 149 | 150 | impl PushElem for Reply { 151 | fn push_to(elem: Self, vec: &mut Vec) { 152 | let rq = ricq::msg::elem::Reply::from(elem); 153 | 154 | let index = match vec.get(0) { 155 | Some(MessageElem::AnonGroupMsg(..)) => 1, 156 | _ => 0, 157 | }; 158 | 159 | vec.insert(index, rq.into()); 160 | } 161 | } 162 | 163 | mod ffi { 164 | use crate::message::meta::MessageReceipt; 165 | use atri_ffi::ffi::ForFFI; 166 | use atri_ffi::message::FFIMessageReceipt; 167 | 168 | impl ForFFI for MessageReceipt { 169 | type FFIValue = FFIMessageReceipt; 170 | 171 | fn into_ffi(self) -> Self::FFIValue { 172 | let MessageReceipt { seqs, rands, time } = self; 173 | 174 | FFIMessageReceipt { 175 | seqs: seqs.into(), 176 | rands: rands.into(), 177 | time, 178 | } 179 | } 180 | 181 | fn from_ffi(FFIMessageReceipt { seqs, rands, time }: Self::FFIValue) -> Self { 182 | Self { 183 | seqs: seqs.into_vec(), 184 | rands: rands.into_vec(), 185 | time, 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/signal/sys/macos.rs: -------------------------------------------------------------------------------- 1 | use crate::signal::{disable_raw_mode, post_print_fatal, pre_print_fatal, DlBacktrace}; 2 | use std::cell::RefCell; 3 | use std::mem::MaybeUninit; 4 | use std::ptr::null_mut; 5 | 6 | pub fn init_crash_handler() { 7 | let mut act = Sigaction { 8 | __sigaction_u: SigactionU { 9 | __sa_sigaction: Some(handle), 10 | }, 11 | sa_mask: 0, 12 | sa_flags: 0, 13 | }; 14 | 15 | extern "C" { 16 | fn sigemptyset(arg1: *mut SigsetT) -> std::ffi::c_int; 17 | 18 | fn sigaction( 19 | arg1: std::ffi::c_int, 20 | arg2: *const Sigaction, 21 | arg3: *mut Sigaction, 22 | ) -> std::ffi::c_int; 23 | } 24 | 25 | unsafe { 26 | sigemptyset(&mut act.sa_mask); 27 | 28 | for sig in [SIGSEGV, SIGBUS, SIGABRT] { 29 | if sigaction(SIGSEGV, &act, null_mut()) != 0 { 30 | eprintln!("signal {} handler 注册失败", sig); 31 | } 32 | } 33 | } 34 | } 35 | 36 | macro_rules! exception_sig { 37 | ($($sig:expr => $name:ident),* $(,)?) => { 38 | $(const $name: std::ffi::c_int = $sig;)* 39 | 40 | #[allow(dead_code)] 41 | const fn sig_name(code: std::ffi::c_int) -> &'static str { 42 | match code { 43 | $($sig => stringify!($name),)* 44 | _ => "UNKNOWN" 45 | } 46 | } 47 | }; 48 | } 49 | 50 | exception_sig! { 51 | 6 => SIGABRT, 52 | 10 => SIGBUS, 53 | 11 => SIGSEGV, 54 | } 55 | 56 | unsafe extern "C" fn handle( 57 | sig: std::ffi::c_int, 58 | _info: *mut Siginfo, 59 | _addr: *mut std::ffi::c_void, 60 | ) { 61 | fn dl_get_name(addr: *const std::ffi::c_void) -> String { 62 | extern "C" { 63 | fn dladdr(arg1: *const std::ffi::c_void, arg2: *mut DlInfo) -> std::ffi::c_int; 64 | } 65 | 66 | let mut di = MaybeUninit::::uninit(); 67 | let di = unsafe { 68 | if dladdr(addr, di.as_mut_ptr()) == 0 { 69 | return String::from("unknown"); 70 | } 71 | 72 | di.assume_init() 73 | }; 74 | 75 | unsafe { 76 | std::ffi::CStr::from_ptr(di.dli_fname) 77 | .to_string_lossy() 78 | .into_owned() 79 | } 80 | } 81 | 82 | let enabled = pre_print_fatal(); 83 | crate::signal::fatal_error_print(); 84 | 85 | eprintln!("exception address: {:p}", (*_info).si_addr); 86 | eprintln!( 87 | "stack backtrace:\n{}", 88 | DlBacktrace { 89 | inner: backtrace::Backtrace::new(), 90 | fun: dl_get_name 91 | } 92 | ); 93 | 94 | eprintln!("Something went wrong, signal: {sig}({})", sig_name(sig)); 95 | post_print_fatal(enabled); 96 | 97 | match sig { 98 | SIGSEGV if crate::service::plugin::is_rec_enabled() => exception_jmp(sig), 99 | or => { 100 | disable_raw_mode(); 101 | std::process::exit(or); 102 | } 103 | } 104 | } 105 | 106 | pub type DarwinPidT = i32; 107 | pub type DarwinUidT = u32; 108 | 109 | pub type DarwinSigsetT = u32; 110 | 111 | pub type PidT = DarwinPidT; 112 | pub type UidT = DarwinUidT; 113 | 114 | pub type SigsetT = DarwinSigsetT; 115 | 116 | #[repr(C)] 117 | #[derive(Copy, Clone)] 118 | pub struct Siginfo { 119 | pub si_signo: std::ffi::c_int, 120 | pub si_errno: std::ffi::c_int, 121 | pub si_code: std::ffi::c_int, 122 | pub si_pid: PidT, 123 | pub si_uid: UidT, 124 | pub si_status: std::ffi::c_int, 125 | pub si_addr: *mut std::ffi::c_void, 126 | pub si_value: Sigval, 127 | pub si_band: std::ffi::c_long, 128 | pub __pad: [std::ffi::c_ulong; 7usize], 129 | } 130 | 131 | #[repr(C)] 132 | #[derive(Copy, Clone)] 133 | pub union Sigval { 134 | pub sival_int: std::ffi::c_int, 135 | pub sival_ptr: *mut std::ffi::c_void, 136 | } 137 | 138 | #[repr(C)] 139 | #[derive(Copy, Clone)] 140 | pub union SigactionU { 141 | pub __sa_handler: Option, 142 | pub __sa_sigaction: Option< 143 | unsafe extern "C" fn( 144 | arg1: std::ffi::c_int, 145 | arg2: *mut Siginfo, 146 | arg3: *mut std::ffi::c_void, 147 | ), 148 | >, 149 | } 150 | 151 | #[repr(C)] 152 | #[derive(Copy, Clone)] 153 | pub struct Sigaction { 154 | pub __sigaction_u: SigactionU, 155 | pub sa_mask: SigsetT, 156 | pub sa_flags: std::ffi::c_int, 157 | } 158 | 159 | #[repr(C)] 160 | #[derive(Debug, Copy, Clone)] 161 | pub struct DlInfo { 162 | pub dli_fname: *const std::ffi::c_char, 163 | pub dli_fbase: *mut std::ffi::c_void, 164 | pub dli_sname: *const std::ffi::c_char, 165 | pub dli_saddr: *mut std::ffi::c_void, 166 | } 167 | 168 | const _JBLEN: usize = (14 + 8 + 2) * 2; 169 | const _SIGJBLEN: usize = _JBLEN + 1; 170 | 171 | type SigjmpBuf = [std::ffi::c_int; _SIGJBLEN]; 172 | 173 | thread_local! { 174 | static JMP_BUF: RefCell> = RefCell::new(None); 175 | } 176 | 177 | pub unsafe fn save_jmp() { 178 | extern "C" { 179 | fn sigsetjmp(buf: *mut std::ffi::c_int, save_mask: std::ffi::c_int) -> std::ffi::c_int; 180 | } 181 | 182 | let mut buf: SigjmpBuf = [0; _SIGJBLEN]; 183 | let ret = unsafe { sigsetjmp(buf.as_mut_ptr(), true as _) }; 184 | 185 | if ret != 0 { 186 | panic!("exception occurred, status: {ret}"); 187 | } 188 | 189 | JMP_BUF.with(|r| { 190 | *r.borrow_mut() = Some(buf); 191 | }); 192 | } 193 | 194 | pub unsafe fn exception_jmp(status: std::ffi::c_int) -> ! { 195 | extern "C" { 196 | fn siglongjmp(buf: *const std::ffi::c_int, val: std::ffi::c_int) -> !; 197 | } 198 | 199 | JMP_BUF.with(|r| unsafe { 200 | if let Some(buf) = &*r.borrow() { 201 | siglongjmp(buf.as_ptr(), status); 202 | } else { 203 | std::process::exit(status); 204 | } 205 | }) 206 | } 207 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: [ '*' ] 6 | 7 | jobs: 8 | release: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | target: 13 | - { name: macOS-x64, os: macos-latest, tool: x86_64-apple-darwin } 14 | - { name: Windows-x64, os: windows-latest, tool: x86_64-pc-windows-msvc } 15 | - { name: Linux-x64, os: ubuntu-latest, tool: x86_64-unknown-linux-musl } 16 | - { name: Linux-x64-GNU, os: ubuntu-18.04, tool: x86_64-unknown-linux-gnu } 17 | - { name: macOS-aarch64, os: macos-latest, tool: aarch64-apple-darwin } 18 | - { name: Windows-aarch64, os: windows-latest, tool: aarch64-pc-windows-msvc } 19 | - { name: Linux-aarch64, os: ubuntu-latest, tool: aarch64-unknown-linux-musl } 20 | - { name: Linux-aarch64-GNU, os: ubuntu-18.04, tool: aarch64-unknown-linux-gnu } 21 | - { name: Linux-armv7, os: ubuntu-latest, tool: armv7-unknown-linux-musleabihf } 22 | - { name: Linux-armv7-GNU, os: ubuntu-18.04, tool: armv7-unknown-linux-gnueabihf } 23 | - { name: Linux-arm, os: ubuntu-latest, tool: arm-unknown-linux-musleabihf } 24 | - { name: Linux-arm-GNU, os: ubuntu-18.04, tool: arm-unknown-linux-gnueabihf } 25 | - { name: Linux-mips64, os: ubuntu-latest, tool: mips64-unknown-linux-muslabi64 } 26 | - { name: Linux-mips64-GNU, os: ubuntu-18.04, tool: mips64-unknown-linux-gnuabi64 } 27 | - { name: Linux-powerpc64, os: ubuntu-latest, tool: powerpc64-unknown-linux-gnu } 28 | - { name: Linux-thumbv7, os: ubuntu-latest, tool: thumbv7neon-unknown-linux-gnueabihf } 29 | - { name: Linux-riscv64, os: ubuntu-latest, tool: riscv64gc-unknown-linux-gnu } 30 | - { name: Linux-s390x, os: ubuntu-latest, tool: s390x-unknown-linux-gnu } 31 | - { name: Linux-sparc64, os: ubuntu-latest, tool: sparc64-unknown-linux-gnu } 32 | - { name: iOS-aarch64, os: macos-latest, tool: aarch64-apple-ios } 33 | #- { name: FreeBSD-x64, os: ubuntu-latest, tool: x86_64-unknown-freebsd } 34 | - { name: NetBSD-x64, os: ubuntu-latest, tool: x86_64-unknown-netbsd } 35 | - { name: Illumos-x64, os: ubuntu-latest, tool: x86_64-unknown-illumos } 36 | 37 | version: [ nightly ] 38 | 39 | name: ${{ matrix.target.name }} 40 | runs-on: ${{ matrix.target.os }} 41 | 42 | env: 43 | CI: 1 44 | CARGO_INCREMENTAL: 0 45 | windows: ${{ startsWith(matrix.target.name, 'Windows') }} 46 | linux: ${{ startsWith(matrix.target.name, 'Linux') }} 47 | 48 | steps: 49 | - uses: actions/checkout@v3 50 | 51 | - if: ${{ endsWith(matrix.target.tool, 'musl') }} 52 | run: sudo apt update && sudo apt install -y musl-tools musl-dev 53 | 54 | - name: Install rust toolchain 55 | run: | 56 | rustup default ${{ matrix.version }} 57 | rustup target add ${{ matrix.target.tool }} 58 | 59 | - name: Build 60 | uses: actions-rs/cargo@v1 61 | with: 62 | command: build 63 | args: --release --target ${{ matrix.target.tool }} 64 | use-cross: true 65 | 66 | - name: Upload 67 | if: env.windows != 'true' 68 | uses: svenstaro/upload-release-action@v2 69 | with: 70 | repo_token: ${{ secrets.GITHUB_TOKEN }} 71 | file: target/${{ matrix.target.tool }}/release/atri_bot 72 | asset_name: atri_bot-${{ matrix.target.tool }} 73 | 74 | - name: Upload exe 75 | if: env.windows == 'true' 76 | uses: svenstaro/upload-release-action@v2 77 | with: 78 | repo_token: ${{ secrets.GITHUB_TOKEN }} 79 | file: target/${{ matrix.target.tool }}/release/atri_bot.exe 80 | asset_name: atri_bot-${{ matrix.target.tool }}.exe 81 | 82 | release-android: 83 | strategy: 84 | fail-fast: false 85 | matrix: 86 | arch: 87 | - aarch64 88 | - x86_64 89 | 90 | name: Android-${{ matrix.arch }} 91 | runs-on: ubuntu-latest 92 | 93 | env: 94 | CI: 1 95 | CARGO_INCREMENTAL: 0 96 | 97 | steps: 98 | - uses: actions/checkout@v3 99 | 100 | - name: Install rust toolchain 101 | run: | 102 | rustup default nightly 103 | rustup target add ${{ matrix.arch }}-linux-android 104 | 105 | - name: Install NDK 106 | id: setup-ndk 107 | uses: nttld/setup-ndk@v1 108 | with: 109 | ndk-version: r25 110 | 111 | - name: Set Android NDK ToolChains Path 112 | run: | 113 | echo "ANDROID_NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV 114 | echo ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin >> $GITHUB_PATH 115 | 116 | - name: Set libgcc 117 | run: | 118 | cat << EOF > ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/14.0.6/lib/linux/${{ matrix.arch }}/libgcc.a 119 | INPUT(-lunwind) 120 | EOF 121 | 122 | - name: Build Android ${{ matrix.arch }} 123 | run: cargo build --target ${{ matrix.arch }}-linux-android --release 124 | env: 125 | CC_AARCH64_LINUX_ANDROID: aarch64-linux-android26-clang 126 | CXX_AARCH64_LINUX_ANDROID: aarch64-linux-android26-clang++ 127 | CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: aarch64-linux-android26-clang 128 | CC_X86_64_LINUX_ANDROID: x86_64-linux-android26-clang 129 | CXX_X86_64_LINUX_ANDROID: x86_64-linux-android26-clang++ 130 | CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER: x86_64-linux-android26-clang 131 | 132 | - name: Upload 133 | uses: svenstaro/upload-release-action@v2 134 | with: 135 | repo_token: ${{ secrets.GITHUB_TOKEN }} 136 | file: target/${{ matrix.arch }}-linux-android/release/atri_bot 137 | asset_name: atri_bot-${{ matrix.arch }}-linux-android 138 | -------------------------------------------------------------------------------- /src/signal/sys/windows.rs: -------------------------------------------------------------------------------- 1 | use crate::signal::{disable_raw_mode, post_print_fatal, pre_print_fatal, DlBacktrace}; 2 | 3 | pub fn init_crash_handler() { 4 | extern "C" { 5 | fn SetUnhandledExceptionFilter( 6 | filter: LpTopLevelExceptionFilter, 7 | ) -> LpTopLevelExceptionFilter; 8 | } 9 | 10 | unsafe { 11 | SetUnhandledExceptionFilter(handle); 12 | } 13 | } 14 | 15 | macro_rules! exception_code { 16 | ($($code:expr => $name:ident),* $(,)?) => { 17 | $(const $name: DWORD = $code;)* 18 | 19 | const fn code_name(code: DWORD) -> &'static str { 20 | match code { 21 | $($code => stringify!($name),)* 22 | _ => "UNKNOWN" 23 | } 24 | } 25 | }; 26 | } 27 | 28 | exception_code! { 29 | 0xC0000005 => STATUS_ACCESS_VIOLATION 30 | } 31 | 32 | unsafe extern "stdcall" fn handle(pinfo: *const ExceptionPointers) -> DWORD { 33 | let record = &*(*pinfo).exception_record; 34 | let code = record.exception_code; 35 | fn dl_get_name(addr: *const std::ffi::c_void) -> String { 36 | const MAX_PATH: usize = 260; 37 | 38 | extern "C" { 39 | fn GetModuleHandleExW( 40 | dw_flags: DWORD, 41 | addr: *const std::ffi::c_void, 42 | ph_module: *mut HMODULE, 43 | ) -> BOOL; 44 | 45 | fn GetModuleFileNameW( 46 | h_module: HMODULE, 47 | lp_filename: *mut WCHAR, 48 | n_size: DWORD, 49 | ) -> DWORD; 50 | } 51 | 52 | let mut module: HMODULE = 0; 53 | let mut buffer = [0 as WCHAR; MAX_PATH]; 54 | let size; 55 | unsafe { 56 | if GetModuleHandleExW( 57 | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT 58 | | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, 59 | addr, 60 | &mut module, 61 | ) == 0 62 | { 63 | return String::from("unknown"); 64 | } 65 | 66 | size = GetModuleFileNameW(module, buffer.as_mut_ptr(), MAX_PATH as DWORD); 67 | } 68 | 69 | let mut slice = &buffer[..size as usize]; 70 | 71 | if buffer.starts_with(&[92, 92, 63, 92]) { 72 | slice = &slice[4..]; 73 | } 74 | 75 | String::from_utf16_lossy(slice) 76 | } 77 | 78 | let enabled = pre_print_fatal(); 79 | crate::signal::fatal_error_print(); 80 | 81 | eprintln!("exception address: {:p}", record.exception_address); 82 | eprintln!( 83 | "stack backtrace:\n{}", 84 | DlBacktrace { 85 | inner: backtrace::Backtrace::new(), 86 | fun: dl_get_name 87 | } 88 | ); 89 | 90 | eprintln!( 91 | "Something went wrong, code: 0x{code:x}({})", 92 | code_name(code) 93 | ); 94 | post_print_fatal(enabled); 95 | 96 | match code { 97 | STATUS_ACCESS_VIOLATION if crate::service::plugin::is_rec_enabled() => exception_jmp(code), 98 | _ => { 99 | disable_raw_mode(); 100 | 1 101 | } 102 | } 103 | } 104 | 105 | #[repr(C)] 106 | struct ExceptionPointers { 107 | exception_record: *const ExceptionRecord, 108 | context_record: *const std::ffi::c_void, 109 | } 110 | 111 | #[repr(C)] 112 | struct ExceptionRecord { 113 | exception_code: DWORD, 114 | exception_flags: DWORD, 115 | exception_record: *const ExceptionRecord, 116 | exception_address: *const std::ffi::c_void, 117 | number_parameters: DWORD, 118 | exception_information: [ULONG_PTR; EXCEPTION_MAXIMUM_PARAMETERS], 119 | } 120 | 121 | const EXCEPTION_MAXIMUM_PARAMETERS: usize = 15; 122 | 123 | type LpTopLevelExceptionFilter = unsafe extern "stdcall" fn(*const ExceptionPointers) -> DWORD; 124 | 125 | type DWORD = std::ffi::c_ulong; 126 | type BOOL = std::ffi::c_int; 127 | type HANDLE = usize; 128 | type HMODULE = HANDLE; 129 | type WCHAR = u16; 130 | 131 | #[allow(non_camel_case_types)] 132 | type ULONG_PTR = std::ffi::c_ulong; 133 | 134 | const GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT: DWORD = 0x00000002; 135 | const GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS: DWORD = 0x00000004; 136 | 137 | #[cfg(target_arch = "x86_64")] 138 | mod x64 { 139 | use std::cell::RefCell; 140 | use std::mem::MaybeUninit; 141 | use std::sync::atomic::{AtomicU32, Ordering}; 142 | use winapi::um::winnt::{RtlCaptureContext, RtlRestoreContext, CONTEXT}; 143 | thread_local! { 144 | static CONTEXT: RefCell> = RefCell::new(None); 145 | static STATUS: AtomicU32 = AtomicU32::new(0); 146 | } 147 | 148 | pub unsafe fn save_jmp() { 149 | let mut buf = MaybeUninit::uninit(); 150 | 151 | unsafe { 152 | RtlCaptureContext(buf.as_mut_ptr()); 153 | } 154 | 155 | let buf = buf.assume_init(); 156 | 157 | let status = STATUS.with(|r| r.load(Ordering::Relaxed)); 158 | if status != 0 { 159 | panic!("exception occurred, status: 0x{status:x}"); 160 | } 161 | 162 | CONTEXT.with(|r| { 163 | *r.borrow_mut() = Some(buf); 164 | }); 165 | } 166 | 167 | pub unsafe fn exception_jmp(status: u32) -> ! { 168 | CONTEXT.with(|r| unsafe { 169 | let mut brr = r.borrow_mut(); 170 | if let Some(ctx) = &mut *brr { 171 | STATUS.with(|r| r.store(status, Ordering::Relaxed)); 172 | RtlRestoreContext(ctx, std::ptr::null_mut()); 173 | } 174 | 175 | std::process::exit(status as i32); 176 | }) 177 | } 178 | } 179 | 180 | #[cfg(target_arch = "x86_64")] 181 | pub use x64::*; 182 | 183 | #[cfg(target_arch = "aarch64")] 184 | mod aarch64 { 185 | pub unsafe fn save_jmp() {} 186 | 187 | pub unsafe fn exception_jmp(status: u32) -> ! { 188 | std::process::exit(status as i32); 189 | } 190 | } 191 | 192 | #[cfg(target_arch = "aarch64")] 193 | pub use aarch64::*; 194 | -------------------------------------------------------------------------------- /src/plugin/mod.rs: -------------------------------------------------------------------------------- 1 | mod ffi; 2 | 3 | use crate::plugin::ffi::client::{client_clone, client_drop}; 4 | use crate::plugin::ffi::friend::{friend_clone, friend_drop, friend_upload_image_ex}; 5 | use crate::plugin::ffi::group::{group_clone, group_drop, group_upload_image_ex}; 6 | use crate::plugin::ffi::rt::{plugin_manager_block_on, plugin_manager_spawn}; 7 | use crate::plugin::ffi::string::{c_str_cvt, rust_str_cvt, rust_string_drop}; 8 | use ffi::client::{ 9 | client_find_friend, client_find_group, client_get_friends, client_get_groups, client_get_id, 10 | client_get_list, client_get_nickname, find_client, 11 | }; 12 | use ffi::env::env_get_workspace; 13 | use ffi::event::{ 14 | event_intercept, event_is_intercepted, friend_message_event_get_friend, 15 | friend_message_event_get_message, group_message_event_get_group, 16 | group_message_event_get_message, group_message_event_get_sender, 17 | }; 18 | use ffi::friend::{ 19 | friend_get_client, friend_get_id, friend_get_nickname, friend_send_message, 20 | friend_send_message_blocking, friend_upload_image, friend_upload_image_blocking, 21 | }; 22 | use ffi::group::{ 23 | group_change_name, group_change_name_blocking, group_find_member, group_get_client, 24 | group_get_id, group_get_members, group_get_name, group_invite, group_invite_blocking, 25 | group_quit, group_quit_blocking, group_send_forward_message, 26 | group_send_forward_message_blocking, group_send_message, group_send_message_blocking, 27 | group_upload_image, group_upload_image_blocking, 28 | }; 29 | use ffi::listener::{ 30 | listener_next_event_with_priority, listener_next_event_with_priority_blocking, new_listener, 31 | new_listener_c_func, new_listener_closure, 32 | }; 33 | use ffi::log::log; 34 | use ffi::member::{ 35 | named_member_change_card_name, named_member_change_card_name_blocking, 36 | named_member_get_card_name, named_member_get_group, named_member_get_id, 37 | named_member_get_nickname, 38 | }; 39 | use ffi::message::{image_get_id, image_get_url, message_chain_from_json, message_chain_to_json}; 40 | use tracing::error; 41 | 42 | pub extern "C" fn plugin_get_function(sig: u16) -> *const () { 43 | extern "C" fn not_impl() { 44 | let bt = std::backtrace::Backtrace::force_capture(); 45 | error!("插件执行了一个不存在的操作, stack backtrace: {}", bt); 46 | 47 | std::process::abort(); 48 | } 49 | 50 | macro_rules! match_function { 51 | (input: $input:expr; $($sig:expr => $fun:expr),* $(,)?) => { 52 | match $input { 53 | $($sig => $fun as *const (),)* 54 | _ => not_impl as *const (), 55 | } 56 | }; 57 | } 58 | 59 | match_function! { 60 | input: sig; 61 | // plugin manager 62 | 0 => plugin_manager_spawn, 63 | 1 => plugin_manager_block_on, 64 | 65 | // listener 66 | 100 => new_listener, 67 | 101 => listener_next_event_with_priority, 68 | 150 => new_listener_c_func, 69 | 151 => new_listener_closure, 70 | 152 => listener_next_event_with_priority_blocking, 71 | 72 | // event 73 | 200 => event_intercept, 74 | 201 => event_is_intercepted, 75 | 76 | // client 77 | 300 => client_get_id, 78 | 301 => client_get_nickname, 79 | 302 => client_get_list, 80 | 303 => find_client, 81 | 304 => client_find_group, 82 | 305 => client_find_friend, 83 | 306 => client_get_groups, 84 | 307 => client_get_friends, 85 | 86 | // client handle 87 | 320 => client_clone, 88 | 321 => client_drop, 89 | 90 | 91 | // group 92 | 400 => group_get_id, 93 | 401 => group_get_name, 94 | 402 => group_get_client, 95 | 403 => group_get_members, 96 | 404 => group_find_member, 97 | // 405 98 | 406 => group_send_message, 99 | 407 => group_upload_image, 100 | 408 => group_quit, 101 | 409 => group_change_name, 102 | 410 => group_send_forward_message, 103 | 411 => group_invite, 104 | 105 | //group handle 106 | 420 => group_clone, 107 | 421 => group_drop, 108 | 109 | // blocking api 110 | 456 => group_send_message_blocking, 111 | 457 => group_upload_image_blocking, 112 | 458 => group_quit_blocking, 113 | 459 => group_change_name_blocking, 114 | 460 => group_send_forward_message_blocking, 115 | 461 => group_invite_blocking, 116 | 117 | // extension 118 | 480 => group_upload_image_ex, 119 | 120 | // friend 121 | 500 => friend_get_id, 122 | 501 => friend_get_nickname, 123 | 502 => friend_get_client, 124 | 503 => friend_send_message, 125 | 504 => friend_upload_image, 126 | 127 | // friend handle 128 | 520 => friend_clone, 129 | 521 => friend_drop, 130 | 131 | // blocking api 132 | 553 => friend_send_message_blocking, 133 | 554 => friend_upload_image_blocking, 134 | 135 | // extension 136 | 580 => friend_upload_image_ex, 137 | 138 | // named member 139 | 600 => named_member_get_id, 140 | 601 => named_member_get_nickname, 141 | 602 => named_member_get_card_name, 142 | 603 => named_member_get_group, 143 | 604 => named_member_change_card_name, 144 | 145 | // blocking api 146 | 654 => named_member_change_card_name_blocking, 147 | 148 | 149 | // group message event 150 | 10000 => group_message_event_get_group, 151 | 10001 => group_message_event_get_message, 152 | 10002 => group_message_event_get_sender, 153 | 154 | // friend message event 155 | 10100 => friend_message_event_get_friend, 156 | 10101 => friend_message_event_get_message, 157 | 158 | 2000 => image_get_id, 159 | // flash => 2001 160 | 2002 => image_get_url, 161 | 162 | // log 163 | 20000 => log, 164 | 165 | // env 166 | 30000 => env_get_workspace, 167 | 168 | // serialize 169 | 30100 => message_chain_to_json, 170 | 30101 => message_chain_from_json, 171 | 172 | // ffi 173 | 30500 => rust_str_cvt, 174 | 30501 => c_str_cvt, 175 | 30502 => rust_string_drop, 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/signal/sys/linux.rs: -------------------------------------------------------------------------------- 1 | use crate::signal::{disable_raw_mode, post_print_fatal, pre_print_fatal, DlBacktrace}; 2 | use std::mem::MaybeUninit; 3 | use std::ptr::null_mut; 4 | 5 | pub fn init_crash_handler() { 6 | let mut act = Sigaction { 7 | __sigaction_u: SigactionU { 8 | __sa_sigaction: Some(handle), 9 | }, 10 | sa_mask: SigsetT::default(), 11 | sa_flags: 0, 12 | sa_restorer: None, 13 | }; 14 | 15 | extern "C" { 16 | fn sigemptyset(arg1: *mut SigsetT) -> std::ffi::c_int; 17 | 18 | fn sigaction( 19 | arg1: std::ffi::c_int, 20 | arg2: *const Sigaction, 21 | arg3: *mut Sigaction, 22 | ) -> std::ffi::c_int; 23 | } 24 | 25 | unsafe { 26 | sigemptyset(&mut act.sa_mask); 27 | 28 | for sig in [SIGSEGV, SIGBUS, SIGABRT] { 29 | if sigaction(sig, &act, null_mut()) != 0 { 30 | eprintln!("signal {} 注册失败", sig); 31 | } 32 | } 33 | } 34 | } 35 | 36 | macro_rules! exception_sig { 37 | ($($sig:expr => $name:ident),* $(,)?) => { 38 | $(const $name: std::ffi::c_int = $sig;)* 39 | 40 | #[allow(dead_code)] 41 | const fn sig_name(code: std::ffi::c_int) -> &'static str { 42 | match code { 43 | $($sig => stringify!($name),)* 44 | _ => "UNKNOWN" 45 | } 46 | } 47 | }; 48 | } 49 | 50 | exception_sig! { 51 | 6 => SIGABRT, 52 | 7 => SIGBUS, 53 | 11 => SIGSEGV, 54 | } 55 | 56 | unsafe extern "C" fn handle( 57 | sig: std::ffi::c_int, 58 | _info: *mut Siginfo, 59 | _addr: *mut std::ffi::c_void, 60 | ) { 61 | fn dl_get_name(addr: *const std::ffi::c_void) -> String { 62 | extern "C" { 63 | fn dladdr(arg1: *const std::ffi::c_void, arg2: *mut DlInfo) -> std::ffi::c_int; 64 | } 65 | 66 | let mut di = MaybeUninit::::uninit(); 67 | let di = unsafe { 68 | if dladdr(addr, di.as_mut_ptr()) == 0 { 69 | return String::from("unknown"); 70 | } 71 | 72 | di.assume_init() 73 | }; 74 | 75 | unsafe { 76 | std::ffi::CStr::from_ptr(di.dli_fname) 77 | .to_string_lossy() 78 | .into_owned() 79 | } 80 | } 81 | 82 | let enabled = pre_print_fatal(); 83 | crate::signal::fatal_error_print(); 84 | 85 | eprintln!("addr: {:p}", (*_info)._sifields._sigfault.si_addr); 86 | eprintln!( 87 | "stack backtrace:\n{}", 88 | DlBacktrace { 89 | inner: backtrace::Backtrace::new(), 90 | fun: dl_get_name 91 | } 92 | ); 93 | 94 | eprintln!("Something went wrong, signal: {sig}({})", sig_name(sig)); 95 | post_print_fatal(enabled); 96 | 97 | match sig { 98 | SIGSEGV if crate::service::plugin::is_rec_enabled() => exception_jmp(sig), 99 | or => { 100 | disable_raw_mode(); 101 | std::process::exit(or); 102 | } 103 | } 104 | } 105 | 106 | type PidT = std::ffi::c_int; 107 | type UidT = std::ffi::c_uint; 108 | 109 | const _SIGSET_NWORDS: usize = 1024 / (8 * std::mem::size_of::()); 110 | 111 | #[repr(C)] 112 | #[derive(Debug, Copy, Clone, Default)] 113 | struct SigsetT { 114 | __val: [std::ffi::c_ulong; _SIGSET_NWORDS], 115 | } 116 | 117 | //pub type SigsetT = DarwinSigsetT; 118 | 119 | #[repr(C)] 120 | #[derive(Copy, Clone)] 121 | struct Siginfo { 122 | pub si_signo: std::ffi::c_int, 123 | pub si_errno: std::ffi::c_int, 124 | pub si_code: std::ffi::c_int, 125 | #[cfg(target_pointer_width = "64")] 126 | __pad0: std::ffi::c_int, 127 | pub _sifields: Sifields, 128 | } 129 | 130 | const __SI_MAX_SIZE: usize = 128; 131 | const __SI_PAD_SIZE: usize = (__SI_MAX_SIZE / std::mem::size_of::()) - 4; 132 | 133 | #[repr(C)] 134 | #[derive(Copy, Clone)] 135 | union Sifields { 136 | _pad: [std::ffi::c_int; __SI_PAD_SIZE], 137 | _kill: SiKill, 138 | _timer: SiTimer, 139 | _rt: SiRt, 140 | _sigchld: Sigchld, 141 | _sigfault: Sigfault, 142 | } 143 | 144 | #[repr(C)] 145 | #[derive(Copy, Clone)] 146 | struct SiKill { 147 | si_pid: PidT, 148 | si_uid: UidT, 149 | } 150 | 151 | #[repr(C)] 152 | #[derive(Copy, Clone)] 153 | struct SiTimer { 154 | si_tid: std::ffi::c_int, 155 | si_overrun: std::ffi::c_int, 156 | si_sigval: Sigval, 157 | } 158 | 159 | #[repr(C)] 160 | #[derive(Copy, Clone)] 161 | struct SiRt { 162 | si_pid: PidT, 163 | si_uid: UidT, 164 | si_sigval: Sigval, 165 | } 166 | 167 | #[repr(C)] 168 | #[derive(Copy, Clone)] 169 | struct Sigchld { 170 | si_pid: PidT, 171 | si_uid: UidT, 172 | si_status: std::ffi::c_int, 173 | } 174 | 175 | #[repr(C)] 176 | #[derive(Copy, Clone)] 177 | struct Sigfault { 178 | si_addr: *mut std::ffi::c_void, 179 | si_addr_lsb: std::ffi::c_short, 180 | _bounds: Bounds, 181 | } 182 | 183 | #[repr(C)] 184 | #[derive(Copy, Clone)] 185 | union Bounds { 186 | _addr_bnd: AddrBnd, 187 | _pkey: u32, 188 | } 189 | 190 | #[repr(C)] 191 | #[derive(Copy, Clone)] 192 | struct AddrBnd { 193 | _lower: *mut std::ffi::c_void, 194 | _upper: *mut std::ffi::c_void, 195 | } 196 | 197 | #[repr(C)] 198 | #[derive(Copy, Clone)] 199 | pub union Sigval { 200 | pub sival_int: std::ffi::c_int, 201 | pub sival_ptr: *mut std::ffi::c_void, 202 | } 203 | 204 | #[repr(C)] 205 | #[derive(Copy, Clone)] 206 | union SigactionU { 207 | pub __sa_handler: Option, 208 | pub __sa_sigaction: Option< 209 | unsafe extern "C" fn( 210 | arg1: std::ffi::c_int, 211 | arg2: *mut Siginfo, 212 | arg3: *mut std::ffi::c_void, 213 | ), 214 | >, 215 | } 216 | 217 | #[repr(C)] 218 | #[derive(Copy, Clone)] 219 | struct Sigaction { 220 | pub __sigaction_u: SigactionU, 221 | pub sa_mask: SigsetT, 222 | pub sa_flags: std::ffi::c_int, 223 | pub sa_restorer: Option, 224 | } 225 | 226 | #[repr(C)] 227 | #[derive(Debug, Copy, Clone)] 228 | struct DlInfo { 229 | pub dli_fname: *const std::ffi::c_char, 230 | pub dli_fbase: *mut std::ffi::c_void, 231 | pub dli_sname: *const std::ffi::c_char, 232 | pub dli_saddr: *mut std::ffi::c_void, 233 | } 234 | 235 | pub unsafe fn save_jmp() { 236 | // todo: sigsetjmp 237 | } 238 | 239 | pub fn exception_jmp(status: std::ffi::c_int) -> ! { 240 | std::process::exit(status); // todo: siglongjmp 241 | } 242 | -------------------------------------------------------------------------------- /src/contact/member.rs: -------------------------------------------------------------------------------- 1 | use crate::contact::group::{Group, WeakGroup}; 2 | use crate::error::{AtriError, AtriResult}; 3 | use crate::message::at::At; 4 | use crate::message::meta::{Anonymous, MessageReceipt}; 5 | use crate::message::MessageChain; 6 | use crate::{Client, GroupMemberInfo}; 7 | use atri_ffi::contact::FFIMember; 8 | use atri_ffi::ffi::ForFFI; 9 | use atri_ffi::ManagedCloneable; 10 | use core::fmt; 11 | use std::sync::Arc; 12 | use std::time::Duration; 13 | 14 | #[derive(Clone)] 15 | pub enum Member { 16 | Named(NamedMember), 17 | Anonymous(AnonymousMember), 18 | } 19 | 20 | impl Member { 21 | pub fn id(&self) -> i64 { 22 | match self { 23 | Self::Named(named) => named.id(), 24 | Self::Anonymous(_) => AnonymousMember::ID, 25 | } 26 | } 27 | 28 | pub fn group(&self) -> Group { 29 | match self { 30 | Self::Named(named) => named.group(), 31 | Self::Anonymous(ano) => ano.group(), 32 | } 33 | } 34 | 35 | pub async fn send_message>(&self, msg: M) -> AtriResult { 36 | match self { 37 | Self::Named(named) => named.send_message(msg).await, 38 | Self::Anonymous(..) => Err(AtriError::NotSupported), 39 | } 40 | } 41 | } 42 | 43 | impl ForFFI for Member { 44 | type FFIValue = FFIMember; 45 | 46 | fn into_ffi(self) -> Self::FFIValue { 47 | let (is_named, ma) = match self { 48 | Self::Named(named) => { 49 | let ma = ManagedCloneable::from_value(named); 50 | (true, ma) 51 | } 52 | Self::Anonymous(ano) => { 53 | let ma = ManagedCloneable::from_value(ano); 54 | (false, ma) 55 | } 56 | }; 57 | 58 | FFIMember { 59 | is_named, 60 | inner: ma, 61 | } 62 | } 63 | 64 | fn from_ffi(value: Self::FFIValue) -> Self { 65 | let ma = value.inner; 66 | 67 | unsafe { 68 | if value.is_named { 69 | Self::Named(ma.into_value()) 70 | } else { 71 | Self::Anonymous(ma.into_value()) 72 | } 73 | } 74 | } 75 | } 76 | 77 | #[derive(Clone)] 78 | pub struct NamedMember(Arc); 79 | 80 | impl NamedMember { 81 | pub fn id(&self) -> i64 { 82 | self.0.info.uin 83 | } 84 | 85 | pub fn nickname(&self) -> &str { 86 | &self.0.info.nickname 87 | } 88 | 89 | pub fn card_name(&self) -> &str { 90 | &self.0.info.card_name 91 | } 92 | 93 | pub fn group(&self) -> Group { 94 | self.0.group.force_upgrade() 95 | } 96 | 97 | pub fn client(&self) -> Client { 98 | self.group().client() 99 | } 100 | 101 | pub async fn mute(&self, duration: Duration) -> AtriResult<()> { 102 | self.group() 103 | .client() 104 | .request_client() 105 | .group_mute(self.group().id(), self.id(), duration) 106 | .await 107 | .map_err(AtriError::from) 108 | } 109 | 110 | pub async fn kick>(&self, msg: Option, block: bool) -> AtriResult<()> { 111 | let msg = msg.as_ref().map(AsRef::::as_ref).unwrap_or(""); 112 | 113 | self.group() 114 | .client() 115 | .request_client() 116 | .group_kick(self.group().id(), vec![self.id()], msg, block) 117 | .await 118 | .map_err(AtriError::from) 119 | } 120 | 121 | pub async fn change_card_name>(&self, new: S) -> AtriResult<()> { 122 | self.group() 123 | .client() 124 | .request_client() 125 | .edit_group_member_card(self.group().id(), self.id(), new.into()) 126 | .await 127 | .map_err(AtriError::from) 128 | } 129 | 130 | async fn _send_message(&self, chain: MessageChain) -> AtriResult { 131 | let client = self.group().client(); 132 | let receipt = if let Some(f) = client.find_friend(self.id()) { 133 | f.send_message(chain).await? 134 | } else { 135 | client 136 | .request_client() 137 | .send_group_temp_message(self.group().id(), self.id(), chain.into()) 138 | .await 139 | .map(MessageReceipt::from)? 140 | }; 141 | 142 | Ok(receipt) 143 | } 144 | 145 | pub async fn send_message>(&self, msg: M) -> AtriResult { 146 | self._send_message(msg.into()).await 147 | } 148 | 149 | pub fn at(&self) -> At { 150 | At { 151 | target: self.id(), 152 | display: self.card_name().into(), 153 | } 154 | } 155 | 156 | pub(crate) fn from(group: &Group, info: GroupMemberInfo) -> Self { 157 | let inner = imp::NamedMember { 158 | group: WeakGroup::new(group), 159 | info, 160 | }; 161 | 162 | Self(inner.into()) 163 | } 164 | } 165 | 166 | impl fmt::Debug for NamedMember { 167 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 168 | f.debug_tuple("NamedMember").field(&self.id()).finish() 169 | } 170 | } 171 | 172 | impl fmt::Display for NamedMember { 173 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 174 | write!(f, "群员[{}({})]", self.card_name(), self.id()) 175 | } 176 | } 177 | 178 | #[derive(Clone)] 179 | pub struct AnonymousMember(Arc); 180 | 181 | impl AnonymousMember { 182 | pub(crate) fn from(group: &Group, info: Anonymous) -> Self { 183 | let inner = imp::AnonymousMember { 184 | group: WeakGroup::new(group), 185 | info, 186 | }; 187 | 188 | Self(inner.into()) 189 | } 190 | } 191 | 192 | impl AnonymousMember { 193 | pub const ID: i64 = 80000000; 194 | 195 | pub fn id(&self) -> &[u8] { 196 | &self.0.info.anon_id 197 | } 198 | 199 | pub fn nick(&self) -> &str { 200 | &self.0.info.nick 201 | } 202 | 203 | pub fn group(&self) -> Group { 204 | self.0.group.force_upgrade() 205 | } 206 | 207 | pub fn client(&self) -> Client { 208 | self.group().client() 209 | } 210 | } 211 | 212 | mod imp { 213 | use crate::contact::group::WeakGroup; 214 | use crate::message::meta::Anonymous; 215 | use crate::GroupMemberInfo; 216 | 217 | pub struct NamedMember { 218 | pub group: WeakGroup, 219 | pub info: GroupMemberInfo, 220 | } 221 | 222 | pub struct AnonymousMember { 223 | pub group: WeakGroup, 224 | pub info: Anonymous, 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/service/login.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::Path; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | use std::time::Duration; 5 | 6 | use rand::{thread_rng, Rng}; 7 | use ricq::{LoginResponse, RQError}; 8 | use tokio::fs; 9 | use tokio::io::AsyncWriteExt; 10 | use tracing::{error, info, warn}; 11 | 12 | use crate::client::ClientConfiguration; 13 | use crate::config::login::{LoginConfig, DEFAULT_CONFIG}; 14 | use crate::error::AtriResult; 15 | use crate::{config, global_status, Client}; 16 | 17 | pub async fn login_clients() -> Result<(), RQError> { 18 | let login_conf_dir = { 19 | let path = config::service_config_dir_path(); 20 | if !path.is_dir() { 21 | fs::create_dir_all(&path).await?; 22 | } 23 | path 24 | } 25 | .join("login.toml"); 26 | 27 | let login_conf = { 28 | async fn default_config_write>(path: P) -> io::Result { 29 | fs::write(path, DEFAULT_CONFIG).await?; 30 | 31 | let default_config = LoginConfig::default(); 32 | Ok(default_config) 33 | } 34 | 35 | if login_conf_dir.is_file() { 36 | let s = fs::read_to_string(&login_conf_dir).await?; 37 | 38 | match toml::from_str(&s) { 39 | Ok(conf) => conf, 40 | Err(e) => { 41 | error!("读取登陆配置文件失败: {}", e); 42 | 43 | let cp = config::service_config_dir_path().join("login.toml.bak"); 44 | 45 | fs::copy(&login_conf_dir, cp).await?; 46 | default_config_write(&login_conf_dir).await? 47 | } 48 | } 49 | } else { 50 | default_config_write(login_conf_dir).await? 51 | } 52 | }; 53 | 54 | if login_conf.auto_reconnect { 55 | set_auto_reconnect(true); 56 | } 57 | 58 | let clients_path = config::clients_dir_path(); 59 | if !clients_path.is_dir() { 60 | fs::create_dir(&clients_path).await?; 61 | } 62 | let mut logins = vec![]; 63 | for client in login_conf.clients { 64 | if !client.auto_login { 65 | continue; 66 | } 67 | 68 | let account = client.account; 69 | let pwd = client.password; 70 | 71 | let client_device = clients_path.join(account.to_string()).join("device.json"); 72 | if !client_device.is_file() { 73 | warn!("未找到Client({})的登陆信息,跳过登陆", account); 74 | continue; 75 | } 76 | 77 | let handle = tokio::spawn(async move { 78 | match login_client( 79 | account, 80 | &pwd, 81 | ClientConfiguration { 82 | work_dir: None, 83 | version: client 84 | .protocol 85 | .unwrap_or(login_conf.default_protocol) 86 | .as_version(), 87 | }, 88 | ) 89 | .await 90 | { 91 | Ok(client) => { 92 | global_status().add_client(client.clone()); 93 | info!("{}登陆成功", client); 94 | if let Err(e) = client.refresh_friend_list().await { 95 | warn!("{}刷新好友列表失败: {:?}", client, e); 96 | } 97 | if let Err(e) = client.refresh_group_list().await { 98 | warn!("{}刷新群列表失败: {:?}", client, e); 99 | } 100 | Ok(client) 101 | } 102 | Err(e) => { 103 | global_status().remove_client(account); 104 | error!("Client({})登录失败: {}", account, e); 105 | Err(e) 106 | } 107 | } 108 | }); 109 | logins.push(handle); 110 | 111 | let random = { thread_rng().gen_range(6..44) as f32 / 11.2f32 }; 112 | tokio::time::sleep(Duration::from_secs_f32(random)).await; 113 | } 114 | 115 | for result in logins { 116 | let _ = result.await; 117 | } 118 | 119 | Ok(()) 120 | } 121 | 122 | async fn login_client( 123 | account: i64, 124 | password: &Option, 125 | conf: ClientConfiguration, 126 | ) -> AtriResult { 127 | let client = Client::new(account, conf).await; 128 | client.start().await?; 129 | 130 | info!("Client({})登陆中", account); 131 | match client.try_login().await { 132 | Ok(_) => Ok(client), 133 | Err(e) => { 134 | if let Some(pwd) = password { 135 | info!("{}尝试密码登陆", client); 136 | let mut resp = client.request_client().password_login(account, pwd).await?; 137 | 138 | loop { 139 | match resp { 140 | LoginResponse::DeviceLockLogin(..) => { 141 | resp = client.request_client().device_lock_login().await?; 142 | } 143 | LoginResponse::Success(..) => { 144 | let tokenp = client.work_dir().join("token.json"); 145 | 146 | if let Ok(mut f) = fs::File::create(&tokenp).await { 147 | let token = client.request_client().gen_token().await; 148 | let s = serde_json::to_string_pretty(&token) 149 | .expect("Cannot serialize token"); 150 | let _ = f.write_all(s.as_bytes()).await; 151 | } 152 | 153 | break; 154 | } 155 | LoginResponse::UnknownStatus(ref s) => { 156 | error!("{}登陆失败: {}", client, s.message); 157 | return Err(e); 158 | } 159 | LoginResponse::AccountFrozen => { 160 | error!("{}登陆失败: 账号被冻结", client); 161 | return Err(e); 162 | } 163 | or => { 164 | error!("{}登陆失败, 服务器返回: {:?}", client, or); 165 | return Err(e); 166 | } 167 | } 168 | } 169 | 170 | Ok(client) 171 | } else { 172 | Err(e) 173 | } 174 | } 175 | } 176 | } 177 | 178 | static AUTO_RECONNECT: AtomicBool = AtomicBool::new(false); 179 | 180 | pub fn auto_reconnect() -> bool { 181 | AUTO_RECONNECT.load(Ordering::Relaxed) 182 | } 183 | 184 | pub fn set_auto_reconnect(s: bool) { 185 | AUTO_RECONNECT.store(s, Ordering::Relaxed); 186 | } 187 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | test-and-build: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | target: 13 | - { name: Linux-x64, os: ubuntu-latest, tool: x86_64-unknown-linux-musl } 14 | - { name: Linux-x64-GNU, os: ubuntu-latest, tool: x86_64-unknown-linux-gnu } 15 | - { name: macOS-x64, os: macos-latest, tool: x86_64-apple-darwin } 16 | - { name: Windows-x64, os: windows-latest, tool: x86_64-pc-windows-msvc } 17 | 18 | name: ${{ matrix.target.name }} 19 | runs-on: ${{ matrix.target.os }} 20 | 21 | env: 22 | CI: 1 23 | CARGO_INCREMENTAL: 0 24 | windows: ${{ startsWith(matrix.target.name, 'Windows') }} 25 | linux: ${{ startsWith(matrix.target.name, 'Linux') }} 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | 30 | - if: ${{ endsWith(matrix.target.tool, 'musl') }} 31 | run: sudo apt install -y musl-tools musl-dev 32 | 33 | - name: Install rust toolchain 34 | run: | 35 | rustup default nightly 36 | rustup target add ${{ matrix.target.tool }} 37 | 38 | - name: Cache 39 | uses: Swatinem/rust-cache@v2 40 | 41 | - name: Test 42 | run: cargo test --target ${{ matrix.target.tool }} 43 | 44 | - name: Build 45 | run: cargo build --target ${{ matrix.target.tool }} 46 | 47 | - name: Upload 48 | if: env.windows != 'true' 49 | uses: actions/upload-artifact@v3 50 | with: 51 | name: atri_bot-${{ matrix.target.tool }} 52 | path: target/${{ matrix.target.tool }}/debug/atri_bot 53 | 54 | - name: Upload exe 55 | if: env.windows == 'true' 56 | uses: actions/upload-artifact@v3 57 | with: 58 | name: atri_bot-${{ matrix.target.tool }}.exe 59 | path: target/${{ matrix.target.tool }}/debug/atri_bot.exe 60 | 61 | 62 | build: 63 | strategy: 64 | fail-fast: false 65 | matrix: 66 | target: 67 | - { name: macOS-aarch64, os: macos-latest, tool: aarch64-apple-darwin } 68 | - { name: Windows-aarch64, os: windows-latest, tool: aarch64-pc-windows-msvc } 69 | - { name: Linux-aarch64, os: ubuntu-latest, tool: aarch64-unknown-linux-musl } 70 | - { name: Linux-aarch64-GNU, os: ubuntu-18.04, tool: aarch64-unknown-linux-gnu } 71 | - { name: Linux-armv7, os: ubuntu-latest, tool: armv7-unknown-linux-musleabihf } 72 | - { name: Linux-armv7-GNU, os: ubuntu-18.04, tool: armv7-unknown-linux-gnueabihf } 73 | - { name: Linux-arm, os: ubuntu-latest, tool: arm-unknown-linux-musleabihf } 74 | - { name: Linux-arm-GNU, os: ubuntu-18.04, tool: arm-unknown-linux-gnueabihf } 75 | - { name: Linux-mips64, os: ubuntu-latest, tool: mips64-unknown-linux-muslabi64 } 76 | - { name: Linux-mips64-GNU, os: ubuntu-18.04, tool: mips64-unknown-linux-gnuabi64 } 77 | - { name: Linux-powerpc64, os: ubuntu-latest, tool: powerpc64-unknown-linux-gnu } 78 | - { name: Linux-thumbv7, os: ubuntu-latest, tool: thumbv7neon-unknown-linux-gnueabihf } 79 | - { name: Linux-riscv64, os: ubuntu-latest, tool: riscv64gc-unknown-linux-gnu } 80 | - { name: Linux-s390x, os: ubuntu-latest, tool: s390x-unknown-linux-gnu } 81 | - { name: Linux-sparc64, os: ubuntu-latest, tool: sparc64-unknown-linux-gnu } 82 | - { name: iOS-aarch64, os: macos-latest, tool: aarch64-apple-ios } 83 | #- { name: FreeBSD-x64, os: ubuntu-latest, tool: x86_64-unknown-freebsd } 84 | - { name: NetBSD-x64, os: ubuntu-latest, tool: x86_64-unknown-netbsd } 85 | - { name: Illumos-x64, os: ubuntu-latest, tool: x86_64-unknown-illumos } 86 | 87 | name: ${{ matrix.target.name }} 88 | needs: [ test-and-build ] 89 | runs-on: ${{ matrix.target.os }} 90 | 91 | env: 92 | CI: 1 93 | CARGO_INCREMENTAL: 0 94 | windows: ${{ startsWith(matrix.target.name, 'Windows') }} 95 | linux: ${{ startsWith(matrix.target.name, 'Linux') }} 96 | 97 | steps: 98 | - uses: actions/checkout@v3 99 | 100 | - name: Apt update 101 | if: ${{ startsWith(matrix.target.os, 'ubuntu') }} 102 | run: sudo apt update 103 | 104 | - if: ${{ endsWith(matrix.target.tool, 'musl') }} 105 | run: sudo apt install -y musl-tools musl-dev 106 | 107 | - name: Install rust toolchain 108 | run: | 109 | rustup default nightly 110 | rustup target add ${{ matrix.target.tool }} 111 | 112 | - name: Build 113 | uses: actions-rs/cargo@v1 114 | with: 115 | command: build 116 | args: --target ${{ matrix.target.tool }} 117 | use-cross: true 118 | 119 | - name: Upload 120 | if: env.windows != 'true' 121 | uses: actions/upload-artifact@v3 122 | with: 123 | name: atri_bot-${{ matrix.target.tool }} 124 | path: target/${{ matrix.target.tool }}/debug/atri_bot 125 | 126 | - name: Upload exe 127 | if: env.windows == 'true' 128 | uses: actions/upload-artifact@v3 129 | with: 130 | name: atri_bot-${{ matrix.target.tool }}.exe 131 | path: target/${{ matrix.target.tool }}/debug/atri_bot.exe 132 | 133 | 134 | build-android: 135 | strategy: 136 | fail-fast: false 137 | matrix: 138 | arch: 139 | - aarch64 140 | - x86_64 141 | 142 | name: Android-${{ matrix.arch }} 143 | needs: [ test-and-build ] 144 | runs-on: ubuntu-latest 145 | 146 | env: 147 | CI: 1 148 | CARGO_INCREMENTAL: 0 149 | 150 | steps: 151 | - uses: actions/checkout@v3 152 | 153 | - name: Install rust toolchain 154 | run: | 155 | rustup default nightly 156 | rustup target add ${{ matrix.arch }}-linux-android 157 | 158 | - name: Install NDK 159 | id: setup-ndk 160 | uses: nttld/setup-ndk@v1 161 | with: 162 | ndk-version: r25 163 | 164 | - name: Set Android NDK ToolChains Path 165 | run: | 166 | echo "ANDROID_NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV 167 | echo ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin >> $GITHUB_PATH 168 | 169 | - name: Set libgcc 170 | run: | 171 | cat << EOF > ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/14.0.6/lib/linux/${{ matrix.arch }}/libgcc.a 172 | INPUT(-lunwind) 173 | EOF 174 | 175 | - name: Build Android ${{ matrix.arch }} 176 | run: cargo build --target ${{ matrix.arch }}-linux-android 177 | env: 178 | CC_AARCH64_LINUX_ANDROID: aarch64-linux-android26-clang 179 | CXX_AARCH64_LINUX_ANDROID: aarch64-linux-android26-clang++ 180 | CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: aarch64-linux-android26-clang 181 | CC_X86_64_LINUX_ANDROID: x86_64-linux-android26-clang 182 | CXX_X86_64_LINUX_ANDROID: x86_64-linux-android26-clang++ 183 | CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER: x86_64-linux-android26-clang 184 | 185 | - name: Upload 186 | uses: actions/upload-artifact@v3 187 | with: 188 | name: atri_bot-${{ matrix.arch }}-linux-android 189 | path: target/${{ matrix.arch }}-linux-android/debug/atri_bot 190 | -------------------------------------------------------------------------------- /src/plugin/ffi/group.rs: -------------------------------------------------------------------------------- 1 | use super::rt::future_block_on; 2 | use crate::contact::group::Group; 3 | use crate::message; 4 | use crate::message::forward::ForwardMessage; 5 | use crate::message::meta::MessageReceipt; 6 | use crate::plugin::ffi::cast_ref_phandle; 7 | use crate::plugin::ffi::client::client_to_handle; 8 | use atri_ffi::error::FFIResult; 9 | use atri_ffi::ffi::ForFFI; 10 | use atri_ffi::future::FFIFuture; 11 | use atri_ffi::message::forward::FFIForwardNode; 12 | use atri_ffi::message::{FFIMessageChain, FFIMessageReceipt}; 13 | use atri_ffi::{Handle, ManagedCloneable, RustStr, RustVec}; 14 | use message::MessageChain; 15 | use std::slice; 16 | 17 | pub unsafe fn group_to_handle(group: Group) -> Handle { 18 | unsafe { std::mem::transmute(group) } 19 | } 20 | 21 | pub unsafe fn group_to_ptr_option(group: Option) -> Handle { 22 | group 23 | .map(|c| unsafe { group_to_handle(c) }) 24 | .unwrap_or_else(std::ptr::null) 25 | } 26 | 27 | pub extern "C" fn group_get_id(group: Handle) -> i64 { 28 | let group: &Group = cast_ref_phandle(&group); 29 | group.id() 30 | } 31 | 32 | pub extern "C" fn group_get_name(group: Handle) -> RustStr { 33 | let group: &Group = cast_ref_phandle(&group); 34 | let s = group.name(); 35 | RustStr::from(s) 36 | } 37 | 38 | pub extern "C" fn group_get_client(group: Handle) -> Handle { 39 | let group: &Group = cast_ref_phandle(&group); 40 | unsafe { client_to_handle(group.client()) } 41 | } 42 | 43 | pub extern "C" fn group_get_members(group: Handle) -> FFIFuture> { 44 | FFIFuture::from(async move { 45 | let group: &Group = cast_ref_phandle(&group); 46 | 47 | let named: Vec = group 48 | .members() 49 | .await 50 | .into_iter() 51 | .map(ManagedCloneable::from_value) 52 | .collect(); 53 | 54 | RustVec::from(named) 55 | }) 56 | } 57 | 58 | pub extern "C" fn group_find_member(group: Handle, id: i64) -> FFIFuture { 59 | let group: &Group = cast_ref_phandle(&group); 60 | FFIFuture::from(async move { 61 | group 62 | .find_member(id) 63 | .await 64 | .map(ManagedCloneable::from_value) 65 | .unwrap_or_else(|| unsafe { ManagedCloneable::null() }) 66 | }) 67 | } 68 | 69 | pub extern "C" fn group_send_message( 70 | group: Handle, 71 | chain: FFIMessageChain, 72 | ) -> FFIFuture> { 73 | FFIFuture::from(async move { 74 | let group: &Group = cast_ref_phandle(&group); 75 | let result = group 76 | .send_message(MessageChain::from_ffi(chain)) 77 | .await 78 | .map(MessageReceipt::into_ffi); 79 | 80 | FFIResult::from(result) 81 | }) 82 | } 83 | 84 | pub extern "C" fn group_send_message_blocking( 85 | manager: Handle, 86 | group: Handle, 87 | chain: FFIMessageChain, 88 | ) -> FFIResult { 89 | let group: &Group = cast_ref_phandle(&group); 90 | let chain = MessageChain::from_ffi(chain); 91 | 92 | future_block_on(manager, async move { 93 | let result = group 94 | .send_message(chain) 95 | .await 96 | .map(MessageReceipt::into_ffi); 97 | 98 | FFIResult::from(result) 99 | }) 100 | } 101 | 102 | pub extern "C" fn group_upload_image( 103 | group: Handle, 104 | data: RustVec, 105 | ) -> FFIFuture> { 106 | FFIFuture::from(async move { 107 | let group: &Group = cast_ref_phandle(&group); 108 | let data = data.into_vec(); 109 | let result = group 110 | .upload_image(data) 111 | .await 112 | .map(ManagedCloneable::from_value); 113 | 114 | FFIResult::from(result) 115 | }) 116 | } 117 | 118 | pub extern "C" fn group_upload_image_blocking( 119 | manager: Handle, 120 | group: Handle, 121 | data: RustVec, 122 | ) -> FFIResult { 123 | let group: &Group = cast_ref_phandle(&group); 124 | let data = data.into_vec(); 125 | 126 | future_block_on(manager, async move { 127 | let result = group 128 | .upload_image(data) 129 | .await 130 | .map(ManagedCloneable::from_value); 131 | 132 | FFIResult::from(result) 133 | }) 134 | } 135 | 136 | pub extern "C" fn group_change_name(group: Handle, name: RustStr) -> FFIFuture> { 137 | let s = name.as_ref().to_owned(); 138 | FFIFuture::from(async move { 139 | let group: &Group = cast_ref_phandle(&group); 140 | let result = group.change_name(s).await; 141 | 142 | FFIResult::from(result) 143 | }) 144 | } 145 | 146 | pub extern "C" fn group_change_name_blocking( 147 | manager: Handle, 148 | group: Handle, 149 | name: RustStr, 150 | ) -> FFIResult<()> { 151 | let s = name.as_ref().to_owned(); 152 | let group: &Group = cast_ref_phandle(&group); 153 | 154 | future_block_on(manager, async move { 155 | let result = group.change_name(s).await; 156 | 157 | FFIResult::from(result) 158 | }) 159 | } 160 | 161 | pub extern "C" fn group_quit(group: Handle) -> FFIFuture { 162 | let group: &Group = cast_ref_phandle(&group); 163 | FFIFuture::from(group.quit()) 164 | } 165 | 166 | pub extern "C" fn group_quit_blocking(manager: Handle, group: Handle) -> bool { 167 | let group: &Group = cast_ref_phandle(&group); 168 | future_block_on(manager, async move { group.quit().await }) 169 | } 170 | 171 | pub extern "C" fn group_send_forward_message( 172 | group: Handle, 173 | msg: RustVec, 174 | ) -> FFIFuture> { 175 | let group: &Group = cast_ref_phandle(&group); 176 | let forward = ForwardMessage::from_ffi(msg); 177 | FFIFuture::from(async move { 178 | group 179 | .send_forward_message(forward) 180 | .await 181 | .map(MessageReceipt::into_ffi) 182 | .into() 183 | }) 184 | } 185 | 186 | pub extern "C" fn group_send_forward_message_blocking( 187 | manager: Handle, 188 | group: Handle, 189 | msg: RustVec, 190 | ) -> FFIResult { 191 | let group: &Group = cast_ref_phandle(&group); 192 | let forward = ForwardMessage::from_ffi(msg); 193 | future_block_on(manager, async move { 194 | group 195 | .send_forward_message(forward) 196 | .await 197 | .map(MessageReceipt::into_ffi) 198 | .into() 199 | }) 200 | } 201 | 202 | pub extern "C" fn group_invite(group: Handle, id: i64) -> FFIFuture> { 203 | let group: &Group = cast_ref_phandle(&group); 204 | FFIFuture::from(async move { group.invite(id).await.into() }) 205 | } 206 | 207 | pub extern "C" fn group_invite_blocking(manager: Handle, group: Handle, id: i64) -> FFIResult<()> { 208 | let group: &Group = cast_ref_phandle(&group); 209 | future_block_on(manager, async move { group.invite(id).await.into() }) 210 | } 211 | 212 | pub extern "C" fn group_upload_image_ex( 213 | group: Handle, 214 | ptr: *const u8, 215 | size: usize, 216 | ) -> FFIFuture> { 217 | let slice = unsafe { slice::from_raw_parts(ptr, size) }; 218 | FFIFuture::from(async { 219 | let group: &Group = cast_ref_phandle(&group); 220 | let result = group 221 | .upload_image(slice) 222 | .await 223 | .map(ManagedCloneable::from_value); 224 | 225 | FFIResult::from(result) 226 | }) 227 | } 228 | 229 | pub extern "C" fn group_clone(group: Handle) -> Handle { 230 | let g: &Group = cast_ref_phandle(&group); 231 | unsafe { group_to_handle(g.clone()) } 232 | } 233 | 234 | pub extern "C" fn group_drop(group: Handle) { 235 | drop::(unsafe { std::mem::transmute(group) }) 236 | } 237 | -------------------------------------------------------------------------------- /src/terminal/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod buffer; 2 | pub use buffer::{ 3 | enter_alternate_screen, exit_alternate_screen, is_alternate_screen_enabled, is_terminal_closed, 4 | }; 5 | 6 | mod sys; 7 | 8 | pub use sys::handle_standard_output; 9 | 10 | use crate::service::command::{builtin::handle_plugin_command, PLUGIN_COMMAND}; 11 | use crate::terminal::buffer::{INPUT_BUFFER, INPUT_CACHE}; 12 | use crate::PluginManager; 13 | use crossterm::cursor::MoveToColumn; 14 | use crossterm::event::{ 15 | DisableBracketedPaste, EnableBracketedPaste, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, 16 | }; 17 | use crossterm::terminal::{ 18 | disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, 19 | }; 20 | use crossterm::{event, execute}; 21 | use event::Event; 22 | use std::error::Error; 23 | use std::io::{stdout, Write}; 24 | use tracing::{error, info}; 25 | 26 | pub const BUFFER_SIZE: usize = 512; 27 | 28 | pub const PROMPT: &[u8] = b">> "; 29 | 30 | pub fn stop_info() { 31 | info!("正在停止AtriBot"); 32 | } 33 | 34 | pub fn start_read_input(manager: &mut PluginManager) -> Result<(), Box> { 35 | enable_raw_mode()?; 36 | 37 | INPUT_BUFFER.write()?.reserve(BUFFER_SIZE); 38 | 39 | execute!(stdout(), EnableBracketedPaste)?; 40 | 41 | loop { 42 | let e = event::read()?; 43 | 44 | match e { 45 | Event::Key(KeyEvent { 46 | code: KeyCode::Char('c'), 47 | modifiers: KeyModifiers::CONTROL, 48 | kind: KeyEventKind::Press, 49 | state: _, 50 | }) => { 51 | stop_info(); 52 | break; 53 | } 54 | Event::Key(KeyEvent { 55 | code: KeyCode::Char('q'), 56 | modifiers: KeyModifiers::CONTROL, 57 | kind: KeyEventKind::Press, 58 | state: _, 59 | }) => { 60 | execute!(stdout(), EnterAlternateScreen)?; 61 | loop { 62 | match event::read()? { 63 | Event::Key(KeyEvent { 64 | code: KeyCode::Char('q'), 65 | modifiers: KeyModifiers::CONTROL, 66 | kind: KeyEventKind::Press, 67 | state: _, 68 | }) => { 69 | break; 70 | } 71 | _ => {} 72 | } 73 | } 74 | execute!(stdout(), LeaveAlternateScreen)?; 75 | } 76 | Event::Key(KeyEvent { 77 | code: k @ (KeyCode::Up | KeyCode::Down), 78 | modifiers: KeyModifiers::NONE, 79 | kind: KeyEventKind::Press, 80 | state: _, 81 | }) => { 82 | fn output(buffer: &mut String, output: &str) -> std::io::Result<()> { 83 | let mut stdout = stdout().lock(); 84 | execute!(stdout, Clear(ClearType::CurrentLine), MoveToColumn(0))?; 85 | stdout.write_all(PROMPT)?; 86 | stdout.write_all(output.as_bytes())?; 87 | stdout.flush()?; 88 | 89 | buffer.clear(); 90 | buffer.push_str(output); 91 | 92 | Ok(()) 93 | } 94 | 95 | let mut cache = INPUT_CACHE.lock()?; 96 | let mut buffer = INPUT_BUFFER.write()?; 97 | match k { 98 | KeyCode::Up => { 99 | let index = cache.index; 100 | let len = cache.caches.len(); 101 | if index == 0 { 102 | if let Some(str) = cache.caches.get(0) { 103 | output(&mut buffer, str)?; 104 | } 105 | } else if index <= len { 106 | cache.index -= 1; 107 | 108 | if index == len { 109 | cache.last_input.clear(); 110 | cache.last_input.push_str(&buffer); 111 | } 112 | 113 | output(&mut buffer, &cache.caches[cache.index])?; 114 | } 115 | } 116 | KeyCode::Down => { 117 | cache.index += 1; 118 | let index = cache.index; 119 | let len = cache.caches.len(); 120 | if index < len { 121 | output(&mut buffer, &cache.caches[cache.index])?; 122 | } else if index >= len { 123 | cache.index = len; 124 | output(&mut buffer, &cache.last_input)?; 125 | } 126 | } 127 | _ => unreachable!(), 128 | } 129 | } 130 | Event::Key(KeyEvent { 131 | code, 132 | modifiers: _, 133 | kind: KeyEventKind::Press, 134 | state: _, 135 | }) => match code { 136 | KeyCode::Char(c) => { 137 | INPUT_BUFFER.write()?.push(c); 138 | let mut stdout = stdout().lock(); 139 | stdout.write_all(&[c as u8])?; 140 | stdout.flush()?; 141 | } 142 | KeyCode::Backspace => { 143 | if INPUT_BUFFER.write()?.pop().is_some() { 144 | let mut stdout = stdout().lock(); 145 | stdout.write_all(&[8, b' ', 8])?; 146 | stdout.flush()?; 147 | }; 148 | } 149 | KeyCode::Enter => { 150 | let mut wl = INPUT_BUFFER.write()?; 151 | 152 | let mut stdout = stdout().lock(); 153 | stdout.write_all(b"\n")?; 154 | stdout.flush()?; 155 | 156 | let cmd = wl.trim_end(); 157 | match cmd { 158 | "" => { 159 | stdout.write_all(PROMPT)?; 160 | stdout.flush()?; 161 | wl.clear(); 162 | continue; 163 | } 164 | "help" | "?" | "h" => { 165 | static INFOS: &[&str] = &["help: 显示本帮助", "exit: 退出程序"]; 166 | 167 | let mut s = String::from('\n'); 168 | for &info in INFOS { 169 | s.push_str(info); 170 | s.push('\n'); 171 | } 172 | s.pop(); 173 | info!("{}", s); 174 | } 175 | "exit" | "quit" | "stop" | "q" => { 176 | break; 177 | } 178 | plugin if plugin.starts_with(PLUGIN_COMMAND) => { 179 | if let Err(e) = handle_plugin_command(plugin, manager) { 180 | error!("{}", e); 181 | } 182 | } 183 | or => { 184 | info!("未知的命令 '{}', 使用 'help' 显示帮助信息", or); 185 | } 186 | } 187 | 188 | { 189 | let mut cache = INPUT_CACHE.lock()?; 190 | cache.caches.push(String::from(&*wl)); 191 | cache.index = cache.caches.len(); 192 | cache.last_input.clear(); 193 | } 194 | 195 | wl.clear(); 196 | } 197 | _ => {} 198 | }, 199 | Event::Paste(s) => { 200 | INPUT_BUFFER.write()?.push_str(&s); 201 | let mut stdout = stdout().lock(); 202 | stdout.write_all(s.as_bytes())?; 203 | stdout.flush()?; 204 | } 205 | _ => {} 206 | } 207 | } 208 | 209 | execute!( 210 | stdout(), 211 | DisableBracketedPaste, 212 | Clear(ClearType::CurrentLine), 213 | MoveToColumn(0) 214 | )?; 215 | 216 | disable_raw_mode()?; 217 | Ok(()) 218 | } 219 | -------------------------------------------------------------------------------- /src/message/forward.rs: -------------------------------------------------------------------------------- 1 | use crate::message::MessageChain; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize, Debug, Clone)] 5 | pub struct ForwardMessage(Vec); 6 | 7 | impl ForwardMessage { 8 | pub fn len(&self) -> usize { 9 | self.0.len() 10 | } 11 | 12 | pub fn is_empty(&self) -> bool { 13 | self.len() == 0 14 | } 15 | 16 | pub fn iter(&self) -> std::slice::Iter { 17 | self.0.iter() 18 | } 19 | 20 | pub fn from_message_chain( 21 | sender_id: i64, 22 | sender_name: String, 23 | time: i32, 24 | chain: MessageChain, 25 | ) -> Self { 26 | Self(vec![ForwardNode::NormalMessage { 27 | info: ForwardNodeInfo { 28 | sender_id, 29 | sender_name, 30 | time, 31 | }, 32 | chain, 33 | }]) 34 | } 35 | } 36 | 37 | impl IntoIterator for ForwardMessage { 38 | type Item = ForwardNode; 39 | type IntoIter = std::vec::IntoIter; 40 | 41 | fn into_iter(self) -> Self::IntoIter { 42 | self.0.into_iter() 43 | } 44 | } 45 | 46 | #[derive(Serialize, Deserialize, Debug, Clone)] 47 | #[serde(tag = "type")] 48 | #[serde(rename_all = "snake_case")] 49 | pub enum ForwardNode { 50 | NormalMessage { 51 | info: ForwardNodeInfo, 52 | chain: MessageChain, 53 | }, 54 | ForwardMessage { 55 | info: ForwardNodeInfo, 56 | forward: ForwardMessage, 57 | }, 58 | } 59 | 60 | #[derive(Serialize, Deserialize, Debug, Clone)] 61 | pub struct ForwardNodeInfo { 62 | pub sender_id: i64, 63 | pub sender_name: String, 64 | pub time: i32, 65 | } 66 | 67 | impl From<[ForwardNode; N]> for ForwardMessage { 68 | fn from(value: [ForwardNode; N]) -> Self { 69 | Self(value.to_vec()) 70 | } 71 | } 72 | 73 | impl From> for ForwardMessage { 74 | fn from(value: Vec) -> Self { 75 | let mut nodes: Vec = Vec::with_capacity(value.len()); 76 | 77 | for msg in value { 78 | nodes.push(match msg { 79 | ricq::structs::ForwardMessage::Message(ricq::structs::MessageNode { 80 | sender_id, 81 | time, 82 | sender_name, 83 | elements, 84 | }) => ForwardNode::NormalMessage { 85 | info: ForwardNodeInfo { 86 | sender_id, 87 | sender_name, 88 | time, 89 | }, 90 | chain: elements.into(), 91 | }, 92 | ricq::structs::ForwardMessage::Forward(ricq::structs::ForwardNode { 93 | sender_id, 94 | time, 95 | sender_name, 96 | nodes, 97 | }) => ForwardNode::ForwardMessage { 98 | info: ForwardNodeInfo { 99 | sender_id, 100 | sender_name, 101 | time, 102 | }, 103 | forward: nodes.into(), 104 | }, 105 | }); 106 | } 107 | 108 | Self(nodes) 109 | } 110 | } 111 | 112 | impl From for Vec { 113 | fn from(value: ForwardMessage) -> Self { 114 | let mut nodes = Self::with_capacity(value.len()); 115 | 116 | for node in value.0 { 117 | nodes.push(match node { 118 | ForwardNode::NormalMessage { 119 | info: 120 | ForwardNodeInfo { 121 | sender_id, 122 | sender_name, 123 | time, 124 | }, 125 | chain, 126 | } => ricq::structs::ForwardMessage::Message(ricq::structs::MessageNode { 127 | sender_id, 128 | time, 129 | sender_name, 130 | elements: chain.into(), 131 | }), 132 | ForwardNode::ForwardMessage { 133 | info: 134 | ForwardNodeInfo { 135 | sender_id, 136 | sender_name, 137 | time, 138 | }, 139 | forward: msg, 140 | } => ricq::structs::ForwardMessage::Forward(ricq::structs::ForwardNode { 141 | sender_id, 142 | time, 143 | sender_name, 144 | nodes: msg.into(), 145 | }), 146 | }); 147 | } 148 | 149 | nodes 150 | } 151 | } 152 | 153 | mod ffi { 154 | use super::{ForwardMessage, ForwardNode, ForwardNodeInfo}; 155 | use crate::message::MessageChain; 156 | use atri_ffi::ffi::ForFFI; 157 | use atri_ffi::message::forward::{FFIForwardNode, FFIForwardNodeInfo, ForwardNodeUnion}; 158 | use atri_ffi::RustVec; 159 | use std::mem::ManuallyDrop; 160 | 161 | impl ForFFI for ForwardNode { 162 | type FFIValue = FFIForwardNode; 163 | 164 | fn into_ffi(self) -> Self::FFIValue { 165 | match self { 166 | Self::NormalMessage { info, chain } => FFIForwardNode { 167 | is_normal: true, 168 | info: info.into_ffi(), 169 | inner: ForwardNodeUnion { 170 | normal: ManuallyDrop::new(chain.into_ffi()), 171 | }, 172 | }, 173 | Self::ForwardMessage { info, forward } => FFIForwardNode { 174 | is_normal: false, 175 | info: info.into_ffi(), 176 | inner: ForwardNodeUnion { 177 | forward: ManuallyDrop::new(forward.into_ffi()), 178 | }, 179 | }, 180 | } 181 | } 182 | 183 | fn from_ffi( 184 | FFIForwardNode { 185 | is_normal, 186 | info, 187 | inner, 188 | }: Self::FFIValue, 189 | ) -> Self { 190 | unsafe { 191 | if is_normal { 192 | Self::NormalMessage { 193 | info: ForwardNodeInfo::from_ffi(info), 194 | chain: MessageChain::from_ffi(ManuallyDrop::into_inner(inner.normal)), 195 | } 196 | } else { 197 | Self::ForwardMessage { 198 | info: ForwardNodeInfo::from_ffi(info), 199 | forward: ForwardMessage::from_ffi(ManuallyDrop::into_inner(inner.forward)), 200 | } 201 | } 202 | } 203 | } 204 | } 205 | 206 | impl ForFFI for ForwardNodeInfo { 207 | type FFIValue = FFIForwardNodeInfo; 208 | 209 | fn into_ffi(self) -> Self::FFIValue { 210 | let ForwardNodeInfo { 211 | sender_id, 212 | sender_name, 213 | time, 214 | } = self; 215 | 216 | FFIForwardNodeInfo { 217 | sender_id, 218 | sender_name: sender_name.into(), 219 | time, 220 | } 221 | } 222 | 223 | fn from_ffi( 224 | FFIForwardNodeInfo { 225 | sender_id, 226 | sender_name, 227 | time, 228 | }: Self::FFIValue, 229 | ) -> Self { 230 | Self { 231 | sender_id, 232 | sender_name: sender_name.into(), 233 | time, 234 | } 235 | } 236 | } 237 | 238 | impl ForFFI for ForwardMessage { 239 | type FFIValue = RustVec; 240 | 241 | fn into_ffi(self) -> Self::FFIValue { 242 | self.0 243 | .into_iter() 244 | .map(ForwardNode::into_ffi) 245 | .collect::>() 246 | .into() 247 | } 248 | 249 | fn from_ffi(rs: Self::FFIValue) -> Self { 250 | Self( 251 | rs.into_vec() 252 | .into_iter() 253 | .map(ForwardNode::from_ffi) 254 | .collect(), 255 | ) 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/event/listener.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | use std::sync::Arc; 5 | use std::time::Duration; 6 | 7 | use tokio::sync::Mutex; 8 | 9 | use crate::channel::global_receiver; 10 | use crate::event::FromEvent; 11 | use crate::{global_listener_runtime, global_listener_worker, Event}; 12 | 13 | pub type ListenerHandler = 14 | Box Pin + Send + 'static>> + Send + 'static>; 15 | 16 | pub struct Listener { 17 | pub name: Arc, 18 | pub(crate) concurrent_mutex: Option>, 19 | pub(crate) handler: ListenerHandler, 20 | pub(crate) closed: Arc, 21 | pub(crate) priority: Priority, 22 | } 23 | 24 | impl Listener { 25 | pub fn listening_on(handler: F) -> ListenerGuard 26 | where 27 | F: Fn(E) -> Fu, 28 | F: Send + 'static, 29 | Fu: Future, 30 | Fu: Send + 'static, 31 | E: FromEvent, 32 | { 33 | ListenerBuilder::listening_on(handler).start() 34 | } 35 | 36 | pub fn listening_on_always(handler: F) -> ListenerGuard 37 | where 38 | F: Fn(E) -> Fu, 39 | F: Send + 'static, 40 | Fu: Future, 41 | Fu: Send + 'static, 42 | E: FromEvent, 43 | { 44 | ListenerBuilder::listening_on_always(handler).start() 45 | } 46 | 47 | pub async fn next_event_with_priority( 48 | timeout: Duration, 49 | filter: F, 50 | priority: Priority, 51 | ) -> Option 52 | where 53 | E: FromEvent, 54 | E: Send + 'static, 55 | F: Fn(&E) -> bool, 56 | { 57 | tokio::time::timeout(timeout, async { 58 | let (tx, mut rx) = tokio::sync::mpsc::channel(8); 59 | let _guard = ListenerBuilder::listening_on_always(move |e: E| { 60 | let tx = tx.clone(); 61 | async move { 62 | let _ = tx.send(e).await; 63 | } 64 | }) 65 | .priority(priority) 66 | .start(); 67 | 68 | while let Some(e) = rx.recv().await { 69 | if !filter(&e) { 70 | continue; 71 | } 72 | drop(_guard); 73 | 74 | return e; 75 | } 76 | 77 | unreachable!() 78 | }) 79 | .await 80 | .ok() 81 | } 82 | 83 | #[inline] 84 | pub async fn next_event(timeout: Duration, filter: F) -> Option 85 | where 86 | E: FromEvent, 87 | E: Send + 'static, 88 | F: Fn(&E) -> bool, 89 | { 90 | Self::next_event_with_priority(timeout, filter, Default::default()).await 91 | } 92 | 93 | #[inline] 94 | pub fn name(&self) -> &str { 95 | &self.name 96 | } 97 | } 98 | 99 | pub struct ListenerBuilder { 100 | pub name: Option, 101 | pub concurrent: bool, 102 | pub watcher: bool, 103 | handler: ListenerHandler, 104 | pub priority: Priority, 105 | } 106 | 107 | impl ListenerBuilder { 108 | fn new(handler: F) -> Self 109 | where 110 | F: Fn(Event) -> Fu, 111 | F: Send + 'static, 112 | Fu: Future, 113 | Fu: Send + 'static, 114 | { 115 | let handler = Box::new(move |e| { 116 | let fu = handler(e); 117 | let b: Box + Send + 'static> = Box::new(fu); 118 | 119 | Box::into_pin(b) 120 | }); 121 | 122 | Self { 123 | name: None, 124 | concurrent: true, 125 | watcher: false, 126 | handler, 127 | priority: Priority::Middle, 128 | } 129 | } 130 | 131 | fn new_always(handler: F) -> Self 132 | where 133 | F: Fn(Event) -> Fu, 134 | F: Send + 'static, 135 | Fu: Future, 136 | Fu: Send + 'static, 137 | { 138 | Self::new(move |e| { 139 | let fu = handler(e); 140 | 141 | async move { 142 | fu.await; 143 | true 144 | } 145 | }) 146 | } 147 | 148 | pub fn listening_on(handler: F) -> Self 149 | where 150 | F: Fn(E) -> Fu, 151 | F: Send + 'static, 152 | Fu: Future, 153 | Fu: Send + 'static, 154 | E: FromEvent, 155 | { 156 | Self::new(move |e| { 157 | let fu = E::from_event(e).map(&handler); 158 | 159 | async move { 160 | if let Some(fu) = fu { 161 | fu.await 162 | } else { 163 | true 164 | } 165 | } 166 | }) 167 | } 168 | 169 | pub fn listening_on_always(handler: F) -> Self 170 | where 171 | F: Fn(E) -> Fu, 172 | F: Send + 'static, 173 | Fu: Future, 174 | Fu: Send + 'static, 175 | E: FromEvent, 176 | { 177 | Self::new_always(move |e| { 178 | let fu = E::from_event(e).map(&handler); 179 | 180 | async move { 181 | if let Some(fu) = fu { 182 | fu.await; 183 | } 184 | } 185 | }) 186 | } 187 | 188 | pub fn start(self) -> ListenerGuard { 189 | let Self { 190 | name, 191 | concurrent, 192 | watcher, 193 | handler, 194 | priority, 195 | } = self; 196 | 197 | let name = Arc::new(name.unwrap_or_else(|| String::from("Unnamed-Listener"))); 198 | let arc_name = name.clone(); 199 | let closed = Arc::new(AtomicBool::new(false)); 200 | let arc_closed = closed.clone(); 201 | let listener = Listener { 202 | name, 203 | concurrent_mutex: if concurrent { 204 | None 205 | } else { 206 | Some(Mutex::new(())) 207 | }, 208 | handler, 209 | closed, 210 | priority, 211 | }; 212 | 213 | if watcher { 214 | global_listener_runtime().spawn(async move { 215 | while let Ok(event) = global_receiver().recv().await { 216 | if let Some(ref mutex) = listener.concurrent_mutex { 217 | let _ = mutex.lock().await; 218 | } 219 | 220 | let fu = (listener.handler)(event); 221 | 222 | let keep: bool = fu.await; 223 | if (!keep) || listener.closed.load(Ordering::Relaxed) { 224 | break; 225 | }; 226 | } 227 | }); 228 | } else { 229 | global_listener_runtime().spawn(global_listener_worker().schedule(listener)); 230 | } 231 | 232 | ListenerGuard { 233 | name: arc_name, 234 | closed: arc_closed, 235 | } 236 | } 237 | 238 | #[inline] 239 | pub fn name>(mut self, name: S) -> Self { 240 | self.name = Some(name.into()); 241 | self 242 | } 243 | 244 | #[inline] 245 | pub fn concurrent(mut self, is: bool) -> Self { 246 | self.concurrent = is; 247 | self 248 | } 249 | 250 | #[inline] 251 | pub fn priority(mut self, priority: Priority) -> Self { 252 | self.priority = priority; 253 | self 254 | } 255 | 256 | pub fn watcher(mut self, is: bool) -> Self { 257 | self.watcher = is; 258 | self 259 | } 260 | } 261 | 262 | #[derive(Copy, Clone, Default)] 263 | pub enum Priority { 264 | Top = 0, 265 | High = 1, 266 | #[default] 267 | Middle = 2, 268 | Low = 3, 269 | Base = 4, 270 | } 271 | 272 | impl From for Priority { 273 | #[inline] 274 | fn from(value: u8) -> Self { 275 | match value { 276 | 0 => Self::Top, 277 | 1 => Self::High, 278 | 2 => Self::Middle, 279 | 3 => Self::Low, 280 | 4 => Self::Base, 281 | _ => Self::Middle, 282 | } 283 | } 284 | } 285 | 286 | unsafe impl Send for Listener {} 287 | 288 | unsafe impl Sync for Listener {} 289 | 290 | #[must_use = "if unused the Listener will immediately close"] 291 | pub struct ListenerGuard { 292 | name: Arc, 293 | closed: Arc, 294 | } 295 | 296 | impl ListenerGuard { 297 | #[inline] 298 | pub fn name(&self) -> &str { 299 | &self.name 300 | } 301 | 302 | #[inline] 303 | pub fn closed(&self) -> bool { 304 | self.closed.load(Ordering::Acquire) 305 | } 306 | } 307 | 308 | impl Drop for ListenerGuard { 309 | #[inline] 310 | fn drop(&mut self) { 311 | self.closed.store(true, Ordering::Relaxed); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/message/ffi.rs: -------------------------------------------------------------------------------- 1 | use super::MessageChain; 2 | use crate::message::at::At; 3 | use crate::message::face::Face; 4 | use crate::message::meta::{Anonymous, MessageMetadata, Reply}; 5 | use crate::message::MessageElement; 6 | use atri_ffi::ffi::ForFFI; 7 | use atri_ffi::message::meta::{ 8 | FFIAnonymous, FFIMessageMetadata, FFIReply, ANONYMOUS_FLAG, NONE_META, REPLY_FLAG, 9 | }; 10 | use atri_ffi::message::{ 11 | FFIAt, FFIMessageChain, FFIMessageElement, MessageElementFlag, MessageElementUnion, 12 | }; 13 | use atri_ffi::{ManagedCloneable, RustString, RustVec}; 14 | use std::mem::{ManuallyDrop, MaybeUninit}; 15 | 16 | impl ForFFI for MessageChain { 17 | type FFIValue = FFIMessageChain; 18 | 19 | fn into_ffi(self) -> Self::FFIValue { 20 | let meta = self.meta.into_ffi(); 21 | let ffi: Vec = self 22 | .elements 23 | .into_iter() 24 | .map(MessageElement::into_ffi) 25 | .collect(); 26 | 27 | let raw = RustVec::from(ffi); 28 | FFIMessageChain { meta, inner: raw } 29 | } 30 | 31 | fn from_ffi(ffi: Self::FFIValue) -> Self { 32 | let meta = MessageMetadata::from_ffi(ffi.meta); 33 | 34 | let v = ffi.inner.into_vec(); 35 | let values: Vec = v.into_iter().map(MessageElement::from_ffi).collect(); 36 | 37 | Self { 38 | meta, 39 | elements: values, 40 | } 41 | } 42 | } 43 | 44 | impl ForFFI for MessageElement { 45 | type FFIValue = FFIMessageElement; 46 | 47 | fn into_ffi(self) -> Self::FFIValue { 48 | match self { 49 | MessageElement::Text(s) => FFIMessageElement { 50 | t: MessageElementFlag::Text.value(), 51 | union: MessageElementUnion { 52 | text: ManuallyDrop::new(RustString::from(s)), 53 | }, 54 | }, 55 | MessageElement::Image(img) => FFIMessageElement { 56 | t: MessageElementFlag::Image.value(), 57 | union: MessageElementUnion { 58 | image: ManuallyDrop::new(ManagedCloneable::from_value(img)), 59 | }, 60 | }, 61 | MessageElement::At(At { target, display }) => FFIMessageElement { 62 | t: MessageElementFlag::At.value(), 63 | union: MessageElementUnion { 64 | at: ManuallyDrop::new({ 65 | FFIAt { 66 | target, 67 | display: RustString::from(display), 68 | } 69 | }), 70 | }, 71 | }, 72 | MessageElement::AtAll => FFIMessageElement { 73 | t: MessageElementFlag::AtAll.value(), 74 | union: MessageElementUnion { at_all: () }, 75 | }, 76 | MessageElement::Face(face) => FFIMessageElement { 77 | t: MessageElementFlag::Face.value(), 78 | union: MessageElementUnion { 79 | face: ManuallyDrop::new(face.into_ffi()), 80 | }, 81 | }, 82 | MessageElement::Unknown(rq) => FFIMessageElement { 83 | t: MessageElementFlag::Unknown.value(), 84 | union: MessageElementUnion { 85 | unknown: ManuallyDrop::new(ManagedCloneable::from_value(rq)), 86 | }, 87 | }, 88 | } 89 | } 90 | 91 | fn from_ffi(value: Self::FFIValue) -> Self { 92 | unsafe { 93 | match MessageElementFlag::try_from(value.t) 94 | .unwrap_or_else(|e| panic!("Unknown message value flag: {e}")) 95 | { 96 | MessageElementFlag::Text => { 97 | MessageElement::Text(ManuallyDrop::into_inner(value.union.text).into()) 98 | } 99 | MessageElementFlag::Image => { 100 | MessageElement::Image(ManuallyDrop::into_inner(value.union.image).into_value()) 101 | } 102 | MessageElementFlag::At => { 103 | let inner = ManuallyDrop::into_inner(value.union.at); 104 | MessageElement::At(At { 105 | target: inner.target, 106 | display: String::from(inner.display), 107 | }) 108 | } 109 | MessageElementFlag::AtAll => MessageElement::AtAll, 110 | MessageElementFlag::Face => { 111 | MessageElement::Face(Face::from_ffi(ManuallyDrop::into_inner(value.union.face))) 112 | } 113 | MessageElementFlag::Unknown => MessageElement::Unknown( 114 | ManuallyDrop::into_inner(value.union.unknown).into_value(), 115 | ), 116 | } 117 | } 118 | } 119 | } 120 | 121 | impl ForFFI for Reply { 122 | type FFIValue = FFIReply; 123 | 124 | fn into_ffi(self) -> Self::FFIValue { 125 | let ffi: Vec = self 126 | .elements 127 | .into_iter() 128 | .map(MessageElement::into_ffi) 129 | .collect(); 130 | let ffi_chain = RustVec::from(ffi); 131 | 132 | FFIReply { 133 | reply_seq: self.reply_seq, 134 | sender: self.sender, 135 | time: self.time, 136 | elements: ffi_chain, 137 | } 138 | } 139 | 140 | fn from_ffi( 141 | FFIReply { 142 | reply_seq, 143 | sender, 144 | time, 145 | elements, 146 | }: Self::FFIValue, 147 | ) -> Self { 148 | let elems = elements.into_vec(); 149 | let values: Vec = elems.into_iter().map(MessageElement::from_ffi).collect(); 150 | 151 | Self { 152 | reply_seq, 153 | sender, 154 | time, 155 | elements: values, 156 | } 157 | } 158 | } 159 | 160 | impl ForFFI for Anonymous { 161 | type FFIValue = FFIAnonymous; 162 | 163 | fn into_ffi(self) -> Self::FFIValue { 164 | let Self { 165 | anon_id, 166 | nick, 167 | portrait_index, 168 | bubble_index, 169 | expire_time, 170 | color, 171 | } = self; 172 | 173 | let anon_id = RustVec::from(anon_id); 174 | let nick = RustString::from(nick); 175 | let color = RustString::from(color); 176 | 177 | FFIAnonymous { 178 | anon_id, 179 | nick, 180 | portrait_index, 181 | bubble_index, 182 | expire_time, 183 | color, 184 | } 185 | } 186 | 187 | fn from_ffi( 188 | FFIAnonymous { 189 | anon_id, 190 | nick, 191 | portrait_index, 192 | bubble_index, 193 | expire_time, 194 | color, 195 | }: Self::FFIValue, 196 | ) -> Self { 197 | let anon_id = anon_id.into_vec(); 198 | let nick = String::from(nick); 199 | let color = String::from(color); 200 | 201 | Self { 202 | anon_id, 203 | nick, 204 | portrait_index, 205 | bubble_index, 206 | expire_time, 207 | color, 208 | } 209 | } 210 | } 211 | 212 | impl ForFFI for At { 213 | type FFIValue = FFIAt; 214 | 215 | fn into_ffi(self) -> Self::FFIValue { 216 | let Self { target, display } = self; 217 | 218 | FFIAt { 219 | target, 220 | display: RustString::from(display), 221 | } 222 | } 223 | 224 | fn from_ffi(FFIAt { target, display }: Self::FFIValue) -> Self { 225 | Self { 226 | target, 227 | display: String::from(display), 228 | } 229 | } 230 | } 231 | 232 | impl ForFFI for MessageMetadata { 233 | type FFIValue = FFIMessageMetadata; 234 | 235 | fn into_ffi(self) -> Self::FFIValue { 236 | let Self { 237 | seqs, 238 | rands, 239 | time, 240 | sender, 241 | anonymous, 242 | reply, 243 | } = self; 244 | let mut flags = NONE_META; 245 | 246 | let mut ffi_anonymous = MaybeUninit::uninit(); 247 | if let Some(anonymous) = anonymous { 248 | flags |= ANONYMOUS_FLAG; 249 | ffi_anonymous.write(anonymous.into_ffi()); 250 | } 251 | 252 | let mut ffi_reply = MaybeUninit::uninit(); 253 | if let Some(reply) = reply { 254 | flags |= REPLY_FLAG; 255 | ffi_reply.write(reply.into_ffi()); 256 | } 257 | 258 | FFIMessageMetadata { 259 | seqs: seqs.into(), 260 | rands: rands.into(), 261 | time, 262 | sender, 263 | flags, 264 | anonymous: ffi_anonymous, 265 | reply: ffi_reply, 266 | } 267 | } 268 | 269 | fn from_ffi( 270 | FFIMessageMetadata { 271 | seqs, 272 | rands, 273 | time, 274 | sender, 275 | flags, 276 | anonymous, 277 | reply, 278 | }: Self::FFIValue, 279 | ) -> Self { 280 | unsafe { 281 | Self { 282 | seqs: seqs.into_vec(), 283 | rands: rands.into_vec(), 284 | time, 285 | sender, 286 | anonymous: if flags & ANONYMOUS_FLAG != 0 { 287 | Some(Anonymous::from_ffi(anonymous.assume_init())) 288 | } else { 289 | None 290 | }, 291 | reply: if flags & REPLY_FLAG != 0 { 292 | Some(Reply::from_ffi(reply.assume_init())) 293 | } else { 294 | None 295 | }, 296 | } 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/message/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod at; 2 | pub mod face; 3 | pub mod ffi; 4 | pub mod forward; 5 | pub mod image; 6 | pub mod macros; 7 | pub mod meta; 8 | 9 | use crate::event::{Event, FromEvent}; 10 | use crate::message::at::At; 11 | use crate::message::face::Face; 12 | use crate::message::meta::{Anonymous, MessageMetadata, MessageReceipt, RecallMessage, Reply}; 13 | use crate::Text; 14 | use image::Image; 15 | use ricq::msg::elem::RQElem; 16 | use ricq::msg::{MessageChain as RQMessageChain, MessageElem, PushElem}; 17 | use ricq::structs::{FriendMessage, GroupMessage}; 18 | use serde::{Deserialize, Serialize}; 19 | use std::fmt::{Debug, Formatter, Write}; 20 | use std::vec; 21 | 22 | #[derive(Serialize, Deserialize, Clone, Default)] 23 | pub struct MessageChain { 24 | meta: MessageMetadata, 25 | elements: Vec, 26 | } 27 | 28 | impl MessageChain { 29 | pub fn iter(&self) -> std::slice::Iter<'_, MessageElement> { 30 | self.into_iter() 31 | } 32 | 33 | pub fn metadata(&self) -> &MessageMetadata { 34 | &self.meta 35 | } 36 | 37 | pub fn metadata_mut(&mut self) -> &mut MessageMetadata { 38 | &mut self.meta 39 | } 40 | 41 | pub fn referred(&self) -> Option<&Reply> { 42 | self.metadata().reply.as_ref() 43 | } 44 | 45 | pub fn reply(&self) -> Reply { 46 | Reply { 47 | reply_seq: self.meta.seqs[0], 48 | sender: self.meta.sender, 49 | time: self.meta.time, 50 | elements: self.elements.clone(), 51 | } 52 | } 53 | 54 | pub fn into_reply(self) -> Reply { 55 | Reply { 56 | reply_seq: self.meta.seqs[0], 57 | sender: self.meta.sender, 58 | time: self.meta.time, 59 | elements: self.elements, 60 | } 61 | } 62 | 63 | pub fn with_reply(&mut self, reply: Reply) { 64 | self.metadata_mut().reply = Some(reply) 65 | } 66 | 67 | pub fn with_anonymous(&mut self, ano: Anonymous) { 68 | self.metadata_mut().anonymous = Some(ano) 69 | } 70 | 71 | pub fn to_json(&self) -> String { 72 | let known: Vec = self 73 | .into_iter() 74 | .filter_map(|e| match e { 75 | MessageElement::Unknown(_) => None, 76 | or => Some(or.clone()), 77 | }) 78 | .collect(); 79 | 80 | serde_json::to_string(&MessageChain { 81 | meta: self.metadata().clone(), 82 | elements: known, 83 | }) 84 | .expect("Serializing error") 85 | } 86 | 87 | pub fn from_json(s: &str) -> serde_json::Result { 88 | serde_json::from_str(s) 89 | } 90 | } 91 | 92 | impl RecallMessage for MessageChain { 93 | fn receipt(&self) -> MessageReceipt { 94 | MessageReceipt { 95 | seqs: self.metadata().seqs.clone(), 96 | rands: self.metadata().rands.clone(), 97 | time: self.metadata().time as i64, 98 | } 99 | } 100 | } 101 | 102 | impl FromEvent for MessageChain { 103 | fn from_event(e: Event) -> Option { 104 | match e { 105 | Event::GroupMessage(e) => Some(e.message().to_owned()), 106 | Event::FriendMessage(e) => Some(e.message().to_owned()), 107 | _ => None, 108 | } 109 | } 110 | } 111 | 112 | impl ToString for MessageChain { 113 | fn to_string(&self) -> String { 114 | self.iter().map(|value| value.to_string()).collect() 115 | } 116 | } 117 | 118 | impl Debug for MessageChain { 119 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 120 | f.write_str(&self.to_string()) 121 | } 122 | } 123 | 124 | impl IntoIterator for MessageChain { 125 | type Item = MessageElement; 126 | type IntoIter = vec::IntoIter; 127 | 128 | fn into_iter(self) -> Self::IntoIter { 129 | self.elements.into_iter() 130 | } 131 | } 132 | 133 | impl<'a> IntoIterator for &'a MessageChain { 134 | type Item = &'a MessageElement; 135 | type IntoIter = std::slice::Iter<'a, MessageElement>; 136 | 137 | fn into_iter(self) -> Self::IntoIter { 138 | self.elements.iter() 139 | } 140 | } 141 | 142 | impl From for MessageChain { 143 | fn from(g: GroupMessage) -> Self { 144 | let mut ran = Self::from(g.elements); 145 | ran.meta = MessageMetadata { 146 | seqs: g.seqs, 147 | rands: g.rands, 148 | time: g.time, 149 | sender: g.from_uin, 150 | ..ran.meta 151 | }; 152 | 153 | ran 154 | } 155 | } 156 | 157 | impl From for MessageChain { 158 | fn from(f: FriendMessage) -> Self { 159 | let mut ran = Self::from(f.elements); 160 | ran.meta = MessageMetadata { 161 | seqs: f.seqs, 162 | rands: f.rands, 163 | time: f.time, 164 | sender: f.from_uin, 165 | ..ran.meta 166 | }; 167 | 168 | ran 169 | } 170 | } 171 | 172 | impl From> for MessageChain { 173 | fn from(elems: Vec) -> Self { 174 | Self { 175 | elements: elems, 176 | ..Default::default() 177 | } 178 | } 179 | } 180 | 181 | impl From for MessageChain { 182 | fn from(chain: ricq::msg::MessageChain) -> Self { 183 | let mut meta = MessageMetadata::default(); 184 | let mut value: Vec = vec![]; 185 | 186 | for val in chain.0 { 187 | match val { 188 | MessageElem::AnonGroupMsg(msg) => { 189 | let rq = ricq::msg::elem::Anonymous::from(msg); 190 | meta.anonymous = Some(Anonymous::from(rq)); 191 | } 192 | MessageElem::SrcMsg(src) => { 193 | let rq = ricq::msg::elem::Reply::from(src); 194 | meta.reply = Some(Reply::from(rq)); 195 | } 196 | or => { 197 | let rq = ricq::msg::elem::RQElem::from(or); 198 | value.push(MessageElement::from(rq)); 199 | } 200 | } 201 | } 202 | 203 | Self { 204 | meta, 205 | elements: value, 206 | } 207 | } 208 | } 209 | 210 | impl From for RQMessageChain { 211 | fn from(chain: MessageChain) -> Self { 212 | let mut rq = RQMessageChain::default(); 213 | MessageChain::push_to(chain, &mut rq.0); 214 | rq 215 | } 216 | } 217 | 218 | impl PushElem for MessageChain { 219 | fn push_to(elem: Self, vec: &mut Vec) { 220 | if let Some(reply) = elem.meta.reply { 221 | let rq = ricq::msg::elem::Reply::from(reply); 222 | vec.push(rq.into()); 223 | } 224 | 225 | if let Some(ano) = elem.meta.anonymous { 226 | let rq = ricq::msg::elem::Anonymous::from(ano); 227 | vec.push(rq.into()); 228 | } 229 | 230 | for value in elem.elements { 231 | MessageElement::push_to(value, vec); 232 | } 233 | } 234 | } 235 | 236 | #[derive(Serialize, Deserialize, Clone)] 237 | #[serde(tag = "type", content = "content")] 238 | #[serde(rename_all = "snake_case")] 239 | pub enum MessageElement { 240 | Text(String), 241 | Image(Image), 242 | At(At), 243 | AtAll, 244 | Face(Face), 245 | #[serde(skip)] 246 | Unknown(RQElem), 247 | } 248 | 249 | impl ToString for MessageElement { 250 | fn to_string(&self) -> String { 251 | let mut s = String::new(); 252 | 253 | match self { 254 | Self::Text(t) => s.push_str(t), 255 | Self::Image(img) => { 256 | let _ = write!(s, "$[Image:{}]", img.url()); 257 | } 258 | Self::At(At { target, display }) => { 259 | let _ = write!(s, "$[At:{display}({target})]"); 260 | } 261 | Self::AtAll => s.push_str("$[AtAll]"), 262 | Self::Face(f) => { 263 | let _ = write!(s, "$[Face:{}]", f.name); 264 | } 265 | Self::Unknown(rq) => s.push_str(&rq.to_string()), 266 | } 267 | 268 | s 269 | } 270 | } 271 | 272 | impl From for MessageElement { 273 | fn from(s: String) -> Self { 274 | Self::Text(s) 275 | } 276 | } 277 | 278 | impl From for RQElem { 279 | fn from(val: MessageElement) -> Self { 280 | match val { 281 | MessageElement::Text(s) => RQElem::Text(Text { content: s }), 282 | MessageElement::Image(img) => match img { 283 | Image::Friend(img) => RQElem::FriendImage(img), 284 | Image::Group(img) => RQElem::GroupImage(img), 285 | }, 286 | MessageElement::At(at) => RQElem::At(at.into()), 287 | MessageElement::AtAll => RQElem::At(At::ALL.into()), 288 | MessageElement::Face(face) => RQElem::Face(face.into()), 289 | MessageElement::Unknown(rq) => rq, 290 | } 291 | } 292 | } 293 | 294 | impl From for MessageElement { 295 | fn from(elem: RQElem) -> Self { 296 | match elem { 297 | RQElem::Text(Text { content }) => MessageElement::Text(content), 298 | RQElem::GroupImage(img) => MessageElement::Image(Image::Group(img)), 299 | RQElem::FriendImage(img) => MessageElement::Image(Image::Friend(img)), 300 | RQElem::At(at) => { 301 | if at.target == 0 { 302 | MessageElement::AtAll 303 | } else { 304 | MessageElement::At(At { 305 | target: at.target, 306 | display: at.display, 307 | }) 308 | } 309 | } 310 | RQElem::Face(face) => MessageElement::Face(face.into()), 311 | or => Self::Unknown(or), 312 | } 313 | } 314 | } 315 | 316 | impl PushElem for MessageElement { 317 | fn push_to(elem: Self, vec: &mut Vec) { 318 | match elem { 319 | Self::Text(s) => PushElem::push_to(Text::new(s), vec), 320 | Self::Image(img) => PushElem::push_to(img, vec), 321 | Self::At(at) => PushElem::push_to(at, vec), 322 | Self::AtAll => PushElem::push_to(At::ALL, vec), 323 | Self::Face(face) => PushElem::push_to(face, vec), 324 | Self::Unknown(_rq) => {} 325 | } 326 | } 327 | } 328 | 329 | #[cfg(test)] 330 | mod tests { 331 | use crate::message::{MessageChain, MessageElement}; 332 | 333 | #[test] 334 | fn serde() { 335 | let mut chain = MessageChain::default(); 336 | chain.elements.push(MessageElement::Text("114514".into())); 337 | chain.elements.push(MessageElement::Text("514".into())); 338 | 339 | println!("{}", chain.to_json()); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/channel/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::sync::OnceLock; 3 | 4 | use async_trait::async_trait; 5 | use regex::Regex; 6 | use ricq::handler::QEvent; 7 | use tokio::sync::broadcast::{channel, Receiver, Sender}; 8 | use tracing::{error, info, warn}; 9 | 10 | use crate::contact::friend::Friend; 11 | use crate::contact::member::{AnonymousMember, Member, NamedMember}; 12 | use crate::event::{ 13 | ClientLoginEvent, DeleteFriendEvent, Event, FriendMessageEvent, FriendPokeEvent, 14 | GroupMessageEvent, GroupPokeEvent, NewFriendEvent, 15 | }; 16 | use crate::global_listener_worker; 17 | use crate::{global_listener_runtime, global_status, Client}; 18 | 19 | static GLOBAL_EVENT_CHANNEL: OnceLock> = OnceLock::>::new(); 20 | 21 | pub fn global_sender() -> &'static Sender { 22 | GLOBAL_EVENT_CHANNEL.get_or_init(|| { 23 | let channel = channel(128); 24 | 25 | channel.0 26 | }) 27 | } 28 | 29 | pub fn global_receiver() -> Receiver { 30 | global_sender().subscribe() 31 | } 32 | 33 | pub struct GlobalEventBroadcastHandler; 34 | 35 | #[async_trait] 36 | impl ricq::handler::Handler for GlobalEventBroadcastHandler { 37 | async fn handle(&self, event: QEvent) { 38 | let client: Client; 39 | 40 | fn get_client(id: i64) -> Option { 41 | global_status().clients.get(&id).map(|b| b.clone()) 42 | } 43 | 44 | macro_rules! get_client { 45 | ($client:expr) => { 46 | if let Some(b) = global_status() 47 | .clients 48 | .get(&$client.uin().await) 49 | .map(|b| b.clone()) 50 | { 51 | b 52 | } else { 53 | return; 54 | } 55 | }; 56 | } 57 | 58 | let self_event = match event { 59 | QEvent::Login(id) => { 60 | client = if let Some(b) = get_client(id) { 61 | b 62 | } else { 63 | return; 64 | }; 65 | 66 | let base = ClientLoginEvent::from(client); 67 | Event::ClientLogin(base) 68 | } 69 | QEvent::GroupMessage(e) => { 70 | fn get_filter_regex() -> &'static Regex { 71 | static FILTER_REGEX: OnceLock = OnceLock::new(); 72 | FILTER_REGEX.get_or_init(|| Regex::new("<[$&].+>").expect("Cannot parse regex")) 73 | } 74 | 75 | client = get_client!(e.client); 76 | let group_id = e.inner.group_code; 77 | 78 | let group_name = || get_filter_regex().replace_all(&e.inner.group_name, ""); 79 | 80 | let message = || e.inner.elements.to_string().replace('\n', "\\n"); 81 | 82 | if client.id() == e.inner.from_uin { 83 | info!( 84 | "{client} >> 群[{}({})]: {}", 85 | group_name(), 86 | group_id, 87 | message(), 88 | ); 89 | return; 90 | } 91 | 92 | let Some(group) = client.find_or_refresh_group(group_id).await else { 93 | cannot_find_group(group_id); 94 | error_more_info(&e); 95 | 96 | return; 97 | }; 98 | 99 | let sender = e.inner.from_uin; 100 | 101 | let member: Member; 102 | let holder: NamedMember; 103 | let nick = if sender == AnonymousMember::ID { 104 | let Some(info) = e.inner.elements.anonymous() else { 105 | error!("获取匿名信息失败. Raw event: {:?}", e.inner); 106 | return; 107 | }; 108 | 109 | member = Member::Anonymous(AnonymousMember::from(&group, info.into())); 110 | "匿名" 111 | } else { 112 | match group.try_refresh_member(sender).await { 113 | Ok(Some(m)) => { 114 | holder = m.clone(); 115 | member = Member::Named(m); 116 | holder.nickname() 117 | } 118 | Ok(None) => { 119 | warn!( 120 | "群成员({})信息获取失败, 或许成员已不在本群. RawEvent: {:?}", 121 | sender, e.inner 122 | ); 123 | return; 124 | } 125 | Err(err) => { 126 | error!( 127 | "获取群成员({})发生错误: {}. RawEvent: {:?}", 128 | sender, err, e.inner 129 | ); 130 | return; 131 | } 132 | } 133 | }; 134 | 135 | info!( 136 | "{}({}) >> 群[{}({})] >> {client}: {}", 137 | nick, 138 | sender, 139 | group_name(), 140 | group_id, 141 | message(), 142 | ); 143 | 144 | let base = GroupMessageEvent::from(group, member, e); 145 | Event::GroupMessage(base) 146 | } 147 | QEvent::FriendMessage(e) => { 148 | client = get_client!(e.client); 149 | 150 | let friend_id = e.inner.from_uin; 151 | 152 | let Some(friend) = client.find_or_refresh_friend_list(friend_id).await else { 153 | error!("无法找到好友: {}({})", e.inner.from_nick, friend_id); 154 | return; 155 | }; 156 | 157 | info!("{friend} >> {client}: {}", e.inner.elements,); 158 | 159 | let base = FriendMessageEvent::from(friend, e); 160 | 161 | Event::FriendMessage(base) 162 | } 163 | QEvent::NewFriend(e) => { 164 | client = get_client!(e.client); 165 | 166 | let f = Friend::from(&client, e.inner); 167 | client.cache_friend(f.clone()); 168 | 169 | info!("{f}已添加"); 170 | 171 | Event::NewFriend(NewFriendEvent::from(f)) 172 | } 173 | QEvent::DeleteFriend(e) => { 174 | client = get_client!(e.client); 175 | 176 | let id = e.inner.uin; 177 | let Some(f) = client.remove_friend_cache(id) else { 178 | return; 179 | }; 180 | 181 | info!("{f}已删除"); 182 | 183 | Event::DeleteFriend(DeleteFriendEvent::from(f)) 184 | } 185 | QEvent::FriendPoke(e) => { 186 | client = get_client!(e.client); 187 | let friend_id = e.inner.sender; 188 | 189 | let Some(f) = client.find_friend(friend_id) else { 190 | error!("寻找好友{friend_id}失败"); 191 | return; 192 | }; 193 | 194 | info!("{f}戳了戳{client}"); 195 | 196 | Event::FriendPoke(FriendPokeEvent::from(f)) 197 | } 198 | QEvent::GroupPoke(e) => { 199 | client = get_client!(e.client); 200 | let group_id = e.inner.group_code; 201 | let Some(group) = client.find_or_refresh_group(group_id).await else { 202 | cannot_find_group(group_id); 203 | error_more_info(&e); 204 | 205 | return; 206 | }; 207 | 208 | let sender = e.inner.sender; 209 | let Some(sender) = group.find_member(sender).await else { 210 | error!("无法找到群成员{sender}, Raw event: {:?}", e); 211 | return; 212 | }; 213 | 214 | let target = e.inner.sender; 215 | let Some(target) = group.find_member(target).await else { 216 | error!("无法找到群成员{target}, Raw event: {:?}", e); 217 | return; 218 | }; 219 | 220 | info!("{sender}戳了戳{target} >> {group} >> {client}"); 221 | 222 | Event::GroupPoke(GroupPokeEvent::from(group, sender, target)) 223 | } 224 | QEvent::GroupDisband(e) => { 225 | client = get_client!(e.client); 226 | 227 | let group_id = e.inner.group_code; 228 | let op_id = e.inner.operator_uin; 229 | 230 | if let Some(g) = client.find_or_refresh_group(group_id).await { 231 | let member = g.members_cache().get(&op_id).and_then(|r| r.to_owned()); 232 | 233 | let name = member 234 | .map(|n| n.card_name().to_owned()) 235 | .unwrap_or_else(|| op_id.to_string()); 236 | info!("群#{}({})解散, 操作人: {}", g.name(), g.id(), name); 237 | } else { 238 | info!("群({})解散, 操作人: {}", group_id, op_id); 239 | } 240 | 241 | client.remove_group_cache(e.inner.group_code); 242 | 243 | Event::Unknown(QEvent::GroupDisband(e).into()) 244 | } 245 | QEvent::NewMember(e) => { 246 | client = get_client!(e.client); 247 | let group_id = e.inner.group_code; 248 | let member_id = e.inner.member_uin; 249 | 250 | let Some(group) = client.find_or_refresh_group(group_id).await else { 251 | cannot_find_group(group_id); 252 | error_more_info(&e); 253 | 254 | return; 255 | }; 256 | 257 | if member_id != client.id() { 258 | let _member = group.try_refresh_member(member_id).await; 259 | } 260 | 261 | Event::Unknown(QEvent::NewMember(e).into()) 262 | } 263 | QEvent::GroupLeave(e) => { 264 | client = get_client!(e.client); 265 | let group_id = e.inner.group_code; 266 | let member = e.inner.member_uin; 267 | if member == client.id() { 268 | client.remove_group_cache(group_id); 269 | } else { 270 | let Some(group) = client.find_group(group_id) else { 271 | return; // already removed? 272 | }; 273 | 274 | group.remove_member_cache(member); 275 | } 276 | 277 | Event::Unknown(QEvent::GroupLeave(e).into()) 278 | } 279 | QEvent::KickedOffline(e) => { 280 | client = get_client!(e.client); 281 | 282 | info!("{}下线, Kicked: {:?}", client, e); 283 | 284 | global_status().remove_client(client.id()); 285 | 286 | Event::Unknown(QEvent::KickedOffline(e).into()) 287 | } 288 | QEvent::MSFOffline(e) => { 289 | client = get_client!(e.client); 290 | 291 | info!("{}下线, MSF: {:?}", client, e); 292 | 293 | global_status().remove_client(client.id()); 294 | 295 | Event::Unknown(QEvent::MSFOffline(e).into()) 296 | } 297 | or => { 298 | info!("Other event: {:?}", or); 299 | 300 | Event::Unknown(or.into()) 301 | } 302 | }; 303 | 304 | global_listener_runtime().spawn(async move { 305 | global_listener_worker().handle(&self_event).await; 306 | 307 | let _ = global_sender().send(self_event); 308 | }); 309 | } 310 | } 311 | 312 | fn cannot_find_group(group_id: i64) { 313 | error!("无法找到群({}), 这是一个Bug, 请报告此问题", group_id); 314 | } 315 | 316 | fn error_more_info(d: &D) { 317 | error!("额外信息: {:?}", d); 318 | } 319 | -------------------------------------------------------------------------------- /src/event/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, Ordering}; 2 | use std::sync::Arc; 3 | use std::time::Duration; 4 | 5 | use atri_ffi::ffi::FFIEvent; 6 | use atri_ffi::ManagedCloneable; 7 | use ricq::handler::QEvent; 8 | 9 | use crate::contact::friend::Friend; 10 | use crate::contact::group::Group; 11 | use crate::contact::member::{Member, NamedMember}; 12 | use crate::contact::{Contact, ContactSubject}; 13 | use crate::message::MessageChain; 14 | use crate::{Client, Listener}; 15 | 16 | pub mod custom; 17 | pub mod listener; 18 | 19 | #[derive(Clone)] 20 | pub enum Event { 21 | ClientLogin(ClientLoginEvent), 22 | GroupMessage(GroupMessageEvent), 23 | FriendMessage(FriendMessageEvent), 24 | NewFriend(NewFriendEvent), 25 | DeleteFriend(DeleteFriendEvent), 26 | FriendPoke(FriendPokeEvent), 27 | GroupPoke(GroupPokeEvent), 28 | Unknown(SharedEvent), 29 | } 30 | 31 | impl Event { 32 | pub fn into_ffi(self) -> FFIEvent { 33 | macro_rules! ffi_get { 34 | ($($e:ident => $t:expr);* $(;)?) => { 35 | match self { 36 | $( 37 | Self::$e(e) => ($t, &e.event.intercepted as *const AtomicBool, ManagedCloneable::from_value(e)), 38 | )* 39 | } 40 | }; 41 | } 42 | 43 | let (t, intercepted, base) = ffi_get! { 44 | ClientLogin => 0; 45 | GroupMessage => 1; 46 | FriendMessage => 2; 47 | NewFriend => 3; 48 | DeleteFriend => 4; 49 | FriendPoke => 5; 50 | GroupPoke => 6; 51 | Unknown => 255; 52 | }; 53 | 54 | FFIEvent::from(t, intercepted as _, base) 55 | } 56 | } 57 | 58 | macro_rules! event_impl { 59 | ($($variant:ident),* ;$name:ident: $ret:ty as $func:expr) => { 60 | impl Event { 61 | pub fn $name(&self) -> $ret { 62 | match self { 63 | $(Self::$variant(e) => { 64 | ($func)(e) 65 | })* 66 | } 67 | } 68 | } 69 | }; 70 | } 71 | 72 | macro_rules! event_fun_impl { 73 | ($($name:ident: $ret:ty as $func:expr);+ $(;)?) => { 74 | $( 75 | event_impl! { 76 | ClientLogin, 77 | GroupMessage, 78 | FriendMessage, 79 | NewFriend, 80 | DeleteFriend, 81 | FriendPoke, 82 | GroupPoke, 83 | Unknown; 84 | $name: $ret as $func 85 | } 86 | )* 87 | }; 88 | } 89 | 90 | event_fun_impl! { 91 | intercept: () as SharedEvent::intercept; 92 | is_intercepted: bool as SharedEvent::is_intercepted; 93 | } 94 | 95 | impl FromEvent for Event { 96 | fn from_event(e: Event) -> Option { 97 | Some(e) 98 | } 99 | } 100 | 101 | #[derive(Debug)] 102 | pub struct SharedEvent { 103 | event: Arc>, 104 | } 105 | 106 | impl SharedEvent { 107 | pub fn inner(&self) -> &T { 108 | &self.event.inner 109 | } 110 | } 111 | 112 | impl Clone for SharedEvent { 113 | fn clone(&self) -> Self { 114 | Self { 115 | event: self.event.clone(), 116 | } 117 | } 118 | } 119 | 120 | #[derive(Debug)] 121 | struct EventWithFlag { 122 | intercepted: AtomicBool, 123 | inner: T, 124 | } 125 | 126 | impl SharedEvent { 127 | fn new(event: T) -> Self { 128 | Self { 129 | event: EventWithFlag { 130 | intercepted: AtomicBool::new(false), 131 | inner: event, 132 | } 133 | .into(), 134 | } 135 | } 136 | 137 | pub fn intercept(&self) { 138 | self.event.intercepted.store(true, Ordering::Relaxed); 139 | } 140 | 141 | pub fn is_intercepted(&self) -> bool { 142 | self.event.intercepted.load(Ordering::Relaxed) 143 | } 144 | 145 | pub fn try_into_inner(self) -> Result { 146 | let e = Arc::try_unwrap(self.event); 147 | 148 | match e { 149 | Ok(e) => Ok(e.inner), 150 | Err(arc) => Err(Self { event: arc }), 151 | } 152 | } 153 | } 154 | 155 | pub type GroupMessageEvent = SharedEvent; 156 | 157 | impl GroupMessageEvent { 158 | pub fn group(&self) -> &Group { 159 | &self.inner().group 160 | } 161 | 162 | pub fn client(&self) -> Client { 163 | self.group().client() 164 | } 165 | 166 | pub fn sender(&self) -> &Member { 167 | &self.inner().sender 168 | } 169 | 170 | pub fn message(&self) -> &MessageChain { 171 | &self.inner().message 172 | } 173 | 174 | pub async fn next_event(&self, timeout: Duration, filter: F) -> Option 175 | where 176 | F: Fn(&GroupMessageEvent) -> bool, 177 | { 178 | let group_id = self.group().id(); 179 | let sender_id = self.sender().id(); 180 | 181 | if sender_id == crate::contact::member::AnonymousMember::ID { 182 | return None; 183 | } 184 | 185 | Listener::next_event(timeout, |e: &GroupMessageEvent| { 186 | if e.group().id() != group_id { 187 | return false; 188 | } 189 | 190 | if e.sender().id() != sender_id { 191 | return false; 192 | } 193 | 194 | filter(e) 195 | }) 196 | .await 197 | } 198 | 199 | pub async fn next_message(&self, timeout: Duration, filter: F) -> Option 200 | where 201 | F: Fn(&MessageChain) -> bool, 202 | { 203 | self.next_event(timeout, |e| filter(e.message())) 204 | .await 205 | .map(|e: GroupMessageEvent| match e.try_into_inner() { 206 | Ok(e) => e.message, 207 | Err(e) => e.message().clone(), 208 | }) 209 | } 210 | 211 | pub(crate) fn from( 212 | group: Group, 213 | sender: Member, 214 | ori: ricq::client::event::GroupMessageEvent, 215 | ) -> Self { 216 | Self::new(imp::GroupMessageEvent { 217 | group, 218 | sender, 219 | message: ori.inner.into(), 220 | }) 221 | } 222 | } 223 | 224 | impl ContactSubject for GroupMessageEvent { 225 | fn subject(&self) -> Contact { 226 | Contact::Group(self.group().clone()) 227 | } 228 | } 229 | 230 | impl FromEvent for GroupMessageEvent { 231 | fn from_event(e: Event) -> Option { 232 | if let Event::GroupMessage(e) = e { 233 | Some(e) 234 | } else { 235 | None 236 | } 237 | } 238 | } 239 | 240 | pub type FriendMessageEvent = SharedEvent; 241 | 242 | impl FriendMessageEvent { 243 | pub fn friend(&self) -> &Friend { 244 | &self.inner().friend 245 | } 246 | 247 | pub fn client(&self) -> Client { 248 | self.friend().client() 249 | } 250 | 251 | pub fn message(&self) -> &MessageChain { 252 | &self.inner().message 253 | } 254 | 255 | pub(crate) fn from(friend: Friend, ori: ricq::client::event::FriendMessageEvent) -> Self { 256 | let imp = imp::FriendMessageEvent { 257 | friend, 258 | message: ori.inner.into(), 259 | }; 260 | 261 | Self::new(imp) 262 | } 263 | } 264 | 265 | impl FromEvent for FriendMessageEvent { 266 | fn from_event(e: Event) -> Option { 267 | if let Event::FriendMessage(e) = e { 268 | Some(e) 269 | } else { 270 | None 271 | } 272 | } 273 | } 274 | 275 | impl ContactSubject for FriendMessageEvent { 276 | fn subject(&self) -> Contact { 277 | Contact::Friend(self.friend().clone()) 278 | } 279 | } 280 | 281 | pub type ClientLoginEvent = SharedEvent; 282 | 283 | impl ClientLoginEvent { 284 | pub fn client(&self) -> &Client { 285 | &self.inner().client 286 | } 287 | } 288 | 289 | impl ClientLoginEvent { 290 | pub(crate) fn from(client: Client) -> Self { 291 | Self::new(imp::ClientLoginEvent { client }) 292 | } 293 | } 294 | 295 | pub type NewFriendEvent = SharedEvent; 296 | 297 | impl NewFriendEvent { 298 | pub fn friend(&self) -> &Friend { 299 | &self.inner().friend 300 | } 301 | 302 | pub fn client(&self) -> Client { 303 | self.friend().client() 304 | } 305 | } 306 | 307 | impl NewFriendEvent { 308 | pub(crate) fn from(friend: Friend) -> Self { 309 | Self::new(imp::NewFriendEvent { friend }) 310 | } 311 | } 312 | 313 | pub type DeleteFriendEvent = SharedEvent; 314 | 315 | impl DeleteFriendEvent { 316 | pub fn friend(&self) -> &Friend { 317 | &self.inner().friend 318 | } 319 | 320 | pub fn client(&self) -> Client { 321 | self.friend().client() 322 | } 323 | } 324 | 325 | impl DeleteFriendEvent { 326 | pub(crate) fn from(friend: Friend) -> Self { 327 | Self::new(imp::DeleteFriendEvent { friend }) 328 | } 329 | } 330 | 331 | pub type FriendPokeEvent = SharedEvent; 332 | 333 | impl FriendPokeEvent { 334 | pub fn friend(&self) -> &Friend { 335 | &self.inner().friend 336 | } 337 | 338 | pub fn client(&self) -> Client { 339 | self.friend().client() 340 | } 341 | } 342 | 343 | impl FriendPokeEvent { 344 | pub(crate) fn from(friend: Friend) -> Self { 345 | Self::new(imp::FriendPokeEvent { friend }) 346 | } 347 | } 348 | 349 | pub type GroupPokeEvent = SharedEvent; 350 | 351 | impl GroupPokeEvent { 352 | pub fn group(&self) -> &Group { 353 | &self.inner().group 354 | } 355 | 356 | pub fn client(&self) -> Client { 357 | self.group().client() 358 | } 359 | 360 | pub fn sender(&self) -> &NamedMember { 361 | &self.inner().sender 362 | } 363 | 364 | pub fn target(&self) -> &NamedMember { 365 | &self.inner().target 366 | } 367 | } 368 | 369 | impl GroupPokeEvent { 370 | pub(crate) fn from(group: Group, sender: NamedMember, target: NamedMember) -> Self { 371 | Self::new(imp::GroupPokeEvent { 372 | group, 373 | sender, 374 | target, 375 | }) 376 | } 377 | } 378 | 379 | impl From for SharedEvent { 380 | fn from(value: QEvent) -> Self { 381 | Self::new(value) 382 | } 383 | } 384 | 385 | mod imp { 386 | 387 | use crate::contact::friend::Friend; 388 | use crate::contact::group::Group; 389 | use crate::contact::member::{Member, NamedMember}; 390 | use crate::message::MessageChain; 391 | use crate::Client; 392 | 393 | pub struct ClientLoginEvent { 394 | pub client: Client, 395 | } 396 | 397 | pub struct GroupMessageEvent { 398 | pub group: Group, 399 | pub sender: Member, 400 | pub message: MessageChain, 401 | } 402 | 403 | pub struct FriendMessageEvent { 404 | pub friend: Friend, 405 | pub message: MessageChain, 406 | } 407 | 408 | pub struct NewFriendEvent { 409 | pub friend: Friend, 410 | } 411 | 412 | pub struct DeleteFriendEvent { 413 | pub friend: Friend, // for information purpose 414 | } 415 | 416 | pub struct FriendPokeEvent { 417 | pub friend: Friend, 418 | } 419 | 420 | pub struct GroupPokeEvent { 421 | pub group: Group, 422 | pub sender: NamedMember, 423 | pub target: NamedMember, 424 | } 425 | } 426 | 427 | pub enum MessageEvent { 428 | Group(GroupMessageEvent), 429 | Friend(FriendMessageEvent), 430 | } 431 | 432 | impl FromEvent for MessageEvent { 433 | fn from_event(e: Event) -> Option { 434 | match e { 435 | Event::GroupMessage(e) => Some(Self::Group(e)), 436 | Event::FriendMessage(e) => Some(Self::Friend(e)), 437 | _ => None, 438 | } 439 | } 440 | } 441 | 442 | pub trait FromEvent: Sized { 443 | fn from_event(e: Event) -> Option; 444 | } 445 | --------------------------------------------------------------------------------