Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions src/handlers/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,10 @@ async fn handle_privacy_token_notification(client: &Arc<Client>, node: &Node) {
use wacore::iq::tctoken::parse_privacy_token_notification;
use wacore::store::traits::TcTokenEntry;

// Resolve the sender to a LID JID for storage.
// Extract from JID upfront for LID resolution and presence re-subscription.
let from_jid = node.attrs().optional_jid("from");

// Resolve the sender to a LID key for storage.
// WA Web uses `sender_lid` attr if present, otherwise resolves from `from`.
let sender_lid = node
.attrs()
Expand All @@ -557,27 +560,27 @@ async fn handle_privacy_token_notification(client: &Arc<Client>, node: &Node) {
Some(lid) if !lid.is_empty() => lid,
_ => {
// Fall back to resolving from the `from` JID via LID-PN cache
let from_jid = match node.attrs().optional_jid("from") {
let from = match &from_jid {
Some(jid) => jid,
None => {
warn!(target: "Client/TcToken", "privacy_token notification missing 'from' attribute");
return;
}
};

if from_jid.is_lid() {
from_jid.user.clone()
if from.is_lid() {
from.user.clone()
} else {
// Try to resolve phone number to LID
match client.lid_pn_cache.get_current_lid(&from_jid.user).await {
match client.lid_pn_cache.get_current_lid(&from.user).await {
Some(lid) => lid,
None => {
debug!(
target: "Client/TcToken",
"Cannot resolve LID for privacy_token sender {}, storing under PN",
from_jid
from
);
from_jid.user.clone()
from.user.clone()
}
}
}
Expand All @@ -599,10 +602,16 @@ async fn handle_privacy_token_notification(client: &Arc<Client>, node: &Node) {
}

let backend = client.persistence_manager.backend();
let mut token_stored = false;

for received in &received_tokens {
match backend.get_tc_token(&sender_lid).await {
Ok(Some(existing)) => {
// Skip if token bytes are identical (matches WA Web arrayBuffersEqualUNSAFE check)
if existing.token == received.token {
continue;
}

// Timestamp monotonicity guard: only store if incoming >= existing
if received.timestamp < existing.token_timestamp {
debug!(
Expand All @@ -624,6 +633,7 @@ async fn handle_privacy_token_notification(client: &Arc<Client>, node: &Node) {
warn!(target: "Client/TcToken", "Failed to update tc_token for {}: {e}", sender_lid);
} else {
debug!(target: "Client/TcToken", "Updated tc_token for {} (t={})", sender_lid, received.timestamp);
token_stored = true;
}
}
Ok(None) => {
Expand All @@ -638,13 +648,23 @@ async fn handle_privacy_token_notification(client: &Arc<Client>, node: &Node) {
warn!(target: "Client/TcToken", "Failed to store tc_token for {}: {e}", sender_lid);
} else {
debug!(target: "Client/TcToken", "Stored new tc_token for {} (t={})", sender_lid, received.timestamp);
token_stored = true;
}
}
Err(e) => {
warn!(target: "Client/TcToken", "Failed to read tc_token for {}: {e}, skipping", sender_lid);
}
}
}

// Re-subscribe presence for the sender if we have an active subscription and stored a new token.
// Matches WA Web: PresenceCollection.reSubscribeWhenActive(wid) after handleIncomingTcToken.
if token_stored && let Some(from) = &from_jid {
let is_subscribed = client.presence_subscriptions.lock().await.contains(from);
if is_subscribed && let Err(e) = client.presence().subscribe(from).await {
debug!(target: "Client/TcToken", "Failed to re-subscribe presence for {from}: {e}");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}
}

/// Handle business notification (WhatsApp Web: `WAWebHandleBusinessNotification`).
Expand Down
41 changes: 41 additions & 0 deletions tests/e2e/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::sync::Arc;

use wacore::store::traits::TcTokenEntry;
use wacore::types::events::{Event, EventHandler};
use whatsapp_rust::Jid;
use whatsapp_rust::bot::Bot;
Expand Down Expand Up @@ -172,6 +173,46 @@ impl TestClient {
.to_non_ad()
}

/// Get the storage key used for this client's tcToken entries.
///
/// Notification handling stores tcTokens under the sender's LID when it is
/// available, otherwise it falls back to the phone-number user part.
pub async fn tc_token_key(&self) -> anyhow::Result<String> {
if let Some(lid) = self.client.get_lid().await {
return Ok(lid.user);
}

self.client
.get_pn()
.await
.map(|jid| jid.user)
.ok_or_else(|| anyhow::anyhow!("Client should have a JID after connect"))
}

/// Wait until a tcToken entry exists for the given storage key.
pub async fn wait_for_tc_token(
&self,
jid_key: &str,
timeout_secs: u64,
) -> anyhow::Result<TcTokenEntry> {
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(timeout_secs);

loop {
if let Some(entry) = self.client.tc_token().get(jid_key).await? {
return Ok(entry);
}

if tokio::time::Instant::now() >= deadline {
return Err(anyhow::anyhow!(
"Timed out waiting for tc_token entry for {}",
jid_key
));
}

tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
}

// ── Event waiting ───────────────────────────────────────────────────────

/// Wait for an event matching the predicate, with a timeout in seconds.
Expand Down
Loading
Loading