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_non_fatal_errors() {
120        let json_str = r#"{
121            "data": null,
122            "errors": [
123                {
124                    "message": "User not found",
125                    "extensions": {
126                        "error_code": 404,
127                        "is_summary": false,
128                        "is_retryable": false,
129                        "severity": "WARNING"
130                    }
131                }
132            ]
133        }"#;
134
135        let response: MexResponse = serde_json::from_str(json_str).unwrap();
136        assert!(!response.has_data());
137        assert!(response.has_errors());
138        assert!(response.fatal_error().is_none());
139
140        let errors = response.errors.as_ref().unwrap();
141        assert_eq!(errors.len(), 1);
142        assert_eq!(errors[0].message, "User not found");
143        assert_eq!(errors[0].error_code(), Some(404));
144        assert!(!errors[0].is_fatal());
145    }
146
147    #[test]
148    fn test_mex_response_with_fatal_error() {
149        let json_str = r#"{
150            "data": null,
151            "errors": [
152                {
153                    "message": "Fatal server error",
154                    "extensions": {
155                        "error_code": 500,
156                        "is_summary": true,
157                        "severity": "CRITICAL"
158                    }
159                }
160            ]
161        }"#;
162
163        let response: MexResponse = serde_json::from_str(json_str).unwrap();
164        assert!(!response.has_data());
165        assert!(response.has_errors());
166
167        let fatal = response.fatal_error();
168        assert!(fatal.is_some());
169
170        let fatal = fatal.unwrap();
171        assert_eq!(fatal.message, "Fatal server error");
172        assert_eq!(fatal.error_code(), Some(500));
173        assert!(fatal.is_fatal());
174    }
175
176    #[test]
177    fn test_mex_response_real_world() {
178        let json_str = r#"{
179            "data": {
180                "xwa2_fetch_wa_users": [
181                    {
182                        "__typename": "XWA2User",
183                        "about_status_info": {
184                            "__typename": "XWA2AboutStatus",
185                            "text": "Hello",
186                            "timestamp": "1766267670"
187                        },
188                        "country_code": "BR",
189                        "id": null,
190                        "jid": "559984726662@s.whatsapp.net",
191                        "username_info": {
192                            "__typename": "XWA2ResponseStatus",
193                            "status": "EMPTY"
194                        }
195                    }
196                ]
197            }
198        }"#;
199
200        let response: MexResponse = serde_json::from_str(json_str).unwrap();
201        assert!(response.has_data());
202        assert!(!response.has_errors());
203
204        let data = response.data.unwrap();
205        let users = data["xwa2_fetch_wa_users"].as_array().unwrap();
206        assert_eq!(users.len(), 1);
207        assert_eq!(users[0]["country_code"], "BR");
208        assert_eq!(users[0]["jid"], "559984726662@s.whatsapp.net");
209    }
210
211    #[test]
212    fn test_mex_error_extensions_all_fields() {
213        let json_str = r#"{
214            "error_code": 400,
215            "is_summary": false,
216            "is_retryable": true,
217            "severity": "WARNING"
218        }"#;
219
220        let ext: MexErrorExtensions = serde_json::from_str(json_str).unwrap();
221        assert_eq!(ext.error_code, Some(400));
222        assert_eq!(ext.is_summary, Some(false));
223        assert_eq!(ext.is_retryable, Some(true));
224        assert_eq!(ext.severity, Some("WARNING".to_string()));
225    }
226
227    #[test]
228    fn test_mex_error_extensions_minimal() {
229        let json_str = r#"{}"#;
230
231        let ext: MexErrorExtensions = serde_json::from_str(json_str).unwrap();
232        assert!(ext.error_code.is_none());
233        assert!(ext.is_summary.is_none());
234        assert!(ext.is_retryable.is_none());
235        assert!(ext.severity.is_none());
236    }
237}