fix(receipt): preserve sender device in delivery receipt to - #649
Conversation
For multi-device LID senders the wire `<message from="USER:DEV@lid">` arrives with an explicit device. `build_delivery_receipt_node` was assembling `<receipt to=info.source.chat>` where `chat = from.to_non_ad()`, so the device was stripped before send. The LID server then replayed the stanza from the offline queue and eventually closed the stream with `<stream:error><ack class="message"/></stream:error>`. whatsmeow `buildBaseReceipt` echoes `node.Attrs["from"]` verbatim and the encoder writes an AD-JID with the device byte. WA Web `sendDeliveryReceiptsAfterDecryption` resolves `to` via `extractJidFromJidWithType(widToJidWithType(from))`, which for `lidDevice` returns `deviceJid` literally (device intact); the WAP encoder then writes `WapJid.createJidU(user, LID, device)`. Both keep the device. Match that: for non-group / non-status receipts, address `to` with `info.source.sender` (which is `from.clone()` and keeps the device). Group and status@broadcast receipts stay on `info.source.chat` since the group JID / status JID never carries a device. Verified empirically against a prod log: senders whose `from` had no device acked successfully; the three stream:errors all carried `from=...:7@lid`.
|
Warning Review limit reached
More reviews will be available in 10 minutes and 54 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
…fail gate Address the remaining review nits (all verified; no code blocker found): - handle_decrypt_failure self-fanout short-circuit now gates on `is_self_fanout() && should_send_delivery_receipt()`, matching the normal ack path's eligibility (coderabbit). is_self_fanout() stays the load-bearing prefix; the extra check only affects the unreachable empty-id case. - own_bot_author_dm_acks_not_sender_receipt: added a short settle window so a future regression that emitted a receipt on a later tick can't slip past the negative assertion. (No current race: ack_received_message is synchronous and the bot-author branch returns before the receipt branch.) - bot_self_fanout test now uses a device-bearing sender and asserts the device survives into the receipt `to` (end-to-end guard for #649, previously only covered by the isolated builder unit test). - is_self_fanout_matches_only_own_dm_with_recipient: isolated coverage for each exclusion, including the load-bearing chat.is_group() guard with is_group=false (the own-from parser path). - as_wire_str_round_trips_through_parse: guards ReceiptType::as_wire_str (the hand-maintained inverse of parse) against hyphen/underscore drift.
Summary
<message from=\"USER:DEV@lid\">carries an explicit device. The receipt was being addressed withinfo.source.chat(=from.to_non_ad()), stripping the device before send.<stream:error><ack class=\"message\"/></stream:error>. This is the residual cause of the disconnects that PR fix(offline): transport-ack stanzas with only unrecognized enc types #648 did not cover.buildBaseReceiptechoesnode.Attrs[\"from\"]verbatim (encoder writes AD-JID with device byte). WA WebsendDeliveryReceiptsAfterDecryptionresolvestoviaextractJidFromJidWithType(widToJidWithType(from))which forlidDevicereturnsdeviceJidliterally (device intact). Match that by addressingtowithinfo.source.senderfor DM / peer / self fanout. Group and status@broadcast keepinfo.source.chatsince the group/status JID has no device.Empirical confirmation (prod log)
fromhad no device (e.g.185323896221943@lid,119009819262985@lid) were acked successfully.from=...:7@lid, the only LID-routed DMs in the log with an explicit device.Test plan
cargo fmt --all,cargo clippy --all --tests(clean)cargo test -p whatsapp-rust(572 passed, 0 failed)cargo test -p e2e-tests --test offline_receipts --test offline_messages(8 passed)to; LID DM without device unchanged; grouptostays group_jid with participant; peer_msgtokeeps device; statustostays status@broadcast with participant + context.