1use crate::client::Client;
2use crate::types::events::{Event, Receipt};
3use crate::types::presence::ReceiptType;
4use log::debug;
5use std::sync::Arc;
6use wacore_binary::builder::NodeBuilder;
7use wacore_binary::jid::{Jid, JidExt as _};
8
9use wacore_binary::node::Node;
10
11impl Client {
12 fn should_send_delivery_receipt(info: &crate::types::message::MessageInfo) -> bool {
13 use wacore_binary::jid::STATUS_BROADCAST_USER;
14
15 if info.id.is_empty()
16 || info.source.chat.user == STATUS_BROADCAST_USER
17 || info.source.chat.is_newsletter()
18 {
19 return false;
20 }
21
22 info.category == "peer" || !info.source.is_from_me
27 }
28
29 pub(crate) async fn handle_receipt(self: &Arc<Self>, node: Arc<Node>) {
30 let mut attrs = node.attrs();
31 let from = attrs.jid("from");
32 let id = match attrs.optional_string("id") {
33 Some(id) => id.to_string(),
34 None => {
35 log::warn!("Receipt stanza missing required 'id' attribute");
36 return;
37 }
38 };
39 let receipt_type_cow = attrs.optional_string("type");
40 let receipt_type_str = receipt_type_cow.as_deref().unwrap_or("delivery");
41 let participant = attrs.optional_jid("participant");
42
43 let receipt_type = ReceiptType::from(receipt_type_str.to_string());
44
45 debug!("Received receipt type '{receipt_type:?}' for message {id} from {from}");
46
47 let from_clone = from.clone();
48 let sender = if from.is_group() {
49 if let Some(participant) = participant {
50 participant
51 } else {
52 from_clone
53 }
54 } else {
55 from.clone()
56 };
57
58 let receipt = Receipt {
59 message_ids: vec![id.clone()],
60 source: crate::types::message::MessageSource {
61 chat: from.clone(),
62 sender: sender.clone(),
63 ..Default::default()
64 },
65 timestamp: wacore::time::now_utc(),
66 r#type: receipt_type.clone(),
67 message_sender: sender.clone(),
68 };
69
70 if receipt_type == ReceiptType::Retry {
71 let client_clone = Arc::clone(self);
72 let node_clone = Arc::clone(&node);
74 self.runtime
75 .spawn(Box::pin(async move {
76 if let Err(e) = client_clone
77 .handle_retry_receipt(&receipt, &node_clone)
78 .await
79 {
80 log::warn!(
81 "Failed to handle retry receipt for {}: {:?}",
82 receipt.message_ids[0],
83 e
84 );
85 }
86 }))
87 .detach();
88 } else if receipt_type == ReceiptType::EncRekeyRetry {
89 if let Some(child) = node.get_optional_child("enc_rekey") {
96 let mut attrs = child.attrs();
97 log::debug!(
98 "Received enc_rekey_retry receipt for call-id={} from {} \
99 (call-creator={}, count={}). VoIP not implemented, forwarding as event.",
100 attrs
101 .optional_string("call-id")
102 .as_deref()
103 .unwrap_or_default(),
104 from,
105 attrs
106 .optional_string("call-creator")
107 .as_deref()
108 .unwrap_or_default(),
109 attrs
110 .optional_string("count")
111 .and_then(|s| s.parse::<u8>().ok())
112 .unwrap_or(1),
113 );
114 }
115 self.core.event_bus.dispatch(&Event::Receipt(receipt));
116 } else {
117 self.core.event_bus.dispatch(&Event::Receipt(receipt));
118 }
119 }
120
121 pub(crate) async fn send_delivery_receipt(&self, info: &crate::types::message::MessageInfo) {
131 if !Self::should_send_delivery_receipt(info) {
132 return;
133 }
134
135 let mut builder = NodeBuilder::new("receipt")
136 .attr("id", &info.id)
137 .attr("to", info.source.chat.clone());
138
139 if info.category == "peer" {
142 builder = builder.attr("type", "peer_msg");
143 }
144
145 if info.source.is_group {
147 builder = builder.attr("participant", info.source.sender.clone());
148 }
149
150 let receipt_node = builder.build();
151
152 debug!(target: "Client/Receipt", "Sending {} receipt for message {} to {}",
153 if info.category == "peer" { "peer_msg" } else { "delivery" },
154 info.id, info.source.sender);
155
156 if let Err(e) = self.send_node(receipt_node).await {
157 log::warn!(target: "Client/Receipt", "Failed to send delivery receipt for message {}: {:?}", info.id, e);
158 }
159 }
160
161 pub async fn mark_as_read(
165 &self,
166 chat: &Jid,
167 sender: Option<&Jid>,
168 message_ids: Vec<String>,
169 ) -> Result<(), anyhow::Error> {
170 if message_ids.is_empty() {
171 return Ok(());
172 }
173
174 let timestamp = (wacore::time::now_secs() as u64).to_string();
175
176 let mut builder = NodeBuilder::new("receipt")
177 .attr("to", chat.clone())
178 .attr("type", "read")
179 .attr("id", &message_ids[0])
180 .attr("t", ×tamp);
181
182 if let Some(sender) = sender {
183 builder = builder.attr("participant", sender.clone());
184 }
185
186 if message_ids.len() > 1 {
188 let items: Vec<wacore_binary::node::Node> = message_ids[1..]
189 .iter()
190 .map(|id| NodeBuilder::new("item").attr("id", id).build())
191 .collect();
192 builder = builder.children(vec![NodeBuilder::new("list").children(items).build()]);
193 }
194
195 let node = builder.build();
196
197 debug!(target: "Client/Receipt", "Sending read receipt for {} message(s) to {}", message_ids.len(), chat);
198
199 self.send_node(node)
200 .await
201 .map_err(|e| anyhow::anyhow!("Failed to send read receipt: {}", e))
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::store::persistence_manager::PersistenceManager;
209 use crate::test_utils::MockHttpClient;
210 use crate::types::message::{MessageInfo, MessageSource};
211 use std::sync::Mutex;
212 use wacore::types::events::EventHandler;
213
214 #[derive(Default)]
215 struct TestEventCollector {
216 events: Mutex<Vec<Event>>,
217 }
218
219 impl EventHandler for TestEventCollector {
220 fn handle_event(&self, event: &Event) {
221 self.events
222 .lock()
223 .expect("collector mutex should not be poisoned")
224 .push(event.clone());
225 }
226 }
227
228 impl TestEventCollector {
229 fn events(&self) -> Vec<Event> {
230 self.events
231 .lock()
232 .expect("collector mutex should not be poisoned")
233 .clone()
234 }
235 }
236
237 #[tokio::test]
238 async fn test_send_delivery_receipt_dm() {
239 let backend = crate::test_utils::create_test_backend().await;
240 let pm = Arc::new(
241 PersistenceManager::new(backend)
242 .await
243 .expect("persistence manager should initialize"),
244 );
245 let (client, _rx) = Client::new(
246 Arc::new(crate::runtime_impl::TokioRuntime),
247 pm,
248 Arc::new(crate::transport::mock::MockTransportFactory::new()),
249 Arc::new(MockHttpClient),
250 None,
251 )
252 .await;
253
254 let info = MessageInfo {
255 id: "TEST-ID-123".to_string(),
256 source: MessageSource {
257 chat: "12345@s.whatsapp.net"
258 .parse()
259 .expect("test JID should be valid"),
260 sender: "12345@s.whatsapp.net"
261 .parse()
262 .expect("test JID should be valid"),
263 is_from_me: false,
264 is_group: false,
265 ..Default::default()
266 },
267 ..Default::default()
268 };
269
270 client.send_delivery_receipt(&info).await;
274
275 }
280
281 #[tokio::test]
282 async fn test_send_delivery_receipt_group() {
283 let backend = crate::test_utils::create_test_backend().await;
284 let pm = Arc::new(
285 PersistenceManager::new(backend)
286 .await
287 .expect("persistence manager should initialize"),
288 );
289 let (client, _rx) = Client::new(
290 Arc::new(crate::runtime_impl::TokioRuntime),
291 pm,
292 Arc::new(crate::transport::mock::MockTransportFactory::new()),
293 Arc::new(MockHttpClient),
294 None,
295 )
296 .await;
297
298 let info = MessageInfo {
299 id: "GROUP-MSG-ID".to_string(),
300 source: MessageSource {
301 chat: "120363021033254949@g.us"
302 .parse()
303 .expect("test JID should be valid"),
304 sender: "15551234567@s.whatsapp.net"
305 .parse()
306 .expect("test JID should be valid"),
307 is_from_me: false,
308 is_group: true,
309 ..Default::default()
310 },
311 ..Default::default()
312 };
313
314 client.send_delivery_receipt(&info).await;
316 }
317
318 #[tokio::test]
319 async fn test_skip_delivery_receipt_for_own_messages() {
320 let backend = crate::test_utils::create_test_backend().await;
321 let pm = Arc::new(
322 PersistenceManager::new(backend)
323 .await
324 .expect("persistence manager should initialize"),
325 );
326 let (client, _rx) = Client::new(
327 Arc::new(crate::runtime_impl::TokioRuntime),
328 pm,
329 Arc::new(crate::transport::mock::MockTransportFactory::new()),
330 Arc::new(MockHttpClient),
331 None,
332 )
333 .await;
334
335 let info = MessageInfo {
336 id: "OWN-MSG-ID".to_string(),
337 source: MessageSource {
338 chat: "12345@s.whatsapp.net"
339 .parse()
340 .expect("test JID should be valid"),
341 sender: "12345@s.whatsapp.net"
342 .parse()
343 .expect("test JID should be valid"),
344 is_from_me: true, is_group: false,
346 ..Default::default()
347 },
348 ..Default::default()
349 };
350
351 client.send_delivery_receipt(&info).await;
355 }
356
357 #[tokio::test]
358 async fn test_skip_delivery_receipt_for_empty_id() {
359 let backend = crate::test_utils::create_test_backend().await;
360 let pm = Arc::new(
361 PersistenceManager::new(backend)
362 .await
363 .expect("persistence manager should initialize"),
364 );
365 let (client, _rx) = Client::new(
366 Arc::new(crate::runtime_impl::TokioRuntime),
367 pm,
368 Arc::new(crate::transport::mock::MockTransportFactory::new()),
369 Arc::new(MockHttpClient),
370 None,
371 )
372 .await;
373
374 let info = MessageInfo {
375 id: "".to_string(), source: MessageSource {
377 chat: "12345@s.whatsapp.net"
378 .parse()
379 .expect("test JID should be valid"),
380 sender: "12345@s.whatsapp.net"
381 .parse()
382 .expect("test JID should be valid"),
383 is_from_me: false,
384 is_group: false,
385 ..Default::default()
386 },
387 ..Default::default()
388 };
389
390 client.send_delivery_receipt(&info).await;
392 }
393
394 #[tokio::test]
395 async fn test_skip_delivery_receipt_for_status_broadcast() {
396 let backend = crate::test_utils::create_test_backend().await;
397 let pm = Arc::new(
398 PersistenceManager::new(backend)
399 .await
400 .expect("persistence manager should initialize"),
401 );
402 let (client, _rx) = Client::new(
403 Arc::new(crate::runtime_impl::TokioRuntime),
404 pm,
405 Arc::new(crate::transport::mock::MockTransportFactory::new()),
406 Arc::new(MockHttpClient),
407 None,
408 )
409 .await;
410
411 let info = MessageInfo {
412 id: "STATUS-MSG-ID".to_string(),
413 source: MessageSource {
414 chat: "status@broadcast"
415 .parse()
416 .expect("test JID should be valid"), sender: "12345@s.whatsapp.net"
418 .parse()
419 .expect("test JID should be valid"),
420 is_from_me: false,
421 is_group: true,
422 ..Default::default()
423 },
424 ..Default::default()
425 };
426
427 client.send_delivery_receipt(&info).await;
429 }
430
431 #[test]
432 fn test_should_skip_delivery_receipt_for_newsletter() {
433 let info = MessageInfo {
434 id: "NEWSLETTER-MSG-ID".to_string(),
435 source: MessageSource {
436 chat: "120363173003902460@newsletter"
437 .parse()
438 .expect("newsletter JID should be valid"),
439 sender: "120363173003902460@newsletter"
440 .parse()
441 .expect("newsletter JID should be valid"),
442 is_from_me: false,
443 is_group: false,
444 ..Default::default()
445 },
446 ..Default::default()
447 };
448
449 assert!(
450 !Client::should_send_delivery_receipt(&info),
451 "generic delivery receipts must be skipped for newsletters"
452 );
453 }
454
455 #[test]
456 fn test_should_send_peer_msg_receipt_for_self_synced_messages() {
457 let info = MessageInfo {
460 id: "PEER-MSG-ID".to_string(),
461 source: MessageSource {
462 chat: "155500012345@s.whatsapp.net"
463 .parse()
464 .expect("own PN JID should be valid"),
465 sender: "155500012345@s.whatsapp.net"
466 .parse()
467 .expect("own PN JID should be valid"),
468 is_from_me: true,
469 is_group: false,
470 ..Default::default()
471 },
472 category: "peer".to_string(),
473 ..Default::default()
474 };
475
476 assert!(
477 Client::should_send_delivery_receipt(&info),
478 "peer device messages must get delivery receipts even when is_from_me"
479 );
480 }
481
482 async fn setup_client_with_collector() -> (Arc<Client>, Arc<TestEventCollector>) {
484 let backend = crate::test_utils::create_test_backend().await;
485 let pm = Arc::new(
486 PersistenceManager::new(backend)
487 .await
488 .expect("persistence manager should initialize"),
489 );
490 let (client, _rx) = Client::new(
491 Arc::new(crate::runtime_impl::TokioRuntime),
492 pm,
493 Arc::new(crate::transport::mock::MockTransportFactory::new()),
494 Arc::new(MockHttpClient),
495 None,
496 )
497 .await;
498
499 let collector = Arc::new(TestEventCollector::default());
500 client.register_handler(collector.clone());
501 (client, collector)
502 }
503
504 #[tokio::test]
507 async fn test_enc_rekey_retry_receipt_dispatches_event() {
508 let (client, collector) = setup_client_with_collector().await;
509
510 let node = Arc::new(
512 NodeBuilder::new("receipt")
513 .attr("from", "5511999999999@s.whatsapp.net")
514 .attr("id", "3EB0AABBCCDD")
515 .attr("type", "enc_rekey_retry")
516 .children([
517 NodeBuilder::new("enc_rekey")
518 .attr("call-creator", "5511888888888@s.whatsapp.net")
519 .attr("call-id", "CALL-123")
520 .attr("count", "1")
521 .build(),
522 NodeBuilder::new("registration")
523 .bytes(12345u32.to_be_bytes().to_vec())
524 .build(),
525 ])
526 .build(),
527 );
528
529 client.handle_receipt(node).await;
530
531 let events = collector.events();
533 let receipt_events: Vec<_> = events
534 .iter()
535 .filter_map(|e| match e {
536 Event::Receipt(r) => Some(r),
537 _ => None,
538 })
539 .collect();
540 assert_eq!(
541 receipt_events.len(),
542 1,
543 "enc_rekey_retry must dispatch exactly one Receipt event"
544 );
545 assert_eq!(
546 receipt_events[0].r#type,
547 ReceiptType::EncRekeyRetry,
548 "dispatched receipt must have EncRekeyRetry type"
549 );
550 assert_eq!(receipt_events[0].message_ids, vec!["3EB0AABBCCDD"]);
551 }
552
553 #[tokio::test]
556 async fn test_enc_rekey_retry_receipt_without_child_still_dispatches() {
557 let (client, collector) = setup_client_with_collector().await;
558
559 let node = Arc::new(
561 NodeBuilder::new("receipt")
562 .attr("from", "5511999999999@s.whatsapp.net")
563 .attr("id", "3EB0AABBCCDD")
564 .attr("type", "enc_rekey_retry")
565 .build(),
566 );
567
568 client.handle_receipt(node).await;
569
570 let events = collector.events();
572 let receipt_events: Vec<_> = events
573 .iter()
574 .filter_map(|e| match e {
575 Event::Receipt(r) => Some(r),
576 _ => None,
577 })
578 .collect();
579 assert_eq!(
580 receipt_events.len(),
581 1,
582 "malformed enc_rekey_retry must still dispatch Receipt event"
583 );
584 assert_eq!(receipt_events[0].r#type, ReceiptType::EncRekeyRetry);
585 }
586
587 #[test]
588 fn test_should_skip_non_peer_self_messages() {
589 let info = MessageInfo {
591 id: "SELF-MSG-ID".to_string(),
592 source: MessageSource {
593 chat: "155500012345@s.whatsapp.net"
594 .parse()
595 .expect("own PN JID should be valid"),
596 sender: "155500012345@s.whatsapp.net"
597 .parse()
598 .expect("own PN JID should be valid"),
599 is_from_me: true,
600 is_group: false,
601 ..Default::default()
602 },
603 ..Default::default()
604 };
605
606 assert!(
607 !Client::should_send_delivery_receipt(&info),
608 "non-peer self messages must not get delivery receipts"
609 );
610 }
611
612 #[test]
615 fn test_receipt_node_uses_jid_attrs() {
616 use wacore_binary::node::NodeValue;
617
618 let chat_jid: Jid = "120363021033254949@g.us"
619 .parse()
620 .expect("test JID should be valid");
621 let sender_jid: Jid = "15551234567@s.whatsapp.net"
622 .parse()
623 .expect("test JID should be valid");
624
625 let node = NodeBuilder::new("receipt")
627 .attr("id", "MSG-123")
628 .attr("to", chat_jid.clone())
629 .attr("participant", sender_jid.clone())
630 .build();
631
632 let to_attr = node.attrs.get("to").expect("receipt must have 'to' attr");
634 assert!(
635 matches!(to_attr, NodeValue::Jid(_)),
636 "'to' attr should be JID-typed, got: {:?}",
637 to_attr
638 );
639 assert_eq!(to_attr.to_jid().unwrap(), chat_jid);
640
641 let participant_attr = node
643 .attrs
644 .get("participant")
645 .expect("group receipt must have 'participant' attr");
646 assert!(
647 matches!(participant_attr, NodeValue::Jid(_)),
648 "'participant' attr should be JID-typed, got: {:?}",
649 participant_attr
650 );
651 assert_eq!(participant_attr.to_jid().unwrap(), sender_jid);
652 }
653}