Skip to main content

wechat_oa_sdk/api/
message.rs

1use quick_xml::de::from_str;
2use serde::Deserialize;
3
4use crate::client::WeChatClient;
5use crate::crypto::{
6    check_msg_signature, check_signature, compute_msg_signature, decode_aes_key, decrypt_message,
7    encrypt_message, generate_encrypted_xml, generate_nonce,
8};
9use crate::error::{Result, WeChatError};
10use crate::models::event::*;
11use crate::models::message::*;
12
13/// Incoming message/event from WeChat.
14#[derive(Debug, Clone)]
15pub enum IncomingMessage {
16    Text(TextMessage),
17    Image(ImageMessage),
18    Voice(VoiceMessage),
19    Video(VideoMessage),
20    ShortVideo(ShortVideoMessage),
21    Location(LocationMessage),
22    Link(LinkMessage),
23    SubscribeEvent(SubscribeEvent),
24    UnsubscribeEvent(SubscribeEvent),
25    ScanEvent(ScanEvent),
26    LocationEvent(LocationEvent),
27    MenuClickEvent(MenuClickEvent),
28    MenuViewEvent(MenuViewEvent),
29    TemplateSendJobFinishEvent(TemplateSendJobFinishEvent),
30    Unknown(String),
31}
32
33impl WeChatClient {
34    /// Verify the callback signature from WeChat server.
35    ///
36    /// This should be called when WeChat sends a verification request
37    /// (GET request with signature, timestamp, nonce, echostr).
38    ///
39    /// Returns `true` if the signature is valid.
40    pub fn verify_signature(&self, signature: &str, timestamp: &str, nonce: &str) -> bool {
41        check_signature(&self.config.token, signature, timestamp, nonce)
42    }
43
44    /// Parse an incoming message or event from WeChat.
45    ///
46    /// The `xml_body` is the raw XML POST body from WeChat callback.
47    pub fn parse_message(&self, xml_body: &str) -> Result<IncomingMessage> {
48        // First, peek at the MsgType
49        let peek: MsgTypePeek = from_str(xml_body)?;
50        let msg_type = peek.msg_type.to_lowercase();
51
52        let message = match msg_type.as_str() {
53            "text" => IncomingMessage::Text(from_str(xml_body)?),
54            "image" => IncomingMessage::Image(from_str(xml_body)?),
55            "voice" => IncomingMessage::Voice(from_str(xml_body)?),
56            "video" => IncomingMessage::Video(from_str(xml_body)?),
57            "shortvideo" => IncomingMessage::ShortVideo(from_str(xml_body)?),
58            "location" => IncomingMessage::Location(from_str(xml_body)?),
59            "link" => IncomingMessage::Link(from_str(xml_body)?),
60            "event" => self.parse_event(xml_body)?,
61            _ => IncomingMessage::Unknown(xml_body.to_string()),
62        };
63
64        Ok(message)
65    }
66
67    fn parse_event(&self, xml_body: &str) -> Result<IncomingMessage> {
68        let peek: EventTypePeek = from_str(xml_body)?;
69        let event_type = peek.event.to_uppercase();
70
71        let event = match event_type.as_str() {
72            "SUBSCRIBE" => IncomingMessage::SubscribeEvent(from_str(xml_body)?),
73            "UNSUBSCRIBE" => IncomingMessage::UnsubscribeEvent(from_str(xml_body)?),
74            "SCAN" => IncomingMessage::ScanEvent(from_str(xml_body)?),
75            "LOCATION" => IncomingMessage::LocationEvent(from_str(xml_body)?),
76            "CLICK" => IncomingMessage::MenuClickEvent(from_str(xml_body)?),
77            "VIEW" => IncomingMessage::MenuViewEvent(from_str(xml_body)?),
78            "TEMPLATESENDJOBFINISH" => {
79                IncomingMessage::TemplateSendJobFinishEvent(from_str(xml_body)?)
80            }
81            _ => IncomingMessage::Unknown(xml_body.to_string()),
82        };
83
84        Ok(event)
85    }
86
87    // ==================== Encrypted Message Handling ====================
88
89    /// Get the decoded AES key from config.
90    ///
91    /// Returns an error if `encoding_aes_key` is not configured.
92    fn get_aes_key(&self) -> Result<[u8; 32]> {
93        let encoding_aes_key = self
94            .config
95            .encoding_aes_key
96            .as_ref()
97            .ok_or(WeChatError::AesKeyNotConfigured)?;
98        decode_aes_key(encoding_aes_key)
99    }
100
101    /// Verify the encrypted message signature.
102    ///
103    /// Used to verify incoming encrypted messages from WeChat.
104    pub fn verify_msg_signature(
105        &self,
106        msg_signature: &str,
107        timestamp: &str,
108        nonce: &str,
109        encrypt_msg: &str,
110    ) -> bool {
111        check_msg_signature(
112            &self.config.token,
113            msg_signature,
114            timestamp,
115            nonce,
116            encrypt_msg,
117        )
118    }
119
120    /// Parse an encrypted incoming message or event from WeChat.
121    ///
122    /// This should be used when your server is configured in "safe mode" or "compatible mode".
123    ///
124    /// # Arguments
125    /// - `xml_body`: The raw XML POST body containing the encrypted message
126    /// - `msg_signature`: The signature from query parameter
127    /// - `timestamp`: The timestamp from query parameter
128    /// - `nonce`: The nonce from query parameter
129    ///
130    /// # Example
131    /// ```ignore
132    /// let msg = client.parse_encrypted_message(
133    ///     &xml_body,
134    ///     &query.msg_signature,
135    ///     &query.timestamp,
136    ///     &query.nonce,
137    /// )?;
138    /// ```
139    pub fn parse_encrypted_message(
140        &self,
141        xml_body: &str,
142        msg_signature: &str,
143        timestamp: &str,
144        nonce: &str,
145    ) -> Result<IncomingMessage> {
146        // Parse the encrypted XML to get the Encrypt field
147        let encrypted_xml: EncryptedXml = from_str(xml_body)?;
148
149        // Verify the signature
150        if !self.verify_msg_signature(msg_signature, timestamp, nonce, &encrypted_xml.encrypt) {
151            return Err(WeChatError::InvalidSignature);
152        }
153
154        // Decrypt the message
155        let aes_key = self.get_aes_key()?;
156        let (decrypted_xml, app_id) = decrypt_message(&aes_key, &encrypted_xml.encrypt)?;
157
158        // Verify AppID matches
159        if app_id != self.config.app_id {
160            return Err(WeChatError::AppIdMismatch);
161        }
162
163        // Parse the decrypted XML as a regular message
164        self.parse_message(&decrypted_xml)
165    }
166
167    /// Decrypt the echostr for server verification in encrypted mode.
168    ///
169    /// When WeChat verifies your server URL in encrypted mode, it sends an encrypted echostr.
170    /// You need to decrypt it and return the decrypted content.
171    ///
172    /// # Arguments
173    /// - `encrypted_echostr`: The encrypted echostr from query parameter
174    ///
175    /// # Returns
176    /// The decrypted echostr that should be returned to WeChat.
177    pub fn decrypt_echostr(&self, encrypted_echostr: &str) -> Result<String> {
178        let aes_key = self.get_aes_key()?;
179        let (decrypted, _app_id) = decrypt_message(&aes_key, encrypted_echostr)?;
180        Ok(decrypted)
181    }
182
183    /// Encrypt a reply XML for sending back to WeChat.
184    ///
185    /// This should be used when your server is configured in "safe mode".
186    ///
187    /// # Arguments
188    /// - `reply_xml`: The plain reply XML (e.g., from `TextReply::to_xml()`)
189    /// - `timestamp`: The timestamp to use (can be current time or from the original request)
190    /// - `nonce`: The nonce to use (can generate a new one or use from the original request)
191    ///
192    /// # Returns
193    /// The encrypted XML that should be returned to WeChat.
194    ///
195    /// # Example
196    /// ```ignore
197    /// let reply = TextReply::new(&to_user, &from_user, "Hello!");
198    /// let encrypted_xml = client.encrypt_reply(&reply.to_xml(), &timestamp, &nonce)?;
199    /// ```
200    pub fn encrypt_reply(&self, reply_xml: &str, timestamp: &str, nonce: &str) -> Result<String> {
201        let aes_key = self.get_aes_key()?;
202
203        // Encrypt the reply XML
204        let encrypted = encrypt_message(&aes_key, &self.config.app_id, reply_xml)?;
205
206        // Generate the signature
207        let signature = compute_msg_signature(&self.config.token, timestamp, nonce, &encrypted);
208
209        // Generate the final encrypted XML
210        Ok(generate_encrypted_xml(
211            &encrypted, &signature, timestamp, nonce,
212        ))
213    }
214
215    /// Encrypt a reply XML with auto-generated timestamp and nonce.
216    ///
217    /// Convenience method that generates timestamp and nonce automatically.
218    pub fn encrypt_reply_auto(&self, reply_xml: &str) -> Result<String> {
219        let timestamp = std::time::SystemTime::now()
220            .duration_since(std::time::UNIX_EPOCH)
221            .unwrap()
222            .as_secs()
223            .to_string();
224        let nonce = generate_nonce();
225        self.encrypt_reply(reply_xml, &timestamp, &nonce)
226    }
227}
228
229/// Encrypted message XML structure.
230#[derive(Debug, Deserialize)]
231#[serde(rename_all = "PascalCase")]
232struct EncryptedXml {
233    encrypt: String,
234}