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}