whatsapp_rust/
request.rs

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