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}