use crate::client::Client;
use crate::types::events::Event;
use log::{debug, info, warn};
use std::sync::Arc;
use wacore::stanza::business::BusinessNotification;
use wacore::types::events::{BusinessStatusUpdate, BusinessUpdateType};
use wacore_binary::NodeRef;
#[cfg_attr(
feature = "tracing",
tracing::instrument(name = "wa.notif.privacy_token", level = "debug", skip_all)
)]
pub(crate) async fn handle_privacy_token_notification(client: &Arc<Client>, node: &NodeRef<'_>) {
use wacore::iq::tctoken::parse_privacy_token_notification;
let from_jid = node.attrs().optional_jid("from");
let sender_lid_jid = node
.attrs()
.optional_jid("sender_lid")
.filter(|j| !j.user.is_empty());
let resolved_lid: Option<wacore_binary::CompactString>;
let sender_lid: &str = if let Some(ref lid_jid) = sender_lid_jid {
&lid_jid.user
} else {
let from = match &from_jid {
Some(jid) => jid,
None => {
warn!(target: "Client/TcToken", "privacy_token notification missing 'from' attribute");
return;
}
};
if from.is_lid() {
&from.user
} else {
resolved_lid = client.lid_pn_cache.get_current_lid(&from.user).await;
match &resolved_lid {
Some(lid) => lid.as_str(),
None => {
debug!(
target: "Client/TcToken",
"Cannot resolve LID for privacy_token sender {}, storing under PN",
from.observe()
);
&from.user
}
}
}
};
let received_tokens = match parse_privacy_token_notification(node) {
Ok(tokens) => tokens,
Err(e) => {
warn!(target: "Client/TcToken", "Failed to parse privacy_token notification: {e}");
return;
}
};
if received_tokens.is_empty() {
debug!(target: "Client/TcToken", "privacy_token notification had no trusted_contact tokens");
return;
}
let backend = client.persistence_manager.backend();
let mut token_stored = false;
for received in &received_tokens {
let existing = match backend.get_tc_token(sender_lid).await {
Ok(entry) => entry,
Err(e) => {
warn!(target: "Client/TcToken", "Failed to read tc_token for {}: {e}, skipping", sender_lid);
continue;
}
};
if let Some(existing) = &existing {
if existing.token == received.token {
if received.timestamp > existing.token_timestamp
&& let Err(e) = backend
.store_received_tc_token(sender_lid, &received.token, received.timestamp)
.await
{
warn!(target: "Client/TcToken", "Failed to refresh tc_token timestamp for {}: {e}", sender_lid);
}
continue;
}
if !existing.token.is_empty() && received.timestamp < existing.token_timestamp {
debug!(
target: "Client/TcToken",
"Skipping older token for {} (incoming={}, existing={})",
sender_lid, received.timestamp, existing.token_timestamp
);
continue;
}
}
if let Err(e) = backend
.store_received_tc_token(sender_lid, &received.token, received.timestamp)
.await
{
warn!(target: "Client/TcToken", "Failed to store tc_token for {}: {e}", sender_lid);
} else {
debug!(target: "Client/TcToken", "Stored tc_token for {} (t={})", sender_lid, received.timestamp);
token_stored = true;
}
}
if token_stored
&& let Some(from) = &from_jid
&& let Err(e) = client.presence().re_subscribe_when_active(from).await
{
debug!(target: "Client/TcToken", "Failed to re-subscribe presence for {}: {e}", from.observe());
}
}
pub(crate) async fn handle_business_notification(client: &Arc<Client>, node: &NodeRef<'_>) {
let notification = match BusinessNotification::try_parse(node) {
Ok(n) => n,
Err(e) => {
warn!(target: "Client/Business", "Failed to parse business notification: {e}");
return;
}
};
debug!(
target: "Client/Business",
"Business notification: from={}, type={}, jid={:?}",
notification.from.observe(),
notification.notification_type,
notification.jid
);
let update_type = BusinessUpdateType::from(notification.notification_type.clone());
let verified_name = notification
.verified_name
.as_ref()
.and_then(|vn| vn.name.clone());
let event = Event::BusinessStatusUpdate(
BusinessStatusUpdate::builder()
.jid(notification.from.clone())
.update_type(update_type)
.timestamp(wacore::time::from_secs_or_now(notification.timestamp))
.maybe_target_jid(notification.jid.clone())
.maybe_hash(notification.hash.clone())
.maybe_verified_name(verified_name)
.product_ids(notification.product_ids.clone())
.collection_ids(notification.collection_ids.clone())
.subscriptions(notification.subscriptions.clone())
.build(),
);
match notification.notification_type {
wacore::stanza::business::BusinessNotificationType::RemoveJid
| wacore::stanza::business::BusinessNotificationType::RemoveHash => {
info!(
target: "Client/Business",
"Contact {} is no longer a business account",
notification.from.observe()
);
}
wacore::stanza::business::BusinessNotificationType::VerifiedNameJid
| wacore::stanza::business::BusinessNotificationType::VerifiedNameHash => {
if let Some(name) = ¬ification
.verified_name
.as_ref()
.and_then(|vn| vn.name.as_ref())
{
info!(
target: "Client/Business",
"Contact {} verified business name: {}",
notification.from.observe(),
name
);
}
}
wacore::stanza::business::BusinessNotificationType::Profile
| wacore::stanza::business::BusinessNotificationType::ProfileHash => {
debug!(
target: "Client/Business",
"Contact {} business profile updated (hash: {:?})",
notification.from.observe(),
notification.hash
);
}
_ => {}
}
client.core.event_bus.dispatch(event);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::create_test_client;
use wacore::store::traits::TcTokenEntry;
use wacore_binary::builder::NodeBuilder;
fn privacy_token_notification(from_lid: &str, t: i64, bytes: Vec<u8>) -> wacore_binary::Node {
NodeBuilder::new("notification")
.attr("type", "privacy_token")
.attr("from", from_lid)
.children([NodeBuilder::new("tokens")
.children([NodeBuilder::new("token")
.attr("type", "trusted_contact")
.attr("t", t.to_string())
.bytes(bytes)
.build()])
.build()])
.build()
}
#[tokio::test]
async fn stores_new_token_with_no_sender_timestamp() {
let client = create_test_client().await;
let node = privacy_token_notification("880000001@lid", 1_700_000_000, vec![0xAA, 0xBB]);
handle_privacy_token_notification(&client, &node.as_node_ref()).await;
let stored = client
.persistence_manager
.backend()
.get_tc_token("880000001")
.await
.unwrap()
.expect("token should be stored");
assert_eq!(stored.token, vec![0xAA, 0xBB]);
assert_eq!(stored.token_timestamp, 1_700_000_000);
assert_eq!(stored.sender_timestamp, None);
}
#[tokio::test]
async fn real_token_replaces_byteless_placeholder_and_keeps_sender_timestamp() {
let client = create_test_client().await;
let backend = client.persistence_manager.backend();
backend
.put_tc_token(
"880000002",
&TcTokenEntry {
token: Vec::new(),
token_timestamp: 1_700_000_500,
sender_timestamp: Some(1_700_000_500),
},
)
.await
.unwrap();
let node =
privacy_token_notification("880000002@lid", 1_700_000_000, vec![0x01, 0x02, 0x03]);
handle_privacy_token_notification(&client, &node.as_node_ref()).await;
let stored = backend.get_tc_token("880000002").await.unwrap().unwrap();
assert_eq!(
stored.token,
vec![0x01, 0x02, 0x03],
"real token must replace the placeholder even with an older timestamp"
);
assert_eq!(stored.token_timestamp, 1_700_000_000);
assert_eq!(
stored.sender_timestamp,
Some(1_700_000_500),
"sender_timestamp must survive the first real token"
);
}
}