whatsapp_rust/
request.rs

1use crate::client::Client;
2use crate::socket::error::SocketError;
3use log::warn;
4use std::time::Duration;
5use thiserror::Error;
6use tokio::time::timeout;
7use wacore_binary::node::Node;
8
9pub use wacore::request::{InfoQuery, InfoQueryType, RequestUtils};
10
11#[derive(Debug, Error)]
12pub enum IqError {
13    #[error("IQ request timed out")]
14    Timeout,
15    #[error("Client is not connected")]
16    NotConnected,
17    #[error("Socket error: {0}")]
18    Socket(#[from] SocketError),
19    #[error("Received disconnect node during IQ wait: {0:?}")]
20    Disconnected(Node),
21    #[error("Received a server error response: code={code}, text='{text}'")]
22    ServerError { code: u16, text: String },
23    #[error("Internal channel closed unexpectedly")]
24    InternalChannelClosed,
25}
26
27impl From<wacore::request::IqError> for IqError {
28    fn from(err: wacore::request::IqError) -> Self {
29        match err {
30            wacore::request::IqError::Timeout => Self::Timeout,
31            wacore::request::IqError::NotConnected => Self::NotConnected,
32            wacore::request::IqError::Disconnected(node) => Self::Disconnected(node),
33            wacore::request::IqError::ServerError { code, text } => {
34                Self::ServerError { code, text }
35            }
36            wacore::request::IqError::InternalChannelClosed => Self::InternalChannelClosed,
37            wacore::request::IqError::Network(msg) => Self::Socket(SocketError::Crypto(msg)),
38        }
39    }
40}
41
42impl Client {
43    pub(crate) fn generate_request_id(&self) -> String {
44        self.get_request_utils().generate_request_id()
45    }
46
47    /// Generates a unique message ID that conforms to the WhatsApp protocol format.
48    ///
49    /// This is an advanced function that allows library users to generate message IDs
50    /// that are compatible with the WhatsApp protocol. The generated ID includes
51    /// timestamp, user JID, and random components to ensure uniqueness.
52    ///
53    /// # Advanced Use Case
54    ///
55    /// This function is intended for advanced users who need to build custom protocol
56    /// interactions or manage message IDs manually. Most users should use higher-level
57    /// methods like `send_message` which handle ID generation automatically.
58    ///
59    /// # Returns
60    ///
61    /// A string containing the generated message ID in the format expected by WhatsApp.
62    pub async fn generate_message_id(&self) -> String {
63        let device_snapshot = self.persistence_manager.get_device_snapshot().await;
64        self.get_request_utils()
65            .generate_message_id(device_snapshot.pn.as_ref())
66    }
67
68    fn get_request_utils(&self) -> RequestUtils {
69        RequestUtils::with_counter(self.unique_id.clone(), self.id_counter.clone())
70    }
71
72    /// Sends a custom IQ (Info/Query) stanza to the WhatsApp server.
73    ///
74    /// This is an advanced function that allows library users to send custom IQ stanzas
75    /// for protocol interactions that are not covered by higher-level methods. Common
76    /// use cases include live location updates, custom presence management, or other
77    /// advanced WhatsApp features.
78    ///
79    /// # Advanced Use Case
80    ///
81    /// This function bypasses some of the higher-level abstractions and safety checks
82    /// provided by other client methods. Users should be familiar with the WhatsApp
83    /// protocol and IQ stanza format before using this function.
84    ///
85    /// # Arguments
86    ///
87    /// * `query` - The IQ query to send, containing the stanza type, namespace, content, and optional timeout
88    ///
89    /// # Returns
90    ///
91    /// * `Ok(Node)` - The response node from the server
92    /// * `Err(IqError)` - Various error conditions including timeout, connection issues, or server errors
93    ///
94    /// # Example
95    ///
96    /// ```rust,no_run
97    /// use wacore::request::{InfoQuery, InfoQueryType};
98    /// use wacore_binary::builder::NodeBuilder;
99    /// use wacore_binary::node::NodeContent;
100    /// use wacore_binary::jid::Jid;
101    ///
102    /// // This is a simplified example - real usage requires proper setup
103    /// # async fn example(client: &whatsapp_rust::Client) -> Result<(), Box<dyn std::error::Error>> {
104    /// let query_node = NodeBuilder::new("presence")
105    ///     .attr("type", "available")
106    ///     .build();
107    ///
108    /// let server_jid = Jid::new("", "s.whatsapp.net");
109    ///
110    /// let query = InfoQuery {
111    ///     query_type: InfoQueryType::Set,
112    ///     namespace: "presence",
113    ///     to: server_jid,
114    ///     target: None,
115    ///     content: Some(NodeContent::Nodes(vec![query_node])),
116    ///     id: None,
117    ///     timeout: None,
118    /// };
119    ///
120    /// let response = client.send_iq(query).await?;
121    /// # Ok(())
122    /// # }
123    /// ```
124    pub async fn send_iq(&self, query: InfoQuery<'_>) -> Result<Node, IqError> {
125        let req_id = query
126            .id
127            .clone()
128            .unwrap_or_else(|| self.generate_request_id());
129        let default_timeout = Duration::from_secs(75);
130
131        let (tx, rx) = tokio::sync::oneshot::channel();
132        self.response_waiters
133            .lock()
134            .await
135            .insert(req_id.clone(), tx);
136
137        let request_utils = self.get_request_utils();
138        let node = request_utils.build_iq_node(&query, Some(req_id.clone()));
139
140        if let Err(e) = self.send_node(node).await {
141            self.response_waiters.lock().await.remove(&req_id);
142            return match e {
143                crate::client::ClientError::Socket(s_err) => Err(IqError::Socket(s_err)),
144                crate::client::ClientError::NotConnected => Err(IqError::NotConnected),
145                _ => Err(IqError::Socket(SocketError::Crypto(e.to_string()))),
146            };
147        }
148
149        match timeout(query.timeout.unwrap_or(default_timeout), rx).await {
150            Ok(Ok(response_node)) => {
151                request_utils.parse_iq_response(&response_node)?;
152                Ok(response_node)
153            }
154            Ok(Err(_)) => Err(IqError::InternalChannelClosed),
155            Err(_) => {
156                self.response_waiters.lock().await.remove(&req_id);
157                Err(IqError::Timeout)
158            }
159        }
160    }
161
162    pub(crate) async fn handle_iq_response(&self, node: Node) -> bool {
163        let id_opt = node.attrs.get("id").cloned();
164        if let Some(id) = id_opt
165            && let Some(waiter) = self.response_waiters.lock().await.remove(&id)
166        {
167            if waiter.send(node).is_err() {
168                warn!(target: "Client/IQ", "Failed to send IQ response to waiter for ID {id}. Receiver was likely dropped.");
169            }
170            return true;
171        }
172        false
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    // This test verifies that the public API methods are accessible
181    // It's a compile-time test - if the methods aren't public, this won't compile
182    #[tokio::test]
183    async fn test_public_api_accessibility() {
184        // We can't test the actual functionality without a real client setup,
185        // but we can verify the methods exist and are public by checking they exist
186
187        // This is a compile-time test for method visibility
188        // The fact that this compiles means the methods are public and accessible
189
190        // If these were private, this would fail to compile
191        fn _check_methods_exist() {
192            // Check that generate_message_id exists and is accessible
193            let _method = Client::generate_message_id;
194
195            // Check that send_iq exists and is accessible
196            let _method = Client::send_iq;
197        }
198
199        // Test passes if it compiles - which it will if methods are public
200        _check_methods_exist();
201    }
202}