Skip to content

fix(status): use LID addressing, skip unresolvable recipients - #568

Merged
jlucaso1 merged 4 commits into
mainfrom
fix/status-addressing-lid
Apr 18, 2026
Merged

jlucaso1 merged 4 commits into
mainfrom
fix/status-addressing-lid

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 18, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Reacting to a status from a LID-only contact (someone whose status the bot can decrypt but whose phone number it has never learned) used to fail permanently with:

No PN mapping for LID 148610331189399@lid. Ensure the recipient has been contacted previously.

Two compounding issues caused the break, and the fix has to touch both the addressing direction AND the device-resolution fallback that the new direction depends on:

  1. Cache-aside bypass. send_status_message called lid_pn_cache.get_phone_number directly — in-memory only. Mappings persisted in the backend but not yet loaded were invisible. Client::get_lid_pn_entry has a cache-aside fallback (PR fix: cache-aside fallback in get_lid_pn_entry #565) but the status path was not using it.

  2. Wrong addressing direction. The code forced every recipient to PN and aborted if any LID could not be resolved. That contradicts current WA Web behaviour. In WAWebEncryptAndSendStatusMsg:

    $ = r("compactMap")(x.list, o("WAWebLidMigrationUtils").toUserLid);

    WA Web maps every recipient through toUserLid (returns the LID or null), and compactMap silently drops the nulls. Since the LID 1:1 migration, status is LID-addressed, not PN.

  3. Device resolution in LID mode misses own companions. LID usync is unreliable for own JID. prepare_group_stanza already works around this via GroupInfo::phone_jid_for_lid_user + phone_device_jid_to_lid — but only if the GroupInfo has an lid_to_pn_map populated, and only in the full-SKDM path. resolve_skdm_targets had no equivalent fallback.

What changed

Addressing + resolution

  • send_status_message uses LID addressing. Each recipient is resolved to its LID form: LID passes through, PN goes through the cache-aside Client::get_lid_pn_entry. Unresolvable recipients are skipped silently. AddressingMode::Lid on the GroupInfo, signed with own_lid, emits addressing_mode="lid".
  • GroupInfo is built via with_lid_to_pn_map seeded with own's LID↔PN plus every resolved (PN→LID) recipient pair. That's the map prepare_group_stanza needs for its fallback to work on first send.
  • resolve_skdm_targets now takes &GroupInfo. In LID mode it queries devices via PN where the map has a mapping (same pattern as prepare_group_stanza), then converts the resulting device JIDs back to LID via phone_device_jid_to_lid so they match the cache and the stanza. Fixes the incremental-SKDM case where a new companion invalidated the device cache and the old direct-LID query missed own companions. Group sends benefit from the same fallback (bug was pre-existing there too).
  • Fail-fast for any recipient that is not a user JID (PN or LID). Error upfront rather than silently dropping non-user inputs through resolve_recipient_to_lid. Mirrors WA Web's asUserWidOrThrow inside toUserLid.
  • Refuse to send when device_snapshot.lid is None. Avoids emitting addressing_mode="lid" while signing with a PN sender.

Helpers

  • wacore::send::assemble_status_participants — pure helper, dedup + own-LID anchoring + empty-list guard. Linear Vec scan (no HashSet alloc). Six unit tests: dedup, compactMap-style skip, empty-after-skip error, ad-vs-bare own.
  • Client::resolve_recipient_to_lid — single DRY call site for per-JID PN→LID resolution. Mirrors WAWebLidMigrationUtils.toUserLid.
  • Sender-key derivation uses device-scoped own_lid.to_protocol_address() — matches WAWebSignalAddress.toString (device suffix included when non-zero) and stays consistent with prepare_group_stanza.

Evidence of wire-format parity

Observation WA Web whatsapp-rust (before) whatsapp-rust (after)
Addressing mode LID PN LID
Unresolvable recipient Silently dropped (compactMap) Hard abort Silently dropped
Non-user recipient Throws (asUserWidOrThrow) Only group/broadcast rejected All non-user rejected
PN→LID resolution getCurrentLid — (wrong direction) get_lid_pn_entry cache-aside
Device query for LID own PN fallback PN fallback via lid_to_pn_map
Sender-key address format user:device@lid.0 user:device@pn.0 user:device@lid.0

Test plan

  • cargo fmt --all
  • cargo clippy --all --tests --exclude e2e-tests — clean
  • cargo test --workspace --exclude e2e-tests --exclude bench-integration — 29 suites green (544 wacore lib + 12 wire_enum_serde + per-crate)
  • Six new unit tests in wacore::send::assemble_status_participants: dedup, compactMap-style skip, empty-list error, own-LID anchoring, ad-vs-bare handling
  • resolve_skdm_targets + prepare_group_stanza verified to share the same LID→PN fallback path
  • send_status_message seeds lid_to_pn_map for own + resolved recipients so the fallback has data to work with

Breaking changes

None at the caller surface. status().send_text(...) etc. still take &[Jid] — LIDs and PNs both work, with strictly more resolving correctly now. The wire format change (addressing_mode="lid" on status stanzas) matches WA Web and is what the server expects post-LID-migration.

The internal Client::resolve_skdm_targets signature changed from (group_jid, &[Jid], &Jid) to (group_jid, &GroupInfo, &Jid). pub(crate), so no public-API impact.

`send_status_message` used to force every recipient to PN addressing
via an in-memory-only lookup, then abort the whole send if any LID had
no PN mapping in the cache. That made status reactions to LID-only
contacts fail indefinitely — the bot can decrypt the status (Signal
state is under the LID), but the status sender has no way to learn the
poster's phone number without a DM or usync round-trip.

This mismatch also contradicts WA Web. `WAWebEncryptAndSendStatusMsg`
(`docs/captured-js/WAWeb/Encrypt/AndSendStatusMsg.js:46`) does:

    compactMap(x.list, WAWebLidMigrationUtils.toUserLid)

i.e. every recipient is mapped to its LID, and `compactMap` silently
drops entries that can't be resolved. Status is LID-addressed, not PN,
since the LID migration.

Changes:

- `send_status_message` now resolves each recipient to its LID form
  (LID passes through; PN goes through the cache-aside
  `Client::get_lid_pn_entry`, which falls back to the backend on a
  cold cache — added in PR #565). Unresolvable entries are skipped
  silently, matching WA Web. `AddressingMode::Lid` and `own_lid` drive
  the downstream encryption path.
- New pure helper `wacore::send::assemble_status_participants` owns
  the dedup + own-LID anchoring + empty-list guard. Linear Vec scan
  (no HashSet allocation — status lists are small) and it consumes
  the resolved JIDs by value, so CompactString lives exactly once per
  kept recipient. 6 unit tests cover dedup, compactMap-style skipping,
  the empty-after-skip error path, and the ad-vs-bare own-LID case.
- New helper `Client::resolve_recipient_to_lid` mirrors
  `WAWebLidMigrationUtils.toUserLid` and is the single call site for
  PN→LID resolution in the status path (DRY).
- The sender-key-name derivation and `resolve_skdm_targets` now key
  off `own_lid` to match the stanza's addressing mode.

Error only when every resolved recipient is unresolvable — a single
LID-only contact no longer breaks the whole send.
@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9770a548-643b-45af-ad18-b86f207d30ab

📥 Commits

Reviewing files that changed from the base of the PR and between 87f3b38 and 765a318.

📒 Files selected for processing (1)
  • src/send.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Recipient resolution now skips unresolvable entries and returns LID-form recipients for status messages.
    • Participant assembly added to deduplicate by user, preserve order, and ensure the sender is included.
  • Improvements

    • Status sending enforces sender LID, validates recipients, and uses LID-based addressing and updated sender-key handling.
  • Tests

    • Unit tests added for deduplication, sender anchoring, skipping unresolved recipients, and error cases.

Walkthrough

Adds LID-based recipient resolution and participant assembly for status messages: introduces Client::resolve_recipient_to_lid, updates send_status_message to require and use LIDs and skip unresolved recipients, and adds wacore::assemble_status_participants to filter, deduplicate, and anchor participants.

Changes

Cohort / File(s) Summary
LID resolution
src/client/lid_pn.rs
Added pub(crate) async fn resolve_recipient_to_lid(&self, jid: &Jid) -> Option<Jid>: returns bare LID for LID inputs, None for non-user inputs, resolves PN via get_lid_pn_entry (cache-aside); warns and returns None on lookup error.
Status sending
src/send.rs
send_status_message now requires device_snapshot.lid, validates recipients as user JIDs, resolves each via resolve_recipient_to_lid and omits None entries, delegates participant construction to wacore::assemble_status_participants, and uses AddressingMode::Lid and own_lid for sender-key/SKDM handling.
Participant assembly & tests
wacore/src/send.rs
Added pub fn assemble_status_participants<I>(resolved: I, own_lid: &Jid) -> anyhow::Result<Vec<Jid>>: drops None, deduplicates by jid.user preserving first occurrence, errors if result empty, ensures sender bare-user present, includes unit tests for dedupe, None-filtering, anchoring, and device-stripping.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Resolver as resolve_recipient_to_lid
    participant Cache as get_lid_pn_entry
    participant Assembler as assemble_status_participants
    participant Wacore as wacore::send

    Client->>Resolver: resolve_recipient_to_lid(jid)
    alt input is LID
        Resolver-->>Client: Some(bare LID)
    else input is PN
        Resolver->>Cache: get_lid_pn_entry(pn)
        alt OK(Some(entry))
            Cache-->>Resolver: Some(LID)
            Resolver-->>Client: Some(LID)
        else OK(None) or Err
            Cache-->>Resolver: None / Err
            Resolver-->>Client: None (warn on Err)
        end
    else non-user JID
        Resolver-->>Client: None
    end
    Client->>Assembler: [Option<Jid>, ...], own_lid
    Assembler->>Assembler: drop None -> dedupe by user -> ensure own bare user present
    Assembler-->>Client: Vec<Jid>
    Client->>Wacore: build GroupInfo(AddressingMode::Lid) & send
    Wacore-->>Client: status send result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: switching status sending to LID addressing mode and dropping unresolvable recipients, which are the central fixes across all modified files.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the motivation, specific issues fixed, implementation details, and testing performed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/status-addressing-lid

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/send.rs (1)

331-335: ⚠️ Potential issue | 🟠 Major

Require a real own LID before sending a LID-addressed status.

This path now assumes own_lid is the sender identity for sender-key lookup, participant anchoring, and exact-sender filtering in prepare_group_stanza. Falling back to own_jid here means we can emit addressing_mode="lid" while deriving the sender key from PN, and we can re-include the sending device in SKDM fanout. That should fail fast, not silently downgrade.

Suggested fix
-        let own_lid = device_snapshot
-            .lid
-            .take()
-            .unwrap_or_else(|| own_jid.clone());
+        let own_lid = device_snapshot
+            .lid
+            .take()
+            .ok_or_else(|| anyhow!("LID not set, cannot send status"))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/send.rs` around lines 331 - 335, The code currently falls back to own_jid
when device_snapshot.lid is missing which allows emitting addressing_mode="lid"
while using the wrong sender identity; change the logic around
device_snapshot.lid/own_lid (the variable set here and any callers like
prepare_group_stanza) to require a real LID: if device_snapshot.lid is None,
return an error or early-fail before constructing a LID-addressed status instead
of using unwrap_or_else(|| own_jid.clone()); ensure prepare_group_stanza and any
sender-key lookup / SKDM fanout logic use that guaranteed LID and do not proceed
when no device LID exists (fail fast with a clear error referencing
own_lid/device_snapshot.lid).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/send.rs`:
- Around line 336-345: The loop that currently rejects only
groups/status/broadcasts should instead fail fast for any non-user JID so
invalid types don't reach resolve_recipient_to_lid() and get silently dropped;
update the recipients validation in the block iterating over recipients to
assert the JID is a user (e.g., use an existing jid.is_user() check or compare
JID kind to the user/PN/LID variant) and return an Err with the same style
message when it's not, referencing the same identifiers (recipients, jid,
resolve_recipient_to_lid) so mixed valid+invalid lists cannot partially send.

---

Outside diff comments:
In `@src/send.rs`:
- Around line 331-335: The code currently falls back to own_jid when
device_snapshot.lid is missing which allows emitting addressing_mode="lid" while
using the wrong sender identity; change the logic around
device_snapshot.lid/own_lid (the variable set here and any callers like
prepare_group_stanza) to require a real LID: if device_snapshot.lid is None,
return an error or early-fail before constructing a LID-addressed status instead
of using unwrap_or_else(|| own_jid.clone()); ensure prepare_group_stanza and any
sender-key lookup / SKDM fanout logic use that guaranteed LID and do not proceed
when no device LID exists (fail fast with a clear error referencing
own_lid/device_snapshot.lid).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 03eaed02-d235-41c1-8249-2fc9c027b89a

📥 Commits

Reviewing files that changed from the base of the PR and between af5ef34 and b679e02.

📒 Files selected for processing (3)
  • src/client/lid_pn.rs
  • src/send.rs
  • wacore/src/send.rs

Comment thread src/send.rs Outdated
@github-actions

github-actions Bot commented Apr 18, 2026

Copy link
Copy Markdown

Benchmark Results

59 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 3,933 3,933 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 12,038 12,038 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 43,514 43,514 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 19,365 19,365 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 68,579 68,579 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 76,679 76,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 2,230 2,230 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 5,988 5,988 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 169,079 168,927 +0.1%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 190,846 190,994 -0.1%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 875,238 875,111 +0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 966,350 966,224 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,453,095 1,453,124 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 2,568,546 2,575,232 -0.3%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 9,376,128 9,375,557 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 44,461,018 44,459,925 +0.0%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 12,493,705 12,654,021 -1.3%
binary_benchmark::marshal_group::bench_marshal_allocating 71,247 71,247 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 71,300 71,300 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 98,367 98,367 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 78,801 78,801 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 71,347 71,347 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 7,518 7,518 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 7,561 7,561 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 9,273 9,273 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 530,504 530,504 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 530,072 530,072 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 531,427 531,427 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 8,506,160 8,506,160 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 8,450,412 8,450,412 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 19,677,947 19,677,947 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 2,468 2,468 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 33,558 33,558 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 787 787 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 526,732 526,732 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 4,986 4,986 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 5,315 5,315 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 61,874 61,874 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 5,347 5,347 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 61,942 61,942 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 6,734 6,734 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 85,564 85,564 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 477,570 477,570 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 11,563 11,563 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 17,177,594 17,299,502 -0.7%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 157,113 157,113 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 5,510,200 5,510,200 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 157,827 157,827 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 296,631 296,767 -0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 706,282 706,282 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 12,729,831 12,582,422 +1.2%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 27,593,344 27,674,546 -0.3%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 3,467,011 3,467,011 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 124,677,703 123,581,353 +0.9%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,830,452 2,830,452 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 46,003 46,003 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 5,072,844 5,072,844 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 316,083 316,083 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 14,255,917 14,255,917 +0.0%
No significant changes detected.

Two review findings confirmed against the current code; both
legitimate. Fixes:

- Reject any recipient that is not a user JID (PN or LID) upfront.
  Previously only group/status/broadcast JIDs errored; anything else
  (newsletter, hosted, server, etc.) silently became `None` in
  resolve_recipient_to_lid and got filtered by the compactMap-style
  skip, masking caller bugs. WA Web's `toUserLid` starts with
  `asUserWidOrThrow(e)` and propagates the throw, which is the
  behavior we now match.

- Refuse to send when `device_snapshot.lid` is None. The prior
  `unwrap_or_else(|| own_jid.clone())` would emit
  `addressing_mode="lid"` on the stanza while signing with the PN
  sender, and the sender key would be stored under the wrong address.
  Post-LID-migration every real account has a device LID, so the
  missing-LID branch is a "not ready to send status" state, not
  something to paper over.

Both wire behaviors (reject non-user and require device LID) match
WA Web; the existing `assemble_status_participants` tests already
cover downstream behavior, and the whole workspace test suite stays
green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/send.rs`:
- Around line 295-308: The docblock for status message LID addressing is too
verbose and implementation-specific; trim it to state the external contract
(e.g., "Status messages use LID addressing: recipients already in LID pass
through, PN recipients are resolved to LID via cache-aside lookup, and
unresolved recipients are silently skipped; resulting GroupInfo uses
AddressingMode::Lid and stanzas are signed with own_lid and emit
addressing_mode=\"lid\""). Remove file-paths, step-by-step pipeline rationale
(e.g., WAWebEncryptAndSendStatusMsg, WAWebLidMigrationUtils, compactMap) and
move those details into tests or a design note; keep references to
Client::get_lid_pn_entry, GroupInfo, AddressingMode::Lid, prepare_group_stanza
and own_lid only as needed to state behavior, not implementation.
- Around line 372-377: The sender-key lookup uses a device-scoped own_lid but
receiver-side expects a bare LID; change the construction of the sender key so
it normalizes own_lid to the non-device (bare) LID before converting to protocol
address: call own_lid.to_non_ad().to_protocol_address() and use that result when
creating the SenderKeyName in the block that currently calls
SenderKeyName::from_parts(&to_str, sender_address.as_str()); also verify
prepare_group_stanza uses the same bare form (replace any use of
own_lid.to_protocol_address() with own_lid.to_non_ad().to_protocol_address()
where this status path builds sender keys) so both write and read use the same
user@lid identity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c564e519-1f53-4926-88b9-6babc7d87cc7

📥 Commits

Reviewing files that changed from the base of the PR and between b679e02 and 083f903.

📒 Files selected for processing (1)
  • src/send.rs

Comment thread src/send.rs Outdated
Comment thread src/send.rs
Comment on lines +372 to 377
// Sender key name tracks the addressing mode of the group stanza.
// Since status now uses LID addressing (see send_status_message
// header), the key is stored under own_lid, matching the address
// prepare_group_stanza derives internally.
let sender_address = own_lid.to_protocol_address();
let sender_key_name = SenderKeyName::from_parts(&to_str, sender_address.as_str());

@coderabbitai coderabbitai Bot Apr 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Normalize the sender-key identity to the bare LID before building the key name.

Right now this keys sender-key state off the device-scoped own_lid. src/message.rs loads sender keys with info.source.sender.to_non_ad(), so this status path can write/look up user:device@lid while receive-side lookup uses user@lid. That mismatch will keep missing cached sender-key state and can break follow-up status decrypt/reuse. Use own_lid.to_non_ad().to_protocol_address() here, and make sure prepare_group_stanza is using the same bare form.

Suggested fix
         let force_skdm = {
             use wacore::libsignal::store::sender_key_name::SenderKeyName;
@@
-            let sender_address = own_lid.to_protocol_address();
+            let sender_address = own_lid.to_non_ad().to_protocol_address();
             let sender_key_name = SenderKeyName::from_parts(&to_str, sender_address.as_str());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/send.rs` around lines 372 - 377, The sender-key lookup uses a
device-scoped own_lid but receiver-side expects a bare LID; change the
construction of the sender key so it normalizes own_lid to the non-device (bare)
LID before converting to protocol address: call
own_lid.to_non_ad().to_protocol_address() and use that result when creating the
SenderKeyName in the block that currently calls
SenderKeyName::from_parts(&to_str, sender_address.as_str()); also verify
prepare_group_stanza uses the same bare form (replace any use of
own_lid.to_protocol_address() with own_lid.to_non_ad().to_protocol_address()
where this status path builds sender keys) so both write and read use the same
user@lid identity.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the statement "receiver-side expects bare LID" in docs/captured-js/WAWeb/Signal/Address.js:15-34 and Signal/CommonUtils.js:30-48:

toString = function () {

var t = this.wid.device != null && this.wid.device !== 0 ? ":" + this.wid.device : "";

...

return [u.user, t, "@lid"].join("");

}

WA Web includes :device in the name when device != 0, and createSignalLikeSenderKeyName uses this toString() + .0. It forms device-scoped, not bare.

The current code uses own_lid.to_protocol_address() both in the force_skdm check and within prepare_group_stanza (line 1054). Both match each other and match with WA Web. Changing to .to_non_ad().to_protocol_address() would break:

Parity with WA Web receivers (which expect device-scoped).

Consistency with the group send path, which already works with device-scoped.

Applying this finding would introduce a bug where there wasn't one. The reviewer started from an incorrect premise about what the receiver reads.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jlucaso1, you're right — I was wrong on the underlying premise. WA Web's SenderKeyName derivation is device-scoped: toString() includes :device when device !== 0, and createSignalLikeSenderKeyName appends .0 to that full device-scoped address. The current code using own_lid.to_protocol_address() (device-scoped) is consistent between the force-SKDM check and prepare_group_stanza, and matches what WA Web produces. My suggestion to normalize to .to_non_ad() would have broken that parity and introduced a real bug. I'm withdrawing this comment — the implementation is correct as-is.


✏️ Learnings added
Learnt from: jlucaso1
Repo: oxidezap/whatsapp-rust PR: 568
File: src/send.rs:374-379
Timestamp: 2026-04-18T16:30:22.140Z
Learning: In the whatsapp-rust codebase (src/send.rs), WA Web derives sender-key names using a device-scoped LID address, not a bare one. WA Web's Signal/Address.js toString() includes `:device` when device != 0, and createSignalLikeSenderKeyName appends `.0` to that full string. So the correct form for SenderKeyName construction is `own_lid.to_protocol_address()` (device-scoped), NOT `own_lid.to_non_ad().to_protocol_address()`. Both the force_skdm check in send_status_message and prepare_group_stanza use device-scoped own_lid consistently, matching WA Web behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 083f903350

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/send.rs Outdated
First review finding is valid: the docblock referenced a captured-js
file path and line number, which the project style rule
(.claude/rules/code-style.md) forbids. Trimmed both the
`send_status_message` header and `assemble_status_participants` comment
to state the external contract plus a WA Web module name (allowed per
the same rule), dropping the pipeline/compactMap rationale that
duplicates what the code already shows.

Second review finding is rejected after verifying against WA Web.
`WAWebSignalAddress.toString` (docs/captured-js/WAWeb/Signal/Address.js)
includes the device in the sender-key name when device != 0, and
`createSignalLikeSenderKeyName` wraps that via SignalAddress + ".0".
Our code matches: both `send_status_message` (force_skdm check) and
`prepare_group_stanza` derive the sender address from the device-scoped
`own_lid` via `to_protocol_address()`. Switching to `to_non_ad()` would
break parity with WA Web receivers and diverge from the working group
send path.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87f3b38a83

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/send.rs Outdated
Both Codex findings confirmed against the current code.

send_status_message now populates GroupInfo's lid_to_pn_map with own
LID↔PN plus every resolved recipient whose input was a PN. Without this,
prepare_group_stanza's full-SKDM path (force_skdm=true on first status
send) could not translate LID participants back to PN for device
resolution, and LID usync on own JID is unreliable — own companion
devices would drop out of the distribution list.

