Skip to main content

whatsapp_rust/
receipt.rs

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            // Arc clone is cheap - just reference count increment
56            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    /// Sends a delivery receipt to the sender of a message.
75    ///
76    /// This function handles:
77    /// - Direct messages (DMs) - sends receipt to the sender's JID.
78    /// - Group messages - sends receipt to the group JID with the sender as a participant.
79    /// - It correctly skips sending receipts for self-sent messages, status broadcasts, or messages without an ID.
80    pub(crate) async fn send_delivery_receipt(&self, info: &crate::types::message::MessageInfo) {
81        use wacore_binary::jid::STATUS_BROADCAST_USER;
82
83        // Don't send receipts for our own messages, status broadcasts, or if ID is missing.
84        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        // The 'to' attribute is always the JID from which the message originated (the chat JID for groups).
94        attrs.insert("to".to_string(), info.source.chat.to_string());
95        // WhatsApp Web omits the type attribute for delivery receipts (DROP_ATTR).
96        // The absence of type IS the delivery signal. Only read/played/etc. get explicit type.
97
98        // For group messages, the 'participant' attribute is required to identify the sender.
99        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    /// Sends read receipts for one or more messages.
113    ///
114    /// For group messages, pass the message sender as `sender`.
115    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", &timestamp);
136
137        if let Some(sender) = sender {
138            builder = builder.attr("participant", sender.to_string());
139        }
140
141        // Additional message IDs go into <list><item id="..."/></list>
142        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        // This should complete without panicking. The actual node sending
200        // would fail since we're not connected, but the function should
201        // handle that gracefully and log a warning.
202        client.send_delivery_receipt(&info).await;
203
204        // If we got here, the function executed successfully.
205        // In a real scenario, we'd need to mock the transport to verify
206        // the exact node sent, but basic functionality testing confirms
207        // the method doesn't panic and logs appropriately.
208    }
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        // Should complete without panicking for group messages too.
243        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, // Own message
272                is_group: false,
273                ..Default::default()
274            },
275            ..Default::default()
276        };
277
278        // Should return early without attempting to send.
279        // We can't easily assert that send_node was not called without
280        // refactoring, but at least verify the function completes.
281        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(), // Empty ID
302            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        // Should return early without attempting to send.
317        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"), // Status broadcast
342                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        // Should return early without attempting to send for status broadcasts.
353        client.send_delivery_receipt(&info).await;
354    }
355}