--- 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: - id: openwiki-source-651d1fb6c9e49916a916ab51 resource: repo://Cargo.toml - id: openwiki-source-7ea5eac45e5030178a6ea05b resource: repo://src/whatsapp.rs generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" } verified: - by: openwiki/0.7.2 at: 2026-10-19T19:53:56.480Z --- # 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:211-244`): 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: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 `WaEvent` (`src/whatsapp.rs:81-110`): | 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 | | `OfflineSyncCompleted` | All messages received while the app was offline have been delivered (and, being sequential, already emitted as `Message`); triggers the startup [catch-up](../workflows/ai-autoreply.md) | | `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")`. ## JIDs - JIDs are stored as `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. ## Live messages `handle_live_message` (`src/whatsapp.rs:472-519`): - 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:807-810`); - attaches an `ImageRef` only when the rendered text is an image text; - emits `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](../workflows/ai-autoreply.md) compares them with the own JIDs to decide whether a group message is addressed to the user. ## Contact names 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](../concepts/history-store.md) persists them. ## History sync 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`. ## Message rendering `describe_message` (`src/whatsapp.rs:826-844`) turns a protobuf message into text, in this order: 1. plain or extended text; 2. image → `[immagine]` or `[immagine]