//! ## 微信 iLink Bot API 协议类型 //! //! 定义了与微信 iLink Bot API 通信的全部数据结构: //! //! - `BaseInfo` — 公共请求元数据 //! - `WeixinMessage` — 微信消息结构(支持 text/image/voice/file/video) //! - `MessageItem` — 消息条目(5 种类型) //! - `GetUpdatesReq/Resp` — 长轮询收消息的请求/响应 //! - `SendMessageReq` — 发送消息请求 //! - `QRCodeResponse` / `StatusResponse` — 扫码登录相关 //! - `LoginResult` / `MessageEvent` — 业务层结果类型 use serde::{Deserialize, Serialize}; // ─── 基础类型 ─── /// 公共请求元数据 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct BaseInfo { #[serde(skip_serializing_if = "Option::is_none")] pub channel_version: Option, } impl BaseInfo { pub fn new() -> Self { Self { channel_version: Some(env!("CARGO_PKG_VERSION").to_string()), } } } // ─── 消息类型 ─── #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TextItem { #[serde(skip_serializing_if = "Option::is_none")] pub text: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CDNMedia { #[serde(skip_serializing_if = "Option::is_none")] pub encrypt_query_param: Option, #[serde(skip_serializing_if = "Option::is_none")] pub aes_key: Option, #[serde(skip_serializing_if = "Option::is_none")] pub encrypt_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub full_url: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImageItem { #[serde(skip_serializing_if = "Option::is_none")] pub media: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thumb_media: Option, #[serde(skip_serializing_if = "Option::is_none")] pub aeskey: Option, #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mid_size: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thumb_size: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thumb_height: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thumb_width: Option, #[serde(skip_serializing_if = "Option::is_none")] pub hd_size: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VoiceItem { #[serde(skip_serializing_if = "Option::is_none")] pub media: Option, #[serde(skip_serializing_if = "Option::is_none")] pub encode_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bits_per_sample: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sample_rate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playtime: Option, #[serde(skip_serializing_if = "Option::is_none")] pub text: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileItem { #[serde(skip_serializing_if = "Option::is_none")] pub media: Option, #[serde(skip_serializing_if = "Option::is_none")] pub file_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub md5: Option, #[serde(skip_serializing_if = "Option::is_none")] pub len: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VideoItem { #[serde(skip_serializing_if = "Option::is_none")] pub media: Option, #[serde(skip_serializing_if = "Option::is_none")] pub video_size: Option, #[serde(skip_serializing_if = "Option::is_none")] pub play_length: Option, #[serde(skip_serializing_if = "Option::is_none")] pub video_md5: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thumb_media: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thumb_size: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thumb_height: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thumb_width: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RefMessage { #[serde(skip_serializing_if = "Option::is_none")] pub message_item: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, } /// ## 消息条目 —— 消息的具体内容 /// /// 支持 5 种消息类型:text(1), image(2), voice(3), file(4), video(5)。 /// 每种类型有对应的 `*_item` 字段,其他字段保持 None。 /// /// `text()` 便捷方法用于快速构造文本消息(发送消息时使用)。 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MessageItem { #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub item_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub create_time_ms: Option, #[serde(skip_serializing_if = "Option::is_none")] pub update_time_ms: Option, #[serde(skip_serializing_if = "Option::is_none")] pub is_completed: Option, #[serde(skip_serializing_if = "Option::is_none")] pub msg_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ref_msg: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub text_item: Option, #[serde(skip_serializing_if = "Option::is_none")] pub image_item: Option, #[serde(skip_serializing_if = "Option::is_none")] pub voice_item: Option, #[serde(skip_serializing_if = "Option::is_none")] pub file_item: Option, #[serde(skip_serializing_if = "Option::is_none")] pub video_item: Option, } impl MessageItem { pub const TYPE_TEXT: i32 = 1; #[allow(dead_code)] pub const TYPE_IMAGE: i32 = 2; #[allow(dead_code)] pub const TYPE_VOICE: i32 = 3; #[allow(dead_code)] pub const TYPE_FILE: i32 = 4; #[allow(dead_code)] pub const TYPE_VIDEO: i32 = 5; pub fn text(text: &str) -> Self { Self { item_type: Some(Self::TYPE_TEXT), text_item: Some(TextItem { text: Some(text.to_string()), }), ..Default::default() } } } impl Default for MessageItem { fn default() -> Self { Self { item_type: None, create_time_ms: None, update_time_ms: None, is_completed: None, msg_id: None, ref_msg: None, text_item: None, image_item: None, voice_item: None, file_item: None, video_item: None, } } } /// ## 微信消息结构 /// /// 对应 iLink API 返回的单个消息。所有字段都是 Option 的, /// 因为不同消息类型携带不同字段。 /// /// ### 关键字段 /// * `msg_type` — 消息类型:1=用户消息(TYPE_USER), 2=机器人消息(TYPE_BOT) /// * `from_user_id` — 发送者微信 ID /// * `to_user_id` — 接收者微信 ID /// * `item_list` — 消息内容列表(支持 text/image/voice/file/video) /// * `context_token` — 微信上下文令牌(用于多轮对话的上下文恢复) /// * `group_id` — 群聊 ID(群消息时非空) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WeixinMessage { #[serde(skip_serializing_if = "Option::is_none")] pub seq: Option, #[serde(skip_serializing_if = "Option::is_none")] pub message_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub from_user_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub to_user_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub client_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub create_time_ms: Option, #[serde(skip_serializing_if = "Option::is_none")] pub update_time_ms: Option, #[serde(skip_serializing_if = "Option::is_none")] pub delete_time_ms: Option, #[serde(skip_serializing_if = "Option::is_none")] pub session_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub group_id: Option, #[serde(rename = "message_type", skip_serializing_if = "Option::is_none")] pub msg_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub message_state: Option, #[serde(skip_serializing_if = "Option::is_none")] pub item_list: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub context_token: Option, } impl Default for WeixinMessage { fn default() -> Self { Self { seq: None, message_id: None, from_user_id: None, to_user_id: None, client_id: None, create_time_ms: None, update_time_ms: None, delete_time_ms: None, session_id: None, group_id: None, msg_type: None, message_state: None, item_list: None, context_token: None, } } } impl WeixinMessage { #[allow(dead_code)] pub const TYPE_NONE: i32 = 0; pub const TYPE_USER: i32 = 1; pub const TYPE_BOT: i32 = 2; /// 获取文本内容(如果有纯文本 item) pub fn text_content(&self) -> Option<&str> { self.item_list .as_ref()? .iter() .find(|item| item.item_type == Some(MessageItem::TYPE_TEXT)) .and_then(|item| item.text_item.as_ref()) .and_then(|t| t.text.as_deref()) } } // ─── API 请求/响应 ─── /// 获取二维码响应 #[derive(Debug, Deserialize)] pub struct QRCodeResponse { pub qrcode: String, pub qrcode_img_content: String, } /// 扫码状态响应 #[derive(Debug, Deserialize)] pub struct StatusResponse { pub status: String, #[serde(skip_serializing_if = "Option::is_none")] pub bot_token: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ilink_bot_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub baseurl: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ilink_user_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub redirect_host: Option, } /// GetUpdates 请求 #[derive(Debug, Serialize)] pub struct GetUpdatesReq { pub get_updates_buf: String, pub base_info: BaseInfo, } /// GetUpdates 响应 #[derive(Debug, Deserialize)] pub struct GetUpdatesResp { #[serde(default)] pub ret: i32, #[serde(skip_serializing_if = "Option::is_none")] pub errcode: Option, #[serde(skip_serializing_if = "Option::is_none")] pub errmsg: Option, #[serde(default)] pub msgs: Vec, #[serde(default)] pub get_updates_buf: String, #[serde(skip_serializing_if = "Option::is_none")] #[allow(dead_code)] pub longpolling_timeout_ms: Option, } /// SendMessage 请求 #[derive(Debug, Serialize)] pub struct SendMessageReq { pub msg: WeixinMessage, } /// NotifyStart/NotifyStop 响应 #[derive(Debug, Deserialize)] pub struct NotifyResp { #[serde(default)] pub ret: i32, #[serde(skip_serializing_if = "Option::is_none")] pub errmsg: Option, } /// 登录结果 #[derive(Debug, Clone)] pub struct LoginResult { pub token: String, pub account_id: String, pub base_url: String, #[allow(dead_code)] pub user_id: String, } /// 收到的消息事件 #[derive(Debug, Clone)] #[allow(dead_code)] pub struct MessageEvent { pub from_user_id: String, pub text: String, pub context_token: Option, pub message_id: i64, }