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