resolve_skdm_targets now takes &GroupInfo and applies the same LID→PN
fallback prepare_group_stanza uses: in LID mode, LID participants with
a known PN mapping are queried via PN; the resulting device JIDs are
converted back to LID via phone_device_jid_to_lid so they match the
cache and the stanza's addressing. Fixes the incremental-SKDM case
(force_skdm=false) where a newly paired companion invalidated the
sender-key-devices cache and resolve_skdm_targets returned an LID
device list that missed own companions.

Group sends benefit from the same fallback (the bug was pre-existing
there too — groups exercise the same LID usync path).
@jlucaso1
jlucaso1 merged commit dfdfc95 into main Apr 18, 2026
14 checks passed
@jlucaso1
jlucaso1 deleted the fix/status-addressing-lid branch April 18, 2026 16:35

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 765a31821d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/client/lid_pn.rs
e
);
None
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate recipient mapping lookup failures

When get_lid_pn_entry fails (e.g., transient DB/backend outage), this path converts the error into None, and send_status_message treats that recipient as merely “unresolvable” and silently drops it. That means status sends can partially succeed while omitting valid recipients without surfacing an error to callers, which is especially risky for status revoke flows that must target the original audience. Lookup failures should be surfaced distinctly from a true “no mapping” miss.

Useful? React with 👍 / 👎.

jlucaso1 added a commit that referenced this pull request Apr 18, 2026
Status reactions started getting NACK'd with 479 SmaxInvalid after
PR #568 actually put stanzas on the wire (previously they aborted
client-side). Root cause: `send_status_message` was attaching
`<meta status_setting>` to every non-revoke send, including reactions.

That meta describes the POSTER's privacy on their own status. WA Web
attaches it only inside `WAWebEncryptAndSendStatusMsg`, which runs
for status posts. Reactions route through `WAWebSendReactionMsgAction`
→ `WAWebSendAddonMsgChatAction` → `sendAddonRecord` and never visit
the status-post path, so the meta is absent. Baileys doesn't emit
`status_setting` at all. Only whatsapp-rust was gratuitously attaching
it to reactions, which the server rejects.

Extracted `wacore::send::status_carries_privacy_meta(&Message)` as a
pure helper (true only for actual posts — not reactions, not revokes)
and drove `send_status_message` off it. Six unit tests pin the
classification: text post, image post, reaction, enc-reaction, revoke,
non-revoke protocol message.

Fixes reactions to LID-only contacts not appearing on the poster's
side and the resulting silent failure in consumer bots.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant