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}