1use crate::client::Client;
2use crate::types::events::{Event, Receipt};
3use crate::types::presence::ReceiptType;
4use log::info;
5use std::collections::HashMap;
6use std::sync::Arc;
7use wacore_binary::builder::NodeBuilder;
8use wacore_binary::jid::{Jid, JidExt as _};
9
10use wacore_binary::node::Node;
11
12impl Client {
13 pub(crate) async fn handle_receipt(self: &Arc<Self>, node: Arc<Node>) {
14 let mut attrs = node.attrs();
15 let from = attrs.jid("from");
16 let id = match attrs.optional_string("id") {
17 Some(id) => id.to_string(),
18 None => {
19 log::warn!("Receipt stanza missing required 'id' attribute");
20 return;
21 }
22 };
23 let receipt_type_str = attrs.optional_string("type").unwrap_or("delivery");
24 let participant = attrs.optional_jid("participant");
25
26 let receipt_type = ReceiptType::from(receipt_type_str.to_string());
27
28 info!("Received receipt type '{receipt_type:?}' for message {id} from {from}");
29
30 let from_clone = from.clone();
31 let sender = if from.is_group() {
32 if let Some(participant) = participant {
33 participant
34 } else {
35 from_clone
36 }
37 } else {
38 from.clone()
39 };
40
41 let receipt = Receipt {
42 message_ids: vec![id.clone()],
43 source: crate::types::message::MessageSource {
44 chat: from.clone(),
45 sender: sender.clone(),
46 ..Default::default()
47 },
48 timestamp: chrono::Utc::now(),
49 r#type: receipt_type.clone(),
50 message_sender: sender.clone(),
51 };
52
53 if receipt_type == ReceiptType::Retry {
54 let client_clone = Arc::clone(self);
55 let node_clone = Arc::clone(&node);
57 tokio::spawn(async move {
58 if let Err(e) = client_clone
59 .handle_retry_receipt(&receipt, &node_clone)
60 .await
61 {
62 log::warn!(
63 "Failed to handle retry receipt for {}: {:?}",
64 receipt.message_ids[0],
65 e
66 );
67 }
68 });
69 } else {
70 self.core.event_bus.dispatch(&Event::Receipt(receipt));
71 }
72 }
73
74 pub(crate) async fn send_delivery_receipt(&self, info: &crate::types::message::MessageInfo) {
81 use wacore_binary::jid::STATUS_BROADCAST_USER;
82
83 if info.source.is_from_me
85 || info.id.is_empty()
86 || info.source.chat.user == STATUS_BROADCAST_USER
87 {
88 return;
89 }
90
91 let mut attrs = HashMap::new();
92 attrs.insert("id".to_string(), info.id.clone());
93 attrs.insert("to".to_string(), info.source.chat.to_string());
95 if info.source.is_group {
100 attrs.insert("participant".to_string(), info.source.sender.to_string());
101 }
102
103 let receipt_node = NodeBuilder::new("receipt").attrs(attrs).build();
104
105 info!(target: "Client/Receipt", "Sending delivery receipt for message {} to {}", info.id, info.source.sender);
106
107 if let Err(e) = self.send_node(receipt_node).await {
108 log::warn!(target: "Client/Receipt", "Failed to send delivery receipt for message {}: {:?}", info.id, e);
109 }
110 }
111
112 pub async fn mark_as_read(
116 &self,
117 chat: &Jid,
118 sender: Option<&Jid>,
119 message_ids: Vec<String>,
120 ) -> Result<(), anyhow::Error> {
121 if message_ids.is_empty() {
122 return Ok(());
123 }
124
125 let timestamp = std::time::SystemTime::now()
126 .duration_since(std::time::UNIX_EPOCH)
127 .unwrap_or_default()
128 .as_secs()
129 .to_string();
130
131 let mut builder = NodeBuilder::new("receipt")
132 .attr("to", chat.to_string())
133 .attr("type", "read")
134 .attr("id", &message_ids[0])
135 .attr("t", ×tamp);
136
137 if let Some(sender) = sender {
138 builder = builder.attr("participant", sender.to_string());
139 }
140
141 if message_ids.len() > 1 {
143 let items: Vec<wacore_binary::node::Node> = message_ids[1..]
144 .iter()
145 .map(|id| NodeBuilder::new("item").attr("id", id).build())
146 .collect();
147 builder = builder.children(vec![NodeBuilder::new("list").children(items).build()]);
148 }
149
150 let node = builder.build();
151
152 info!(target: "Client/Receipt", "Sending read receipt for {} message(s) to {}", message_ids.len(), chat);
153
154 self.send_node(node)
155 .await
156 .map_err(|e| anyhow::anyhow!("Failed to send read receipt: {}", e))
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::store::persistence_manager::PersistenceManager;
164 use crate::test_utils::MockHttpClient;
165 use crate::types::message::{MessageInfo, MessageSource};
166
167 #[tokio::test]
168 async fn test_send_delivery_receipt_dm() {
169 let backend = crate::test_utils::create_test_backend().await;
170 let pm = Arc::new(
171 PersistenceManager::new(backend)
172 .await
173 .expect("persistence manager should initialize"),
174 );
175 let (client, _rx) = Client::new(
176 pm,
177 Arc::new(crate::transport::mock::MockTransportFactory::new()),
178 Arc::new(MockHttpClient),
179 None,
180 )
181 .await;
182
183 let info = MessageInfo {
184 id: "TEST-ID-123".to_string(),
185 source: MessageSource {
186 chat: "12345@s.whatsapp.net"
187 .parse()
188 .expect("test JID should be valid"),
189 sender: "12345@s.whatsapp.net"
190 .parse()
191 .expect("test JID should be valid"),
192 is_from_me: false,
193 is_group: false,
194 ..Default::default()
195 },
196 ..Default::default()
197 };
198
199 client.send_delivery_receipt(&info).await;
203
204 }
209
210 #[tokio::test]
211 async fn test_send_delivery_receipt_group() {
212 let backend = crate::test_utils::create_test_backend().await;
213 let pm = Arc::new(
214 PersistenceManager::new(backend)
215 .await
216 .expect("persistence manager should initialize"),
217 );
218 let (client, _rx) = Client::new(
219 pm,
220 Arc::new(crate::transport::mock::MockTransportFactory::new()),
221 Arc::new(MockHttpClient),
222 None,
223 )
224 .await;
225
226 let info = MessageInfo {
227 id: "GROUP-MSG-ID".to_string(),
228 source: MessageSource {
229 chat: "120363021033254949@g.us"
230 .parse()
231 .expect("test JID should be valid"),
232 sender: "15551234567@s.whatsapp.net"
233 .parse()
234 .expect("test JID should be valid"),
235 is_from_me: false,
236 is_group: true,
237 ..Default::default()
238 },
239 ..Default::default()
240 };
241
242 client.send_delivery_receipt(&info).await;
244 }
245
246 #[tokio::test]
247 async fn test_skip_delivery_receipt_for_own_messages() {
248 let backend = crate::test_utils::create_test_backend().await;
249 let pm = Arc::new(
250 PersistenceManager::new(backend)
251 .await
252 .expect("persistence manager should initialize"),
253 );
254 let (client, _rx) = Client::new(
255 pm,
256 Arc::new(crate::transport::mock::MockTransportFactory::new()),
257 Arc::new(MockHttpClient),
258 None,
259 )
260 .await;
261
262 let info = MessageInfo {
263 id: "OWN-MSG-ID".to_string(),
264 source: MessageSource {
265 chat: "12345@s.whatsapp.net"
266 .parse()
267 .expect("test JID should be valid"),
268 sender: "12345@s.whatsapp.net"
269 .parse()
270 .expect("test JID should be valid"),
271 is_from_me: true, is_group: false,
273 ..Default::default()
274 },
275 ..Default::default()
276 };
277
278 client.send_delivery_receipt(&info).await;
282 }
283
284 #[tokio::test]
285 async fn test_skip_delivery_receipt_for_empty_id() {
286 let backend = crate::test_utils::create_test_backend().await;
287 let pm = Arc::new(
288 PersistenceManager::new(backend)
289 .await
290 .expect("persistence manager should initialize"),
291 );
292 let (client, _rx) = Client::new(
293 pm,
294 Arc::new(crate::transport::mock::MockTransportFactory::new()),
295 Arc::new(MockHttpClient),
296 None,
297 )
298 .await;
299
300 let info = MessageInfo {
301 id: "".to_string(), source: MessageSource {
303 chat: "12345@s.whatsapp.net"
304 .parse()
305 .expect("test JID should be valid"),
306 sender: "12345@s.whatsapp.net"
307 .parse()
308 .expect("test JID should be valid"),
309 is_from_me: false,
310 is_group: false,
311 ..Default::default()
312 },
313 ..Default::default()
314 };
315
316 client.send_delivery_receipt(&info).await;
318 }
319
320 #[tokio::test]
321 async fn test_skip_delivery_receipt_for_status_broadcast() {
322 let backend = crate::test_utils::create_test_backend().await;
323 let pm = Arc::new(
324 PersistenceManager::new(backend)
325 .await
326 .expect("persistence manager should initialize"),
327 );
328 let (client, _rx) = Client::new(
329 pm,
330 Arc::new(crate::transport::mock::MockTransportFactory::new()),
331 Arc::new(MockHttpClient),
332 None,
333 )
334 .await;
335
336 let info = MessageInfo {
337 id: "STATUS-MSG-ID".to_string(),
338 source: MessageSource {
339 chat: "status@broadcast"
340 .parse()
341 .expect("test JID should be valid"), sender: "12345@s.whatsapp.net"
343 .parse()
344 .expect("test JID should be valid"),
345 is_from_me: false,
346 is_group: true,
347 ..Default::default()
348 },
349 ..Default::default()
350 };
351
352 client.send_delivery_receipt(&info).await;
354 }
355}