--- 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] verified: - by: openwiki/0.7.2 at: 2026-10-19T18:58:46.219Z sources: - id: openwiki-source-651d1fb6c9e49916a916ab51 resource: repo://Cargo.toml - id: openwiki-source-7ea5eac45e5030178a6ea05b resource: repo://src/whatsapp.rs generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" } --- # WhatsApp client integration `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: - a stream of plain `WaEvent` values sent to the [event loop](../architecture/runtime-and-event-loop.md); - a cloneable `WaHandle` used by [`App`](../architecture/app-state.md) 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`). ## Startup `start(session_db, &events)` (`src/whatsapp.rs:204-237`): 1. requires a UTF-8 session path and creates its parent directory; 2. opens the session store with `SqliteStore::new` (`session.db`, see [Configuration and data](../operations/configuration-and-data.md)); 3. builds a `Bot` with that backend and an `EventForwarder` handler; 4. spawns the `Translator` task and the bot; 5. when the bot task ends, emits `Disconnected("client stopped")`. `EventForwarder` (`src/whatsapp.rs:240-261`) only pushes raw events, in order, into an internal unbounded channel; its interest set is `SUBSCRIBED_KINDS` (`src/whatsapp.rs:51-69`). All translation happens in `Translator`, which also owns a name cache. ``` whatsapp-rust Bot --raw events--> EventForwarder --chan--> Translator --WaEvent--> App ``` ## WaEvent `WaEvent` (`src/whatsapp.rs:82-103`): | Variant | Meaning | |---|---| | `Qr(String)` | Pairing QR payload to render ([Terminal UI](../architecture/terminal-ui.md)) | | `Connected` | Session is online | | `Disconnected(String)` | Connection lost, with reason | | `LoggedOut` | Device unlinked; pairing must restart | | `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:409-448`) maps raw events to these variants. 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")`. ## JIDs - JIDs are stored as `user@server` without device suffix (`jid_to_string` uses `to_non_ad_string`, `src/whatsapp.rs:805`). - `canonical_jid` (`src/whatsapp.rs:362-377`) 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:187-193`) returns this account's PN and LID; it is empty before pairing. ## Live messages `handle_live_message` (`src/whatsapp.rs:464-510`): - skips `status@broadcast` and messages with no renderable content; - keeps group JIDs as the chat; for 1:1 chats canonicalizes using `recipient_alt` for own messages and `sender_alt` otherwise; - records the sender push name as a contact name (`push_name_of` drops own and empty names, `src/whatsapp.rs:797-800`); - attaches an `ImageRef` only when the rendered text is an image text; - emits `StoredMessage` with the WhatsApp timestamp and `is_ai = false`. ## Contact names Names come from two sources ranked `Push < Saved` (`NameSource`, `src/whatsapp.rs:264-270`): push names from messages and `PushNameUpdate`, saved names from `ContactUpdate` (full name, else first name, `src/whatsapp.rs:515-537`) and from history sync. `note_name` (`src/whatsapp.rs:325-343`) ignores empty names, never lets a push name override a saved one, skips unchanged names and otherwise emits `WaEvent::Contact`. The [history store](../concepts/history-store.md) persists them. ## History sync Each `HistorySync` chunk is processed in its own task (`handle_history`, `src/whatsapp.rs:544-564`). Decoding (inflate + protobuf) runs in `spawn_blocking` via `extract_history` (`src/whatsapp.rs:693-734`). `emit_history` (`src/whatsapp.rs:570-636`) 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; 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`. ## Message rendering `describe_message` (`src/whatsapp.rs:816-828`) turns a protobuf message into text, in this order: 1. plain or extended text; 2. image → `[immagine]` or `[immagine] `; audio (including voice notes) → `[messaggio vocale]` (`src/whatsapp.rs:836-848`); 3. caption of other media; 4. `describe_other` (`src/whatsapp.rs:856-874`): 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](../workflows/ai-autoreply.md) (`src/whatsapp.rs:899-943`): `is_placeholder` (only `[media]` and `[messaggio non supportato]`), `is_voice_note`, `is_image` and `image_caption`. ## Sending and downloading - `WaHandle::send_text(chat_jid, text)` (`src/whatsapp.rs:146-159`) rejects empty text, parses the JID and returns the new message id. - `WaHandle::download_image(&ImageRef)` (`src/whatsapp.rs:166-179`) 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:107-131`). ## Tests `src/whatsapp.rs:945-1173` covers 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](../testing/unit-tests.md).