use crate::client::Client;
use crate::types::events::{Event, Receipt};
use crate::types::presence::ReceiptType;
use log::info;
use std::collections::HashMap;
use std::sync::Arc;
use wacore_binary_ng::builder::NodeBuilder;
use wacore_binary_ng::jid::{Jid, JidExt as _};
use wacore_binary_ng::node::Node;
impl Client {
pub(crate) async fn handle_receipt(self: &Arc<Self>, node: Arc<Node>) {
let mut attrs = node.attrs();
let from = attrs.jid("from");
let id = match attrs.optional_string("id") {
Some(id) => id.to_string(),
None => {
log::warn!("Receipt stanza missing required 'id' attribute");
return;
}
};
let receipt_type_str = attrs.optional_string("type").unwrap_or("delivery");
let participant = attrs.optional_jid("participant");
let receipt_type = ReceiptType::from(receipt_type_str.to_string());
info!("Received receipt type '{receipt_type:?}' for message {id} from {from}");
let from_clone = from.clone();
let sender = if from.is_group() {
if let Some(participant) = participant {
participant
} else {
from_clone
}
} else {
from.clone()
};
let receipt = Receipt {
message_ids: vec![id.clone()],
source: crate::types::message::MessageSource {
chat: from.clone(),
sender: sender.clone(),
..Default::default()
},
timestamp: chrono::Utc::now(),
r#type: receipt_type.clone(),
message_sender: sender.clone(),
};
if receipt_type == ReceiptType::Retry {
let client_clone = Arc::clone(self);
let node_clone = Arc::clone(&node);
tokio::spawn(async move {
if let Err(e) = client_clone
.handle_retry_receipt(&receipt, &node_clone)
.await
{
log::warn!(
"Failed to handle retry receipt for {}: {:?}",
receipt.message_ids[0],
e
);
}
});
} else {
self.core.event_bus.dispatch(&Event::Receipt(receipt));
}
}
pub(crate) async fn send_delivery_receipt(&self, info: &crate::types::message::MessageInfo) {
use wacore_binary_ng::jid::STATUS_BROADCAST_USER;
if info.source.is_from_me
|| info.id.is_empty()
|| info.source.chat.user == STATUS_BROADCAST_USER
{
return;
}
let mut attrs = HashMap::new();
attrs.insert("id".to_string(), info.id.clone());
attrs.insert("to".to_string(), info.source.chat.to_string());
if info.source.is_group {
attrs.insert("participant".to_string(), info.source.sender.to_string());
}
let receipt_node = NodeBuilder::new("receipt").attrs(attrs).build();
info!(target: "Client/Receipt", "Sending delivery receipt for message {} to {}", info.id, info.source.sender);
if let Err(e) = self.send_node(receipt_node).await {
log::warn!(target: "Client/Receipt", "Failed to send delivery receipt for message {}: {:?}", info.id, e);
}
}
pub async fn mark_as_read(
&self,
chat: &Jid,
sender: Option<&Jid>,
message_ids: Vec<String>,
) -> Result<(), anyhow::Error> {
if message_ids.is_empty() {
return Ok(());
}
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
.to_string();
let mut builder = NodeBuilder::new("receipt")
.attr("to", chat.to_string())
.attr("type", "read")
.attr("id", &message_ids[0])
.attr("t", ×tamp);
if let Some(sender) = sender {
builder = builder.attr("participant", sender.to_string());
}
if message_ids.len() > 1 {
let items: Vec<wacore_binary_ng::node::Node> = message_ids[1..]
.iter()
.map(|id| NodeBuilder::new("item").attr("id", id).build())
.collect();
builder = builder.children(vec![NodeBuilder::new("list").children(items).build()]);
}
let node = builder.build();
info!(target: "Client/Receipt", "Sending read receipt for {} message(s) to {}", message_ids.len(), chat);
self.send_node(node)
.await
.map_err(|e| anyhow::anyhow!("Failed to send read receipt: {}", e))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::persistence_manager::PersistenceManager;
use crate::test_utils::MockHttpClient;
use crate::types::message::{MessageInfo, MessageSource};
#[tokio::test]
async fn test_send_delivery_receipt_dm() {
let backend = crate::test_utils::create_test_backend().await;
let pm = Arc::new(
PersistenceManager::new(backend)
.await
.expect("persistence manager should initialize"),
);
let (client, _rx) = Client::new(
pm,
Arc::new(crate::transport::mock::MockTransportFactory::new()),
Arc::new(MockHttpClient),
None,
)
.await;
let info = MessageInfo {
id: "TEST-ID-123".to_string(),
source: MessageSource {
chat: "12345@s.whatsapp.net"
.parse()
.expect("test JID should be valid"),
sender: "12345@s.whatsapp.net"
.parse()
.expect("test JID should be valid"),
is_from_me: false,
is_group: false,
..Default::default()
},
..Default::default()
};
client.send_delivery_receipt(&info).await;
}
#[tokio::test]
async fn test_send_delivery_receipt_group() {
let backend = crate::test_utils::create_test_backend().await;
let pm = Arc::new(
PersistenceManager::new(backend)
.await
.expect("persistence manager should initialize"),
);
let (client, _rx) = Client::new(
pm,
Arc::new(crate::transport::mock::MockTransportFactory::new()),
Arc::new(MockHttpClient),
None,
)
.await;
let info = MessageInfo {
id: "GROUP-MSG-ID".to_string(),
source: MessageSource {
chat: "120363021033254949@g.us"
.parse()
.expect("test JID should be valid"),
sender: "15551234567@s.whatsapp.net"
.parse()
.expect("test JID should be valid"),
is_from_me: false,
is_group: true,
..Default::default()
},
..Default::default()
};
client.send_delivery_receipt(&info).await;
}
#[tokio::test]
async fn test_skip_delivery_receipt_for_own_messages() {
let backend = crate::test_utils::create_test_backend().await;
let pm = Arc::new(
PersistenceManager::new(backend)
.await
.expect("persistence manager should initialize"),
);
let (client, _rx) = Client::new(
pm,
Arc::new(crate::transport::mock::MockTransportFactory::new()),
Arc::new(MockHttpClient),
None,
)
.await;
let info = MessageInfo {
id: "OWN-MSG-ID".to_string(),
source: MessageSource {
chat: "12345@s.whatsapp.net"
.parse()
.expect("test JID should be valid"),
sender: "12345@s.whatsapp.net"
.parse()
.expect("test JID should be valid"),
is_from_me: true, is_group: false,
..Default::default()
},
..Default::default()
};
client.send_delivery_receipt(&info).await;
}
#[tokio::test]
async fn test_skip_delivery_receipt_for_empty_id() {
let backend = crate::test_utils::create_test_backend().await;
let pm = Arc::new(
PersistenceManager::new(backend)
.await
.expect("persistence manager should initialize"),
);
let (client, _rx) = Client::new(
pm,
Arc::new(crate::transport::mock::MockTransportFactory::new()),
Arc::new(MockHttpClient),
None,
)
.await;
let info = MessageInfo {
id: "".to_string(), source: MessageSource {
chat: "12345@s.whatsapp.net"
.parse()
.expect("test JID should be valid"),
sender: "12345@s.whatsapp.net"
.parse()
.expect("test JID should be valid"),
is_from_me: false,
is_group: false,
..Default::default()
},
..Default::default()
};
client.send_delivery_receipt(&info).await;
}
#[tokio::test]
async fn test_skip_delivery_receipt_for_status_broadcast() {
let backend = crate::test_utils::create_test_backend().await;
let pm = Arc::new(
PersistenceManager::new(backend)
.await
.expect("persistence manager should initialize"),
);
let (client, _rx) = Client::new(
pm,
Arc::new(crate::transport::mock::MockTransportFactory::new()),
Arc::new(MockHttpClient),
None,
)
.await;
let info = MessageInfo {
id: "STATUS-MSG-ID".to_string(),
source: MessageSource {
chat: "status@broadcast"
.parse()
.expect("test JID should be valid"), sender: "12345@s.whatsapp.net"
.parse()
.expect("test JID should be valid"),
is_from_me: false,
is_group: true,
..Default::default()
},
..Default::default()
};
client.send_delivery_receipt(&info).await;
}
}