whatsapp_rust/features/
mex.rs

1use 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}