-
-
Notifications
You must be signed in to change notification settings - Fork 127
feat: unknown device detection and deferred device sync #480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -160,6 +160,14 @@ async fn handle_ib_impl(client: Arc<Client>, node: &Node) { | |
|
|
||
| debug!(target: "Client/OfflineSync", "Offline sync completed, received {} items", count); | ||
| client.complete_offline_sync(count); | ||
|
|
||
| let client_clone = Arc::clone(&client); | ||
| client | ||
| .runtime | ||
| .spawn(Box::pin(async move { | ||
| client_clone.flush_pending_device_sync().await; | ||
| })) | ||
| .detach(); | ||
|
Comment on lines
+164
to
+175
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait for the offline pipeline to drain before flushing. This is fired off the terminal Suggested fix debug!(target: "Client/OfflineSync", "Offline sync completed, received {} items", count);
client.complete_offline_sync(count);
-
- let client_clone = Arc::clone(&client);
- client
- .runtime
- .spawn(Box::pin(async move {
- client_clone.flush_pending_device_sync().await;
- }))
- .detach();
+ client.wait_for_offline_delivery_end().await;
+ if !client.is_shutting_down() {
+ client.flush_pending_device_sync().await;
+ }🤖 Prompt for AI Agents |
||
| } | ||
| "thread_metadata" => { | ||
| // Present in some sessions; safe to ignore for now until feature implemented. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1025,6 +1025,17 @@ impl Client { | |
|
|
||
| match decrypt_result { | ||
| Ok(padded_plaintext) => { | ||
| // WA Web: isFromKnownDevice() in preProcessMsg | ||
| if !self.is_from_known_device(&info.source.sender).await { | ||
| warn!( | ||
| "[msg:{}] Unknown device {}, triggering device sync", | ||
| info.id, info.source.sender | ||
| ); | ||
| self.handle_unknown_device_sync(info).await; | ||
| self.spawn_retry_receipt(info, RetryReason::UnknownCompanionNoPrekey); | ||
| continue; | ||
| } | ||
|
|
||
| if let Err(e) = self | ||
| .clone() | ||
| .handle_decrypted_plaintext( | ||
|
|
@@ -1058,15 +1069,24 @@ impl Client { | |
| continue; | ||
| } | ||
|
|
||
| // No sender key for this group/sender — the SKDM was never received | ||
| // (sender thinks we have it from a previous status/session). | ||
| // Send retry receipt to ask sender to re-distribute SKDM. | ||
| let is_unknown_device = !self.is_from_known_device(&info.source.sender).await; | ||
| let retry_reason = if is_unknown_device { | ||
| RetryReason::UnknownCompanionNoPrekey | ||
| } else { | ||
| RetryReason::NoSession | ||
| }; | ||
|
|
||
| warn!( | ||
| "No sender key state for group message [msg:{}] from {}: {}. Sending retry receipt.", | ||
| info.id, info.source.sender, msg | ||
| ); | ||
|
|
||
| if is_unknown_device { | ||
| self.handle_unknown_device_sync(info).await; | ||
| } | ||
|
|
||
| self.dispatch_undecryptable_event(info, decrypt_fail_mode); | ||
| self.spawn_retry_receipt(info, RetryReason::NoSession); | ||
| self.spawn_retry_receipt(info, retry_reason); | ||
| } | ||
| Err(e) => { | ||
| if info.is_expired_status() { | ||
|
|
@@ -1092,6 +1112,26 @@ impl Client { | |
| Ok(()) | ||
| } | ||
|
|
||
| /// WA Web: online → `syncDeviceListJob`, offline → `OfflinePendingDeviceCache`. | ||
| async fn handle_unknown_device_sync(self: &Arc<Self>, info: &MessageInfo) { | ||
| let user_jid = info.source.sender.to_non_ad(); | ||
|
|
||
| if info.offline.is_some() { | ||
| log::debug!("Queueing {} for pending device sync (offline)", user_jid); | ||
| self.pending_device_sync.add(user_jid).await; | ||
| } else { | ||
| log::debug!("Triggering immediate device sync for {}", user_jid); | ||
| let client = Arc::clone(self); | ||
| self.runtime | ||
| .spawn(Box::pin(async move { | ||
| if let Err(e) = client.get_user_devices(&[user_jid]).await { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Calling Useful? React with 👍 / 👎. |
||
| log::warn!("Immediate device sync failed: {e:?}"); | ||
| } | ||
| })) | ||
| .detach(); | ||
|
Comment on lines
+1117
to
+1136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== handle_unknown_device_sync =="
sed -n '1115,1135p' src/message.rs
echo
echo "== get_user_devices / flush_pending_device_sync definitions =="
rg -nP --type rust -C8 '^\s*(pub(?:\([^)]*\))?\s+)?async\s+fn\s+(get_user_devices|flush_pending_device_sync)\b'
echo
echo "== LID->PN normalization around device queries =="
rg -n --type rust -C4 'get_phone_number|phone_jid_for_lid_user|sender_alt|is_lid\(|to_non_ad\(|pending_device_sync'Repository: jlucaso1/whatsapp-rust Length of output: 50379 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== handle_unknown_device_sync function signature and context ==="
sed -n '1100,1140p' src/message.rs | head -50
echo
echo "=== Client fields and lid_pn_cache access ==="
rg -n 'struct Client|pub.*lid_pn_cache' src/client.rs | head -20
echo
echo "=== Check if sender_alt is available in handle_unknown_device_sync (MessageInfo struct) ==="
rg -n 'pub struct MessageInfo|pub sender_alt' --type rust wacore/ | grep -A2 MessageInfo
echo
echo "=== Examples of sender_alt usage for LID→PN conversion elsewhere ==="
rg -n 'sender_alt' src/message.rs | head -10Repository: jlucaso1/whatsapp-rust Length of output: 2628 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== MessageInfo struct with sender_alt field ==="
sed -n '132,150p' wacore/src/types/message.rs
echo
echo "=== Verify sender_alt is populated in group messages with LID senders ==="
sed -n '170,195p' wacore/src/messages.rs
echo
echo "=== cache_lid_pn_from_message implementation ==="
rg -n 'cache_lid_pn_from_message' src/message.rs -A 10 | head -20Repository: jlucaso1/whatsapp-rust Length of output: 2736 Use
Derive the query JID as:
This matches the pattern used elsewhere in the codebase (e.g., 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| async fn handle_decrypted_plaintext( | ||
| self: Arc<Self>, | ||
| enc_type: &str, | ||
|
|
@@ -3833,6 +3873,7 @@ mod tests { | |
| verified_name: None, | ||
| device_sent_meta: None, | ||
| ephemeral_expiration: None, | ||
| offline: None, | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| //! Batches unknown-device users during offline sync for deferred usync. | ||
| //! WA Web: `OfflinePendingDeviceCache` + `doPendingDeviceSync()`. | ||
|
|
||
| use std::collections::HashSet; | ||
| use wacore_binary::jid::Jid; | ||
|
|
||
| pub(crate) struct PendingDeviceSync { | ||
| pending: async_lock::Mutex<HashSet<Jid>>, | ||
| } | ||
|
|
||
| impl PendingDeviceSync { | ||
| pub(crate) fn new() -> Self { | ||
| Self { | ||
| pending: async_lock::Mutex::new(HashSet::new()), | ||
| } | ||
| } | ||
|
|
||
| pub(crate) async fn add(&self, jid: Jid) { | ||
| self.pending.lock().await.insert(jid); | ||
| } | ||
|
|
||
| pub(crate) async fn take_all(&self) -> Vec<Jid> { | ||
| self.pending.lock().await.drain().collect() | ||
| } | ||
|
|
||
| pub(crate) async fn clear(&self) { | ||
| self.pending.lock().await.clear(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -146,6 +146,8 @@ pub struct MessageInfo { | |
| pub device_sent_meta: Option<DeviceSentMeta>, | ||
| /// Ephemeral duration in seconds, extracted from `contextInfo.expiration`. | ||
| pub ephemeral_expiration: Option<u32>, | ||
| /// Stanza `offline` attribute. `Some` = offline delivery, `None` = online. | ||
| pub offline: Option<bool>, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify whether `MessageInfo.offline` is actually used as a tri-state anywhere.
rg -n --type rust '\boffline\s*:\s*(Some\(true\)|Some\(false\)|None)' .
rg -n --type rust '\.offline\b' .Repository: jlucaso1/whatsapp-rust Length of output: 216 Model Current usage only creates 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| impl MessageInfo { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This schedules exactly one
flush_pending_device_sync()call 2 seconds after the offline end marker, but unknown-device entries are enqueued fromhandle_unknown_device_syncwhile offline messages are still being processed. Under large offline backlogs, message handlers can continue adding users after this one-shot flush runs, leaving those users stuck inPendingDeviceSyncwith no later trigger to flush them, so device lists never refresh and retries can keep failing for those senders.Useful? React with 👍 / 👎.