whatsapp-client.md 6.8 KB


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-651d1fb6c9 resource: repo://Cargo.toml
  • id: openwiki-source-7ea5eac45e 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;
  • a cloneable 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).

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);
  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)
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 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] <caption>; 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 (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.