whatsapp_rust/features/
mex.rs1use crate::client::Client;
2use crate::jid_utils::server_jid;
3use crate::request::InfoQuery;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use thiserror::Error;
7use wacore_binary::builder::NodeBuilder;
8use wacore_binary::node::{Node, NodeContent};
9
10#[derive(Debug, Error)]
11pub enum MexError {
12 #[error("MEX payload parsing error: {0}")]
13 PayloadParsing(String),
14
15 #[error("MEX extension error: code={code}, message='{message}'")]
16 ExtensionError { code: i32, message: String },
17
18 #[error("IQ request failed: {0}")]
19 Request(#[from] Box<crate::request::IqError>),
20
21 #[error("JSON error: {0}")]
22 Json(#[from] serde_json::Error),
23}
24
25#[derive(Debug, Clone)]
26pub struct MexRequest<'a> {
27 pub doc_id: &'a str,
28
29 pub variables: Value,
30}
31
32#[derive(Serialize)]
33struct MexPayload<'a> {
34 variables: &'a Value,
35}
36
37#[derive(Debug, Clone, Deserialize)]
38pub struct MexResponse {
39 pub data: Option<Value>,
40
41 pub errors: Option<Vec<MexGraphQLError>>,
42}
43
44impl MexResponse {
45 #[inline]
46 pub fn has_data(&self) -> bool {
47 self.data.is_some()
48 }
49
50 #[inline]
51 pub fn has_errors(&self) -> bool {
52 self.errors.as_ref().is_some_and(|e| !e.is_empty())
53 }
54
55 pub fn fatal_error(&self) -> Option<&MexGraphQLError> {
56 self.errors.as_ref()?.iter().find(|e| {
57 e.extensions
58 .as_ref()
59 .is_some_and(|ext| ext.is_summary == Some(true))
60 })
61 }
62}
63
64#[derive(Debug, Clone, Deserialize)]
65pub struct MexGraphQLError {
66 pub message: String,
67
68 pub extensions: Option<MexErrorExtensions>,
69}
70
71impl MexGraphQLError {
72 #[inline]
73 pub fn error_code(&self) -> Option<i32> {
74 self.extensions.as_ref()?.error_code
75 }
76
77 #[inline]
78 pub fn is_fatal(&self) -> bool {
79 self.extensions
80 .as_ref()
81 .is_some_and(|ext| ext.is_summary == Some(true))
82 }
83}
84
85#[derive(Debug, Clone, Deserialize)]
86pub struct MexErrorExtensions {
87 pub error_code: Option<i32>,
88
89 pub is_summary: Option<bool>,
90
91 #[serde(default)]
92 pub is_retryable: Option<bool>,
93
94 pub severity: Option<String>,
95}
96
97pub struct Mex<'a> {
98 client: &'a Client,
99}
100
101impl<'a> Mex<'a> {
102 pub(crate) fn new(client: &'a Client) -> Self {
103 Self { client }
104 }
105
106 #[inline]
107 pub async fn query(&self, request: MexRequest<'_>) -> Result<MexResponse, MexError> {
108 self.execute(request).await
109 }
110
111 #[inline]
112 pub async fn mutate(&self, request: MexRequest<'_>) -> Result<MexResponse, MexError> {
113 self.execute(request).await
114 }
115
116 async fn execute(&self, request: MexRequest<'_>) -> Result<MexResponse, MexError> {
117 let payload = MexPayload {
118 variables: &request.variables,
119 };
120 let payload_bytes = serde_json::to_vec(&payload)?;
121
122 let query_node = NodeBuilder::new("query")
123 .attr("query_id", request.doc_id)
124 .bytes(payload_bytes)
125 .build();
126
127 let iq = InfoQuery::get(
128 "w:mex",
129 server_jid(),
130 Some(NodeContent::Nodes(vec![query_node])),
131 );
132
133 let response_node = self.client.send_iq(iq).await.map_err(Box::new)?;
134
135 Self::parse_response(&response_node)
136 }
137
138 fn parse_response(node: &Node) -> Result<MexResponse, MexError> {
139 let result_node = node
140 .get_optional_child("result")
141 .ok_or_else(|| MexError::PayloadParsing("Missing <result> node".into()))?;
142
143 let result_bytes = match &result_node.content {
144 Some(NodeContent::Bytes(bytes)) => bytes,
145 _ => return Err(MexError::PayloadParsing("Result not binary".into())),
146 };
147
148 let response: MexResponse = serde_json::from_slice(result_bytes)?;
149
150 if let Some(fatal) = response.fatal_error() {
151 let code = fatal.error_code().unwrap_or(500);
152 return Err(MexError::ExtensionError {
153 code,
154 message: fatal.message.clone(),
155 });
156 }
157
158 Ok(response)
159 }
160}
161
162impl Client {
163 #[inline]
164 pub fn mex(&self) -> Mex<'_> {
165 Mex::new(self)
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use serde_json::json;
173
174 #[test]
175 fn test_mex_payload_serialization() {
176 let variables = json!({
177 "input": {
178 "query_input": [{"jid": "1234567890@s.whatsapp.net"}]
179 },
180 "include_username": true
181 });
182
183 let payload = MexPayload {
184 variables: &variables,
185 };
186
187 let serialized = serde_json::to_string(&payload).unwrap();
188
189 assert!(serialized.starts_with("{\"variables\":"));
190 assert!(!serialized.contains("\"id\":"));
191 assert!(serialized.contains("\"include_username\":true"));
192 assert!(serialized.contains("\"query_input\""));
193 }
194
195 #[test]
196 fn test_mex_request_borrows_doc_id() {
197 let doc_id = "29829202653362039";
198 let request = MexRequest {
199 doc_id,
200 variables: json!({}),
201 };
202
203 assert_eq!(request.doc_id, "29829202653362039");
204 }
205
206 #[test]
207 fn test_mex_response_deserialization() {
208 let json_str = r#"{
209 "data": {
210 "xwa2_fetch_wa_users": [
211 {"jid": "1234567890@s.whatsapp.net", "country_code": "1"}
212 ]
213 }
214 }"#;
215
216 let response: MexResponse = serde_json::from_str(json_str).unwrap();
217 assert!(response.has_data());
218 assert!(!response.has_errors());
219 assert!(response.fatal_error().is_none());
220 }
221
222 #[test]
223 fn test_mex_response_with_non_fatal_errors() {
224 let json_str = r#"{
225 "data": null,
226 "errors": [
227 {
228 "message": "User not found",
229 "extensions": {
230 "error_code": 404,
231 "is_summary": false,
232 "is_retryable": false,
233 "severity": "WARNING"
234 }
235 }
236 ]
237 }"#;
238
239 let response: MexResponse = serde_json::from_str(json_str).unwrap();
240 assert!(!response.has_data());
241 assert!(response.has_errors());
242 assert!(response.fatal_error().is_none());
243
244 let errors = response.errors.as_ref().unwrap();
245 assert_eq!(errors.len(), 1);
246 assert_eq!(errors[0].message, "User not found");
247 assert_eq!(errors[0].error_code(), Some(404));
248 assert!(!errors[0].is_fatal());
249 }
250
251 #[test]
252 fn test_mex_response_with_fatal_error() {
253 let json_str = r#"{
254 "data": null,
255 "errors": [
256 {
257 "message": "Fatal server error",
258 "extensions": {
259 "error_code": 500,
260 "is_summary": true,
261 "severity": "CRITICAL"
262 }
263 }
264 ]
265 }"#;
266
267 let response: MexResponse = serde_json::from_str(json_str).unwrap();
268 assert!(!response.has_data());
269 assert!(response.has_errors());
270
271 let fatal = response.fatal_error();
272 assert!(fatal.is_some());
273
274 let fatal = fatal.unwrap();
275 assert_eq!(fatal.message, "Fatal server error");
276 assert_eq!(fatal.error_code(), Some(500));
277 assert!(fatal.is_fatal());
278 }
279
280 #[test]
281 fn test_mex_response_real_world() {
282 let json_str = r#"{
283 "data": {
284 "xwa2_fetch_wa_users": [
285 {
286 "__typename": "XWA2User",
287 "about_status_info": {
288 "__typename": "XWA2AboutStatus",
289 "text": "Hello",
290 "timestamp": "1766267670"
291 },
292 "country_code": "BR",
293 "id": null,
294 "jid": "559984726662@s.whatsapp.net",
295 "username_info": {
296 "__typename": "XWA2ResponseStatus",
297 "status": "EMPTY"
298 }
299 }
300 ]
301 }
302 }"#;
303
304 let response: MexResponse = serde_json::from_str(json_str).unwrap();
305 assert!(response.has_data());
306 assert!(!response.has_errors());
307
308 let data = response.data.unwrap();
309 let users = data["xwa2_fetch_wa_users"].as_array().unwrap();
310 assert_eq!(users.len(), 1);
311 assert_eq!(users[0]["country_code"], "BR");
312 assert_eq!(users[0]["jid"], "559984726662@s.whatsapp.net");
313 }
314
315 #[test]
316 fn test_mex_error_extensions_all_fields() {
317 let json_str = r#"{
318 "error_code": 400,
319 "is_summary": false,
320 "is_retryable": true,
321 "severity": "WARNING"
322 }"#;
323
324 let ext: MexErrorExtensions = serde_json::from_str(json_str).unwrap();
325 assert_eq!(ext.error_code, Some(400));
326 assert_eq!(ext.is_summary, Some(false));
327 assert_eq!(ext.is_retryable, Some(true));
328 assert_eq!(ext.severity, Some("WARNING".to_string()));
329 }
330
331 #[test]
332 fn test_mex_error_extensions_minimal() {
333 let json_str = r#"{}"#;
334
335 let ext: MexErrorExtensions = serde_json::from_str(json_str).unwrap();
336 assert!(ext.error_code.is_none());
337 assert!(ext.is_summary.is_none());
338 assert!(ext.is_retryable.is_none());
339 assert!(ext.severity.is_none());
340 }
341}