Skip to content

fix(status): drop <meta status_setting> on reactions and revokes - #569

Merged
jlucaso1 merged 3 commits into
mainfrom
fix/status-reaction-drop-meta
Apr 18, 2026
Merged

jlucaso1 merged 3 commits into
mainfrom
fix/status-reaction-drop-meta

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 18, 2026

Copy link
Copy Markdown
Collaborator

Motivation

After PR #568 fixed the client-side abort on LID-only recipients, status reactions actually reached the server — and started coming back as NACK with error=\"479\" (SmaxInvalid). Reactions never appeared on the poster's side.

Observed wire exchange:

⟶  <message addressing_mode=\"lid\" to=\"status@broadcast\" type=\"reaction\">
     <participants>
       <to jid=\"271060335329480@lid\"/>
       <to jid=\"236395184570386@lid\"/>
     </participants>
     <enc decrypt-fail=\"hide\" type=\"skmsg\" v=\"2\">...</enc>
     <meta status_setting=\"contacts\"/>      ← trigger
   </message>

⟵  <ack error=\"479\"/>                        ← 160 ms later

Cause

send_status_message attached <meta status_setting> to every non-revoke send. That semantic is wrong: status_setting describes the poster's privacy on their own status — meaningless when reacting to someone else's content. WA Web handles the split architecturally:

  • Status post: WAWebEncryptAndSendStatusMsg attaches the meta, reading WAWebUserPrefsStatus.getStatusList().setting.
  • Reaction: WAWebSendReactionMsgActionWAWebSendAddonMsgChatActionsendAddonRecord. Never visits the status-post path, never attaches the meta.

Upstream Baileys doesn't emit status_setting on any stanza (grep status_setting in WhiskeySockets/Baileys/src/Socket/messages-send.ts returns zero).

whatsapp-rust was the lone outlier, gratuitously attaching the meta to every status-shaped send regardless of whether it was an actual post or a reaction.

Fix

New pure helper in wacore::send:

pub fn status_carries_privacy_meta(message: &wa::Message) -> bool

Returns true only for actual status posts — not reactions (reaction_message or enc_reaction_message), not revokes (protocol_message.type == Revoke). Descends ephemeral_message / device_sent_message / view-once wrappers via unwrap_message (same pattern stanza_type_from_message uses) so a reaction nested inside a wrapper is still detected.

send_status_message calls this before building extra_stanza_nodes. Reactions and revokes ship without the meta; posts keep it.

Tests

Eight unit tests in wacore::send::tests::status_carries_privacy_meta:

  • true_for_text_postExtendedTextMessage
  • true_for_image_postImageMessage
  • false_for_reactionReactionMessage (primary regression guard for 479)
  • false_for_enc_reactionEncReactionMessage
  • false_for_revokeProtocolMessage { type: Revoke }
  • true_for_non_revoke_protocol_message — e.g. EphemeralSetting
  • false_for_reaction_inside_ephemeral_wrapper — wrapped-reaction regression guard
  • false_for_revoke_inside_device_sent_wrapper — wrapped-revoke regression guard

Perf polish

Two small Jid-allocation wins landed alongside the fix:

  • assemble_status_participants: compare against own_lid.user before calling to_non_ad(), so the non-ad copy only runs when the push is actually needed. Saves one Jid clone per call when own is already in the resolved list.
  • resolve_skdm_targets: inspect is_lid() / user on the borrowed &Jid before picking PN vs LID, instead of pre-allocating base = jid.to_non_ad() and then discarding it on the PN branch. Saves one Jid clone per participant that has a phone mapping (typically own + any PN-resolved recipient).

Pure refactors, no behavior change.

Verification

  • cargo fmt --all
  • cargo clippy --all --tests --exclude e2e-tests — clean
  • cargo test --workspace --exclude e2e-tests --exclude bench-integration — 29 suites green

Scope note

