Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3428,6 +3428,20 @@ impl Client {
self.send_raw_bytes(plaintext_buf).await
}

/// Register a oneshot waiter for a server ack by message ID.
/// Returns the receiver — caller sends the node separately and awaits this in background.
pub(crate) async fn register_ack_waiter(
&self,
message_id: &str,
) -> futures::channel::oneshot::Receiver<wacore_binary::Node> {
let (tx, rx) = futures::channel::oneshot::channel();
self.response_waiters
.lock()
.await
.insert(message_id.to_string(), tx);
rx
}

pub(crate) async fn update_push_name_and_notify(self: &Arc<Self>, new_name: String) {
let device_snapshot = self.persistence_manager.get_device_snapshot().await;
let old_name = device_snapshot.push_name.clone();
Expand Down
79 changes: 73 additions & 6 deletions src/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,17 +465,29 @@ impl Client {
.ensure_status_participants(prepared.node, &group_info)
.await?;

let our_phash = stanza
.attrs()
.optional_string("phash")
.map(|s| s.into_owned());
let ack_rx = if our_phash.is_some() {
Some(self.register_ack_waiter(&request_id).await)
} else {
None
};

self.send_node(stanza).await?;

if let Some(rx) = ack_rx {
self.spawn_phash_validation(rx, our_phash.unwrap(), to.clone(), false);
}

self.update_sender_key_devices(&to_str, &prepared.skdm_devices)
.await;

// Invalidate device registry for users whose devices returned 406
for user in &prepared.stale_device_users {
self.invalidate_device_cache(user).await;
}

// Flush cached Signal state to DB after encryption
if let Err(e) = self.flush_signal_cache().await {
log::error!("Failed to flush signal cache after send_status_message: {e:?}");
}
Expand Down Expand Up @@ -604,6 +616,47 @@ impl Client {
self.sender_key_device_cache.invalidate(group_jid).await;
}

/// Spawn a background task to validate phash from server ack.
/// On mismatch, invalidates sender key device cache and group info cache.
fn spawn_phash_validation(
&self,
rx: futures::channel::oneshot::Receiver<wacore_binary::Node>,
our_phash: String,
jid: Jid,
invalidate_group_cache: bool,
) {
let Some(client) = self.self_weak.get().and_then(|w| w.upgrade()) else {
return;
};
self.runtime
.spawn(Box::pin(async move {
let ack = match tokio::time::timeout(
std::time::Duration::from_secs(10),
rx,
)
.await
{
Ok(Ok(node)) => node,
_ => return,
};
if let Some(server) = ack.attrs().optional_string("phash")
&& *server != our_phash
{
log::warn!(
"Phash mismatch for {jid}: ours={our_phash}, server={server}. Invalidating caches."
);
client
.sender_key_device_cache
.invalidate(&jid.to_string())
.await;
if invalidate_group_cache {
client.get_group_cache().await.invalidate(&jid).await;
}
}
}))
.detach();
}

/// Ensure the status stanza has a <participants> node listing all recipient
/// user JIDs. WhatsApp Web's `participantList` uses bare USER JIDs (not
/// device JIDs) — `<to jid="user@s.whatsapp.net"/>` — to tell the server
Expand Down Expand Up @@ -1059,15 +1112,29 @@ impl Client {
.await?
};

let our_phash = stanza_to_send
.attrs()
.optional_string("phash")
.map(|s| s.into_owned());
let ack_rx = if our_phash.is_some() {
let msg_id = stanza_to_send.attrs().optional_string("id");
Some(
self.register_ack_waiter(msg_id.as_deref().unwrap_or_default())
.await,
)
} else {
None
};

self.send_node(stanza_to_send).await?;

// Update SKDM recipient cache AFTER server ACK (matches WhatsApp Web behavior).
// WA Web only calls markHasSenderKey() after the server confirms receipt.
if let Some(rx) = ack_rx {
self.spawn_phash_validation(rx, our_phash.unwrap(), tc_issue_target.clone(), true);
}

if let Some(update) = skdm_update {
self.update_sender_key_devices(&update.to_str, &update.devices)
.await;
// Invalidate device registry for users whose devices returned 406
// so the next send re-fetches from server (without stale devices)
for user in &update.stale_users {
self.invalidate_device_cache(user).await;
}
Expand Down
Loading