Skip to content

fix(whatsapp): deliver replies to @lid chats via PN JID rewrite#1116

Closed
juanlotito wants to merge 1 commit into
moltis-org:mainfrom
juanlotito:fix/whatsapp-lid-delivery
Closed

fix(whatsapp): deliver replies to @lid chats via PN JID rewrite#1116
juanlotito wants to merge 1 commit into
moltis-org:mainfrom
juanlotito:fix/whatsapp-lid-delivery

Conversation

@juanlotito

Copy link
Copy Markdown

Summary

Replies sent to a privacy-enabled sender's @lid chat were silently dropped on WhatsApp. The
gateway ran the agent, produced a reply (visible in the web UI), and called the outbound
sender — but the message never reached the user and no Delivered receipt ever came back.
Sending the same text to the sender's phone-number JID (@s.whatsapp.net) delivered fine.

Root cause: inbound chats from senders who have number-privacy enabled are keyed by LID
(@lid). The bundled whatsapp-rust 0.5 does not auto-resolve a @lid destination to its
phone number before sending (LID/PN address unification landed upstream in 0.6). The
WhatsApp server accepts the stanza addressed to @lid but drops it.

Fix: before each outbound send (text, media, typing), resolve a @lid destination to its PN
JID via the existing Client::get_phone_number_from_lid mapping (already populated by the
library). When no mapping is known the JID is left unchanged, so the behaviour matches the
previous path. The @lid check uses the HIDDEN_USER_SERVER constant instead of a string
literal.

The change is isolated to crates/whatsapp/src/outbound.rs (+65/-4).

Validation

Completed (locally, pinned nightly nightly-2025-12-27)

  • cargo +nightly-2025-12-27 fmt --all -- --check — pass
  • cargo +nightly-2025-12-27 clippy -p moltis-whatsapp --all-targets -- -D warnings — pass
  • cargo +nightly-2025-12-27 test -p moltis-whatsapp — 95 passed, 0 failed (includes 4 new
    unit tests for needs_lid_resolution)

Remaining (left to CI)

  • just test (full nextest --workspace)
  • just release-preflight (full-workspace clippy)
  • UI e2e — N/A, no web UI changes

Manual QA

Reproduced and verified end-to-end on a live self-hosted gateway (ZimaBoard / Docker) paired
to a real WhatsApp number, with the sender's number-privacy enabled so the chat is keyed by
@lid.

  • Before: messaging the bot produced a reply in the UI but nothing on WhatsApp; logs showed
    the send "succeed" with no Delivered receipt. A diagnostic send_message to the same person's
    classic @s.whatsapp.net JID did arrive — confirming transport works and only @lid
    addressing was broken.
  • After (patched image): messaging the bot delivers the reply on WhatsApp. Logs:
    DEBUG moltis_whatsapp::outbound: rewriting LID destination to PN JID lid=@lid
    pn=
    DEBUG whatsapp_rust::receipt: Received receipt type 'Delivered' for message
  • The Delivered receipt (never present before) confirms the fix.

Notes for maintainers

  • This is a targeted fix at the moltis layer. The deeper fix is bumping whatsapp-rust 0.5 →
    0.6, which brings unified LID/PN addressing; that is a larger change with breakage risk, so
    this PR keeps the surface minimal. Happy to follow up on the bump if preferred.
  • No new dependencies; uses an existing public method (get_phone_number_from_lid) and an
    existing constant (HIDDEN_USER_SERVER).

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes silent delivery failures for WhatsApp chats keyed by @lid (privacy-enabled senders) by rewriting the destination JID to its phone-number equivalent before each outbound send while remaining on whatsapp-rust 0.5.

  • Introduces to_deliverable_jid, an async helper that calls client.get_phone_number_from_lid and rewrites to a @s.whatsapp.net JID; falls back to the original JID when no mapping exists, preserving pre-fix behaviour.
  • Applied consistently across all three send paths (send_text, send_media, send_typing); streaming goes through send_text so is covered automatically.
  • Four new unit tests validate needs_lid_resolution for LID, PN, group, and bare-number JID inputs.

Confidence Score: 4/5

Safe to merge; the change is narrow and well-contained, with the only realistic failure mode being the pre-existing silent drop that already occurred before this fix.

The fix correctly rewrites @lid JIDs across all three outbound paths and falls back gracefully when no mapping is known. The one thing worth watching is that the fallback path — where no PN mapping exists — logs at debug level, so a message silently failing to deliver would be invisible in production logs unless debug logging is enabled. That is a diagnostics gap rather than a regression, but it could make future incidents harder to triage.