The proposal that surfaced this bug also claimed upstream Baileys emits reactions as type=\"text\" rather than \"reaction\". I verified against WhiskeySockets/Baileys/master/src/Socket/messages-send.ts:1050-1051: Baileys does emit 'reaction' for reaction messages, same as whatsapp-rust and WA Web. Not a divergence; not addressed here.

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.
@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR delegates status privacy-meta decision to wacore::send::status_carries_privacy_meta(&message), updates Client::send_status_message to use that predicate, and tweaks SKDM target resolution for LID-mode participant JID mapping to use jid.is_lid() / &jid.user directly.

Changes

Cohort / File(s) Summary
Client send changes
src/send.rs
Client::send_status_message now uses wacore::send::status_carries_privacy_meta(&message) for deciding the <meta status_setting="..."/> stanza nodes. Also adjusted SKDM target resolution for LID-mode participant mapping to check jid.is_lid() and use &jid.user.
Centralized predicate & tests
wacore/src/send.rs
Added pub fn status_carries_privacy_meta(message: &wa::Message) -> bool which unwraps wrappers and returns false for revokes and reactions; updated assemble_status_participants anchoring logic (compare against own_lid.user and push own_lid.to_non_ad()). Added unit tests covering post, image, protocol, reaction, encrypted-reaction, and wrapped cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: removing the stanza from reactions and revokes, which is the core bug being addressed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The PR description precisely maps the bug (status_setting incorrectly attached to reactions), root cause, and fix with clear wire-level evidence and test coverage.

✏️ 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-reaction-drop-meta

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

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

Inline comments:
In `@wacore/src/send.rs`:
- Around line 1431-1437: The function status_carries_privacy_meta should inspect
the unwrapped/inner message like the other classifiers do instead of only
top-level fields; modify status_carries_privacy_meta to first descend into
wrappers (e.g., ephemeral_message and device_sent_message) to obtain the inner
wa::Message reference and then check that inner message's protocol_message,
reaction_message, and enc_reaction_message fields (symbols: ephemeral_message,
device_sent_message, protocol_message, reaction_message, enc_reaction_message)
so Revokes and Reactions wrapped inside those containers are detected and the
privacy meta is not attached.
🪄 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: 0d462190-9dfd-45c1-8308-cbc8d3849477

📥 Commits

Reviewing files that changed from the base of the PR and between dfdfc95 and 8076193.

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

Comment thread wacore/src/send.rs
@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,085 169,514 -0.3%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 190,994 190,994 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 875,179 875,188 -0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 966,356 966,193 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,453,163 1,453,159 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 2,575,083 2,575,315 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 9,341,156 9,342,215 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 44,460,067 44,459,193 +0.0%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 12,684,128 12,729,713 -0.4%
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,267,002 17,334,626 -0.4%
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,767 296,699 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 706,214 706,282 -0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 12,537,711 12,453,641 +0.7%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 27,375,481 27,514,737 -0.5%
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() 126,434,943 125,717,423 +0.6%
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.

Match the existing classifier pattern in this file (see
stanza_type_from_message): call unwrap_message() before inspecting
protocol_message / reaction_message / enc_reaction_message, so a
reaction or revoke wrapped inside ephemeral_message,
device_sent_message, or any view-once wrapper is detected and the
privacy meta is still omitted. Without this, a wrapped reaction
would slip past and re-trigger the 479 SmaxInvalid NACK the PR is
meant to fix.

Two regression tests added: reaction inside ephemeral_message and
revoke inside device_sent_message.
- assemble_status_participants: check own_lid.user before allocating a
  non-ad copy, so the final to_non_ad() only runs when the push is
  actually going to happen. Previously the copy was built up-front and
  discarded whenever own was already in the resolved list.

- resolve_skdm_targets: inspect server/user on the borrowed &Jid before
  deciding whether to swap LID→PN, instead of allocating `base =
  jid.to_non_ad()` and then throwing it away on the PN branch. Saves
  one Jid (user CompactString + fields) per participant that has a
  phone mapping — typically own + any resolved-from-PN recipient.

Pure refactors. All 29 workspace test suites remain green.
@jlucaso1
jlucaso1 merged commit 5b05e22 into main Apr 18, 2026
10 checks passed
@jlucaso1
jlucaso1 deleted the fix/status-reaction-drop-meta branch April 18, 2026 18:13
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