Skip to main content

weixin_agent/
types.rs

1//! Protocol types mirroring the Weixin iLink Bot API.
2
3use serde::{Deserialize, Serialize};
4use serde_repr::{Deserialize_repr, Serialize_repr};
5
6// ── Protocol constants ──────────────────────────────────────────────
7
8/// iLink-App-Id header value.
9pub const ILINK_APP_ID: &str = "bot";
10/// Channel version sent in `base_info`.
11pub const CHANNEL_VERSION: &str = "2.4.3";
12/// Fixed QR code base URL.
13pub const QR_CODE_BASE_URL: &str = "https://ilinkai.weixin.qq.com/";
14/// Default bot type for QR login.
15pub const DEFAULT_ILINK_BOT_TYPE: &str = "3";
16/// Session-expired error code from server.
17pub const SESSION_EXPIRED_ERRCODE: i32 = -14;
18/// Text chunk limit (characters).
19pub const TEXT_CHUNK_LIMIT: usize = 4000;
20
21// ── Timing constants (ms) ───────────────────────────────────────────
22
23/// Long-poll timeout.
24pub const DEFAULT_LONG_POLL_TIMEOUT_MS: u64 = 35_000;
25/// Regular API timeout.
26pub const DEFAULT_API_TIMEOUT_MS: u64 = 15_000;
27/// Config/typing API timeout.
28pub const DEFAULT_CONFIG_TIMEOUT_MS: u64 = 10_000;
29/// Session pause after expiry.
30pub const SESSION_PAUSE_DURATION_MS: u64 = 3_600_000;
31/// Max consecutive poll failures before backoff.
32pub const MAX_CONSECUTIVE_FAILURES: u32 = 3;
33/// Backoff delay after max failures.
34pub const BACKOFF_DELAY_MS: u64 = 30_000;
35/// Normal retry delay.
36pub const RETRY_DELAY_MS: u64 = 2_000;
37/// CDN upload max retries.
38pub const UPLOAD_MAX_RETRIES: u32 = 3;
39/// Config cache TTL.
40pub const CONFIG_CACHE_TTL_MS: u64 = 86_400_000;
41/// Max QR refresh count.
42pub const MAX_QR_REFRESH_COUNT: u32 = 3;
43/// QR poll timeout.
44pub const DEFAULT_QR_POLL_TIMEOUT_MS: u64 = 35_000;
45
46// ── Enums ───────────────────────────────────────────────────────────
47
48/// CDN upload media type.
49#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
50#[repr(u8)]
51pub enum UploadMediaType {
52    /// Image upload.
53    Image = 1,
54    /// Video upload.
55    Video = 2,
56    /// Generic file upload.
57    File = 3,
58    /// Voice upload.
59    Voice = 4,
60}
61
62/// Message sender type.
63#[derive(Debug, Clone, Copy, Default, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
64#[repr(u8)]
65pub enum MessageType {
66    /// Unset.
67    #[default]
68    None = 0,
69    /// From a human user.
70    User = 1,
71    /// From a bot.
72    Bot = 2,
73}
74
75/// Message item content type.
76#[derive(Debug, Clone, Copy, Default, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
77#[repr(u8)]
78pub enum MessageItemType {
79    /// Unset.
80    #[default]
81    None = 0,
82    /// Text content.
83    Text = 1,
84    /// Image content.
85    Image = 2,
86    /// Voice content.
87    Voice = 3,
88    /// File attachment.
89    File = 4,
90    /// Video content.
91    Video = 5,
92}
93
94/// Message generation state.
95#[derive(Debug, Clone, Copy, Default, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
96#[repr(u8)]
97pub enum MessageState {
98    /// New / finished.
99    #[default]
100    New = 0,
101    /// Still generating (streaming).
102    Generating = 1,
103    /// Generation complete.
104    Finish = 2,
105}
106
107/// Typing indicator status.
108#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
109#[repr(u8)]
110pub enum TypingStatus {
111    /// Currently typing.
112    Typing = 1,
113    /// Cancel typing indicator.
114    Cancel = 2,
115}
116
117/// High-level media type for inbound messages.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum MediaType {
120    /// Image media.
121    Image,
122    /// Video media.
123    Video,
124    /// Voice media.
125    Voice,
126    /// Generic file.
127    File,
128}
129
130// ── BaseInfo ────────────────────────────────────────────────────────
131
132/// Metadata attached to every outgoing API request.
133#[derive(Debug, Clone, Default, Serialize, Deserialize)]
134pub struct BaseInfo {
135    /// Channel version string.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub channel_version: Option<String>,
138    /// Bot agent UA string.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub bot_agent: Option<String>,
141}
142
143/// Build a `BaseInfo` with the current channel version and bot agent.
144pub fn build_base_info_with_agent(bot_agent: &str) -> BaseInfo {
145    BaseInfo {
146        channel_version: Some(CHANNEL_VERSION.to_owned()),
147        bot_agent: Some(bot_agent.to_owned()),
148    }
149}
150
151/// Build a `BaseInfo` with the current channel version (legacy, prefer `build_base_info_with_agent`).
152pub fn build_base_info() -> BaseInfo {
153    build_base_info_with_agent("weixin-agent-rs")
154}
155
156// ── CDN / Media sub-structures ──────────────────────────────────────
157
158/// CDN media reference.
159#[derive(Debug, Clone, Default, Serialize, Deserialize)]
160pub struct CdnMedia {
161    /// Encrypted query parameter for CDN download.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub encrypt_query_param: Option<String>,
164    /// AES key (base64-encoded).
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub aes_key: Option<String>,
167    /// Encrypt type: 0 = fileid only, 1 = packed.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub encrypt_type: Option<i32>,
170    /// Full download URL from server.
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub full_url: Option<String>,
173}
174
175/// Text item.
176#[derive(Debug, Clone, Default, Serialize, Deserialize)]
177pub struct TextItem {
178    /// Text content.
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub text: Option<String>,
181}
182
183/// Image item.
184#[derive(Debug, Clone, Default, Serialize, Deserialize)]
185pub struct ImageItem {
186    /// Original image CDN reference.
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub media: Option<CdnMedia>,
189    /// Thumbnail CDN reference.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub thumb_media: Option<CdnMedia>,
192    /// Raw AES key as hex string (preferred for inbound decryption).
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub aeskey: Option<String>,
195    /// Image URL.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub url: Option<String>,
198    /// Mid-size ciphertext bytes.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub mid_size: Option<i64>,
201    /// Thumbnail size.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub thumb_size: Option<i64>,
204    /// Thumbnail height.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub thumb_height: Option<i64>,
207    /// Thumbnail width.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub thumb_width: Option<i64>,
210    /// HD size.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub hd_size: Option<i64>,
213}
214
215/// Voice item.
216#[derive(Debug, Clone, Default, Serialize, Deserialize)]
217pub struct VoiceItem {
218    /// Voice CDN reference.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub media: Option<CdnMedia>,
221    /// Encoding type.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub encode_type: Option<i32>,
224    /// Bits per sample.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub bits_per_sample: Option<i32>,
227    /// Sample rate (Hz).
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub sample_rate: Option<i32>,
230    /// Duration in milliseconds.
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub playtime: Option<i64>,
233    /// Speech-to-text result.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub text: Option<String>,
236}
237
238/// File item.
239#[derive(Debug, Clone, Default, Serialize, Deserialize)]
240pub struct FileItem {
241    /// File CDN reference.
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub media: Option<CdnMedia>,
244    /// Original file name.
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub file_name: Option<String>,
247    /// File MD5.
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub md5: Option<String>,
250    /// Plaintext file size as string.
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub len: Option<String>,
253}
254
255/// Video item.
256#[derive(Debug, Clone, Default, Serialize, Deserialize)]
257pub struct VideoItem {
258    /// Video CDN reference.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub media: Option<CdnMedia>,
261    /// Video ciphertext size.
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub video_size: Option<i64>,
264    /// Play length in seconds.
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub play_length: Option<i64>,
267    /// Video MD5.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub video_md5: Option<String>,
270    /// Thumbnail CDN reference.
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub thumb_media: Option<CdnMedia>,
273    /// Thumbnail size.
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub thumb_size: Option<i64>,
276    /// Thumbnail height.
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub thumb_height: Option<i64>,
279    /// Thumbnail width.
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub thumb_width: Option<i64>,
282}
283
284/// Reference (quoted) message.
285#[derive(Debug, Clone, Default, Serialize, Deserialize)]
286pub struct RefMessage {
287    /// Quoted message item.
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub message_item: Option<Box<MessageItem>>,
290    /// Summary title.
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub title: Option<String>,
293}
294
295/// A single content item within a message.
296#[derive(Debug, Clone, Default, Serialize, Deserialize)]
297pub struct MessageItem {
298    /// Item type.
299    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
300    pub item_type: Option<MessageItemType>,
301    /// Creation timestamp (ms).
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub create_time_ms: Option<i64>,
304    /// Update timestamp (ms).
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub update_time_ms: Option<i64>,
307    /// Whether generation is complete.
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub is_completed: Option<bool>,
310    /// Item-level message ID.
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub msg_id: Option<String>,
313    /// Referenced (quoted) message.
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub ref_msg: Option<RefMessage>,
316    /// Text content.
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub text_item: Option<TextItem>,
319    /// Image content.
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub image_item: Option<ImageItem>,
322    /// Voice content.
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub voice_item: Option<VoiceItem>,
325    /// File content.
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub file_item: Option<FileItem>,
328    /// Video content.
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub video_item: Option<VideoItem>,
331}
332
333// ── WeixinMessage ───────────────────────────────────────────────────
334
335/// Unified message from `getUpdates`.
336#[derive(Debug, Clone, Default, Serialize, Deserialize)]
337pub struct WeixinMessage {
338    /// Sequence number.
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub seq: Option<i64>,
341    /// Server-assigned message ID.
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub message_id: Option<i64>,
344    /// Sender user ID.
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub from_user_id: Option<String>,
347    /// Recipient user ID.
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub to_user_id: Option<String>,
350    /// Client-generated message ID.
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub client_id: Option<String>,
353    /// Creation timestamp (ms).
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub create_time_ms: Option<i64>,
356    /// Update timestamp (ms).
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub update_time_ms: Option<i64>,
359    /// Deletion timestamp (ms); >0 means recalled.
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub delete_time_ms: Option<i64>,
362    /// Session ID.
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub session_id: Option<String>,
365    /// Group ID.
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub group_id: Option<String>,
368    /// Sender type (user / bot).
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub message_type: Option<MessageType>,
371    /// Generation state.
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub message_state: Option<MessageState>,
374    /// Content items.
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub item_list: Option<Vec<MessageItem>>,
377    /// Context token for replies.
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub context_token: Option<String>,
380}
381
382// ── API request / response types ────────────────────────────────────
383
384/// `getUpdates` request body.
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct GetUpdatesRequest {
387    /// Full context buf from previous response.
388    pub get_updates_buf: String,
389    /// Metadata.
390    pub base_info: BaseInfo,
391}
392
393/// `getUpdates` response body.
394#[derive(Debug, Clone, Default, Serialize, Deserialize)]
395pub struct GetUpdatesResponse {
396    /// Return code (0 = success).
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub ret: Option<i32>,
399    /// Error code.
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub errcode: Option<i32>,
402    /// Error message.
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub errmsg: Option<String>,
405    /// Inbound messages.
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub msgs: Option<Vec<WeixinMessage>>,
408    /// Legacy sync buf (compat).
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub sync_buf: Option<String>,
411    /// New context buf to cache.
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub get_updates_buf: Option<String>,
414    /// Server-suggested next poll timeout (ms).
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub longpolling_timeout_ms: Option<u64>,
417}
418
419/// `sendMessage` request body.
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct SendMessageRequest {
422    /// The message to send.
423    pub msg: WeixinMessage,
424    /// Metadata.
425    pub base_info: BaseInfo,
426}
427
428/// `getUploadUrl` request body.
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct GetUploadUrlRequest {
431    /// Random file key (32 hex chars).
432    pub filekey: String,
433    /// Upload media type.
434    pub media_type: UploadMediaType,
435    /// Recipient user ID.
436    pub to_user_id: String,
437    /// Plaintext file size.
438    pub rawsize: u64,
439    /// Plaintext file MD5 hex.
440    pub rawfilemd5: String,
441    /// Ciphertext file size.
442    pub filesize: u64,
443    /// Whether thumbnail is not needed.
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub no_need_thumb: Option<bool>,
446    /// Thumbnail plaintext size.
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub thumb_rawsize: Option<u64>,
449    /// Thumbnail plaintext MD5 hex.
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub thumb_rawfilemd5: Option<String>,
452    /// Thumbnail ciphertext size.
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub thumb_filesize: Option<u64>,
455    /// AES key hex string.
456    pub aeskey: String,
457    /// Metadata.
458    pub base_info: BaseInfo,
459}
460
461/// `getUploadUrl` response body.
462#[derive(Debug, Clone, Default, Serialize, Deserialize)]
463pub struct GetUploadUrlResponse {
464    /// Upload encrypted parameter.
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub upload_param: Option<String>,
467    /// Thumbnail upload parameter.
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub thumb_upload_param: Option<String>,
470    /// Full upload URL from server.
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub upload_full_url: Option<String>,
473}
474
475/// `getConfig` request body (internal).
476#[derive(Debug, Clone, Serialize)]
477pub(crate) struct GetConfigRequest {
478    /// User ID to get config for.
479    pub ilink_user_id: String,
480    /// Optional context token.
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub context_token: Option<String>,
483    /// Metadata.
484    pub base_info: BaseInfo,
485}
486
487/// `getConfig` response body.
488#[derive(Debug, Clone, Default, Serialize, Deserialize)]
489pub struct GetConfigResponse {
490    /// Return code.
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub ret: Option<i32>,
493    /// Error message.
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub errmsg: Option<String>,
496    /// Typing ticket (base64).
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub typing_ticket: Option<String>,
499}
500
501/// `sendTyping` request body.
502#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct SendTypingRequest {
504    /// Target user ID.
505    pub ilink_user_id: String,
506    /// Typing ticket from `getConfig`.
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub typing_ticket: Option<String>,
509    /// Typing status.
510    pub status: TypingStatus,
511    /// Metadata.
512    pub base_info: BaseInfo,
513}
514
515// ── QR login types ──────────────────────────────────────────────────
516
517/// QR code response from server.
518#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct QrCodeResponse {
520    /// QR code token string.
521    pub qrcode: String,
522    /// QR code image URL.
523    pub qrcode_img_content: String,
524}
525
526/// QR status response from server.
527#[derive(Debug, Clone, Default, Serialize, Deserialize)]
528pub struct QrStatusResponse {
529    /// Current status.
530    pub status: String,
531    /// Bot token (on confirmed).
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub bot_token: Option<String>,
534    /// Bot ID (on confirmed).
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub ilink_bot_id: Option<String>,
537    /// Base URL (on confirmed).
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub baseurl: Option<String>,
540    /// User ID who scanned.
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub ilink_user_id: Option<String>,
543    /// Redirect host for IDC redirect.
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub redirect_host: Option<String>,
546}