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