type: integration title: WhatsApp client integration description: How src/whatsapp.rs wraps whatsapp-rust — session startup, translation of raw events into WaEvent, JID canonicalization, contact names, history sync, message rendering, image download and sending. tags: [whatsapp, whatsapp-rust, events, jid, history-sync] sources:
651d1fb6c9
resource: repo://Cargo.toml7ea5eac45e
resource: repo://src/whatsapp.rs
generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
verified:src/whatsapp.rs is the only module that talks to WhatsApp. It wraps the
whatsapp-rust 0.7 crate (Cargo.toml:10, default features off;
sqlite-storage, tokio-transport, tokio-runtime, ureq-client,
tokio-native, signal on) and exposes two things to the rest of the app:
WaEvent values sent to the
event loop;WaHandle used by App to
send text, download images and query its own JIDs.rusqlite is pinned so that it shares libsqlite3-sys with the diesel
backend of whatsapp-rust-sqlite-storage (Cargo.toml:14).
start(session_db, &events) (src/whatsapp.rs:211-244):
SqliteStore::new (session.db, see
Configuration and data);Bot with that backend and an EventForwarder handler;Translator task and the bot;Disconnected("client stopped").EventForwarder (src/whatsapp.rs:247-268) only pushes raw events, in
order, into an internal unbounded channel; its interest set is
SUBSCRIBED_KINDS (src/whatsapp.rs:51-70), which includes
OfflineSyncCompleted. All translation happens in
Translator, which also owns a name cache.
whatsapp-rust Bot --raw events--> EventForwarder --chan--> Translator --WaEvent--> App
WaEvent (src/whatsapp.rs:81-110):
| Variant | Meaning |
|---|---|
Qr(String) |
Pairing QR payload to render (Terminal UI) |
Connected |
Session is online |
Disconnected(String) |
Connection lost, with reason |
LoggedOut |
Device unlinked; pairing must restart |
OfflineSyncCompleted |
All messages received while the app was offline have been delivered (and, being sequential, already emitted as Message); triggers the startup catch-up |
Message { message, image, addressed_to } |
A StoredMessage (live or history sync); image is Some only for live image messages; addressed_to lists the raw JIDs the message directly addresses (empty for history sync) |
Contact(Contact) |
A new or better contact name |
Error(String) |
Non-fatal problem shown in the status line |
Translator::handle_event (src/whatsapp.rs:416-456) maps raw events to
these variants (Event::OfflineSyncCompleted becomes
WaEvent::OfflineSyncCompleted). Failures become Error texts such as
pairing QR codes exhausted; restart to pair again, pairing failed: ...,
WhatsApp rejected this client version (client outdated),
account temporarily banned: ..., connection failure: ...,
stream error: ... and could not decrypt a message from ...;
StreamReplaced becomes Disconnected("stream replaced by another session").
user@server without device suffix
(jid_to_string uses to_non_ad_string, src/whatsapp.rs:815-817).canonical_jid (src/whatsapp.rs:369-384) prefers the phone-number (PN)
form: non-LID JIDs are kept; a LID uses the alternate PN JID from the
message, else the store's LID→PN mapping; on lookup failure the LID is
kept and LID lookup failed for ... is reported.WaHandle::own_jids (src/whatsapp.rs:194-200) returns this account's PN
and LID; it is empty before pairing.handle_live_message (src/whatsapp.rs:472-519):
status@broadcast and messages with no renderable content;recipient_alt for own messages and sender_alt otherwise;push_name_of drops own
and empty names, src/whatsapp.rs:807-810);ImageRef only when the rendered text is an image text;StoredMessage with the WhatsApp timestamp and is_ai = false,
plus addressed_to from addressed_jids.addressed_jids (src/whatsapp.rs:892-919) reads the ContextInfo of the
first content kind that carries one (extended text, image, video, audio,
document, sticker) and returns the mentioned_jid list followed by the
quoted message's participant — the latter only when the message actually
quotes one (quoted_message or stanza_id set). The JIDs are raw (not
canonicalized); the AI autoreply compares
them with the own JIDs to decide whether a group message is addressed to
the user.
Names come from two sources ranked Push < Saved (NameSource,
src/whatsapp.rs:270-277): push names from messages and PushNameUpdate,
saved names from ContactUpdate (full name, else first name,
src/whatsapp.rs:524-546) and from history sync. note_name
(src/whatsapp.rs:332-350) ignores empty names, never lets a push name
override a saved one, skips unchanged names and otherwise emits
WaEvent::Contact. The history store
persists them.
Each HistorySync chunk is processed in its own task
(handle_history, src/whatsapp.rs:553-573). Decoding (inflate + protobuf)
runs in spawn_blocking via extract_history (src/whatsapp.rs:703-744).
emit_history (src/whatsapp.rs:579-646) then emits push names, inline
address-book contacts, conversation names and every renderable message.
History messages never carry an image reference (so they cannot trigger
vision replies) and always have an empty addressed_to; the sender is the own PN for own messages, otherwise the
group participant or the chat. Undecodable conversations are reported as
history sync skipped N undecodable conversations.
describe_message (src/whatsapp.rs:826-844) turns a protobuf message into
text, in this order:
[immagine] or [immagine] <caption>; audio (including voice
notes) → [messaggio vocale] (src/whatsapp.rs:846-858);describe_other (src/whatsapp.rs:866-884): protocol, reaction,
encrypted reaction, poll vote and sender-key distribution messages are
silent (None, not stored); video, document, sticker and PTV →
[media]; anything else → [messaggio non supportato].Helpers used by the AI autoreply
(src/whatsapp.rs:944-988): is_placeholder (only [media] and
[messaggio non supportato]), is_voice_note, is_image and
image_caption.
WaHandle::send_text(chat_jid, text) (src/whatsapp.rs:153-166) rejects
empty text, parses the JID and returns the new message id.WaHandle::download_image(&ImageRef) (src/whatsapp.rs:173-186) downloads
and decrypts the image into memory (DownloadedImage), falling back to
image/jpeg when the MIME type is blank. Images are never written to disk,
and ImageRef's Debug omits key material (src/whatsapp.rs:112-129).src/whatsapp.rs:990-1282 covers addressed_jids (mentions, quoted
participant, participant without quote, media context info, plain text),
text and extended text, captions and
placeholders, image and voice rendering, image references, placeholder
helpers, silent reactions/protocol messages, JID device-suffix removal and
push-name filtering. See Unit tests.