Skip to main content

whatsapp_rust/features/
mex.rs

1//! MEX (Meta Exchange) GraphQL feature.
2//!
3//! Protocol types are defined in `wacore::iq::mex`.
4
5use crate::client::Client;
6use crate::request::IqError;
7use serde_json::Value;
8use thiserror::Error;
9use wacore::iq::mex::MexQuerySpec;
10
11// Re-export types from wacore
12pub use wacore::iq::mex::{MexErrorExtensions, MexGraphQLError, MexResponse};
13
14/// Error types for MEX operations.
15#[derive(Debug, Error)]
16pub enum MexError {
17    #[error("MEX payload parsing error: {0}")]
18    PayloadParsing(String),
19
20    #[error("MEX extension error: code={code}, message='{message}'")]
21    ExtensionError { code: i32, message: String },
22
23    #[error("IQ request failed: {0}")]
24    Request(#[from] IqError),
25
26    #[error("JSON error: {0}")]
27    Json(#[from] serde_json::Error),
28}
29
30/// MEX request with document ID and variables.
31#[derive(Debug, Clone)]
32pub struct MexRequest<'a> {
33    /// GraphQL document ID.
34    pub doc_id: &'a str,
35    /// Query variables.
36    pub variables: Value,
37}
38
39/// Feature handle for MEX GraphQL operations.
40pub struct Mex<'a> {
41    client: &'a Client,
42}
43
44impl<'a> Mex<'a> {
45    pub(crate) fn new(client: &'a Client) -> Self {
46        Self { client }
47    }
48
49    /// Execute a GraphQL query.
50    #[inline]
51    pub async fn query(&self, request: MexRequest<'_>) -> Result<MexResponse, MexError> {
52        self.execute_request(request).await
53    }
54
55    /// Execute a GraphQL mutation.
56    #[inline]
57    pub async fn mutate(&self, request: MexRequest<'_>) -> Result<MexResponse, MexError> {
58        self.execute_request(request).await
59    }
60
61    async fn execute_request(&self, request: MexRequest<'_>) -> Result<MexResponse, MexError> {
62        let spec = MexQuerySpec::new(request.doc_id, request.variables);
63
64        let response = self.client.execute(spec).await?;
65
66        // Check for fatal errors (the IqSpec already checks, but we want to return our error type)
67        if let Some(fatal) = response.fatal_error() {
68            let code = fatal.error_code().unwrap_or(500);
69            return Err(MexError::ExtensionError {
70                code,
71                message: fatal.message.clone(),
72            });
73        }
74
75        Ok(response)
76    }
77}
78
79impl Client {
80    #[inline]
81    pub fn mex(&self) -> Mex<'_> {
82        Mex::new(self)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use serde_json::json;
90
91    #[test]
92    fn test_mex_request_borrows_doc_id() {
93        let doc_id = "29829202653362039";
94        let request = MexRequest {
95            doc_id,
96            variables: json!({}),
97        };
98
99        assert_eq!(request.doc_id, "29829202653362039");
100    }
101
102    #[test]
103    fn test_mex_response_deserialization() {
104        let json_str = r#"{
105            "data": {
106                "xwa2_fetch_wa_users": [
107                    {"jid": "1234567890@s.whatsapp.net", "country_code": "1"}
108                ]
109            }
110        }"#;
111
112        let response: MexResponse = serde_json::from_str(json_str).unwrap();
113        assert!(response.has_data());
114        assert!(!response.has_errors());
115        assert!(response.fatal_error().is_none());
116    }
117
118    #[test]
119    fn test_mex_response_with_error_code_is_fatal() {
120        // WhatsApp Web treats any error with error_code as fatal
121        let json_str = r#"{
122            "data": null,
123            "errors": [
124                {
125                    "message": "User not found",
126                    "extensions": {
127                        "error_code": 404,
128                        "is_summary": false,
129                        "is_retryable": false,
130                        "severity": "WARNING"
131                    }
132                }
133            ]
134        }"#;
135
136        let response: MexResponse = serde_json::from_str(json_str).unwrap();
137        assert!(!response.has_data());
138        assert!(response.has_errors());
139
140        let fatal = response.fatal_error();
141        assert!(fatal.is_some());
142        assert_eq!(fatal.unwrap().error_code(), Some(404));
143    }
144
145    #[test]
146    fn test_mex_response_with_fatal_error() {
147        let json_str = r#"{
148            "data": null,
149            "errors": [
150                {
151                    "message": "Fatal server error",
152                    "extensions": {
153                        "error_code": 500,
154                        "is_summary": true,
155                        "severity": "CRITICAL"
156                    }
157                }
158            ]
159        }"#;
160
161        let response: MexResponse = serde_json::from_str(json_str).unwrap();
162        assert!(!response.has_data());
163        assert!(response.has_errors());
164
165        let fatal = response.fatal_error();
166        assert!(fatal.is_some());
167
168        let fatal = fatal.unwrap();
169        assert_eq!(fatal.message, "Fatal server error");
170        assert_eq!(fatal.error_code(), Some(500));
171        assert!(fatal.is_summary());
172    }
173
174    #[test]
175    fn test_mex_response_real_world() {
176        let json_str = r#"{
177            "data": {
178                "xwa2_fetch_wa_users": [
179                    {
180                        "__typename": "XWA2User",
181                        "about_status_info": {
182                            "__typename": "XWA2AboutStatus",
183                            "text": "Hello",
184                            "timestamp": "1766267670"
185                        },
186                        "country_code": "BR",
187                        "id": null,
188                        "jid": "551199887766@s.whatsapp.net",
189                        "username_info": {
190                            "__typename": "XWA2ResponseStatus",
191                            "status": "EMPTY"
192                        }
193                    }
194                ]
195            }
196        }"#;
197
198        let response: MexResponse = serde_json::from_str(json_str).unwrap();
199        assert!(response.has_data());
200        assert!(!response.has_errors());
201
202        let data = response.data.unwrap();
203        let users = data["xwa2_fetch_wa_users"].as_array().unwrap();
204        assert_eq!(users.len(), 1);
205        assert_eq!(users[0]["country_code"], "BR");
206        assert_eq!(users[0]["jid"], "551199887766@s.whatsapp.net");
207    }
208
209    #[test]
210    fn test_mex_error_extensions_all_fields() {
211        let json_str = r#"{
212            "error_code": 400,
213            "is_summary": false,
214            "is_retryable": true,
215            "severity": "WARNING"
216        }"#;
217
218        let ext: MexErrorExtensions = serde_json::from_str(json_str).unwrap();
219        assert_eq!(ext.error_code, Some(400));
220        assert_eq!(ext.is_summary, Some(false));
221        assert_eq!(ext.is_retryable, Some(true));
222        assert_eq!(ext.severity, Some("WARNING".to_string()));
223    }
224
225    #[test]
226    fn test_mex_error_extensions_minimal() {
227        let json_str = r#"{}"#;
228
229        let ext: MexErrorExtensions = serde_json::from_str(json_str).unwrap();
230        assert!(ext.error_code.is_none());
231        assert!(ext.is_summary.is_none());
232        assert!(ext.is_retryable.is_none());
233        assert!(ext.severity.is_none());
234    }
235}