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-21T21:49:12.291Z" }
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-15).
start(session_db, &events) (src/whatsapp.rs:208-241):
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:244-265) 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 and AppStateSyncFailed. 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-108):
| 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 } |
A StoredMessage (live or history sync); image is Some only for live image messages |
Contact(Contact) |
A new or better contact name |
Error(String) |
Non-fatal problem shown in the status line |
Translator::handle_event (src/whatsapp.rs:413-459) 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: <code> (expires in Ns),
connection failure: <reason> (<message>) (describe_connect_failure,
src/whatsapp.rs:796-801), stream error: ... and
could not decrypt a message from ...; StreamReplaced becomes
Disconnected("stream replaced by another session").
AppStateSyncFailed is reported only when at least one collection failed
fatally (is_fatal_sync_failure, src/whatsapp.rs:808-810), as
app state sync incomplete (fatal: [...], retryable: [...]); retryable or
skipped collections are retried by the library on a forced reconnect and are
not shown.
user@server without device suffix
(jid_to_string uses to_non_ad_string, src/whatsapp.rs:825-827).canonical_jid (src/whatsapp.rs:366-386) 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:191-197) returns this account's PN
and LID; it is empty before pairing.handle_live_message (src/whatsapp.rs:475-521):
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:817-820);ImageRef only when the rendered text is an image text;StoredMessage with the WhatsApp timestamp and is_ai = false.Mentions and quotes are not extracted: the message text is passed on as
rendered, and the AI autoreply only rewrites
@<own number> mentions in the text before building the AI context.
Names come from two sources ranked Push < Saved (NameSource,
src/whatsapp.rs:267-275): push names from messages and PushNameUpdate,
saved names from ContactUpdate (full name, else first name,
src/whatsapp.rs:526-540) and from history sync. note_name
(src/whatsapp.rs:329-351) 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:555-575). Decoding (inflate + protobuf)
runs in spawn_blocking via extract_history (src/whatsapp.rs:704-745).
emit_history (src/whatsapp.rs:581-648) then emits push names, inline
address-book contacts, conversation names and every renderable message.
History messages never carry an image reference (so their images are never
described); 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:836-848) turns a protobuf message into
text, in this order:
[immagine] or [immagine] <caption>; audio (including voice
notes) → [messaggio vocale] (describe_image_or_audio,
src/whatsapp.rs:856-868);describe_other (src/whatsapp.rs:876-894): 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:915-963): 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:150-163) rejects
empty text, parses the JID and returns the new message id.WaHandle::download_image(&ImageRef) (src/whatsapp.rs:170-183) 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:109-128).src/whatsapp.rs:965+ covers is_fatal_sync_failure, 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.