crates/whatsapp/src/outbound.rs — specifically the None branch of to_deliverable_jid around line 57.

Important Files Changed

Filename Overview
crates/whatsapp/src/outbound.rs Adds LID→PN JID rewriting via to_deliverable_jid applied to all three outbound paths (text, media, typing). Logic is correct and well-tested; one minor concern about debug! level for the silent-drop fallback.

Sequence Diagram

sequenceDiagram
    participant App as moltis App
    participant Out as WhatsAppOutbound
    participant Fn as to_deliverable_jid
    participant Lib as whatsapp_rust Client
    participant WA as WhatsApp Server

    App->>Out: "send_text(to=id@lid, text)"
    Out->>Out: "resolve_jid to Jid(@lid)"
    Out->>Fn: "to_deliverable_jid(client, jid@lid)"
    Fn->>Fn: "needs_lid_resolution? true"
    Fn->>Lib: "get_phone_number_from_lid(id)"
    alt mapping found
        Lib-->>Fn: "Some(pn)"
        Fn-->>Out: "Jid(@s.whatsapp.net)"
        Out->>Lib: "send_message(pn@s.whatsapp.net, msg)"
        Lib->>WA: "stanza to @s.whatsapp.net"
        WA-->>Lib: "Delivered receipt"
    else no mapping
        Lib-->>Fn: "None"
        Fn-->>Out: "Jid(@lid) unchanged"
        Out->>Lib: "send_message(id@lid, msg)"
        Lib->>WA: "stanza to @lid"
        WA-->>WA: "dropped silently"
    end
Loading

Reviews (1): Last reviewed commit: "fix(whatsapp): deliver replies to @lid c..." | Re-trigger Greptile

Comment thread crates/whatsapp/src/outbound.rs
juanlotito added a commit to juanlotito/moltis that referenced this pull request Jun 12, 2026
Review feedback (PR moltis-org#1116): the no-mapping fallback still sends to the
`@lid` JID, which is exactly the condition the WhatsApp server drops
without a `Delivered` receipt. Logging it at `debug!` left undelivered
replies invisible in production unless debug logging was enabled. Raise it
to `warn!` so the event surfaces by default; this is an unexpected but
recoverable state, matching the project's logging-level guidance.
Replies addressed to a privacy-enabled sender's `@lid` JID were accepted by
the WhatsApp server but silently dropped: no `Delivered` receipt ever
arrived, so the agent appeared to respond (the reply was visible in the web
UI) while the user received nothing on WhatsApp. Sending the same content to
the sender's phone-number JID (`@s.whatsapp.net`) delivered normally.

Root cause: inbound chats from senders with number-privacy enabled are keyed
by LID, and the bundled whatsapp-rust 0.5 does not auto-resolve a `@lid`
destination to its phone number before sending (LID/PN address unification
landed upstream in 0.6).

Fix: before each outbound send (text, media, typing) resolve a `@lid`
destination to its PN JID through the existing
`Client::get_phone_number_from_lid` mapping. When no mapping is known the JID
is left unchanged so behaviour matches the previous path; this fallback logs
at `warn!` so an undelivered reply is visible without enabling debug logging.
The `@lid` check uses the `HIDDEN_USER_SERVER` constant rather than a literal.
Adds unit coverage for the pure `needs_lid_resolution` decision (lid rewrites,
pn/group/bare-number pass through).
@juanlotito juanlotito force-pushed the fix/whatsapp-lid-delivery branch from d0a6fb0 to 47ee49a Compare June 12, 2026 01:10
juanlotito added a commit to juanlotito/moltis that referenced this pull request Jul 2, 2026
Live testing on 2026-07-01 showed that even with whatsapp-rust 0.6's
unified LID/PN addressing, stanzas sent from a companion device to a bare
@lid chat are accepted by the server but never delivered (no Delivered
receipt), while the same message to the mapped PN JID arrives normally.
Inbound LID handling from 0.6 works correctly and is unaffected.

Restore the to_deliverable_jid rewrite (originally PR moltis-org#1116) using the
0.6 API: client.get_lid_pn_entry() replaces the removed
get_phone_number_from_lid(). Lookup failures and missing mappings fall
back to sending to the @lid as-is, with a warn so undelivered replies
are visible at default log level.
@juanlotito juanlotito closed this Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant