Skip to main content

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