# WhatsApp Binary Protocol Source: https://whatsapp-rust.jlucaso.com/advanced/binary-protocol Deep dive into WhatsApp's custom binary serialization format, node marshaling, and protocol specifics ## Overview WhatsApp uses a custom binary protocol for all communication between clients and servers. This format is significantly more compact than JSON or XML and optimized for mobile network conditions. The protocol encodes messages as **nodes** - hierarchical structures with tags, attributes, and content. All nodes are serialized to binary format before encryption and transmission. ## Architecture The binary protocol implementation is in `wacore/binary/`, a platform-agnostic crate: ``` wacore/binary/src/ ├── marshal.rs # Serialization entry points ├── encoder.rs # Binary encoding logic ├── decoder.rs # Binary decoding logic ├── node.rs # Node data structures ├── token.rs # Token dictionary ├── jid.rs # JID (identifier) handling └── builder.rs # Fluent API for node construction ``` ## Node Structure ### Node Definition A node represents a protocol message or message component: ```rust theme={null} pub struct Node { pub tag: String, // e.g., "message", "receipt", "iq" pub attrs: Attrs, // Key-value attributes pub content: Option, // Optional content } pub enum NodeContent { Bytes(Vec), // Binary payload String(String), // Text payload Nodes(Vec), // Child nodes } ``` Location: `wacore/binary/src/node.rs:308-314` ### Attributes Attributes are stored as key-value pairs with specialized value types: ```rust theme={null} pub enum NodeValue { String(String), Jid(Jid), // Optimized for WhatsApp identifiers } pub struct Attrs(Vec<(String, NodeValue)>); ``` **Why Jid as a separate type?** JIDs (Jabber IDs) like `15551234567@s.whatsapp.net` appear frequently in the protocol. Storing them as structured data avoids repeated parsing/formatting overhead: ```rust theme={null} use std::borrow::Cow; pub struct Jid { pub user: String, // "15551234567" pub server: Cow<'static, str>, // "s.whatsapp.net" pub agent: u8, // Domain type (0, 1, 128, 129) pub device: u16, // Device ID (0 for primary) pub integrator: u16, // Integrator ID (used with interop server) } ``` The `server` field uses `Cow<'static, str>` so that known server constants (like `"s.whatsapp.net"`, `"g.us"`, `"lid"`) are borrowed as zero-allocation static references, while unknown servers fall back to an owned `String`. This eliminates per-JID heap allocations on hot paths since most JIDs use one of the \~11 predefined server constants. #### Typed constructors Convenience constructors avoid specifying the server string directly: ```rust theme={null} Jid::pn("15551234567") // @s.whatsapp.net, device 0 Jid::lid("ABC123") // @lid, device 0 Jid::group("12345678") // @g.us Jid::pn_device("15551234567", 1) // @s.whatsapp.net, device 1 Jid::lid_device("ABC123", 2) // @lid, device 2 Jid::status_broadcast() // status@broadcast Jid::new("user", "custom.srv") // arbitrary server (Cow-optimized) ``` #### Borrowing types For zero-allocation lookups and comparisons, the protocol also provides: * **`JidRef<'a>`** — a borrowing version of `Jid` where both `user` and `server` are `Cow<'a, str>`, useful for temporary parsing results * **`DeviceKey<'a>`** — a lightweight key containing `(&'a str, &'a str, u16)` for user/server/device, used for `HashSet` lookups without cloning Location: `wacore/binary/src/node.rs:10-112`, `wacore/binary/src/jid.rs` ### NodeBuilder API The `NodeBuilder` provides a fluent chaining API for constructing nodes. All setter methods consume and return `Self`: ```rust theme={null} use wacore_binary::builder::NodeBuilder; use wacore_binary::jid::Jid; let message = NodeBuilder::new("message") .attr("to", "15551234567@s.whatsapp.net") .attr("type", "text") .attr("id", "ABCD1234") .children(vec![ NodeBuilder::new("body").string_content("Hello, world!").build(), ]) .build(); ``` #### Available methods | Method | Signature | Description | | ---------------- | ---------------------------------------------------------------- | ---------------------------------------- | | `new` | `new(tag: impl Into) -> Self` | Create a builder with a tag | | `attr` | `attr(key: impl Into, value: impl Into) -> Self` | Add a string attribute | | `jid_attr` | `jid_attr(key: impl Into, jid: Jid) -> Self` | Add a JID attribute without stringifying | | `attrs` | `attrs(attrs: impl IntoIterator) -> Self` | Bulk-add attributes from an iterator | | `children` | `children(children: impl IntoIterator) -> Self` | Set child nodes as content | | `bytes` | `bytes(bytes: impl Into>) -> Self` | Set raw bytes as content | | `string_content` | `string_content(s: impl Into) -> Self` | Set string as content | | `apply_content` | `apply_content(content: Option) -> Self` | Set arbitrary content | | `build` | `build(self) -> Node` | Consume the builder and produce a `Node` | Location: `wacore/binary/src/builder.rs` #### jid\_attr vs attr The `jid_attr` method stores JIDs as `NodeValue::Jid(jid)` directly in the attribute map, avoiding the allocation cost of `jid.to_string()`. Use `jid_attr` for JID-valued attributes like `to`, `from`, and `participant` on hot paths: ```rust theme={null} // Prefer jid_attr for JID attributes — avoids string allocation let receipt = NodeBuilder::new("receipt") .attr("id", &message_id) .jid_attr("to", chat_jid.clone()) .jid_attr("participant", sender_jid.clone()) .build(); // Equivalent but allocates a string per JID let receipt = NodeBuilder::new("receipt") .attr("id", &message_id) .attr("to", chat_jid.to_string()) .attr("participant", sender_jid.to_string()) .build(); ``` #### Conditional chaining Use `let mut builder` with reassignment for conditional attributes: ```rust theme={null} let mut builder = NodeBuilder::new("receipt") .attr("id", &info.id) .jid_attr("to", info.source.chat.clone()); if info.category == "peer" { builder = builder.attr("type", "peer_msg"); } if info.source.is_group { builder = builder.jid_attr("participant", info.source.sender.clone()); } let node = builder.build(); ``` ## Token Dictionary The protocol uses a token dictionary to compress common strings into single bytes. ### Token Types ```rust theme={null} // Single-byte tokens (4-235) pub const LIST_EMPTY: u8 = 0; pub const LIST_8: u8 = 248; // List with <256 items pub const LIST_16: u8 = 249; // List with ≥256 items pub const JID_PAIR: u8 = 250; // JID in user@server format pub const AD_JID: u8 = 251; // JID with device ID pub const BINARY_8: u8 = 252; // Binary data <256 bytes pub const BINARY_20: u8 = 253; // Binary data <1MB pub const BINARY_32: u8 = 254; // Binary data ≥1MB pub const NIBBLE_8: u8 = 255; // Packed numeric string pub const HEX_8: u8 = 254; // Packed hex string ``` Location: `wacore/binary/src/token.rs` ### Dictionary Lookup Common protocol strings are mapped to single-byte tokens: ```rust theme={null} index_of_single_token("message") => Some(19) index_of_single_token("iq") => Some(18) index_of_single_token("body") => Some(7) ``` The dictionary includes: * Protocol tags ("message", "iq", "presence") * Common attributes ("id", "type", "to", "from") * Frequent values ("text", "chat", "available") ### Multi-byte Tokens Less common strings use two-byte tokens: ```rust theme={null} index_of_double_byte_token("participant") => Some((dict_index, token_index)) ``` Location: `wacore/binary/src/token.rs:200-300` ## Encoding Process ### Marshal Functions ```rust theme={null} // Basic serialization pub fn marshal(node: &Node) -> Result> // Serialize to existing buffer (zero-copy for output) pub fn marshal_to_vec(node: &Node, output: &mut Vec) -> Result<()> // Two-pass encoding with exact size pre-calculation pub fn marshal_exact(node: &Node) -> Result> // Auto-sizing with heuristics pub fn marshal_auto(node: &Node) -> Result> ``` Location: `wacore/binary/src/marshal.rs:31-76` ### Encoding Strategy The encoder uses multiple strategies based on data characteristics: ```rust theme={null} enum StringHint { Empty, // "" → BINARY_8 + 0 SingleToken(u8), // "message" → 19 DoubleToken { dict: u8, token: u8 }, PackedNibble, // "123-456" → compressed PackedHex, // "DEADBEEF" → compressed Jid(ParsedJidMeta), // JID-specific encoding RawBytes, // Fallback } ``` Location: `wacore/binary/src/encoder.rs:227-237` ### Packed Encoding #### Nibble Packing (Numeric Strings) Strings containing only digits, dash, and dot are packed into 4 bits per character: ```rust theme={null} // Input: "123-456.789" // Encoding: // '1' → 1, '2' → 2, '3' → 3, '-' → 10, '4' → 4, ... // Packed: 0x12, 0x3A, 0x45, 0x67, 0x89 pub const PACKED_MAX: u8 = 127; // Max length for packed strings fn pack_nibble(value: u8) -> u8 { match value { b'-' => 10, b'.' => 11, 0 => 15, // Padding c if c.is_ascii_digit() => c - b'0', _ => panic!("Invalid nibble"), } } ``` Location: `wacore/binary/src/encoder.rs:769-777` #### Hex Packing Uppercase hex strings (0-9, A-F) are packed into 4 bits per character: ```rust theme={null} // Input: "DEADBEEF" // Packed: 0xDE, 0xAD, 0xBE, 0xEF fn pack_hex(value: u8) -> u8 { match value { c if c.is_ascii_digit() => c - b'0', c if (b'A'..=b'F').contains(&c) => 10 + (c - b'A'), 0 => 15, // Padding _ => panic!("Invalid hex"), } } ``` Location: `wacore/binary/src/encoder.rs:780-787` #### SIMD Optimization The encoder uses SIMD instructions for fast packing of long strings: ```rust theme={null} while input_bytes.len() >= 16 { let input = u8x16::from_slice(chunk); let indices = input.saturating_sub(nibble_base); let nibbles = lookup.swizzle_dyn(indices); let (evens, odds) = nibbles.deinterleave( nibbles.rotate_elements_left::<1>() ); let packed = (evens << Simd::splat(4)) | odds; self.write_raw_bytes(&packed.to_array()[..8])?; } ``` Location: `wacore/binary/src/encoder.rs:809-824` ### JID Encoding JIDs have special compact encodings: #### JID\_PAIR (Standard JID) ```rust theme={null} // Format: JID_PAIR + user + server // Example: "15551234567@s.whatsapp.net" self.write_u8(token::JID_PAIR)?; if user.is_empty() { self.write_u8(token::LIST_EMPTY)?; } else { self.write_string(user)?; // "15551234567" } self.write_string(server)?; // "s.whatsapp.net" ``` Location: `wacore/binary/src/encoder.rs:706-715` #### AD\_JID (Device-Specific JID) ```rust theme={null} // Format: AD_JID + domain_type + device + user // Example: "15551234567:1@s.whatsapp.net" (device 1) self.write_u8(token::AD_JID)?; self.write_u8(meta.domain_type)?; // 0 for normal, 1 for lid self.write_u8(device)?; // Device number self.write_string(user)?; // User part only ``` Location: `wacore/binary/src/encoder.rs:699-705` ### List Encoding Lists (including node structures) have length-prefixed encoding: ```rust theme={null} fn write_list_start(&mut self, len: usize) -> Result<()> { if len == 0 { self.write_u8(token::LIST_EMPTY)?; // 0x00 } else if len < 256 { self.write_u8(token::LIST_8)?; // 0xF8 self.write_u8(len as u8)?; } else { self.write_u8(token::LIST_16)?; // 0xF9 self.write_u16_be(len as u16)?; } Ok(()) } ``` Location: `wacore/binary/src/encoder.rs:865-876` ### Node Encoding Format A complete node is encoded as: ``` LIST_START(list_len) tag attr_key_1 attr_value_1 attr_key_2 attr_value_2 ... [content] // If present ``` Where `list_len = 1 (tag) + (num_attrs * 2) + (content ? 1 : 0)` ```rust theme={null} pub fn write_node(&mut self, node: &N) -> Result<()> { let content_len = if node.has_content() { 1 } else { 0 }; let list_len = 1 + (node.attrs_len() * 2) + content_len; self.write_list_start(list_len)?; self.write_string(node.tag())?; node.encode_attrs(self)?; node.encode_content(self)?; Ok(()) } ``` Location: `wacore/binary/src/encoder.rs:879-889` ## Decoding Process ### Decoder Structure ```rust theme={null} pub struct Decoder<'a> { data: &'a [u8], offset: usize, } impl<'a> Decoder<'a> { pub fn read_node_ref(&mut self) -> Result> pub fn read_list_size(&mut self) -> Result pub fn read_string(&mut self) -> Result> } ``` Location: `wacore/binary/src/decoder.rs` ### Zero-Copy Decoding The decoder uses `NodeRef<'a>` to avoid allocations: ```rust theme={null} pub struct NodeRef<'a> { pub tag: Cow<'a, str>, // Borrowed when possible pub attrs: AttrsRef<'a>, // Vec of borrowed pairs pub content: Option>>, } pub enum NodeContentRef<'a> { Bytes(Cow<'a, [u8]>), // Zero-copy for byte content String(Cow<'a, str>), // Zero-copy when valid UTF-8 Nodes(Box>), // Recursive borrowing } ``` Location: `wacore/binary/src/node.rs:316-321`, `288-293` ### Unpacking Reverse of the packing process: ```rust theme={null} fn unpack_nibble(packed: u8, position: u8) -> u8 { let nibble = if position == 0 { (packed >> 4) & 0x0F } else { packed & 0x0F }; match nibble { 0..=9 => b'0' + nibble, 10 => b'-', 11 => b'.', 15 => 0, // Padding _ => panic!("Invalid nibble"), } } ``` Location: `wacore/binary/src/decoder.rs:400-450` ## Performance Optimizations ### Two-Pass Encoding For large or variable-size payloads, exact size calculation prevents buffer growth: ```rust theme={null} pub fn marshal_exact(node: &Node) -> Result> { // Pass 1: Calculate exact size let plan = build_marshaled_node_plan(node); // Pass 2: Encode directly into fixed-size buffer let mut payload = vec![0; plan.size]; let mut encoder = Encoder::new_slice(&mut payload, Some(&plan.hints))?; encoder.write_node(node)?; Ok(payload) } ``` Location: `wacore/binary/src/marshal.rs:67-76` ### String Hint Cache Repeated strings (like JIDs) are analyzed once and cached: ```rust theme={null} pub struct StringHintCache { hints: Vec<(StrKey, StringHint)>, } impl StringHintCache { fn hint_or_insert(&mut self, s: &str) -> StringHint { if let Some(existing) = self.hints.iter().find(...) { return existing; } let hint = classify_string_hint(s); self.hints.push((key, hint)); hint } } ``` Location: `wacore/binary/src/encoder.rs:240-282` ### Capacity Estimation Auto-sizing strategy samples node structure to estimate capacity: ```rust theme={null} fn estimate_capacity_node(node: &Node) -> usize { let mut estimate = DEFAULT_MARSHAL_CAPACITY + 16; estimate += node.tag.len(); estimate += node.attrs.len() * AUTO_ATTR_ESTIMATE; // ~24 bytes/attr if let Some(NodeContent::Nodes(children)) = &node.content { estimate += children.len() * AUTO_CHILD_ESTIMATE; // ~96 bytes/child // Sample first 32 children for better accuracy for child in children.iter().take(AUTO_CHILD_SAMPLE_LIMIT) { estimate += child.tag.len() + ... } } estimate.clamp(DEFAULT_MARSHAL_CAPACITY, AUTO_MAX_HINT_CAPACITY) } ``` Location: `wacore/binary/src/marshal.rs:167-200` ## Common Protocol Patterns ### IQ (Info/Query) Stanzas ```rust theme={null} // Request NodeBuilder::new("iq") .attr("id", "ABC123") .attr("type", "get") .attr("xmlns", "w:g2") .attr("to", "@s.whatsapp.net") .children(vec![ NodeBuilder::new("query").build(), ]) .build() // Response NodeBuilder::new("iq") .attr("id", "ABC123") .attr("type", "result") .attr("from", "@s.whatsapp.net") .children(vec![ NodeBuilder::new("group") .attr("id", "123456@g.us") .attr("subject", "My Group") .build(), ]) .build() ``` ### Messages ```rust theme={null} NodeBuilder::new("message") .attr("to", "15551234567@s.whatsapp.net") .attr("type", "text") .attr("id", message_id) .children(vec![ NodeBuilder::new("enc") .attr("v", "2") .attr("type", "msg") .bytes(encrypted_payload) .build(), ]) .build() ``` ### Receipts ```rust theme={null} NodeBuilder::new("receipt") .attr("to", "15551234567@s.whatsapp.net") .attr("id", message_id) .attr("type", "read") .attr("t", timestamp) .build() ``` ## Wire Format Examples ### Simple Message ``` Node: Binary: F8 03 LIST_8(3) [tag + 2 attrs] 13 Token("message") 16 Token("type") 07 Token("text") ``` ### Message with Body ``` Node: Hi Binary: F8 04 LIST_8(4) [tag + 2 attrs + content] 13 Token("message") 16 Token("type") 07 Token("text") F8 02 LIST_8(2) [child: tag + content] 07 Token("body") FC 02 BINARY_8(2) 48 69 "Hi" ``` ## Debugging Tools ### Inspecting Encoded Data Use `evcxr` REPL for interactive exploration: ```rust theme={null} :dep wacore-binary = { path = "wacore/binary" } :dep hex = "0.4" use wacore_binary::marshal::unmarshal_ref; use wacore_binary::builder::NodeBuilder; // Decode binary data { let data = hex::decode("f8034c1a07").unwrap(); let node = unmarshal_ref(&data).unwrap(); println!("Tag: {}", node.tag); for (k, v) in node.attrs.iter() { println!(" {}: {}", k, v); } } // Encode and inspect { let node = NodeBuilder::new("message") .attr("type", "text") .build(); let bytes = marshal(&node).unwrap(); println!("Encoded: {:02x?}", bytes); } ``` ## Error Handling ```rust theme={null} pub enum BinaryError { UnexpectedEof, InvalidToken(u8), InvalidListSize, AttrParse(String), LeftoverData(usize), Io(std::io::Error), } ``` Location: `wacore/binary/src/error.rs` ## Related Components * [Signal Protocol](/advanced/signal-protocol) - How messages are encrypted before marshaling * [WebSocket Handling](/advanced/websocket-handling) - How binary data is framed and transmitted * [State Management](/advanced/state-management) - Protocol state stored in Device ## References * Source: `wacore/binary/src/` * Token dictionary: `wacore/binary/src/token.rs` * Node builder: `wacore/binary/src/builder.rs` # Signal Protocol Implementation Source: https://whatsapp-rust.jlucaso.com/advanced/signal-protocol Deep dive into end-to-end encryption, Double Ratchet algorithm, and Signal Protocol in whatsapp-rust ## Overview whatsapp-rust implements the Signal Protocol for end-to-end encryption of both one-on-one and group messages. The implementation is based on Signal's libsignal library, adapted for WhatsApp's specific protocol requirements. The Signal Protocol implementation handles cryptographic primitives. Any modifications to this code require expert-level understanding of cryptographic protocols to avoid security vulnerabilities. ## Architecture The Signal Protocol implementation is split across two main locations: * **`wacore/libsignal/`** - Platform-agnostic Signal Protocol core (Rust port of libsignal) * **`src/store/signal*.rs`** - WhatsApp-specific storage integration with Diesel/SQLite ### Key Components ``` wacore/libsignal/src/ ├── protocol/ │ ├── session_cipher.rs # Encryption/decryption for 1:1 messages │ ├── group_cipher.rs # Encryption/decryption for group messages │ ├── ratchet.rs # Double Ratchet implementation │ ├── sender_keys.rs # Sender Key protocol for groups │ └── state/ # Session state management └── crypto/ ├── aes_cbc.rs # AES-256-CBC for message content ├── aes_gcm.rs # AES-GCM for media encryption └── hash.rs # HKDF and HMAC primitives ``` ## Double Ratchet Protocol The Double Ratchet algorithm provides forward secrecy and post-compromise security for 1:1 messages. ### Session Initialization Two participants initialize a session using Diffie-Hellman key exchange: ```rust theme={null} // Alice initiates the session (sender) pub fn initialize_alice_session( parameters: &AliceSignalProtocolParameters, csprng: &mut R, ) -> Result // Bob receives the session (recipient) pub fn initialize_bob_session( parameters: &BobSignalProtocolParameters ) -> Result ``` **Key Derivation:** 1. Compute shared secrets from ephemeral key exchanges 2. Derive root key and chain key using HKDF-SHA256: ``` HKDF(discontinuity_bytes || DH1 || DH2 || DH3 [|| DH4]) → (RootKey[32], ChainKey[32], PQRKey[32]) ``` 3. Initialize sender and receiver chains Location: `wacore/libsignal/src/protocol/ratchet.rs:41-172` ### Message Encryption Each message advances the sender chain and derives ephemeral message keys: ```rust theme={null} // From wacore/libsignal/src/protocol/session_cipher.rs:65-183 pub async fn message_encrypt( ptext: &[u8], remote_address: &ProtocolAddress, session_store: &mut dyn SessionStore, identity_store: &mut dyn IdentityKeyStore, ) -> Result ``` **Process:** 1. Load current session state 2. Get sender chain key and derive message keys: ```rust theme={null} let (message_keys_gen, next_chain_key) = chain_key.step_with_message_keys(); let message_keys = message_keys_gen.generate_keys(); // message_keys contains: cipher_key, mac_key, iv ``` 3. Encrypt plaintext with AES-256-CBC: ```rust theme={null} aes_256_cbc_encrypt_into(ptext, message_keys.cipher_key(), message_keys.iv(), &mut buf) ``` 4. Create SignalMessage with MAC for authentication 5. Advance chain key and save session state **Message Format:** * **SignalMessage**: Standard encrypted message * **PreKeySignalMessage**: Includes prekey bundle for session establishment ### Message Decryption Decryption handles out-of-order delivery and tries multiple session states: ```rust theme={null} // From wacore/libsignal/src/protocol/session_cipher.rs:292-363 pub async fn message_decrypt_signal( ciphertext: &SignalMessage, remote_address: &ProtocolAddress, session_store: &mut dyn SessionStore, identity_store: &mut dyn IdentityKeyStore, csprng: &mut R, ) -> Result> ``` **Process:** 1. Try current session state first 2. If MAC verification fails, try previous (archived) sessions 3. Derive/retrieve message keys for the counter 4. Verify MAC: ```rust theme={null} ciphertext.verify_mac(&their_identity_key, &local_identity_key, message_keys.mac_key()) ``` 5. Decrypt with AES-256-CBC 6. Promote successful session to current if needed The implementation optimizes memory by using take/restore patterns to avoid cloning session states during decryption attempts (see `session_cipher.rs:495-619`). ### Chain Key Ratcheting Message keys are derived from chain keys, which advance with each message: ```rust theme={null} pub struct ChainKey { key: [u8; 32], index: u32, } impl ChainKey { pub fn step_with_message_keys(self) -> (MessageKeyGenerator, ChainKey) { let message_key_gen = MessageKeyGenerator::new(self.key, self.index); let next_chain_key = self.next_chain_key(); (message_key_gen, next_chain_key) } } ``` Location: `wacore/libsignal/src/protocol/ratchet/keys.rs` ### Forward Jumps The protocol tolerates out-of-order messages up to a limit: ```rust theme={null} const MAX_FORWARD_JUMPS: usize = 25000; if jump > MAX_FORWARD_JUMPS { return Err(SignalProtocolError::InvalidMessage( original_message_type, "message from too far into the future", )); } ``` Location: `wacore/libsignal/src/protocol/session_cipher.rs:832-847` ## Sender Keys (Group Encryption) Groups use the Sender Key protocol for efficient multi-recipient encryption. ### Sender Key Distribution Each participant generates and distributes a sender key: ```rust theme={null} // From wacore/libsignal/src/protocol/group_cipher.rs:283-336 pub async fn create_sender_key_distribution_message( sender_key_name: &SenderKeyName, sender_key_store: &mut dyn SenderKeyStore, csprng: &mut R, ) -> Result ``` **Structure:** * **Chain ID**: Random 31-bit identifier for this sender key session * **Iteration**: Message counter (starts at 0) * **Chain Key**: 32-byte seed for deriving message keys * **Signing Key**: Ed25519 public key for message authentication ### Group Encryption Messages are encrypted with the sender's current chain key: ```rust theme={null} // From wacore/libsignal/src/protocol/group_cipher.rs:53-116 pub async fn group_encrypt( sender_key_store: &mut dyn SenderKeyStore, sender_key_name: &SenderKeyName, plaintext: &[u8], csprng: &mut R, ) -> Result ``` **Process:** 1. Load sender key state for the group 2. Derive message keys from current chain key 3. Encrypt with AES-256-CBC 4. Sign message with Ed25519 private key 5. Advance chain key ### Group Decryption Recipients decrypt using the sender's distributed key: ```rust theme={null} // From wacore/libsignal/src/protocol/group_cipher.rs:162-250 pub async fn group_decrypt( skm_bytes: &[u8], sender_key_store: &mut dyn SenderKeyStore, sender_key_name: &SenderKeyName, ) -> Result> ``` **Process:** 1. Parse SenderKeyMessage 2. Look up sender key state by chain ID 3. Verify Ed25519 signature 4. Derive message keys for iteration (handling out-of-order) 5. Decrypt with AES-256-CBC Group decryption maintains up to MAX\_FORWARD\_JUMPS (25000) cached message keys per sender. This prevents resource exhaustion attacks but limits tolerance for extreme out-of-order delivery. ## Cryptographic Primitives ### AES-256-CBC (Message Content) Used for encrypting message bodies in both 1:1 and group messages: ```rust theme={null} pub fn aes_256_cbc_encrypt_into( plaintext: &[u8], key: &[u8], // 32 bytes iv: &[u8], // 16 bytes output: &mut Vec, ) -> Result<()> ``` Location: `wacore/libsignal/src/crypto/aes_cbc.rs` ### Thread-Local Buffers The implementation uses thread-local buffers to reduce allocations: ```rust theme={null} thread_local! { static ENCRYPTION_BUFFER: RefCell = ...; static DECRYPTION_BUFFER: RefCell = ...; } // Usage in session_cipher.rs:99-111 let ctext = ENCRYPTION_BUFFER.with(|buffer| { let mut buf_wrapper = buffer.borrow_mut(); let buf = buf_wrapper.get_buffer(); aes_256_cbc_encrypt_into(ptext, message_keys.cipher_key(), message_keys.iv(), buf)?; let result = std::mem::take(buf); buf.reserve(EncryptionBuffer::INITIAL_CAPACITY); Ok::, SignalProtocolError>(result) })?; ``` Location: `wacore/libsignal/src/protocol/session_cipher.rs:14-54` ### HKDF-SHA256 Used for key derivation in session initialization: ```rust theme={null} pub fn derive_keys(secret_input: &[u8]) -> (RootKey, ChainKey, InitialPQRKey) { let mut secrets = [0; 96]; hkdf::Hkdf::::new(None, secret_input) .expand(b"WhisperText", &mut secrets) .expect("valid length"); // Split into RootKey[32], ChainKey[32], PQRKey[32] } ``` Location: `wacore/libsignal/src/protocol/ratchet.rs:18-39` ## PreKey Management Pre-keys enable asynchronous session establishment in the Signal Protocol. whatsapp-rust manages pre-key generation and upload to match WhatsApp Web's behavior. ### Configuration ```rust theme={null} // src/prekeys.rs /// Matches WA Web's UPLOAD_KEYS_COUNT from WAWebSignalStoreApi const WANTED_PRE_KEY_COUNT: usize = 812; const MIN_PRE_KEY_COUNT: usize = 5; ``` **Constants:** * **WANTED\_PRE\_KEY\_COUNT (812)**: Number of pre-keys uploaded in each batch, matching WhatsApp Web * **MIN\_PRE\_KEY\_COUNT (5)**: Minimum server-side pre-key count before triggering upload ### Pre-key ID Counter Pre-key IDs use a persistent monotonic counter (`Device::next_pre_key_id`) that only increases, matching WhatsApp Web's `NEXT_PK_ID` pattern: ```rust theme={null} // Determine starting ID using both the persistent counter AND the store max let max_id = backend.get_max_prekey_id().await?; let start_id = if device_snapshot.next_pre_key_id > 0 { std::cmp::max(device_snapshot.next_pre_key_id, max_id + 1) } else { // Migration: start from MAX(key_id) + 1 max_id + 1 }; ``` This approach prevents ID collisions when pre-keys are consumed non-sequentially from the store. Location: `src/prekeys.rs` ## Storage Integration whatsapp-rust integrates Signal Protocol storage with SQLite via Diesel: ``` src/store/ ├── signal_identity.rs # Identity key storage ├── signal_prekey.rs # PreKey storage ├── signal_session.rs # Session state storage └── signal_sender_key.rs # Sender key storage ``` Each storage trait from `wacore/libsignal/src/protocol/storage/traits.rs` is implemented using SQL queries: ```rust theme={null} #[async_trait] impl SessionStore for SqliteStore { async fn load_session( &mut self, address: &ProtocolAddress, ) -> Result> { // SQL: SELECT record FROM signal_sessions // WHERE our_jid = ? AND their_jid = ? } } ``` ## Security Considerations ### Identity Key Trust The implementation verifies identity keys before encryption/decryption: ```rust theme={null} if !identity_store .is_trusted_identity(remote_address, &their_identity_key, Direction::Sending) .await? { return Err(SignalProtocolError::UntrustedIdentity( remote_address.clone(), )); } ``` Location: `wacore/libsignal/src/protocol/session_cipher.rs:160-172` ### Duplicate Message Detection The protocol detects and rejects duplicate messages: ```rust theme={null} if chain_index > counter { return match state.get_message_keys(their_ephemeral, counter)? { Some(keys) => Ok(keys), None => Err(SignalProtocolError::DuplicatedMessage(chain_index, counter)), }; } ``` Location: `wacore/libsignal/src/protocol/session_cipher.rs:822-827` ### Session State Corruption Detailed logging helps diagnose crypto failures: ```rust theme={null} fn create_decryption_failure_log( remote_address: &ProtocolAddress, errs: &[SignalProtocolError], record: &SessionRecord, ciphertext: &SignalMessage, ) -> Result ``` This generates comprehensive error logs showing: * All attempted session states * Receiver chain information * Message metadata (sender ratchet key, counter) Location: `wacore/libsignal/src/protocol/session_cipher.rs:365-454` ## Performance Optimizations ### Single-Allocation Session Lock Keys Session lock keys use the full Signal protocol address string (e.g., `5511999887766@c.us.0`). The `JidExt` trait provides two methods for generating these strings, both defined in `wacore/src/types/jid.rs`: ```rust theme={null} pub trait JidExt { /// Signal address string: `{user}[:device]@{server}` /// Device part only included when device != 0. fn to_signal_address_string(&self) -> String; /// Full protocol address string: `{signal_address_string}.0` /// Equivalent to `to_protocol_address().to_string()` but avoids the /// intermediate ProtocolAddress allocation — one String instead of two. fn to_protocol_address_string(&self) -> String; } ``` `to_protocol_address_string()` is used on hot paths (message encryption and decryption) as the key for `session_locks`. It pre-sizes the output buffer and builds the string in a single allocation, avoiding the two-allocation overhead of constructing a `ProtocolAddress` and then calling `.to_string()`. **Format examples:** | JID | Signal address | Protocol address string | | --------------------------------- | ----------------------- | ------------------------- | | `5511999887766@s.whatsapp.net` | `5511999887766@c.us` | `5511999887766@c.us.0` | | `5511999887766:33@s.whatsapp.net` | `5511999887766:33@c.us` | `5511999887766:33@c.us.0` | | `123456789@lid` | `123456789@lid` | `123456789@lid.0` | | `123456789:33@lid` | `123456789:33@lid` | `123456789:33@lid.0` | The server `s.whatsapp.net` is mapped to `c.us` in address strings, matching WhatsApp Web's internal format. The trailing `.0` is the Signal device\_id (always 0 in WhatsApp's usage). **Usage in message processing:** ```rust theme={null} // In message decryption (src/message.rs) let signal_addr_str = sender_encryption_jid.to_protocol_address_string(); let session_mutex = self.session_locks .get_with(signal_addr_str.clone(), async { Arc::new(tokio::sync::Mutex::new(())) }).await; let _session_guard = session_mutex.lock().await; // In message encryption (src/send.rs) let signal_addr_str = encryption_jid.to_protocol_address_string(); ``` Location: `wacore/src/types/jid.rs:4-68` ### Take/Restore Pattern Avoids cloning session states during decryption attempts: ```rust theme={null} // Take ownership instead of cloning if let Some(mut current_state) = record.take_session_state() { let result = decrypt_message_with_state(&mut current_state, ...); match result { Ok(ptext) => { record.set_session_state(current_state); return Ok(ptext); } Err(e) => { record.set_session_state(current_state); // Restore } } } ``` Location: `wacore/libsignal/src/protocol/session_cipher.rs:495-564` ### Buffer Reuse Thread-local buffers eliminate per-message allocations: ```rust theme={null} struct EncryptionBuffer { buffer: Vec, usage_count: usize, } const INITIAL_CAPACITY: usize = 1024; const MAX_CAPACITY: usize = 16 * 1024; const SHRINK_THRESHOLD: usize = 100; fn get_buffer(&mut self) -> &mut Vec { self.usage_count += 1; if self.usage_count.is_multiple_of(SHRINK_THRESHOLD) { if self.buffer.capacity() > MAX_CAPACITY { self.buffer = Vec::with_capacity(INITIAL_CAPACITY); } } &mut self.buffer } ``` Location: `wacore/libsignal/src/protocol/session_cipher.rs:20-54` ## Related Components * [Binary Protocol](/advanced/binary-protocol) - How encrypted messages are serialized * [State Management](/advanced/state-management) - How session state is persisted * [WebSocket Handling](/advanced/websocket-handling) - Transport layer for encrypted messages ## References * [Signal Protocol Specification](https://signal.org/docs/) * [libsignal Repository](https://github.com/signalapp/libsignal) * Source: `wacore/libsignal/src/protocol/` * Storage: `src/store/signal*.rs` # State Management & Persistence Source: https://whatsapp-rust.jlucaso.com/advanced/state-management Device state management, PersistenceManager, and the DeviceCommand pattern in whatsapp-rust ## Overview whatsapp-rust uses a strict state management architecture to ensure consistency and prevent race conditions. All device state modifications must go through the `PersistenceManager` using the `DeviceCommand` pattern. **Critical**: Never modify `Device` state directly. Always use `DeviceCommand` + `PersistenceManager::process_command()` for writes, or `get_device_snapshot()` for reads. ## Architecture The state management system has three main components: ``` src/store/ ├── persistence_manager.rs # Central state coordinator ├── commands.rs # DeviceCommand pattern ├── device.rs # Device state structure └── backend/ # Storage backend (SQLite) ``` ## Device State The `Device` struct holds all client state (defined in `wacore::store::device`): ```rust theme={null} pub struct Device { pub pn: Option, // Phone number JID pub lid: Option, // Linked Identity JID pub registration_id: u32, pub noise_key: KeyPair, // Noise protocol keypair pub identity_key: KeyPair, // Signal protocol identity pub signed_pre_key: KeyPair, pub signed_pre_key_id: u32, pub signed_pre_key_signature: [u8; 64], pub adv_secret_key: [u8; 32], pub account: Option, pub push_name: String, pub app_version_primary: u32, pub app_version_secondary: u32, pub app_version_tertiary: u32, pub app_version_last_fetched_ms: i64, pub device_props: wa::DeviceProps, pub edge_routing_info: Option>, pub props_hash: Option, pub next_pre_key_id: u32, } ``` Location: `wacore/src/store/device.rs` ## PersistenceManager The `PersistenceManager` is the gatekeeper for all state changes. ### Architecture ```rust theme={null} pub struct PersistenceManager { device: Arc>, backend: Arc, dirty: Arc, save_notify: Arc, } ``` Location: `src/store/persistence_manager.rs:11-16` ### Key Methods #### Read-Only Access ```rust theme={null} // Get a snapshot of device state (cheap clone) pub async fn get_device_snapshot(&self) -> Device { self.device.read().await.clone() } ``` Location: `src/store/persistence_manager.rs:61-63` #### State Modification ```rust theme={null} // Modify device state with a closure pub async fn modify_device(&self, modifier: F) -> R where F: FnOnce(&mut Device) -> R, { let mut device_guard = self.device.write().await; let result = modifier(&mut device_guard); // Mark dirty and notify background saver self.dirty.store(true, Ordering::Relaxed); self.save_notify.notify_one(); result } ``` Location: `src/store/persistence_manager.rs:69-80` #### Command Processing ```rust theme={null} // Process a device command (preferred for state changes) pub async fn process_command(&self, command: DeviceCommand) { self.modify_device(|device| { apply_command_to_device(device, command); }).await; } ``` Location: `src/store/persistence_manager.rs:145-150` ### Background Saver The persistence manager runs a background task that periodically saves dirty state: ```rust theme={null} pub fn run_background_saver(self: Arc, interval: Duration) { tokio::spawn(async move { loop { tokio::select! { _ = self.save_notify.notified() => { debug!("Save notification received."); } _ = sleep(interval) => {} } if let Err(e) = self.save_to_disk().await { error!("Error saving device state: {e}"); } } }); } ``` **How it works:** 1. Wakes up when notified OR every `interval` (typically 30s) 2. Checks if state is dirty (`dirty` flag) 3. If dirty, serializes device state and saves to database 4. Clears dirty flag Location: `src/store/persistence_manager.rs:123-140` ### Initialization ```rust theme={null} pub async fn new(backend: Arc) -> Result { // Ensure device row exists in database let exists = backend.exists().await?; if !exists { let id = backend.create().await?; debug!("Created device row with id={id}"); } // Load existing state or create new let device = if let Some(serializable_device) = backend.load().await? { let mut dev = Device::new(backend.clone()); dev.load_from_serializable(serializable_device); dev } else { Device::new(backend.clone()) }; Ok(Self { device: Arc::new(RwLock::new(device)), backend, dirty: Arc::new(AtomicBool::new(false)), save_notify: Arc::new(Notify::new()), }) } ``` Location: `src/store/persistence_manager.rs:23-55` ## DeviceCommand Pattern The `DeviceCommand` enum defines all possible state mutations: ```rust theme={null} pub enum DeviceCommand { SetId(Option), SetLid(Option), SetPushName(String), SetAccount(Option), SetAppVersion((u32, u32, u32)), SetDeviceProps( Option, Option, Option, ), SetPropsHash(Option), SetNextPreKeyId(u32), } ``` Location: `wacore/src/store/commands.rs` ### Why Commands? The command pattern provides: 1. **Type safety**: All state changes are explicitly defined 2. **Auditability**: Easy to log/trace state mutations 3. **Testability**: Commands can be tested in isolation 4. **Consistency**: Single code path for all modifications 5. **Future compatibility**: Easy to add undo/redo or migration logic ### Applying Commands Commands are applied via pattern matching: ```rust theme={null} pub fn apply_command_to_device(device: &mut Device, command: DeviceCommand) { match command { DeviceCommand::SetId(id) => { device.pn = id; } DeviceCommand::SetLid(lid) => { device.lid = lid; } DeviceCommand::SetPushName(name) => { device.push_name = name; } DeviceCommand::SetAccount(account) => { device.account = account; } DeviceCommand::SetAppVersion((p, s, t)) => { device.app_version_primary = p; device.app_version_secondary = s; device.app_version_tertiary = t; } // ... handle all variants } } ``` Location: `wacore/src/store/commands.rs` ## Usage Patterns ### Reading Device State ```rust theme={null} // In async function let device = persistence_manager.get_device_snapshot().await; println!("Device JID: {:?}", device.pn); println!("Push name: {}", device.push_name); ``` `get_device_snapshot()` returns a cloned `Device`. This is efficient because most fields are cheap to clone (strings, numbers). Large data like cryptographic keys use `Arc` internally. ### Modifying Device State (Simple) For simple state changes, use commands: ```rust theme={null} use wacore::store::commands::DeviceCommand; // Update push name persistence_manager.process_command( DeviceCommand::SetPushName("My New Name".to_string()) ).await; // Update props hash persistence_manager.process_command( DeviceCommand::SetPropsHash(Some("new_hash".to_string())) ).await; ``` ### Modifying Device State (Complex) For complex logic involving multiple fields or conditionals: ```rust theme={null} persistence_manager.modify_device(|device| { // Complex mutation logic device.push_name = "Updated Name".to_string(); device.edge_routing_info = Some(new_routing_data); }).await; ``` Keep the closure passed to `modify_device` as short as possible. It holds a write lock on the device state, blocking all other modifications. ## Concurrency Patterns ### RwLock Semantics The `Device` is protected by a `tokio::sync::RwLock`: * **Multiple readers**: `get_device_snapshot()` can be called concurrently * **Single writer**: `modify_device()` blocks all other access * **Writer priority**: Pending writes block new reads (avoid reader starvation) ### Session locks and message queues The `Client` uses two cache-based lock mechanisms for per-chat and per-device serialization: ```rust theme={null} pub struct Client { /// Per-device session locks for Signal protocol operations. /// Prevents race conditions when multiple messages from the same sender /// are processed concurrently across different chats. /// Keys are protocol address strings from to_protocol_address_string() /// (e.g., "5511999887766@c.us.0", "123456789:33@lid.0"). pub(crate) session_locks: Cache>>, /// Per-chat message queues for sequential message processing. /// Prevents race conditions where a later message is processed before /// the PreKey message that establishes the Signal session. pub(crate) message_queues: Cache>>, /// Per-chat mutex for serializing message enqueue operations. /// Ensures messages are enqueued in the order they arrive. pub(crate) message_enqueue_locks: Cache>>, // ... } ``` All three use [moka](https://github.com/moka-rs/moka) `Cache` with TTL-based eviction (configurable via `CacheConfig`), so stale entries are automatically cleaned up. Location: `src/client.rs` ### Blocking Operations CPU-heavy or blocking operations must use `spawn_blocking` to avoid stalling the async runtime: ```rust theme={null} use tokio::task::spawn_blocking; // Bad: Blocks async runtime let encrypted = expensive_crypto_operation(&data); // Good: Offloads to thread pool let encrypted = spawn_blocking(move || { expensive_crypto_operation(&data) }).await?; ``` For more details on async patterns, see the [Architecture](/concepts/architecture) guide. ## Storage Backend ### Backend Trait The `Backend` trait is a combination of four domain-specific traits: ```rust theme={null} pub trait Backend: SignalStore + AppSyncStore + ProtocolStore + DeviceStore + Send + Sync {} impl Backend for T where T: SignalStore + AppSyncStore + ProtocolStore + DeviceStore + Send + Sync {} ``` See [Storage Traits](/api/store) for the full trait definitions. Location: `wacore/src/store/traits.rs` ### SQLite Implementation The default storage backend uses SQLite with the Diesel ORM. See [Storage Traits](/api/store#sqlitestore-implementation) for details on `SqliteStore`, including connection pooling, WAL mode, and multi-device support. Location: `storages/sqlite-storage/` ### Serialization The `Device` struct derives `Serialize` and `Deserialize` (from serde) for persistence. The `PersistenceManager` handles serializing device state to the backend via the `DeviceStore` trait's `save()` and `load()` methods. ## Debugging State ### Database Snapshots The `debug-snapshots` feature enables database snapshots for debugging: ```rust theme={null} // In error handler if let Err(e) = decrypt_message(...) { persistence_manager.create_snapshot( "decrypt_error", Some(error_details.as_bytes()) ).await?; return Err(e); } ``` This creates a timestamped copy of the database: ``` chats.db chats_snapshot_decrypt_error_20260228_143022.db chats_snapshot_decrypt_error_20260228_143022.txt (metadata) ``` Location: `src/store/persistence_manager.rs:99-121` ### Logging State changes are logged at debug level: ```rust theme={null} RUST_LOG=whatsapp_rust::store=debug cargo run ``` Output: ``` [DEBUG] PersistenceManager: Ensuring device row exists. [DEBUG] PersistenceManager: Loaded existing device data (PushName: 'Alice') [DEBUG] Device state is dirty, saving to disk. [DEBUG] Device state saved successfully. ``` ## Best Practices ### 1. Always Use Commands for State Changes ```rust theme={null} // Bad: Direct modification persistence_manager.modify_device(|device| { device.push_name = "New Name".to_string(); }).await; // Good: Use command persistence_manager.process_command( DeviceCommand::SetPushName("New Name".to_string()) ).await; ``` ### 2. Minimize Lock Duration ```rust theme={null} // Bad: Long lock duration persistence_manager.modify_device(|device| { let data = expensive_calculation(&device.pn); // Blocks all access! device.push_name = data; }).await; // Good: Release lock during expensive operation let device = persistence_manager.get_device_snapshot().await; let data = expensive_calculation(&device.pn); persistence_manager.process_command( DeviceCommand::SetPushName(data) ).await; ``` ### 3. Use Chat Locks for Chat-Specific Operations ```rust theme={null} // Per-chat locks serialize operations on the same chat // The Client uses session_locks and message_queues internally // to prevent race conditions during message processing. ``` ### 4. Offload Heavy Operations ```rust theme={null} use tokio::task::spawn_blocking; // Crypto operations should use spawn_blocking let ciphertext = spawn_blocking(move || { encrypt_message(&plaintext, &key) }).await??; ``` ## Cache patching strategy Beyond device state, whatsapp-rust maintains several in-memory caches (device registry, group metadata, LID-PN mappings) that require real-time updates when server notifications arrive. ### Granular patching vs. invalidation The client uses **granular cache patching** rather than the simpler invalidate-and-refetch approach. When a notification indicates a change (for example, a new device added or a group participant removed), the client reads the cached value, applies the diff in memory, and writes the updated value back — all without making any network requests. This avoids an extra IQ round-trip per update. If no cache entry exists when the notification arrives, the patch is silently skipped, and the next read fetches authoritative state from the server. ```rust theme={null} // Internal patching pattern (not a public API) if let Some(mut cached) = cache.get(&key).await { cached.apply_change(notification_data); cache.insert(key, cached).await; } ``` ### Concurrency model The `get` → mutate → `insert` sequence is **not atomic**. A concurrent notification for the same key could race and cause one update to be lost. This is acceptable because: 1. The cache is best-effort — a full server fetch on the next read corrects any drift 2. Races are rare in practice (device and group notifications for the same user rarely overlap) 3. Device registry patches persist to the backend store immediately, so even if the cache entry is evicted, the persistent state is correct ### Where patching is used | Cache | Patched on | Persisted | Fallback | | --------------- | -------------------------------------------------- | ------------------- | ------------------------------------------------------- | | Device registry | Device add/remove/update notifications | Yes (backend store) | Hash-only notifications trigger full invalidation | | Group metadata | Participant add/remove notifications and API calls | No (cache-only) | `leave()` and cache eviction trigger server re-fetch | | LID-PN mappings | Usync, peer messages, device notifications | Yes (backend store) | Timestamp conflict resolution prevents stale overwrites | For full details including patching methods and data flow, see [Granular cache patching](/concepts/storage#granular-cache-patching). ## Related Components * [Signal Protocol](/advanced/signal-protocol) - Cryptographic state stored in Device * [Binary Protocol](/advanced/binary-protocol) - Protocol messages modify device state * [WebSocket Handling](/advanced/websocket-handling) - Connection state in Device * [Storage](/concepts/storage) - Cache patching and pluggable cache stores ## References * Implementation: `src/store/persistence_manager.rs` * Commands: `wacore/src/store/commands.rs` * Device structure: `wacore/src/store/device.rs` * Backend trait: `wacore/src/store/traits.rs` * SQLite backend: `storages/sqlite-storage/src/sqlite_store.rs` # WebSocket & Noise Protocol Source: https://whatsapp-rust.jlucaso.com/advanced/websocket-handling NoiseSocket, connection management, handshake protocol, and frame handling in whatsapp-rust ## Overview whatsapp-rust uses WebSocket for transport and the Noise Protocol for encryption. All messages are encrypted at the transport layer before being sent over the network. ## Architecture The WebSocket handling system has several layers: ``` src/ ├── handshake.rs # Noise protocol handshake ├── socket/ │ ├── noise_socket.rs # Encrypted communication layer │ ├── mod.rs # Re-exports │ └── error.rs # Socket errors ├── transport/ # WebSocket transport abstraction └── client.rs # High-level client orchestration ``` ## Noise Protocol Handshake The handshake establishes an encrypted channel using the Noise XX pattern. A 20-second timeout (`NOISE_HANDSHAKE_RESPONSE_TIMEOUT`) is applied when waiting for the server's handshake response, ensuring the client does not hang indefinitely if the server is unresponsive. ### Handshake State ```rust theme={null} pub struct HandshakeState { noise_key: KeyPair, // Client's static Curve25519 keypair client_payload: Vec, // Client identification data pattern: &'static str, // "Noise_XX_25519_AESGCM_SHA256" prologue: &'static [u8], // "WA" header } ``` Location: `wacore/src/handshake/state.rs` ### Handshake Process ```rust theme={null} // From src/handshake.rs:29-104 pub async fn do_handshake( device: &Device, transport: Arc, transport_events: &mut async_channel::Receiver, ) -> Result> ``` **Step-by-step process:** 1. **Prepare client payload:** ```rust theme={null} let client_payload = device.core.get_client_payload().encode_to_vec(); ``` The payload contains: * Client version * Platform information * Device details 2. **Initialize Noise state:** ```rust theme={null} let mut handshake_state = HandshakeState::new( device.core.noise_key.clone(), client_payload, NOISE_START_PATTERN, // "Noise_XX_25519_AESGCM_SHA256" &WA_CONN_HEADER, // [0x57, 0x41] ("WA") )?; ``` 3. **Send ClientHello:** ```rust theme={null} let client_hello_bytes = handshake_state.build_client_hello()?; // Optionally include edge routing for faster reconnection let (header, used_edge_routing) = build_handshake_header(device.core.edge_routing_info.as_deref()); let framed = wacore::framing::encode_frame( &client_hello_bytes, Some(&header) )?; transport.send(framed).await?; ``` 4. **Receive ServerHello** (with 20s timeout): ```rust theme={null} const NOISE_HANDSHAKE_RESPONSE_TIMEOUT: Duration = Duration::from_secs(20); let resp_frame = loop { match timeout(NOISE_HANDSHAKE_RESPONSE_TIMEOUT, transport_events.recv()).await { Ok(Ok(TransportEvent::DataReceived(data))) => { frame_decoder.feed(&data); if let Some(frame) = frame_decoder.decode_frame() { break frame; } } // Handle errors... } }; ``` 5. **Send ClientFinish:** ```rust theme={null} let client_finish_bytes = handshake_state .read_server_hello_and_build_client_finish(&resp_frame)?; let framed = wacore::framing::encode_frame(&client_finish_bytes, None)?; transport.send(framed).await?; ``` 6. **Complete handshake:** ```rust theme={null} let (write_key, read_key) = handshake_state.finish()?; info!("Handshake complete, switching to encrypted communication"); Ok(Arc::new(NoiseSocket::new(transport, write_key, read_key))) ``` Location: `src/handshake.rs:29-104` ### Edge Routing Pre-Intro For optimized reconnection, the client can include edge routing info in the initial frame: ```rust theme={null} pub fn build_handshake_header( edge_routing_info: Option<&[u8]> ) -> (Vec, bool) { let mut header = WA_CONN_HEADER.to_vec(); // [0x57, 0x41] if let Some(info) = edge_routing_info { if info.len() <= 8192 { // Max size header.extend_from_slice(info); return (header, true); } } (header, false) } ``` Location: `wacore/src/handshake/edge_routing.rs` ### Handshake Errors ```rust theme={null} pub enum HandshakeError { Transport(#[from] anyhow::Error), Core(#[from] CoreHandshakeError), Timeout, UnexpectedEvent(String), } ``` Location: `src/handshake.rs:15-25` ## NoiseSocket The `NoiseSocket` provides encrypted send/receive operations after handshake. ### Architecture ```rust theme={null} pub struct NoiseSocket { read_key: Arc, read_counter: Arc, send_job_tx: mpsc::Sender, sender_task_handle: JoinHandle<()>, } struct SendJob { plaintext_buf: Vec, out_buf: Vec, response_tx: oneshot::Sender, } ``` Location: `src/socket/noise_socket.rs:21-32` ### Design Patterns #### Dedicated Sender Task The socket uses a dedicated task for sending to ensure frame ordering: ```rust theme={null} impl NoiseSocket { pub fn new( transport: Arc, write_key: NoiseCipher, read_key: NoiseCipher, ) -> Self { let (send_job_tx, send_job_rx) = mpsc::channel::(32); // Spawn dedicated sender task let sender_task_handle = tokio::spawn( Self::sender_task(transport, write_key, send_job_rx) ); Self { read_key: Arc::new(read_key), read_counter: Arc::new(AtomicU32::new(0)), send_job_tx, sender_task_handle, } } } ``` **Why a dedicated task?** 1. **Ordering guarantee**: Frames must be sent with sequential counters 2. **Non-blocking**: Callers don't block on encryption or network I/O 3. **Backpressure**: Channel capacity (32) prevents unbounded queuing Location: `src/socket/noise_socket.rs:34-62` #### Sender Task Implementation ```rust theme={null} async fn sender_task( transport: Arc, write_key: Arc, mut send_job_rx: mpsc::Receiver, ) { let mut write_counter: u32 = 0; while let Some(job) = send_job_rx.recv().await { let result = Self::process_send_job( &transport, &write_key, &mut write_counter, job.plaintext_buf, job.out_buf, ).await; // Return buffers to caller for reuse let _ = job.response_tx.send(result); } } ``` Location: `src/socket/noise_socket.rs:64-89` ### Encryption #### Small Messages (≤16KB) Encrypted inline to avoid thread pool overhead: ```rust theme={null} if plaintext_buf.len() <= INLINE_ENCRYPT_THRESHOLD { // Copy to output buffer and encrypt in-place out_buf.clear(); out_buf.extend_from_slice(&plaintext_buf); plaintext_buf.clear(); write_key.encrypt_in_place_with_counter(counter, &mut out_buf)?; // Frame the ciphertext wacore::framing::encode_frame_into(&ciphertext, None, &mut out_buf)?; } ``` Location: `src/socket/noise_socket.rs:103-130` #### Large Messages (>16KB) Offloaded to blocking thread pool: ```rust theme={null} else { let plaintext_arc = Arc::new(plaintext_buf); let plaintext_arc_for_task = plaintext_arc.clone(); // Offload to blocking thread let spawn_result = tokio::task::spawn_blocking(move || { write_key.encrypt_with_counter( counter, &plaintext_arc_for_task[..] ) }).await; // Recover original buffer plaintext_buf = Arc::try_unwrap(plaintext_arc) .unwrap_or_else(|arc| (*arc).clone()); let ciphertext = spawn_result??; // Frame and send wacore::framing::encode_frame_into(&ciphertext, None, &mut out_buf)?; transport.send(out_buf).await?; } ``` Location: `src/socket/noise_socket.rs:131-164` The 16KB threshold is chosen based on benchmarking. Smaller messages benefit from inline encryption (no thread spawning overhead), while larger messages benefit from parallel execution. ### Send API ```rust theme={null} pub async fn encrypt_and_send( &self, plaintext_buf: Vec, out_buf: Vec ) -> SendResult { let (response_tx, response_rx) = oneshot::channel(); let job = SendJob { plaintext_buf, out_buf, response_tx, }; // Send to dedicated task if let Err(send_err) = self.send_job_tx.send(job).await { let job = send_err.0; return Err(EncryptSendError::channel_closed( job.plaintext_buf, job.out_buf, )); } // Wait for result match response_rx.await { Ok(result) => result, Err(_) => Err(EncryptSendError::channel_closed( Vec::new(), Vec::new() )), } } ``` **Buffer Management:** The API returns both buffers for reuse: ```rust theme={null} type SendResult = Result<(Vec, Vec), EncryptSendError>; // ^^^^^^^^^^^^^^^^^^^^^ // (plaintext_buf, out_buf) for reuse ``` This enables the client to reuse buffers across multiple sends: ```rust theme={null} let mut plaintext_buf = Vec::with_capacity(1024); let mut out_buf = Vec::with_capacity(1056); // plaintext + 32 overhead loop { plaintext_buf.clear(); marshal_to_vec(&message, &mut plaintext_buf)?; (plaintext_buf, out_buf) = noise_socket.encrypt_and_send(plaintext_buf, out_buf).await?; } ``` Location: `src/socket/noise_socket.rs:173-200` ### Decryption ```rust theme={null} pub fn decrypt_frame(&self, ciphertext: &[u8]) -> Result> { let counter = self.read_counter.fetch_add(1, Ordering::SeqCst); self.read_key .decrypt_with_counter(counter, ciphertext) .map_err(|e| SocketError::Crypto(e.to_string())) } ``` **Decryption is synchronous** because: 1. Frames arrive sequentially in the transport receiver 2. Decryption is fast (AES-GCM hardware acceleration) 3. No ordering concerns (unlike send) Location: `src/socket/noise_socket.rs:202-207` ### Cleanup ```rust theme={null} impl Drop for NoiseSocket { fn drop(&mut self) { // Abort sender task to prevent resource leaks self.sender_task_handle.abort(); } } ``` Location: `src/socket/noise_socket.rs:210-217` ## Frame Protocol Messages are framed before encryption: ```rust theme={null} pub fn encode_frame( payload: &[u8], header: Option<&[u8]> ) -> Result> { let mut output = Vec::new(); if let Some(header) = header { output.extend_from_slice(header); } // 3-byte big-endian length let len = payload.len() as u32; output.push(((len >> 16) & 0xFF) as u8); output.push(((len >> 8) & 0xFF) as u8); output.push((len & 0xFF) as u8); output.extend_from_slice(payload); Ok(output) } ``` Location: `wacore/src/framing/mod.rs:10-30` ### Frame Format ``` ┌────────────────┬─────────────────┬────────────────────┐ │ Optional Header│ Length (3 bytes)│ Payload │ │ (handshake) │ (big-endian) │ (encrypted) │ └────────────────┴─────────────────┴────────────────────┘ ``` **Frame length limits:** * Maximum frame size: 16MB (enforced by framing layer) * Typical message frame: \< 1KB * Media messages: 100KB - 2MB (encrypted metadata) ### Frame Decoder ```rust theme={null} pub struct FrameDecoder { buffer: Vec, } impl FrameDecoder { pub fn feed(&mut self, data: &[u8]) { self.buffer.extend_from_slice(data); } pub fn decode_frame(&mut self) -> Option> { if self.buffer.len() < 3 { return None; // Need length header } let len = u32::from_be_bytes([ 0, self.buffer[0], self.buffer[1], self.buffer[2], ]) as usize; if self.buffer.len() < 3 + len { return None; // Incomplete frame } let frame = self.buffer[3..3 + len].to_vec(); self.buffer.drain(..3 + len); Some(frame) } } ``` Location: `wacore/src/framing/decoder.rs` ## Transport Abstraction The transport layer abstracts WebSocket implementation: ```rust theme={null} #[async_trait] pub trait Transport: Send + Sync { async fn send(&self, data: Vec) -> Result<(), anyhow::Error>; async fn disconnect(&self); } pub enum TransportEvent { Connected, Disconnected, DataReceived(Vec), } ``` Location: `src/transport/mod.rs` ### WebSocket Implementation ```rust theme={null} pub struct TungsteniteTransport { write_tx: mpsc::Sender>, disconnect_tx: Arc, } #[async_trait] impl Transport for TungsteniteTransport { async fn send(&self, data: Vec) -> Result<()> { self.write_tx.send(data).await .map_err(|_| anyhow!("transport closed")) } async fn disconnect(&self) { self.disconnect_tx.notify_one(); } } ``` Location: `src/transport/tungstenite.rs` ## Connection Lifecycle ### Connect timeout Both the transport connection and the version fetch are wrapped in a 20-second timeout (`TRANSPORT_CONNECT_TIMEOUT`), matching WhatsApp Web's MQTT `CONNECT_TIMEOUT` and DGW `connectTimeoutMs` defaults. Without this, a dead network would block on the OS TCP SYN timeout (\~60-75s). ```rust theme={null} const TRANSPORT_CONNECT_TIMEOUT: Duration = Duration::from_secs(20); ``` The client runs the version fetch and transport connection **in parallel** using `tokio::join!`, both under this timeout: ```rust theme={null} let version_future = tokio::time::timeout( TRANSPORT_CONNECT_TIMEOUT, resolve_and_update_version(&persistence_manager, &http_client, override_version), ); let transport_future = tokio::time::timeout( TRANSPORT_CONNECT_TIMEOUT, transport_factory.create_transport(), ); let (version_result, transport_result) = tokio::join!(version_future, transport_future); ``` If either times out, the connection attempt fails with a descriptive error (e.g., `"Transport connect timed out after 20s"`). Location: `src/client.rs:108-877` ### 1. Connect ```rust theme={null} impl Client { pub async fn connect(&self) -> Result<()> { // Version fetch + transport connection run in parallel, both under 20s timeout let (transport, mut events) = tokio::time::timeout( TRANSPORT_CONNECT_TIMEOUT, self.transport_factory.create_transport(), ).await??; // Perform Noise handshake let device = self.persistence_manager.get_device_snapshot().await; let noise_socket = do_handshake( &device, transport, &mut events ).await?; // Store socket and start receivers self.noise_socket.store(Some(Arc::clone(&noise_socket))); self.start_frame_receiver(events, noise_socket).await; Ok(()) } } ``` ### 2. Frame Receiver ```rust theme={null} async fn start_frame_receiver( &self, mut events: Receiver, noise_socket: Arc, ) { tokio::spawn(async move { let mut frame_decoder = FrameDecoder::new(); while let Ok(event) = events.recv().await { match event { TransportEvent::DataReceived(data) => { frame_decoder.feed(&data); while let Some(frame) = frame_decoder.decode_frame() { // Decrypt frame let plaintext = noise_socket .decrypt_frame(&frame)?; // Process message self.handle_frame(plaintext).await?; } } TransportEvent::Disconnected => { self.handle_disconnect().await; break; } _ => {} } } }); } ``` ### 3. Send Message ```rust theme={null} impl Client { pub async fn send_node(&self, node: &Node) -> Result<()> { let noise_socket = self.noise_socket.load() .ok_or_else(|| anyhow!("not connected"))?; // Reuse buffers for efficiency let mut plaintext_buf = Vec::with_capacity(1024); let mut out_buf = Vec::with_capacity(1056); // Marshal node to binary marshal_to_vec(node, &mut plaintext_buf)?; // Encrypt and send (plaintext_buf, out_buf) = noise_socket .encrypt_and_send(plaintext_buf, out_buf) .await?; // Buffers returned for potential reuse Ok(()) } } ``` ### 4. Disconnect ```rust theme={null} impl Client { pub async fn disconnect(&self) -> Result<()> { if let Some(socket) = self.noise_socket.swap(None) { // Transport will close connection socket.transport.disconnect().await; } Ok(()) } } ``` ### Connection state tracking The client tracks whether the noise socket is established using a dedicated `AtomicBool` (`is_connected`) rather than probing the noise socket mutex. This design prevents a TOCTOU race where `try_lock()` on the mutex fails due to contention (e.g., during frame encryption), not because the socket is absent — which previously caused `is_connected()` to return `false` on live connections, silently dropping receipt acks. **State transitions:** | Event | `is_connected` value | Ordering | | ---------------------------- | -------------------- | ---------------------------------- | | `connect()` start | `false` | `Relaxed` (reset) | | Noise socket stored | `true` | `Release` (after socket is `Some`) | | `cleanup_connection_state()` | `false` | `Release` (after socket is `None`) | The `Release`/`Acquire` ordering ensures that any task reading `is_connected() == true` is guaranteed to see the noise socket as `Some`, and any task reading `false` after cleanup sees the socket as `None`. ```rust theme={null} // Lock-free connection check — never affected by mutex contention pub fn is_connected(&self) -> bool { self.is_connected.load(Ordering::Acquire) } ``` This is critical for the keepalive loop and stanza acknowledgment, both of which call `is_connected()` to decide whether to send data. Under the old `try_lock()` approach, concurrent `send_node()` calls holding the mutex would cause false negatives, leading to skipped keepalive pings or dropped ack stanzas. ## Error Handling ### Socket Errors ```rust theme={null} pub enum SocketError { SocketClosed, NoiseHandshake(String), Io(String), Crypto(String), } pub enum EncryptSendError { Crypto { error: anyhow::Error, ... }, Transport { error: anyhow::Error, ... }, Framing { error: FramingError, ... }, Join { error: JoinError, ... }, ChannelClosed { ... }, } ``` All variants return buffers for reuse: ```rust theme={null} impl EncryptSendError { pub fn into_buffers(self) -> (Vec, Vec) { match self { Self::Crypto { plaintext_buf, out_buf, .. } => (plaintext_buf, out_buf), Self::Transport { plaintext_buf, out_buf, .. } => (plaintext_buf, out_buf), // ... } } } ``` Location: `src/socket/error.rs` ### Stream error handling When the server sends a `` stanza, it is processed inline (not spawned concurrently) because stream errors are critical for connection state. The `StanzaRouter` dispatches the node to a `StreamErrorHandler`, which calls `Client::handle_stream_error()`. Each stream error sets `is_logged_in = false` and fires the `shutdown_notifier` to exit the keepalive loop and other background tasks. **Error code behavior:** | Code | Action | Event | Reconnects? | | ------- | ---------------------------- | ---------------- | ---------------------------------- | | **401** | Disables auto-reconnect | `LoggedOut` | No — session invalid, must re-pair | | **409** | Disables auto-reconnect | `StreamReplaced` | No — prevents displacement loop | | **429** | Adds 5 to backoff counter | None | Yes — extended Fibonacci backoff | | **503** | Normal handling | None | Yes — standard backoff | | **515** | Marks as expected disconnect | None | Yes — immediate, no backoff | | **516** | Disables auto-reconnect | `LoggedOut` | No — device removed | | Unknown | Disables auto-reconnect | `StreamError` | No | **Processing pipeline:** ``` Server sends → read_messages_loop (inline, not spawned) → StanzaRouter → StreamErrorHandler → Client::handle_stream_error() → is_logged_in = false → Code-specific handling (see table above) → shutdown_notifier fires → keepalive exits → run() loop checks enable_auto_reconnect ``` ### Stanza acknowledgment The client automatically sends `` nodes in response to incoming stanzas (messages, receipts, notifications, calls). The ack construction follows WhatsApp Web and whatsmeow behavior: **Ack attributes:** | Attribute | Value | | ------------- | ---------------------------------------------------------------------- | | `class` | Original stanza tag (e.g., `"message"`, `"receipt"`, `"notification"`) | | `id` | Copied from the incoming stanza | | `to` | Flipped from the incoming `from` attribute | | `participant` | Copied from the incoming stanza (when present) | | `from` | Own device phone number JID — only included for message acks | **`type` attribute rules:** The `type` attribute is handled differently depending on the stanza: | Stanza | `type` in ack | Reason | | ------------------------------------------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- | | `message` | **Never included** | Matches whatsmeow: `node.Tag != "message"` guard skips type for messages | | `receipt` (with type, e.g. `"read"`) | **Echoed** | WA Web echoes `type` when explicitly present | | `receipt` (without type, i.e. delivery) | **Omitted** | Delivery receipts have no `type`; including one (e.g., `type="delivery"`) causes `` disconnections | | `notification` | **Echoed** | Type is echoed for most notifications | | `notification type="encrypt"` with `` child | **Omitted** | WA Web specifically drops `type` for identity-change notifications | Sending incorrect `type` attributes in ack stanzas can cause the server to issue `` disconnections. The library handles this automatically — you don't need to build ack nodes manually. Location: `src/client.rs` (`build_ack_node`, `is_encrypt_identity_notification`) ### Fibonacci backoff The reconnection backoff follows the Fibonacci sequence, matching WhatsApp Web's behavior: ``` Sequence: 1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s, 55s, 89s, 144s, ... Maximum: 900s (15 minutes) Jitter: ±10% ``` For rate-limited errors (429), the backoff counter is incremented by 5 before the normal increment, causing the delay to jump significantly on the next reconnection attempt. ### Retry strategy The `run()` method handles reconnection automatically: ```rust theme={null} // Simplified reconnection logic in run() loop { self.connect().await; self.read_messages_loop().await; if !self.enable_auto_reconnect.load(Ordering::Relaxed) { break; // 401, 409, 516 — stop permanently } if self.expected_disconnect.load(Ordering::Relaxed) { continue; // 515 — reconnect immediately } let errors = self.auto_reconnect_errors.fetch_add(1, Ordering::Relaxed); let delay = fibonacci_backoff(errors + 1); sleep(delay).await; } ``` ## Keepalive and Dead Socket Detection The keepalive loop monitors connection health, matching WhatsApp Web's behavior precisely. ### Constants | Constant | Value | Description | | ------------------------------ | ----- | ----------------------------------------------------- | | `KEEP_ALIVE_INTERVAL_MIN` | 15s | Minimum interval between pings | | `KEEP_ALIVE_INTERVAL_MAX` | 30s | Maximum interval between pings | | `KEEP_ALIVE_RESPONSE_DEADLINE` | 20s | Timeout waiting for pong response | | `DEAD_SOCKET_TIME` | 20s | Max silence after a send before declaring socket dead | ### Keepalive loop behavior The loop runs every 15-30 seconds (randomized, matching WA Web's `15 * (1 + random())` formula) and performs these checks in order: 1. **Skip if recently active** — if data was received within `KEEP_ALIVE_INTERVAL_MIN` (15s), the connection is proven alive; skip the ping and reset the error counter 2. **Send keepalive ping** — sends the ping *before* the dead-socket check so that a successful pong updates `last_data_received_ms` and prevents false-positive dead-socket detection on idle-but-healthy connections 3. **RTT-adjusted clock skew** — on pong, calculates server time offset using the midpoint formula: `(startTime + rtt/2) / 1000 - serverTime`, matching WA Web's `onClockSkewUpdate` 4. **Skip ping when IQ pending** — if there are already pending IQ responses, the connection is implicitly being tested; skip the explicit ping ### Dead socket detection Dead socket detection mirrors WA Web's `deadSocketTimer` pattern: * **Not armed** if nothing was ever sent (both timestamps are zero) * **Cancelled** if data was received after the last send * **Fires** if `DEAD_SOCKET_TIME` (20s) has elapsed since the last send with no receive When a dead socket is detected after a transient keepalive failure, the client calls `reconnect_immediately()` and exits the keepalive loop. ### Error classification Keepalive errors are classified exhaustively (compile-time enforced for new error variants): | Error type | Classification | Behavior | | ----------------------------------------------------------------- | -------------- | ---------------------------------------- | | `Socket`, `Disconnected`, `NotConnected`, `InternalChannelClosed` | Fatal | Exit keepalive loop immediately | | `Timeout`, `ServerError`, `ParseError` | Transient | Increment error count, check dead socket | ### Periodic maintenance Approximately every 12 keepalive ticks (\~5 minutes), the keepalive loop runs background cleanup of expired sent messages from the database, based on `CacheConfig::sent_message_ttl_secs`. ## Performance Considerations ### Buffer Sizing Optimal buffer capacity based on payload characteristics: ```rust theme={null} // Encrypted size = plaintext + 16 (AES-GCM tag) + 3 (frame header) let buffer_capacity = plaintext.len() + 32; // Extra headroom let out_buf = Vec::with_capacity(buffer_capacity); ``` Location: `src/socket/noise_socket.rs:373-407` (tests verify this formula) ### SIMD Encryption The Noise cipher uses hardware AES acceleration when available: ```rust theme={null} pub struct NoiseCipher { cipher: Aes256Gcm, // Uses AES-NI on x86_64 } ``` ### Zero-Copy Patterns ```rust theme={null} // Bad: Allocates new buffer let data = node.to_bytes(); socket.send(data).await?; // Good: Reuses buffer let mut buf = Vec::with_capacity(1024); marshal_to_vec(&node, &mut buf)?; socket.send(buf).await?; ``` ## Testing ### Mock Transport ```rust theme={null} pub struct MockTransport; #[async_trait] impl Transport for MockTransport { async fn send(&self, data: Vec) -> Result<()> { // Record for assertions Ok(()) } async fn disconnect(&self) {} } ``` Location: `src/transport/mock.rs` ### Test Cases Key test scenarios: ```rust theme={null} #[tokio::test] async fn test_encrypt_and_send_returns_both_buffers() #[tokio::test] async fn test_concurrent_sends_maintain_order() #[tokio::test] async fn test_encrypted_buffer_sizing_is_sufficient() #[tokio::test] async fn test_handshake_with_edge_routing() ``` Location: `src/socket/noise_socket.rs:219-459` ## Related Components * [Signal Protocol](/advanced/signal-protocol) - Message-level encryption * [Binary Protocol](/advanced/binary-protocol) - Payload serialization * [State Management](/advanced/state-management) - Connection state persistence ## References * Handshake: `src/handshake.rs` * NoiseSocket: `src/socket/noise_socket.rs` * Framing: `wacore/src/framing/` * Transport: `src/transport/` * [Noise Protocol Framework](https://noiseprotocol.org/noise.html) * [Noise XX Pattern](https://noiseprotocol.org/noise.html#interactive-patterns) # Blocking Source: https://whatsapp-rust.jlucaso.com/api/blocking Block and unblock contacts, manage blocklist The `Blocking` trait provides methods for blocking and unblocking contacts, as well as retrieving and checking the blocklist. ## Access Access blocking operations through the client: ```rust theme={null} let blocking = client.blocking(); ``` ## Methods ### block Block a contact. ```rust theme={null} pub async fn block(&self, jid: &Jid) -> Result<(), IqError> ``` **Parameters:** * `jid` - Contact JID to block **Effects:** * Contact will not be able to message you * Contact will not see your presence updates * Contact will not see your profile picture (depending on privacy settings) **Example:** ```rust theme={null} let contact: Jid = "15551234567@s.whatsapp.net".parse()?; client.blocking().block(&contact).await?; println!("Blocked {}", contact); ``` ### unblock Unblock a previously blocked contact. ```rust theme={null} pub async fn unblock(&self, jid: &Jid) -> Result<(), IqError> ``` **Parameters:** * `jid` - Contact JID to unblock **Example:** ```rust theme={null} let contact: Jid = "15551234567@s.whatsapp.net".parse()?; client.blocking().unblock(&contact).await?; println!("Unblocked {}", contact); ``` ### get\_blocklist Retrieve the full list of blocked contacts. ```rust theme={null} pub async fn get_blocklist(&self) -> Result, anyhow::Error> ``` **Returns:** * `Vec` - All blocked contacts **BlocklistEntry fields:** * `jid: Jid` - Blocked contact JID * `timestamp: Option` - Unix timestamp when blocked (if available) **Example:** ```rust theme={null} let blocklist = client.blocking().get_blocklist().await?; println!("Blocked contacts: {}", blocklist.len()); for entry in blocklist { println!(" JID: {}", entry.jid); if let Some(ts) = entry.timestamp { println!(" Blocked at: {}", ts); } } ``` ### is\_blocked Check if a specific contact is blocked. ```rust theme={null} pub async fn is_blocked(&self, jid: &Jid) -> Result ``` **Parameters:** * `jid` - Contact JID to check **Returns:** * `bool` - `true` if blocked, `false` otherwise **Behavior:** * Compares only the user part of the JID (ignores device ID) * Blocking applies to the entire user account, not individual devices **Example:** ```rust theme={null} let contact: Jid = "15551234567@s.whatsapp.net".parse()?; if client.blocking().is_blocked(&contact).await? { println!("{} is blocked", contact); } else { println!("{} is not blocked", contact); } ``` ## BlocklistEntry Type ```rust theme={null} pub struct BlocklistEntry { pub jid: Jid, pub timestamp: Option, } ``` **Fields:** * `jid` - The blocked contact's JID * `timestamp` - Unix timestamp (seconds since epoch) when the contact was blocked The timestamp may be `None` if the server doesn't provide it. ## Error Types ### IqError Returned by `block()` and `unblock()` operations: ```rust theme={null} pub enum IqError { // Network/protocol errors Timeout, InvalidResponse, // ... other variants } ``` ### anyhow::Error Returned by `get_blocklist()` and `is_blocked()` for general errors. ## Wire Format ### Block Request ```xml theme={null} ``` ### Unblock Request ```xml theme={null} ``` ### Get Blocklist Request ```xml theme={null} ``` ### Blocklist Response ```xml theme={null} ``` Or direct items without `` wrapper: ```xml theme={null} ``` ## Usage Examples ### Block a Contact ```rust theme={null} let contact: Jid = "15551234567@s.whatsapp.net".parse()?; match client.blocking().block(&contact).await { Ok(_) => println!("Successfully blocked {}", contact), Err(e) => eprintln!("Failed to block: {}", e), } ``` ### Unblock a Contact ```rust theme={null} let contact: Jid = "15551234567@s.whatsapp.net".parse()?; match client.blocking().unblock(&contact).await { Ok(_) => println!("Successfully unblocked {}", contact), Err(e) => eprintln!("Failed to unblock: {}", e), } ``` ### Check Before Blocking ```rust theme={null} let contact: Jid = "15551234567@s.whatsapp.net".parse()?; if !client.blocking().is_blocked(&contact).await? { client.blocking().block(&contact).await?; println!("Contact blocked"); } else { println!("Contact already blocked"); } ``` ### List All Blocked Contacts ```rust theme={null} let blocklist = client.blocking().get_blocklist().await?; if blocklist.is_empty() { println!("No blocked contacts"); } else { println!("Blocked contacts:"); for entry in blocklist { print!(" - {}", entry.jid); if let Some(ts) = entry.timestamp { println!(" (blocked: {})", ts); } else { println!(); } } } ``` ### Conditional Block/Unblock ```rust theme={null} let contact: Jid = "15551234567@s.whatsapp.net".parse()?; let should_block = true; // Your logic here if should_block { if !client.blocking().is_blocked(&contact).await? { client.blocking().block(&contact).await?; println!("Blocked {}", contact); } } else { if client.blocking().is_blocked(&contact).await? { client.blocking().unblock(&contact).await?; println!("Unblocked {}", contact); } } ``` ### Batch Block Multiple Contacts ```rust theme={null} let contacts_to_block = vec![ "15551111111@s.whatsapp.net".parse()?, "15552222222@s.whatsapp.net".parse()?, "15553333333@s.whatsapp.net".parse()?, ]; for contact in contacts_to_block { match client.blocking().block(&contact).await { Ok(_) => println!("Blocked {}", contact), Err(e) => eprintln!("Failed to block {}: {}", contact, e), } } ``` ## Error Handling ```rust theme={null} use whatsapp_rust::request::IqError; let contact: Jid = "15551234567@s.whatsapp.net".parse()?; match client.blocking().block(&contact).await { Ok(_) => println!("Blocked successfully"), Err(IqError::Timeout) => { eprintln!("Request timed out"); } Err(IqError::InvalidResponse) => { eprintln!("Invalid response from server"); } Err(e) => eprintln!("Error: {}", e), } ``` ## Device ID Handling The `is_blocked()` method compares only the user part of JIDs, ignoring device IDs: ```rust theme={null} let jid1: Jid = "15551234567.0@s.whatsapp.net".parse()?; // Device 0 let jid2: Jid = "15551234567.1@s.whatsapp.net".parse()?; // Device 1 // Block device 0 client.blocking().block(&jid1).await?; // Check if device 1 is blocked (will return true) assert!(client.blocking().is_blocked(&jid2).await?); // Blocking applies to the user account, not specific devices ``` ## Complete Example ```rust theme={null} use whatsapp_rust::features::blocking::BlocklistEntry; async fn manage_blocklist(client: &Client) -> anyhow::Result<()> { // Get current blocklist let blocklist = client.blocking().get_blocklist().await?; println!("Current blocklist: {} contacts", blocklist.len()); // Block a new contact let spam_contact: Jid = "15559999999@s.whatsapp.net".parse()?; if !client.blocking().is_blocked(&spam_contact).await? { client.blocking().block(&spam_contact).await?; println!("Blocked spam contact"); } // Unblock an old contact let old_friend: Jid = "15551234567@s.whatsapp.net".parse()?; if client.blocking().is_blocked(&old_friend).await? { client.blocking().unblock(&old_friend).await?; println!("Unblocked old friend"); } // Print updated blocklist let updated_blocklist = client.blocking().get_blocklist().await?; println!("\nUpdated blocklist:"); for entry in updated_blocklist { println!(" - {}", entry.jid); } Ok(()) } ``` # Bot Source: https://whatsapp-rust.jlucaso.com/api/bot High-level builder for creating WhatsApp bots with event handlers The `Bot` provides a simplified, ergonomic API for building WhatsApp bots. It handles client setup, event routing, and background sync tasks automatically. ## Overview Use the Bot builder pattern to: * Configure storage backend * Set up transport and HTTP client * Register event handlers * Configure device properties and versions * Enable pair code authentication * Skip history sync for bot use cases The Bot is the **recommended way** to use whatsapp-rust. It provides sensible defaults and handles boilerplate setup. ## Basic Usage ```rust theme={null} use whatsapp_rust::bot::Bot; use wacore::types::events::Event; let bot = Bot::builder() .with_backend(backend) .with_transport_factory(transport) .with_http_client(http_client) .on_event(|event, client| async move { match event { Event::Message(msg, info) => { println!("Message from {}: {:?}", info.source.sender, msg); } Event::Connected(_) => { println!("Connected to WhatsApp!"); } _ => {} } }) .build() .await?; let mut bot_handle = bot.run().await?; bot_handle.await?; ``` *** ## Builder Methods ### builder ```rust theme={null} pub fn builder() -> BotBuilder ``` Creates a new bot builder. ### with\_backend ```rust theme={null} pub fn with_backend(self, backend: Arc) -> Self ``` Sets the storage backend (required). Backend implementation providing storage operations **Example:** ```rust theme={null} use whatsapp_rust::store::SqliteStore; let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); let bot = Bot::builder() .with_backend(backend) // ... ``` For multi-account scenarios, use `SqliteStore::new_for_device(path, device_id)` to create isolated storage per account. ### with\_transport\_factory ```rust theme={null} pub fn with_transport_factory(self, factory: F) -> Self where F: TransportFactory + 'static ``` Sets the transport factory for creating WebSocket connections (required). Transport factory implementation **Example:** ```rust theme={null} use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; let bot = Bot::builder() .with_transport_factory(TokioWebSocketTransportFactory::new()) // ... ``` ### with\_http\_client ```rust theme={null} pub fn with_http_client(self, client: C) -> Self where C: HttpClient + 'static ``` Sets the HTTP client for media operations and version fetching (required). HTTP client implementation **Example:** ```rust theme={null} use whatsapp_rust_ureq_http_client::UreqHttpClient; let bot = Bot::builder() .with_http_client(UreqHttpClient::new()) // ... ``` *** ## Event Handling ### on\_event ```rust theme={null} pub fn on_event(self, handler: F) -> Self where F: Fn(Event, Arc) -> Fut + Send + Sync + 'static, Fut: Future + Send + 'static ``` Registers an async event handler. Async function that receives events and client Arc **Example:** ```rust theme={null} use waproto::whatsapp as wa; Bot::builder() .on_event(|event, client| async move { match event { Event::Message(msg, info) => { // Reply to messages let reply = wa::Message { conversation: Some("Hello back!".to_string()), ..Default::default() }; let _ = client.send_message(info.source.chat, reply).await; } Event::Connected(_) => { println!("Bot online!"); } _ => {} } }) // ... ``` See [Events Reference](/concepts/events) for all event types. ### with\_enc\_handler ```rust theme={null} pub fn with_enc_handler(self, enc_type: impl Into, handler: H) -> Self where H: EncHandler + 'static ``` Registers a custom handler for specific encrypted message types. Encrypted message type (e.g., "frskmsg", "skmsg") Handler implementation *** ## Configuration ### with\_version ```rust theme={null} pub fn with_version(self, version: (u32, u32, u32)) -> Self ``` Overrides the WhatsApp version used by the client. Tuple of (primary, secondary, tertiary) version numbers **Example:** ```rust theme={null} Bot::builder() .with_version((2, 3000, 1027868167)) // ... ``` By default, the client automatically fetches the latest version. Only use this if you need to pin a specific version. ### with\_device\_props ```rust theme={null} pub fn with_device_props( self, os_name: Option, version: Option, platform_type: Option ) -> Self ``` Overrides device properties sent to WhatsApp servers. Operating system name (e.g., "macOS", "Windows", "Linux") App version struct Platform type (determines device name shown on phone) **Example:** ```rust theme={null} use waproto::whatsapp::device_props::{AppVersion, PlatformType}; Bot::builder() .with_device_props( Some("macOS".to_string()), Some(AppVersion { primary: Some(2), secondary: Some(0), tertiary: Some(0), ..Default::default() }), Some(PlatformType::Chrome), ) // ... ``` The `platform_type` determines what device name is shown on the phone's "Linked Devices" list. Common values: `Chrome`, `Firefox`, `Safari`, `Desktop`. ### with\_push\_name ```rust theme={null} pub fn with_push_name(self, name: impl Into) -> Self ``` Sets an initial push name on the device before connecting. Display name to set on the device **Example:** ```rust theme={null} Bot::builder() .with_push_name("My Bot") // ... ``` The push name is included in the `ClientPayload` during registration. This is useful for testing scenarios where the server assigns phone numbers based on push name. *** ## Authentication ### with\_pair\_code ```rust theme={null} pub fn with_pair_code(self, options: PairCodeOptions) -> Self ``` Configures pair code authentication to run automatically after connecting. Configuration for pair code authentication **Example:** ```rust theme={null} use whatsapp_rust::pair_code::{PairCodeOptions, PlatformId}; use whatsapp_rust::types::events::Event; Bot::builder() .with_pair_code(PairCodeOptions { phone_number: "15551234567".to_string(), show_push_notification: true, custom_code: None, platform_id: PlatformId::Chrome, platform_display: "Chrome (Linux)".to_string(), }) .on_event(|event, _client| async move { match event { Event::PairingCode { code, timeout } => { println!("Enter this code on your phone: {}", code); println!("Expires in: {} seconds", timeout); } _ => {} } }) // ... ``` Pair code runs concurrently with QR code pairing - whichever completes first wins. *** ## Cache Configuration ### with\_cache\_config ```rust theme={null} pub fn with_cache_config(self, config: CacheConfig) -> Self ``` Configures cache TTL and capacity settings for internal caches. Custom cache configuration **Example:** ```rust theme={null} use whatsapp_rust::{CacheConfig, CacheEntryConfig}; use std::time::Duration; Bot::builder() .with_cache_config(CacheConfig { group_cache: CacheEntryConfig::new(None, 500), // No TTL, 500 entries device_cache: CacheEntryConfig::new(Some(Duration::from_secs(1800)), 2000), ..Default::default() }) // ... ``` See [Cache Configuration](#cache-configuration-reference) for available cache types. *** ## History Sync History sync transfers chat history from the phone to the linked device. The processing pipeline is optimized for minimal RAM usage through zero-copy streaming and lazy parsing. ### How it works When your bot receives history sync data, the pipeline: 1. **Stream-decrypts** external blobs in 8KB chunks (or moves inline payloads without copying) 2. **Decompresses** zlib data on a blocking thread with pre-allocated buffers capped at 8 MiB 3. **Walks protobuf fields manually** instead of decoding the entire message tree — only conversations and pushnames are extracted 4. **Streams conversations** through a bounded channel (capacity 4) as zero-copy `Bytes` slices 5. **Wraps each conversation** in a [`LazyConversation`](/concepts/events#lazyconversation) — protobuf parsing is deferred until you call `.get()`, and embedded messages are cleared on first parse to reclaim memory If no event handlers are registered, conversation extraction is skipped entirely at the protobuf level. ### skip\_history\_sync ```rust theme={null} pub fn skip_history_sync(self) -> Self ``` Skips processing of history sync notifications from the phone. When enabled: * Sends a receipt so the phone stops retrying uploads * Does not download or process historical data * Emits debug log for each skipped notification * Useful for bot use cases where message history is not needed **Example:** ```rust theme={null} Bot::builder() .skip_history_sync() // ... ``` For bots that only need to respond to new messages, enabling this can significantly reduce startup time and bandwidth usage. *** ## Building and Running ### build ```rust theme={null} pub async fn build(self) -> Result ``` Builds the bot with the configured options. **Errors:** * Missing required fields (backend, transport\_factory, http\_client) * Backend initialization failures ### client ```rust theme={null} pub fn client(&self) -> Arc ``` Returns the underlying Client Arc. **Example:** ```rust theme={null} let bot = Bot::builder() .with_backend(backend) .with_transport_factory(transport) .with_http_client(http_client) .build() .await?; let client = bot.client(); let jid = client.get_pn().await; ``` ### run ```rust theme={null} pub async fn run(&mut self) -> Result> ``` Starts the bot's connection loop and background workers. Returns a JoinHandle for the main client task. **Example:** ```rust theme={null} let mut bot = Bot::builder() // ... configuration .build() .await?; let handle = bot.run().await?; // Wait for bot to finish (runs until disconnect) handle.await?; ``` You must call `.await?` on the returned handle to keep the bot running. If you drop the handle, the bot will continue running in the background. *** ## MessageContext A convenience helper for message handling. You can construct it from the `Event::Message` components: ```rust theme={null} pub struct MessageContext { pub message: Box, pub info: MessageInfo, pub client: Arc, } ``` ### send\_message ```rust theme={null} pub async fn send_message(&self, message: wa::Message) -> Result ``` Sends a message to the same chat. ### build\_quote\_context ```rust theme={null} pub fn build_quote_context(&self) -> wa::ContextInfo ``` Builds a quote context for replying to this message. Handles: * Correct stanza\_id/participant for groups and newsletters * Stripping nested mentions * Preserving bot quote chains **Example:** ```rust theme={null} use waproto::whatsapp as wa; use whatsapp_rust::bot::MessageContext; .on_event(|event, client| async move { if let Event::Message(msg, info) = event { let ctx = MessageContext { message: msg, info, client }; let reply = wa::Message { conversation: Some("Quoted reply!".to_string()), context_info: Some(ctx.build_quote_context()), ..Default::default() }; let _ = ctx.send_message(reply).await; } }) ``` ### edit\_message ```rust theme={null} pub async fn edit_message( &self, original_message_id: impl Into, new_message: wa::Message ) -> Result ``` Edits a message in the same chat. ### revoke\_message ```rust theme={null} pub async fn revoke_message( &self, message_id: String, revoke_type: RevokeType ) -> Result<(), anyhow::Error> ``` Deletes a message in the same chat. *** ## Complete Example ```rust theme={null} use whatsapp_rust::bot::Bot; use whatsapp_rust::store::SqliteStore; use wacore::types::events::Event; use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; use whatsapp_rust_ureq_http_client::UreqHttpClient; use waproto::whatsapp as wa; use std::sync::Arc; #[tokio::main] async fn main() -> anyhow::Result<()> { // Set up storage let backend = Arc::new(SqliteStore::new("bot.db").await?); // Build bot let mut bot = Bot::builder() .with_backend(backend) .with_transport_factory(TokioWebSocketTransportFactory::new()) .with_http_client(UreqHttpClient::new()) .skip_history_sync() // Bot only needs new messages .on_event(|event, client| async move { match event { Event::Message(msg, info) => { // Echo messages back if let Some(text) = &msg.conversation { let reply = wa::Message { conversation: Some(format!("You said: {}", text)), ..Default::default() }; let _ = client.send_message(info.source.chat, reply).await; } } Event::Connected(_) => { println!("Bot is now online!"); // Set status let _ = client.presence().set_available().await; } Event::PairingQrCode { code, .. } => { println!("Scan this QR code:"); println!("{}", code); } _ => {} } }) .build() .await?; // Run bot let handle = bot.run().await?; handle.await?; Ok(()) } ``` *** ## Cache Configuration Reference The `CacheConfig` struct controls TTL and capacity for all internal caches. All fields have sensible defaults matching WhatsApp Web behavior. ### CacheEntryConfig ```rust theme={null} pub struct CacheEntryConfig { pub timeout: Option, // None = no time-based expiry pub capacity: u64, // Maximum entries } ``` ### Available Caches #### Timed caches | Cache | Default TTL | Default Capacity | Description | | ------------------------ | ------------ | ---------------- | ------------------------------------------------------------- | | `group_cache` | 1 hour | 250 | Group metadata | | `device_cache` | 1 hour | 5,000 | Device lists | | `device_registry_cache` | 1 hour | 5,000 | Device registry | | `lid_pn_cache` | 1 hour (TTI) | 10,000 | LID-to-phone mapping | | `retried_group_messages` | 5 minutes | 2,000 | Retry tracking | | `recent_messages` | 5 minutes | 0 (disabled) | Optional L1 in-memory cache for sent messages (retry support) | | `message_retry_counts` | 5 minutes | 1,000 | Retry count tracking | | `pdo_pending_requests` | 30 seconds | 500 | PDO pending requests | The `lid_pn_cache` uses time-to-idle (TTI) semantics — entries expire after being idle for the timeout period. All other caches use time-to-live (TTL) semantics. The `recent_messages` cache is disabled by default (capacity 0), meaning sent messages are stored only in the database for retry handling — matching WhatsApp Web's behavior. Set capacity greater than 0 to enable a fast in-memory L1 cache in front of the database. See [DB-backed sent message retry](#db-backed-sent-message-retry) for details. #### Coordination caches (capacity-only, no TTL) | Setting | Default | Description | | -------------------------------- | ------- | ------------------------------------------ | | `session_locks_capacity` | 2,000 | Per-device Signal session lock capacity | | `message_queues_capacity` | 2,000 | Per-chat message processing queue capacity | | `message_enqueue_locks_capacity` | 2,000 | Per-chat message enqueue lock capacity | #### Buffer pool | Setting | Default | Description | | ---------------------------- | ------- | ------------------------------------------------ | | `max_pooled_buffers` | 8 | Max number of reusable plaintext marshal buffers | | `max_pooled_buffer_capacity` | 256 KiB | Max byte capacity per pooled buffer | #### Sent message DB cleanup | Setting | Default | Description | | ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------ | | `sent_message_ttl_secs` | 300 (5 min) | TTL in seconds for sent messages in DB before periodic cleanup. Set to 0 to disable automatic cleanup. | #### Custom cache store overrides You can replace any of the pluggable caches with a custom `CacheStore` backend (e.g., Redis): | Field | Cache | Description | | ------------------------------------ | --------------- | ---------------------------------- | | `cache_stores.group_cache` | Group metadata | Group info lookups | | `cache_stores.device_cache` | Device lists | Per-user device lists | | `cache_stores.device_registry_cache` | Device registry | Device registry entries | | `cache_stores.lid_pn_cache` | LID-PN mapping | LID-to-phone bidirectional lookups | ```rust theme={null} use whatsapp_rust::{CacheConfig, CacheStores}; use std::sync::Arc; let redis = Arc::new(MyRedisCacheStore::new("redis://localhost:6379")); // Route specific caches to Redis let config = CacheConfig { cache_stores: CacheStores { group_cache: Some(redis.clone()), device_cache: Some(redis.clone()), ..Default::default() }, ..Default::default() }; // Or route all pluggable caches at once let config = CacheConfig { cache_stores: CacheStores::all(redis.clone()), ..Default::default() }; ``` Fields left as `None` keep the default in-process moka behavior. See [Custom backends — cache store](/guides/custom-backends#custom-cache-store) for a full implementation guide. Coordination caches (`session_locks`, `message_queues`, `message_enqueue_locks`), the signal write-behind cache, and `pdo_pending_requests` always stay in-process — they hold live Rust objects that cannot be serialized to an external store. ### Custom Configuration Example ```rust theme={null} use whatsapp_rust::{CacheConfig, CacheEntryConfig}; use std::time::Duration; let config = CacheConfig { // Disable TTL for group cache (entries only evicted by capacity) group_cache: CacheEntryConfig::new(None, 500), // Shorter TTL for device cache device_cache: CacheEntryConfig::new(Some(Duration::from_secs(1800)), 3_000), // Enable L1 in-memory cache for sent messages (faster retry lookups) recent_messages: CacheEntryConfig::new(Some(Duration::from_secs(300)), 1_000), // Use defaults for everything else ..Default::default() }; let bot = Bot::builder() .with_cache_config(config) // ... ``` ### DB-backed sent message retry Sent messages are persisted to the database for retry handling, matching WhatsApp Web's `getMessageTable` pattern. When a retry receipt arrives, the client looks up the original message payload from the database, re-encrypts it, and resends. **How it works:** 1. Every `send_message()` call stores the serialized message payload in the `sent_messages` table 2. On retry receipt, the client retrieves and consumes the payload (atomic take) 3. Expired entries are periodically cleaned up based on `sent_message_ttl_secs` **Optional L1 cache:** By default, the `recent_messages` cache capacity is 0 (DB-only mode). If you set capacity greater than 0, sent messages are also cached in memory for faster retrieval. In L1 mode, the DB write is backgrounded since the cache serves reads immediately. In DB-only mode, the write is awaited to guarantee persistence. ```rust theme={null} let config = CacheConfig { // Enable L1 in-memory cache for faster retry lookups recent_messages: CacheEntryConfig::new(Some(Duration::from_secs(300)), 1_000), // Keep sent messages in DB for 10 minutes before cleanup sent_message_ttl_secs: 600, ..Default::default() }; ``` *** ## See Also * [Client](/api/client) - Lower-level client API * [Events](/concepts/events) - All event types * [Sending Messages](/guides/sending-messages) - Sending messages * [Storage](/concepts/storage) - Storage and multi-account patterns # Chat actions Source: https://whatsapp-rust.jlucaso.com/api/chat-actions Archive, pin, mute chats and star messages The `ChatActions` feature provides methods for managing chat organization through archiving, pinning, muting, and starring messages. These operations sync across all your devices via WhatsApp's app state sync mechanism. ## Access Access chat action operations through the client (requires `Arc`): ```rust theme={null} let chat_actions = client.chat_actions(); ``` ## Archive ### archive\_chat Archive a chat to hide it from the main chat list. ```rust theme={null} pub async fn archive_chat(&self, jid: &Jid) -> Result<()> ``` **Parameters:** * `jid` - The chat JID to archive **Example:** ```rust theme={null} let jid: Jid = "15551234567@s.whatsapp.net".parse()?; client.chat_actions().archive_chat(&jid).await?; ``` ### unarchive\_chat Unarchive a chat to show it in the main chat list. ```rust theme={null} pub async fn unarchive_chat(&self, jid: &Jid) -> Result<()> ``` **Parameters:** * `jid` - The chat JID to unarchive **Example:** ```rust theme={null} client.chat_actions().unarchive_chat(&jid).await?; ``` ## Pin ### pin\_chat Pin a chat to keep it at the top of the chat list. ```rust theme={null} pub async fn pin_chat(&self, jid: &Jid) -> Result<()> ``` **Parameters:** * `jid` - The chat JID to pin **Example:** ```rust theme={null} let jid: Jid = "15551234567@s.whatsapp.net".parse()?; client.chat_actions().pin_chat(&jid).await?; ``` WhatsApp limits the number of pinned chats. Attempting to pin too many chats may fail. ### unpin\_chat Unpin a chat. ```rust theme={null} pub async fn unpin_chat(&self, jid: &Jid) -> Result<()> ``` **Parameters:** * `jid` - The chat JID to unpin **Example:** ```rust theme={null} client.chat_actions().unpin_chat(&jid).await?; ``` ## Mute ### mute\_chat Mute a chat indefinitely. ```rust theme={null} pub async fn mute_chat(&self, jid: &Jid) -> Result<()> ``` **Parameters:** * `jid` - The chat JID to mute **Example:** ```rust theme={null} let jid: Jid = "15551234567@s.whatsapp.net".parse()?; client.chat_actions().mute_chat(&jid).await?; ``` ### mute\_chat\_until Mute a chat until a specific time. ```rust theme={null} pub async fn mute_chat_until( &self, jid: &Jid, mute_end_timestamp_ms: i64 ) -> Result<()> ``` **Parameters:** * `jid` - The chat JID to mute * `mute_end_timestamp_ms` - Unix timestamp in milliseconds when mute expires (must be in the future) **Example:** ```rust theme={null} use chrono::{Utc, Duration}; let jid: Jid = "15551234567@s.whatsapp.net".parse()?; // Mute for 8 hours let mute_until = Utc::now() + Duration::hours(8); client.chat_actions() .mute_chat_until(&jid, mute_until.timestamp_millis()) .await?; // Mute for 1 week let mute_until = Utc::now() + Duration::weeks(1); client.chat_actions() .mute_chat_until(&jid, mute_until.timestamp_millis()) .await?; ``` ### unmute\_chat Unmute a chat. ```rust theme={null} pub async fn unmute_chat(&self, jid: &Jid) -> Result<()> ``` **Parameters:** * `jid` - The chat JID to unmute **Example:** ```rust theme={null} client.chat_actions().unmute_chat(&jid).await?; ``` ## Star messages ### star\_message Star a message to mark it as important. ```rust theme={null} pub async fn star_message( &self, chat_jid: &Jid, participant_jid: Option<&Jid>, message_id: &str, from_me: bool ) -> Result<()> ``` **Parameters:** * `chat_jid` - The chat containing the message * `participant_jid` - For group messages from others, pass `Some(&sender_jid)`. For 1-on-1 chats or your own messages, pass `None` * `message_id` - The message ID to star * `from_me` - Whether the message was sent by you **Example:** ```rust theme={null} // Star your own message in a 1-on-1 chat client.chat_actions() .star_message(&chat_jid, None, "MESSAGE_ID", true) .await?; // Star someone else's message in a 1-on-1 chat client.chat_actions() .star_message(&chat_jid, None, "MESSAGE_ID", false) .await?; // Star someone else's message in a group let sender_jid: Jid = "15559876543@s.whatsapp.net".parse()?; client.chat_actions() .star_message(&group_jid, Some(&sender_jid), "MESSAGE_ID", false) .await?; ``` For group messages not sent by you, `participant_jid` is required. The method will return an error if it's not provided. ### unstar\_message Remove the star from a message. ```rust theme={null} pub async fn unstar_message( &self, chat_jid: &Jid, participant_jid: Option<&Jid>, message_id: &str, from_me: bool ) -> Result<()> ``` **Parameters:** Same as `star_message`. **Example:** ```rust theme={null} client.chat_actions() .unstar_message(&chat_jid, None, "MESSAGE_ID", true) .await?; ``` ## App state sync All chat actions are synced across devices using WhatsApp's app state synchronization: | Action | Collection | | ------- | -------------- | | Archive | `regular_low` | | Pin | `regular_low` | | Mute | `regular_high` | | Star | `regular_high` | App state sync requires encryption keys to be available. These are typically obtained during initial sync after authentication. Actions may fail if called immediately after pairing before sync completes. ## Events Chat action changes are emitted as events that you can handle: ```rust theme={null} use wacore::types::events::Event; .on_event(|event, _client| async move { match event { Event::MuteUpdate(update) => { println!("Chat {} muted: {:?}", update.jid, update.action.muted); } Event::PinUpdate(update) => { println!("Chat {} pinned: {:?}", update.jid, update.action.pinned); } Event::ArchiveUpdate(update) => { println!("Chat {} archived: {:?}", update.jid, update.action.archived); } Event::StarUpdate(update) => { println!("Message {} starred: {:?}", update.message_id, update.action.starred); } _ => {} } }) ``` ## Error handling All methods return `Result<(), anyhow::Error>`. Common errors: * No app state sync key available (sync not complete) * Invalid timestamp for `mute_chat_until` * Missing `participant_jid` for group star operations * Network errors ```rust theme={null} match client.chat_actions().mute_chat_until(&jid, 0).await { Ok(_) => println!("Muted"), Err(e) => eprintln!("Failed: {}", e), // Timestamp validation error } ``` ## Complete example ```rust theme={null} use whatsapp_rust::Client; use wacore_binary::jid::Jid; use chrono::{Utc, Duration}; use std::sync::Arc; async fn organize_chats(client: &Arc) -> anyhow::Result<()> { let important_chat: Jid = "15551234567@s.whatsapp.net".parse()?; let noisy_group: Jid = "123456789@g.us".parse()?; let old_chat: Jid = "15559876543@s.whatsapp.net".parse()?; // Pin important conversations client.chat_actions().pin_chat(&important_chat).await?; // Mute noisy group for 1 week let mute_until = Utc::now() + Duration::weeks(1); client.chat_actions() .mute_chat_until(&noisy_group, mute_until.timestamp_millis()) .await?; // Archive old conversations client.chat_actions().archive_chat(&old_chat).await?; // Star an important message client.chat_actions() .star_message(&important_chat, None, "IMPORTANT_MSG_ID", false) .await?; Ok(()) } ``` ## See also * [Events](/concepts/events) - Handle chat action update events * [Groups](/api/groups) - Group management operations * [Client](/api/client) - Core client API # Chatstate Source: https://whatsapp-rust.jlucaso.com/api/chatstate Typing indicators and chat state notifications The `Chatstate` trait provides methods for sending typing indicators and recording notifications to recipients. ## Access Access chatstate operations through the client: ```rust theme={null} let chatstate = client.chatstate(); ``` ## Methods ### send Send a chat state update to a recipient. ```rust theme={null} pub async fn send( &self, to: &Jid, state: ChatStateType, ) -> Result<(), ClientError> ``` **Parameters:** * `to` - Recipient JID (user or group) * `state: ChatStateType` - Type of chat state to send **Returns:** * `Result<(), ClientError>` - Success or client error **Example:** ```rust theme={null} use whatsapp_rust::features::chatstate::ChatStateType; let recipient: Jid = "15551234567@s.whatsapp.net".parse()?; // Send typing indicator client.chatstate().send(&recipient, ChatStateType::Composing).await?; // Send recording indicator client.chatstate().send(&recipient, ChatStateType::Recording).await?; // Send paused (stopped typing) client.chatstate().send(&recipient, ChatStateType::Paused).await?; ``` ### send\_composing Convenience method to send typing indicator. ```rust theme={null} pub async fn send_composing(&self, to: &Jid) -> Result<(), ClientError> ``` **Example:** ```rust theme={null} let recipient: Jid = "15551234567@s.whatsapp.net".parse()?; client.chatstate().send_composing(&recipient).await?; ``` ### send\_recording Convenience method to send audio recording indicator. ```rust theme={null} pub async fn send_recording(&self, to: &Jid) -> Result<(), ClientError> ``` **Example:** ```rust theme={null} let recipient: Jid = "15551234567@s.whatsapp.net".parse()?; client.chatstate().send_recording(&recipient).await?; println!("Showing 'recording audio' indicator"); ``` ### send\_paused Convenience method to send paused/stopped typing indicator. ```rust theme={null} pub async fn send_paused(&self, to: &Jid) -> Result<(), ClientError> ``` **Example:** ```rust theme={null} let recipient: Jid = "15551234567@s.whatsapp.net".parse()?; client.chatstate().send_paused(&recipient).await?; println!("Cleared typing indicator"); ``` ## ChatStateType Enum ```rust theme={null} pub enum ChatStateType { Composing, // Typing text Recording, // Recording audio Paused, // Stopped typing/recording } ``` **Methods:** * `as_str()` - Returns `"composing"`, `"recording"`, or `"paused"` **String conversion:** ```rust theme={null} use whatsapp_rust::features::chatstate::ChatStateType; let state = ChatStateType::Composing; assert_eq!(state.as_str(), "composing"); assert_eq!(state.to_string(), "composing"); // Parse from string let parsed = ChatStateType::try_from("recording")?; assert_eq!(parsed, ChatStateType::Recording); ``` ## Wire Format ### Composing (Typing) ```xml theme={null} ``` ### Recording (Voice) ```xml theme={null} ``` Note: Recording uses `` rather than a separate tag. ### Paused (Stopped) ```xml theme={null} ``` ## Usage Patterns ### Typing Indicator Lifecycle ```rust theme={null} use whatsapp_rust::features::chatstate::ChatStateType; let recipient: Jid = "15551234567@s.whatsapp.net".parse()?; // User starts typing client.chatstate().send_composing(&recipient).await?; // User is typing... (you may want to throttle these) std::thread::sleep(std::time::Duration::from_secs(2)); // User stopped typing (optional, will auto-clear) client.chatstate().send_paused(&recipient).await?; // Send the actual message let message = wa::Message { conversation: Some("Hello!".to_string()), ..Default::default() }; client.send_message(recipient.clone(), message).await?; ``` ### Recording Audio ```rust theme={null} let recipient: Jid = "15551234567@s.whatsapp.net".parse()?; // Start recording client.chatstate().send_recording(&recipient).await?; // Record audio... let audio_data = record_audio(); // Stop indicator (optional) client.chatstate().send_paused(&recipient).await?; // Upload and send audio message let upload = client.upload(audio_data, MediaType::Audio).await?; let message = wa::Message { audio_message: Some(Box::new(wa::message::AudioMessage { url: Some(upload.url), direct_path: Some(upload.direct_path), media_key: Some(upload.media_key), file_enc_sha256: Some(upload.file_enc_sha256), file_sha256: Some(upload.file_sha256), file_length: Some(upload.file_length), mimetype: Some("audio/ogg; codecs=opus".to_string()), ptt: Some(true), ..Default::default() })), ..Default::default() }; client.send_message(recipient.clone(), message).await?; ``` ### Throttling To avoid spamming chat state updates: ```rust theme={null} use std::time::{Duration, Instant}; struct ChatStateThrottler { last_sent: Option, interval: Duration, } impl ChatStateThrottler { fn new() -> Self { Self { last_sent: None, interval: Duration::from_secs(3), } } fn should_send(&mut self) -> bool { if let Some(last) = self.last_sent { if last.elapsed() < self.interval { return false; } } self.last_sent = Some(Instant::now()); true } } // Usage let mut throttler = ChatStateThrottler::new(); let recipient: Jid = "15551234567@s.whatsapp.net".parse()?; // In your typing event handler if throttler.should_send() { client.chatstate().send_composing(&recipient).await?; } ``` ## Group Chats Chat state indicators work in group chats as well: ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; // Show typing in group client.chatstate().send_composing(&group_jid).await?; // Send message let message = wa::Message { conversation: Some("Hello everyone!".to_string()), ..Default::default() }; client.send_message(group_jid.clone(), message).await?; ``` ## Error Handling All methods return `Result<(), ClientError>`. Common errors: * **Not connected**: Client not connected to WhatsApp * **Invalid JID**: Malformed recipient JID * **Network errors**: Connection issues ```rust theme={null} use whatsapp_rust::client::ClientError; let recipient: Jid = "15551234567@s.whatsapp.net".parse()?; match client.chatstate().send_composing(&recipient).await { Ok(_) => println!("Sent typing indicator"), Err(ClientError::NotConnected) => { eprintln!("Not connected to WhatsApp"); } Err(e) => eprintln!("Error: {}", e), } ``` ## Best Practices 1. **Throttle updates**: Don't send chat state updates more than once every 2-3 seconds 2. **Clear on send**: Send `Paused` before sending the actual message 3. **Auto-timeout**: Consider auto-clearing typing indicators after 10-15 seconds of inactivity 4. **Don't spam**: Only send when state actually changes 5. **Groups**: Be mindful that all group members will see the indicator ## Complete Example ```rust theme={null} use whatsapp_rust::features::chatstate::ChatStateType; use std::time::Duration; let recipient: Jid = "15551234567@s.whatsapp.net".parse()?; // Simulate user typing println!("User starts typing..."); client.chatstate().send_composing(&recipient).await?; // Simulate typing delay tokio::time::sleep(Duration::from_secs(2)).await; // User still typing (throttled) println!("Still typing..."); // User finishes and sends message client.chatstate().send_paused(&recipient).await?; let message = wa::Message { conversation: Some("Hello there!".to_string()), ..Default::default() }; client.send_message(recipient.clone(), message).await?; println!("Message sent"); // Or simulate voice recording println!("\nUser starts recording..."); client.chatstate().send_recording(&recipient).await?; tokio::time::sleep(Duration::from_secs(3)).await; client.chatstate().send_paused(&recipient).await?; println!("Recording stopped (would send audio here)"); ``` # Client Source: https://whatsapp-rust.jlucaso.com/api/client Core client for WhatsApp connectivity and protocol operations The `Client` struct is the core of whatsapp-rust, managing connections, encryption, state, and all protocol-level operations. ## Overview The Client handles: * WebSocket connection lifecycle and automatic reconnection * Noise Protocol handshake and encryption * Signal Protocol E2E encryption for messages * App state synchronization * Device state persistence * Event dispatching Most users should use the [`Bot`](/api/bot) builder instead of creating a Client directly. The Bot provides a simplified API with sensible defaults. ## Creating a Client ```rust theme={null} use whatsapp_rust::Client; use std::sync::Arc; let (client, sync_receiver) = Client::new( persistence_manager, transport_factory, http_client, None, // version override ).await; ``` State manager for device credentials, sessions, and app state Factory for creating WebSocket connections HTTP client for media operations and version fetching Optional WhatsApp version override (primary, secondary, tertiary) Returns the client Arc and a receiver for history/app state sync tasks ### Creating with custom cache configuration ```rust theme={null} use whatsapp_rust::{Client, CacheConfig, CacheEntryConfig}; use std::time::Duration; let cache_config = CacheConfig { group_cache: CacheEntryConfig::new(None, 500), // No TTL ..Default::default() }; let (client, sync_receiver) = Client::new_with_cache_config( persistence_manager, transport_factory, http_client, None, cache_config, ).await; ``` See [Bot - Cache Configuration Reference](/api/bot#cache-configuration-reference) for available cache options. *** ## Connection Management ### run ```rust theme={null} pub async fn run(self: &Arc) ``` Main event loop that manages connection lifecycle with automatic reconnection. Runs indefinitely until: * `disconnect()` is called * Auto-reconnect is disabled and connection fails * Client receives a fatal stream error (401 unauthorized, 409 conflict, or 516 device removed) **Example:** ```rust theme={null} let client_clone = client.clone(); tokio::spawn(async move { client_clone.run().await; }); ``` ### connect ```rust theme={null} pub async fn connect(self: &Arc) -> Result<(), anyhow::Error> ``` Establishes WebSocket connection and performs Noise Protocol handshake. Both the transport connection and the version fetch run in parallel under a **20-second timeout** (`TRANSPORT_CONNECT_TIMEOUT`), matching WhatsApp Web's MQTT and DGW connect timeout defaults. Without this, a dead network would block on the OS TCP SYN timeout (\~60-75s). The Noise handshake response also has a separate **20-second timeout** (`NOISE_HANDSHAKE_RESPONSE_TIMEOUT`). **Errors:** * `ClientError::AlreadyConnected` - Already connected * Transport connect timeout (20s exceeded) * Version fetch timeout (20s exceeded) * Noise handshake timeout or failure * Connection/handshake failures ### disconnect ```rust theme={null} pub async fn disconnect(self: &Arc) ``` Disconnects gracefully and disables auto-reconnect. ### reconnect ```rust theme={null} pub async fn reconnect(self: &Arc) ``` Drops the current connection and triggers auto-reconnect with a deliberate \~5s offline window (Fibonacci backoff step 4). The run loop stays active. Use this for: * Handling network changes (e.g., Wi-Fi to cellular) * Forcing a fresh server session * Testing offline message delivery **Example:** ```rust theme={null} // Force reconnection with backoff delay client.reconnect().await; // Wait for the new connection to be ready client.wait_for_connected(Duration::from_secs(30)).await?; ``` ### reconnect\_immediately ```rust theme={null} pub async fn reconnect_immediately(self: &Arc) ``` Drops the current connection and reconnects immediately with no delay. Unlike `reconnect()`, this sets the expected disconnect flag so the run loop skips the backoff delay. **Example:** ```rust theme={null} // Force immediate reconnection (no backoff) client.reconnect_immediately().await; ``` ### wait\_for\_socket ```rust theme={null} pub async fn wait_for_socket( &self, timeout: std::time::Duration ) -> Result<(), anyhow::Error> ``` Waits for the Noise socket to be ready (before login). Useful for pair code flows. Maximum time to wait Ok if socket ready, Err on timeout ### wait\_for\_connected ```rust theme={null} pub async fn wait_for_connected( &self, timeout: std::time::Duration ) -> Result<(), anyhow::Error> ``` Waits for full connection and authentication to complete, including offline sync. Post-login tasks (presence, background queries) are gated behind offline sync completion, which resolves either when the server sends the end marker, all expected items arrive, or the 60-second timeout fires. *** ## Connection State ### is\_connected ```rust theme={null} pub fn is_connected(&self) -> bool ``` Returns `true` if the Noise socket is established. This method uses an internal `AtomicBool` flag (with `Acquire` ordering) instead of probing the noise socket mutex, making it lock-free and immune to false negatives under mutex contention. Prior to this design, connection checks used `try_lock()` on the noise socket mutex. Under contention (e.g., during frame encryption), `try_lock()` would fail and incorrectly report the client as disconnected — silently dropping receipt acks. The `AtomicBool` approach eliminates this race condition entirely. ### is\_logged\_in ```rust theme={null} pub fn is_logged_in(&self) -> bool ``` Returns `true` if authenticated with WhatsApp servers. *** ## Auto-Reconnection The client includes automatic reconnection handling with Fibonacci backoff. ### How it works 1. **On disconnect**: The client detects unexpected disconnections and automatically attempts to reconnect 2. **Fibonacci backoff**: Each failed attempt increases the delay following the Fibonacci sequence (1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s...) with a maximum of 900 seconds (15 minutes) and +/-10% jitter 3. **Expected disconnects**: Protocol-expected disconnects (e.g., 515 stream error after pairing) trigger immediate reconnection without backoff 4. **Keepalive monitoring**: A keepalive loop sends periodic pings (every 15-30s) and forces reconnection if the socket appears dead (no data received for 20s after a send) ### Controlling auto-reconnect ```rust theme={null} // Disable auto-reconnect (default: enabled) client.enable_auto_reconnect.store(false, Ordering::Relaxed); // Check current state let enabled = client.enable_auto_reconnect.load(Ordering::Relaxed); ``` ### Stream error handling The client handles specific `` codes from the WhatsApp server: | Stream error code | Meaning | Event emitted | Auto-reconnect | | ----------------- | ---------------------------------------- | --------------------------- | ------------------------------------- | | **401** | Session invalidated (unauthorized) | `LoggedOut` | Disabled — must re-pair | | **409** | Another client connected (conflict) | `StreamReplaced` | Disabled — prevents displacement loop | | **429** | Rate limited (too many connections) | None (emits `Disconnected`) | Yes, with extended backoff (+5 steps) | | **503** | Service unavailable | None | Yes, normal backoff | | **515** | Expected disconnect (e.g., post-pairing) | None | Yes, immediate (no backoff) | | **516** | Device removed | `LoggedOut` | Disabled — must re-pair | | Unknown | Unrecognized code | `StreamError` | Disabled | When you receive a `LoggedOut` or `StreamReplaced` event, auto-reconnect is permanently disabled for that session. You must create a new client and re-pair to continue. ### Rate limiting (429) When the server returns a 429 stream error, the client bumps the internal backoff counter by 5 Fibonacci steps before reconnecting. This means the reconnection delay jumps significantly (e.g., from \~1s to \~13s on the first rate limit) to respect the server's throttling. ### General reconnection behavior | Scenario | Behavior | | -------------------------------- | -------------------------------- | | Unexpected disconnect | Reconnect with Fibonacci backoff | | 515 stream error (after pairing) | Immediate reconnect | | Keepalive dead socket (20s) | Force disconnect and reconnect | | `disconnect()` called | No reconnect attempt | | Auto-reconnect disabled | No reconnect attempt | *** ## Messaging ### send\_message ```rust theme={null} pub async fn send_message( &self, to: Jid, message: wa::Message ) -> Result ``` Sends an encrypted message to a chat. Recipient JID ([user@s.whatsapp.net](mailto:user@s.whatsapp.net) or [group@g.us](mailto:group@g.us)) Protobuf message content Message ID on success **Example:** ```rust theme={null} use waproto::whatsapp as wa; let message = wa::Message { conversation: Some("Hello!".to_string()), ..Default::default() }; let msg_id = client.send_message(jid, message).await?; ``` ### send\_message\_with\_options ```rust theme={null} pub async fn send_message_with_options( &self, to: Jid, message: wa::Message, options: SendOptions ) -> Result ``` Sends a message with advanced options like extra stanza nodes. Configuration for message sending behavior See `src/send.rs:SendOptions` for available options. ### edit\_message ```rust theme={null} pub async fn edit_message( &self, to: Jid, original_id: impl Into, new_content: wa::Message ) -> Result ``` Edits a previously sent message. ID of the message to edit New message content ### revoke\_message ```rust theme={null} pub async fn revoke_message( &self, to: Jid, message_id: impl Into, revoke_type: RevokeType ) -> Result<(), anyhow::Error> ``` Deletes a message. Use `Sender` to revoke your own message, or `Admin` to revoke another user's message as group admin. `RevokeType::Sender` (delete your own message) or `RevokeType::Admin { original_sender: Jid }` (admin revoke in groups) *** ## Feature APIs The Client provides namespaced access to feature-specific operations: ### blocking ```rust theme={null} pub fn blocking(&self) -> Blocking<'_> ``` Access blocking operations. **Methods:** * `block(jid: &Jid)` - Block a contact * `unblock(jid: &Jid)` - Unblock a contact * `get_blocklist()` - Get all blocked contacts * `is_blocked(jid: &Jid)` - Check if contact is blocked **Example:** ```rust theme={null} client.blocking().block(&jid).await?; let blocked = client.blocking().get_blocklist().await?; ``` ### groups ```rust theme={null} pub fn groups(&self) -> Groups<'_> ``` Access group management operations. **Methods:** * `query_info(jid: &Jid)` - Get cached group info * `get_metadata(jid: &Jid)` - Fetch group metadata from server * `get_participating()` - List all groups you're in * `create_group(options: GroupCreateOptions)` - Create a new group * `set_subject(jid: &Jid, subject: GroupSubject)` - Change group name * `set_description(jid: &Jid, desc: Option, prev: Option)` - Change description * `leave(jid: &Jid)` - Leave a group * `add_participants(jid: &Jid, participants: &[Jid])` - Add members * `remove_participants(jid: &Jid, participants: &[Jid])` - Remove members * `promote_participants(jid: &Jid, participants: &[Jid])` - Make members admins * `demote_participants(jid: &Jid, participants: &[Jid])` - Remove admin status * `get_invite_link(jid: &Jid, reset: bool)` - Get/reset invite link * `set_locked(jid: &Jid, locked: bool)` - Lock/unlock group info editing * `set_announce(jid: &Jid, announce: bool)` - Enable/disable announcement mode * `set_ephemeral(jid: &Jid, expiration: u32)` - Set disappearing messages timer * `set_membership_approval(jid: &Jid, mode: MembershipApprovalMode)` - Require admin approval **Example:** ```rust theme={null} use whatsapp_rust::features::groups::{GroupCreateOptions, GroupParticipantOptions}; let options = GroupCreateOptions::builder() .subject("My Group") .participants(vec![GroupParticipantOptions::new(participant_jid)]) .build(); let result = client.groups().create_group(options).await?; ``` ### presence ```rust theme={null} pub fn presence(&self) -> Presence<'_> ``` Access presence operations. **Methods:** * `set(status: PresenceStatus)` - Set presence status * `set_available()` - Set status to available/online * `set_unavailable()` - Set status to unavailable/offline * `subscribe(jid: &Jid)` - Subscribe to contact's presence updates * `unsubscribe(jid: &Jid)` - Unsubscribe from contact's presence updates Subscriptions are automatically tracked and re-subscribed on reconnect. **Example:** ```rust theme={null} client.presence().set_available().await?; client.presence().subscribe(&jid).await?; // Later, stop receiving updates client.presence().unsubscribe(&jid).await?; ``` ### chatstate ```rust theme={null} pub fn chatstate(&self) -> Chatstate<'_> ``` Access chat state (typing indicator) operations. **Methods:** * `send(to: &Jid, state: ChatStateType)` - Send a chat state update * `send_composing(to: &Jid)` - Send typing indicator * `send_recording(to: &Jid)` - Send recording indicator * `send_paused(to: &Jid)` - Send paused/stopped typing indicator **Example:** ```rust theme={null} // Send typing indicator client.chatstate().send_composing(&jid).await?; // Send recording indicator client.chatstate().send_recording(&jid).await?; // Stop typing indicator client.chatstate().send_paused(&jid).await?; ``` ### contacts ```rust theme={null} pub fn contacts(&self) -> Contacts<'_> ``` Access contact operations. **Methods:** * `is_on_whatsapp(phones: &[&str])` - Check phone numbers on WhatsApp * `get_info(phones: &[&str])` - Get contact info for phone numbers (LID, business status, picture ID) * `get_user_info(jids: &[Jid])` - Get profile info for users by JID * `get_profile_picture(jid: &Jid, preview: bool)` - Get profile picture URL (preview or full size) ### tc\_token ```rust theme={null} pub fn tc_token(&self) -> TcToken<'_> ``` Access trust/privacy token operations. **Methods:** * `issue_tokens(jids: &[Jid])` - Request tokens for contacts * `prune_expired()` - Remove expired tokens * `get(jid: &str)` - Get a stored token by JID * `get_all_jids()` - List all JIDs with stored tokens ### chat\_actions ```rust theme={null} pub fn chat_actions(self: &Arc) -> ChatActions<'_> ``` Access chat management actions. Requires `Arc` because app state mutations need key access. **Methods:** * `archive_chat(jid: &Jid)` - Archive a chat * `unarchive_chat(jid: &Jid)` - Unarchive a chat * `pin_chat(jid: &Jid)` - Pin a chat * `unpin_chat(jid: &Jid)` - Unpin a chat * `mute_chat(jid: &Jid)` - Mute a chat indefinitely * `mute_chat_until(jid: &Jid, mute_end_timestamp_ms: i64)` - Mute until a specific time * `unmute_chat(jid: &Jid)` - Unmute a chat * `star_message(chat_jid: &Jid, participant_jid: Option<&Jid>, message_id: &str, from_me: bool)` - Star a message * `unstar_message(chat_jid: &Jid, participant_jid: Option<&Jid>, message_id: &str, from_me: bool)` - Unstar a message ### status ```rust theme={null} pub fn status(&self) -> Status<'_> ``` Access status/story operations. **Methods:** * `send_text(text, background_argb, font, recipients, options)` - Post a text status * `send_image(upload, thumbnail, caption, recipients, options)` - Post an image status * `send_video(upload, thumbnail, duration_seconds, caption, recipients, options)` - Post a video status * `send_raw(message, recipients, options)` - Post any message type as a status * `revoke(message_id, recipients, options)` - Delete a posted status ### mex ```rust theme={null} pub fn mex(&self) -> Mex<'_> ``` Access Meta Exchange (GraphQL) operations. **Methods:** * `query(request: MexRequest)` - Execute a GraphQL query * `mutate(request: MexRequest)` - Execute a GraphQL mutation ### profile ```rust theme={null} pub fn profile(self: &Arc) -> Profile<'_> ``` Access profile operations. Requires `Arc` because push name updates need app state sync. **Methods:** * `set_push_name(name: &str)` - Set display name (syncs across devices) * `set_status_text(text: &str)` - Set profile "About" text * `set_profile_picture(image_data: Vec)` - Set profile picture (JPEG) * `remove_profile_picture()` - Remove profile picture *** ## Public fields ### http\_client ```rust theme={null} pub http_client: Arc ``` The HTTP client used for media operations, version fetching, and other HTTP requests. This field is public and can be used directly for custom HTTP operations that share the same client configuration. ### enable\_auto\_reconnect ```rust theme={null} pub enable_auto_reconnect: Arc ``` Controls whether the client automatically reconnects after an unexpected disconnection. Defaults to `true`. Set to `false` to disable auto-reconnect. ### auto\_reconnect\_errors ```rust theme={null} pub auto_reconnect_errors: Arc ``` Tracks the number of consecutive reconnection failures. Used internally for Fibonacci backoff calculations. Resets to `0` on a successful connection. ### last\_successful\_connect ```rust theme={null} pub last_successful_connect: Arc>>> ``` Timestamp of the last successful connection. `None` if the client has never successfully connected. ### custom\_enc\_handlers ```rust theme={null} pub custom_enc_handlers: Arc>> ``` Custom handlers for encrypted message types. You can register handlers for specific encryption types that the client doesn't natively support. These are also registerable via `BotBuilder::with_enc_handler()`. ### RECONNECT\_BACKOFF\_STEP ```rust theme={null} pub const RECONNECT_BACKOFF_STEP: u32 = 4; ``` The number of Fibonacci steps added to the backoff counter when `reconnect()` is called, creating an approximately 5-second offline window before the next connection attempt. This prevents tight reconnect loops after intentional disconnects. *** ## Device State ### get\_push\_name ```rust theme={null} pub async fn get_push_name(&self) -> String ``` Returns the current push name (display name). ### get\_pn ```rust theme={null} pub async fn get_pn(&self) -> Option ``` Returns the phone number JID. ### get\_lid ```rust theme={null} pub async fn get_lid(&self) -> Option ``` Returns the LID (Linked Identity). ### get\_phone\_number\_from\_lid ```rust theme={null} pub async fn get_phone_number_from_lid(&self, lid: &str) -> Option ``` Resolves a LID (Linked Identity) to the corresponding phone number string. Returns `None` if the mapping is not cached. The LID user string to look up (e.g., `"ABC123"`) ### persistence\_manager ```rust theme={null} pub fn persistence_manager(&self) -> Arc ``` Access to the persistence manager for multi-account scenarios. *** ## History Sync History sync transfers chat history from the phone to the linked device. The client processes history sync notifications through a RAM-optimized pipeline that minimizes peak memory usage. ### Processing pipeline When a history sync notification arrives, the client: 1. Sends a `HistorySync` receipt immediately (so the phone knows delivery succeeded) 2. Retrieves the data — either from an inline payload (moved via `.take()`, not cloned) or by stream-decrypting an external blob in 8KB chunks 3. Extracts a `compressed_size_hint` from the notification's `file_length` field, which the decompressor uses with a 4x multiplier for better buffer pre-allocation (avoids repeated `Vec` reallocation) 4. Runs decompression and protobuf parsing on a blocking thread (`tokio::task::spawn_blocking`) to avoid stalling the async runtime 5. Streams conversation bytes through a bounded channel (capacity 4) to the event dispatcher 6. Wraps each conversation in a `LazyConversation` for zero-copy delivery to event handlers If no event handlers are registered, the callback is `None` and conversation extraction is skipped entirely at the protobuf level — only the pushname field is extracted. ### set\_skip\_history\_sync ```rust theme={null} pub fn set_skip_history_sync(&self, enabled: bool) ``` Enable or disable skipping of history sync notifications at runtime. When skipping is enabled, the client sends a receipt (so the phone stops retrying uploads) but does not download or process any data. ### skip\_history\_sync\_enabled ```rust theme={null} pub fn skip_history_sync_enabled(&self) -> bool ``` Returns `true` if history sync is currently being skipped. *** ## Offline sync The client automatically manages offline message sync when reconnecting. During sync, message processing is restricted to sequential mode (1 concurrent task) to preserve ordering. ### Timeout fallback If the server advertises offline messages but never completes delivery, a 60-second timeout ensures startup is not blocked indefinitely. On timeout: 1. A warning is logged with the number of processed vs. expected items 2. Offline sync is marked complete 3. `OfflineSyncCompleted` event is emitted 4. Message processing switches from sequential to parallel (64 concurrent tasks) ### State reset on reconnect All offline sync state (counters, timing, concurrency semaphore) is fully reset on reconnect so stale state does not carry over to the next connection. **Related events:** [`OfflineSyncPreview`](/concepts/events#offlinesyncpreview), [`OfflineSyncCompleted`](/concepts/events#offlinesynccompleted) *** ## App State ### fetch\_props ```rust theme={null} pub async fn fetch_props(&self) -> Result<(), IqError> ``` Fetches account properties from WhatsApp servers. ### fetch\_privacy\_settings ```rust theme={null} pub async fn fetch_privacy_settings(&self) -> Result ``` Fetches privacy settings (last seen, profile photo, about, etc.). ### clean\_dirty\_bits ```rust theme={null} pub async fn clean_dirty_bits( &self, type_: &str, timestamp: Option<&str> ) -> Result<(), IqError> ``` Cleans app state dirty bits for a specific type. *** ## Protocol Operations ### send\_node ```rust theme={null} pub async fn send_node(&self, node: Node) -> Result<(), ClientError> ``` Sends a raw protocol node (advanced usage). Binary protocol node to send **Errors:** * `ClientError::NotConnected` - Not connected * `ClientError::EncryptSend` - Encryption/send failure ### generate\_message\_id ```rust theme={null} pub async fn generate_message_id(&self) -> String ``` Generates a unique WhatsApp-protocol-conformant message ID. Combines timestamp, user JID, and random components for uniqueness. This is intended for advanced users who need to build custom protocol interactions or manage message IDs manually. Most users should use `send_message` which handles ID generation automatically. ### send\_iq ```rust theme={null} pub async fn send_iq(&self, query: InfoQuery<'_>) -> Result ``` Sends a custom IQ (Info/Query) stanza to the WhatsApp server. IQ query containing stanza type, namespace, content, and optional timeout **Example:** ```rust theme={null} use wacore::request::{InfoQuery, InfoQueryType}; use wacore_binary::builder::NodeBuilder; use wacore_binary::node::NodeContent; use wacore_binary::jid::{Jid, SERVER_JID}; let query_node = NodeBuilder::new("presence") .attr("type", "available") .build(); let server_jid = Jid::new("", SERVER_JID); let query = InfoQuery { query_type: InfoQueryType::Set, namespace: "presence", to: server_jid, target: None, content: Some(NodeContent::Nodes(vec![query_node])), id: None, timeout: None, }; let response = client.send_iq(query).await?; ``` This bypasses higher-level abstractions and safety checks. You should be familiar with the WhatsApp protocol and IQ stanza format before using this. ### execute ```rust theme={null} pub async fn execute(&self, spec: S) -> Result ``` Executes a typed IQ specification. This is the preferred way to send IQ stanzas — each spec type handles building the request and parsing the response. A typed IQ specification that defines the request structure and response parsing **Example:** ```rust theme={null} // Fetch group metadata using a typed spec let metadata = client.execute(GroupQueryIq::new(&group_jid)).await?; ``` ### wait\_for\_node ```rust theme={null} pub fn wait_for_node(&self, filter: NodeFilter) -> oneshot::Receiver> ``` Waits for a specific incoming protocol node matching the given filter. Returns a receiver that resolves when a matching node arrives. Filter specifying which node to wait for (by tag and attributes) **Example:** ```rust theme={null} // Wait for a group notification let waiter = client.wait_for_node( NodeFilter::tag("notification").attr("type", "w:gp2"), ); // Perform the action that triggers the node client.groups().add_participants(&group_jid, &[jid]).await?; // Receive the notification let node = waiter.await.expect("notification arrived"); ``` Register the waiter **before** performing the action that triggers the expected node. When no waiters are active, this has zero cost (single atomic load per incoming node). #### NodeFilter Builder for matching incoming protocol nodes: ```rust theme={null} // Match by tag let filter = NodeFilter::tag("notification"); // Match by tag and attributes let filter = NodeFilter::tag("notification") .attr("type", "w:gp2"); // Match by tag and source JID let filter = NodeFilter::tag("notification") .from_jid(&group_jid); ``` ### register\_handler ```rust theme={null} pub fn register_handler(&self, handler: Arc) ``` Registers an event handler for protocol events. Handler implementing the EventHandler trait **Example:** ```rust theme={null} use wacore::types::events::{Event, EventHandler}; struct MyHandler; impl EventHandler for MyHandler { fn handle_event(&self, event: &Event) { match event { Event::Message(msg, info) => println!("New message from {}: {:?}", info.source.sender, msg), Event::Connected(_) => println!("Connected!"), _ => {} } } } client.register_handler(Arc::new(MyHandler)); ``` ### register\_chatstate\_handler ```rust theme={null} pub async fn register_chatstate_handler( &self, handler: Arc ) ``` Registers a handler for chat state events (typing indicators). *** ## Spam Reporting ### send\_spam\_report ```rust theme={null} pub async fn send_spam_report( &self, request: SpamReportRequest ) -> Result ``` Send a spam report to WhatsApp for messages or groups. The spam report request containing: * `message_id` - ID of the message being reported * `message_timestamp` - Timestamp of the message * `spam_flow` - Context where report was initiated (MessageMenu, GroupInfoReport, etc.) * `from_jid` - Optional sender JID * `group_jid` - Optional group JID for group spam * `group_subject` - Optional group name/subject for group reports * `participant_jid` - Optional participant JID in group context * `raw_message` - Optional raw message bytes * `media_type` - Optional media type if reporting media * `local_message_type` - Optional local message type **Returns:** `SpamReportResult` indicating success or failure **Example:** ```rust theme={null} use whatsapp_rust::{SpamReportRequest, SpamFlow}; // Report a spam message let result = client.send_spam_report(SpamReportRequest { message_id: "MESSAGE_ID".to_string(), message_timestamp: 1234567890, from_jid: Some(sender_jid), spam_flow: SpamFlow::MessageMenu, ..Default::default() }).await?; ``` **SpamFlow variants:** * `MessageMenu` - Reported from message context menu * `GroupInfoReport` - Reported from group info screen * `GroupSpamBannerReport` - Reported from group spam banner * `ContactInfo` - Reported from contact info screen * `StatusReport` - Reported from status view *** ## Passive Mode ### set\_passive ```rust theme={null} pub async fn set_passive(&self, passive: bool) -> Result<(), IqError> ``` Sets passive mode. When `false` (active), the server starts sending offline messages. *** ## Prekeys ### send\_digest\_key\_bundle ```rust theme={null} pub async fn send_digest_key_bundle(&self) -> Result<(), IqError> ``` Sends Signal Protocol prekey bundle digest to the server. *** ## Error Types ```rust theme={null} pub enum ClientError { NotConnected, Socket(SocketError), EncryptSend(EncryptSendError), AlreadyConnected, NotLoggedIn, } ``` *** ## See Also * [Bot](/api/bot) - High-level builder with event handlers * [Events](/concepts/events) - Event system and types * [Sending Messages](/guides/sending-messages) - Sending and receiving messages * [Group Management](/guides/group-management) - Working with groups # Contacts Source: https://whatsapp-rust.jlucaso.com/api/contacts Contact information and profile picture operations The `Contacts` trait provides methods for checking WhatsApp registration status, fetching contact information, and retrieving profile pictures. ## Access Access contact operations through the client: ```rust theme={null} let contacts = client.contacts(); ``` ## Methods ### is\_on\_whatsapp Check if phone numbers are registered on WhatsApp. ```rust theme={null} pub async fn is_on_whatsapp( &self, phones: &[&str], ) -> Result, anyhow::Error> ``` **Parameters:** * `phones` - Array of phone numbers (with or without `+` prefix) **Returns:** * `Vec` - Registration status for each number **IsOnWhatsAppResult fields:** * `jid: Jid` - WhatsApp JID for the number * `is_registered: bool` - Whether the number is on WhatsApp **Example:** ```rust theme={null} let phones = vec!["15551234567", "+15559876543"]; let results = client.contacts().is_on_whatsapp(&phones).await?; for result in results { if result.is_registered { println!("{} is on WhatsApp", result.jid); } else { println!("{} is NOT on WhatsApp", result.jid); } } ``` ### get\_info Get detailed contact information for phone numbers. ```rust theme={null} pub async fn get_info( &self, phones: &[&str], ) -> Result, anyhow::Error> ``` **Parameters:** * `phones` - Array of phone numbers (with or without `+` prefix) **Returns:** * `Vec` - Detailed info for each registered number **ContactInfo fields:** * `jid: Jid` - WhatsApp JID * `lid: Option` - LID (privacy identifier) if available * `is_registered: bool` - Whether on WhatsApp * `is_business: bool` - Whether this is a business account * `status: Option` - Status message/about * `picture_id: Option` - Profile picture ID **Example:** ```rust theme={null} let phones = vec!["15551234567"]; let contacts = client.contacts().get_info(&phones).await?; for contact in contacts { println!("JID: {}", contact.jid); println!(" Registered: {}", contact.is_registered); println!(" Business: {}", contact.is_business); if let Some(status) = contact.status { println!(" Status: {}", status); } if let Some(lid) = contact.lid { println!(" LID: {}", lid); } if let Some(pic_id) = contact.picture_id { println!(" Picture ID: {}", pic_id); } } ``` ### get\_profile\_picture Get the profile picture URL for a JID. ```rust theme={null} pub async fn get_profile_picture( &self, jid: &Jid, preview: bool, ) -> Result, anyhow::Error> ``` **Parameters:** * `jid` - Target JID (user, group, or newsletter) * `preview` - `true` for preview thumbnail, `false` for full-size image **Returns:** * `Option` - Picture info or `None` if not available **ProfilePicture fields:** * `id: String` - Picture ID * `url: String` - Download URL * `direct_path: Option` - Direct path for media download * `hash: Option` - SHA-256 hash for integrity and cache validation **Example:** ```rust theme={null} let jid: Jid = "15551234567@s.whatsapp.net".parse()?; // Get preview thumbnail if let Some(preview) = client.contacts().get_profile_picture(&jid, true).await? { println!("Preview URL: {}", preview.url); println!("Picture ID: {}", preview.id); } // Get full-size picture if let Some(full) = client.contacts().get_profile_picture(&jid, false).await? { println!("Full URL: {}", full.url); if let Some(path) = full.direct_path { println!("Direct path: {}", path); } } // Handle user with no profile picture let no_pic_jid: Jid = "15559999999@s.whatsapp.net".parse()?; if client.contacts().get_profile_picture(&no_pic_jid, true).await?.is_none() { println!("User has no profile picture"); } ``` **For groups:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; if let Some(pic) = client.contacts().get_profile_picture(&group_jid, true).await? { println!("Group picture: {}", pic.url); } ``` ### get\_user\_info Get user information by JID (more detailed than `get_info`). ```rust theme={null} pub async fn get_user_info( &self, jids: &[Jid], ) -> Result, anyhow::Error> ``` **Parameters:** * `jids` - Array of JIDs to query **Returns:** * `HashMap` - Map of JID to user info **UserInfo fields:** * `jid: Jid` - WhatsApp JID * `lid: Option` - LID if available * `status: Option` - Status message * `picture_id: Option` - Profile picture ID (String format) * `is_business: bool` - Whether business account **Example:** ```rust theme={null} let jids = vec![ "15551234567@s.whatsapp.net".parse()?, "15559876543@s.whatsapp.net".parse()?, ]; let user_info = client.contacts().get_user_info(&jids).await?; for (jid, info) in user_info { println!("User: {}", jid); println!(" Business: {}", info.is_business); if let Some(status) = info.status { println!(" Status: {}", status); } if let Some(pic_id) = info.picture_id { println!(" Picture ID: {}", pic_id); } } ``` ## Type Differences ### ContactInfo vs UserInfo Both types provide similar information but with different use cases: **ContactInfo** (from `get_info`): * Input: phone numbers * `picture_id: Option` - Numeric picture ID * Used for batch phone number lookups **UserInfo** (from `get_user_info`): * Input: JIDs * `picture_id: Option` - String picture ID (may include non-numeric prefixes) * Used for detailed JID-based queries ## Privacy & TC Tokens For user JIDs (not groups/newsletters), the library automatically includes TC tokens when fetching profile pictures. TC tokens are used for privacy-gated operations. The implementation automatically: * Looks up TC tokens for user JIDs * Includes tokens in profile picture requests * Skips tokens for groups and newsletters ## Error Handling All methods return `Result`. Common errors: * Invalid phone number format * Invalid JID format * Network errors * Rate limiting ```rust theme={null} match client.contacts().is_on_whatsapp(&["15551234567"]).await { Ok(results) => { for result in results { println!("Registered: {}", result.is_registered); } } Err(e) => eprintln!("Failed to check registration: {}", e), } ``` ## Batch Operations All lookup methods support batch operations for efficiency: ```rust theme={null} // Check multiple numbers at once let phones = vec!["15551111111", "15552222222", "15553333333"]; let results = client.contacts().is_on_whatsapp(&phones).await?; // Get info for multiple numbers let contacts = client.contacts().get_info(&phones).await?; // Get info for multiple JIDs let jids = vec![ "15551111111@s.whatsapp.net".parse()?, "15552222222@s.whatsapp.net".parse()?, ]; let user_info = client.contacts().get_user_info(&jids).await?; ``` ## Contact notification events The server sends `contacts` notifications when contact data changes. These are emitted as events you can subscribe to: * **`ContactUpdated`** — a contact's profile changed (invalidate cached presence/profile picture) * **`ContactNumberChanged`** — a contact changed their phone number (includes old/new JID and optional LID mappings) * **`ContactSyncRequested`** — the server requests a full contact re-sync ```rust theme={null} Event::ContactUpdated(update) => { // Refresh cached profile data for update.jid } Event::ContactNumberChanged(change) => { // Migrate chat data from change.old_jid to change.new_jid } Event::ContactSyncRequested(sync) => { // Re-sync contacts (optionally filtered by sync.after timestamp) } ``` See the [events reference](/concepts/events#contact-notification-events) for full struct definitions and wire format details. ## Empty input handling Passing empty arrays returns empty results without making network requests: ```rust theme={null} let empty: Vec<&str> = vec![]; let results = client.contacts().is_on_whatsapp(&empty).await?; assert!(results.is_empty()); ``` # download Source: https://whatsapp-rust.jlucaso.com/api/download Download and decrypt media from WhatsApp messages ## download Download and decrypt media from a message. ```rust theme={null} pub async fn download( &self, downloadable: &dyn Downloadable ) -> Result, anyhow::Error> ``` Any message type that implements the `Downloadable` trait. Includes: * `ImageMessage` * `VideoMessage` * `AudioMessage` * `DocumentMessage` * `StickerMessage` * `ExternalBlobReference` (app state) * `HistorySyncNotification` Decrypted media bytes. For encrypted media (E2EE), automatically decrypts using AES-256-CBC and verifies HMAC-SHA256. For plaintext media (newsletters/channels), validates SHA-256 hash. ### Example: Download Image ```rust theme={null} use waproto::whatsapp as wa; // From a received message if let Some(image_msg) = message.image_message { let image_bytes = client.download(image_msg.as_ref()).await?; std::fs::write("downloaded_image.jpg", image_bytes)?; } ``` ### Example: Download with Error Handling ```rust theme={null} match client.download(downloadable).await { Ok(data) => { println!("Downloaded {} bytes", data.len()); // Process data... } Err(e) => { eprintln!("Download failed: {}", e); // Fallback logic... } } ``` ### Automatic retry and URL re-derivation All download methods handle three categories of CDN errors automatically: * **Auth errors (401/403):** The client invalidates the cached media connection, fetches fresh credentials, and retries the download once. * **Media not found (404/410):** When a media URL has expired or the file has been relocated, the CDN returns 404 or 410. The client treats this the same as an auth error — it invalidates the cached connection, re-derives download URLs with fresh credentials and hosts, and retries once. This matches WhatsApp Web's `MediaNotFoundError` handling. * **Other errors (e.g., 500):** The client tries the next available CDN host without refreshing credentials. Hosts are tried in priority order (primary first, then fallback). For streaming downloads, the writer is seeked back to position 0 before retrying so partial writes are overwritten. *** ## download\_to\_file Download media and write directly to a file. ```rust theme={null} pub async fn download_to_file( &self, downloadable: &dyn Downloadable, writer: W, ) -> Result<(), anyhow::Error> ``` Message containing downloadable media Writer to output the decrypted data. Typically a `File` or `BufWriter`. ### Example: Download to File ```rust theme={null} use std::fs::File; use std::io::BufWriter; let file = File::create("video.mp4")?; let writer = BufWriter::new(file); client.download_to_file(video_msg.as_ref(), writer).await?; println!("Video saved to video.mp4"); ``` *** ## download\_to\_writer Download media using streaming (constant memory usage). The entire HTTP download, decryption, and file write happen in a single blocking thread. Memory usage is \~40KB regardless of file size. ```rust theme={null} pub async fn download_to_writer( &self, downloadable: &dyn Downloadable, writer: W, ) -> Result ``` Message containing downloadable media Writer for streaming output. Must be Send + 'static for use in blocking task. Returns the writer after successful download, seeked back to position 0. ### Example: Streaming Download ```rust theme={null} use std::fs::File; let file = File::create("large_video.mp4")?; let file = client.download_to_writer(video_msg.as_ref(), file).await?; // File is seeked back to start and can be reused ``` Use `download_to_writer` for large media files to avoid loading entire files into memory. Memory usage is constant \~40KB (8KB read buffer + decryption state). *** ## download\_from\_params Download and decrypt media from raw parameters without the original message. ```rust theme={null} pub async fn download_from_params( &self, direct_path: &str, media_key: &[u8], file_sha256: &[u8], file_enc_sha256: &[u8], file_length: u64, media_type: MediaType, ) -> Result, anyhow::Error> ``` WhatsApp CDN path (e.g., `/v/t62.7118-24/12345_67890`) 32-byte media encryption key from message SHA-256 hash of decrypted file SHA-256 hash of encrypted file Original file size in bytes Type of media: `Image`, `Video`, `Audio`, `Document`, `Sticker`, etc. Decrypted media bytes ### Example: Download from Stored Metadata ```rust theme={null} use wacore::download::MediaType; // If you stored media metadata separately let image_bytes = client.download_from_params( "/v/t62.7118-24/12345_67890", &media_key, &file_sha256, &file_enc_sha256, file_length, MediaType::Image ).await?; ``` *** ## download\_from\_params\_to\_writer Streaming variant of `download_from_params` that writes to a writer. ```rust theme={null} pub async fn download_from_params_to_writer( &self, direct_path: &str, media_key: &[u8], file_sha256: &[u8], file_enc_sha256: &[u8], file_length: u64, media_type: MediaType, writer: W, ) -> Result ``` WhatsApp CDN path 32-byte media encryption key SHA-256 hash of decrypted file SHA-256 hash of encrypted file Original file size in bytes Type of media Writer for streaming output Returns the writer after successful download *** ## Downloadable Trait The `Downloadable` trait provides a generic interface for downloading media from any message type. ```rust theme={null} pub trait Downloadable: Sync + Send { fn direct_path(&self) -> Option<&str>; fn media_key(&self) -> Option<&[u8]>; fn file_enc_sha256(&self) -> Option<&[u8]>; fn file_sha256(&self) -> Option<&[u8]>; fn file_length(&self) -> Option; fn app_info(&self) -> MediaType; fn static_url(&self) -> Option<&str> { None } fn is_encrypted(&self) -> bool { self.media_key().is_some() } } ``` WhatsApp CDN path for the media file 32-byte encryption key. Present for E2EE media, `None` for plaintext (newsletter/channel) media. SHA-256 hash of the encrypted file. Used for encrypted media validation. SHA-256 hash of the decrypted file. Used for plaintext media validation. Original file size in bytes Media type for HKDF key derivation (`Image`, `Video`, `Audio`, `Document`, etc.) Static CDN URL for direct download. Present on newsletter/channel media, bypasses host construction. Returns `true` if media is encrypted (has `media_key`), `false` for plaintext media ### Built-in Implementations The `Downloadable` trait is automatically implemented for: * `wa::message::ImageMessage` * `wa::message::VideoMessage` * `wa::message::AudioMessage` * `wa::message::DocumentMessage` * `wa::message::StickerMessage` * `wa::ExternalBlobReference` (app state) * `wa::message::HistorySyncNotification` *** ## MediaType Media type enum for encryption/decryption. ```rust theme={null} pub enum MediaType { Image, Video, Audio, Document, History, AppState, Sticker, StickerPack, LinkThumbnail, } ``` Each media type has specific HKDF info strings used for key derivation: * `Image` / `Sticker` → `"WhatsApp Image Keys"` * `Video` → `"WhatsApp Video Keys"` * `Audio` → `"WhatsApp Audio Keys"` * `Document` → `"WhatsApp Document Keys"` * `History` → `"WhatsApp History Keys"` * `AppState` → `"WhatsApp App State Keys"` *** ## Media Decryption WhatsApp uses different handling for encrypted (E2EE) and plaintext media: ### Encrypted Media (E2EE) 1. **Download** encrypted bytes from CDN 2. **Verify** HMAC-SHA256 (last 10 bytes) 3. **Decrypt** using AES-256-CBC with keys derived from `media_key` via HKDF 4. **Return** decrypted plaintext The `media_key` is expanded using HKDF-SHA256 to derive: * 16-byte IV * 32-byte cipher key * 32-byte MAC key ### Plaintext Media (Newsletter/Channel) 1. **Download** plaintext bytes from CDN (often via `static_url`) 2. **Verify** SHA-256 hash matches `file_sha256` 3. **Return** plaintext (no decryption needed) Newsletter and channel media is **not encrypted**. The library automatically detects this when `media_key` is absent and switches to plaintext validation. ### Example: Detect Media Type ```rust theme={null} if downloadable.is_encrypted() { println!("E2EE media - will decrypt"); } else { println!("Plaintext media - no decryption needed"); } ``` # Groups Source: https://whatsapp-rust.jlucaso.com/api/groups Group management operations - create, modify, and manage WhatsApp groups The `Groups` trait provides methods for managing WhatsApp groups, including creating groups, managing participants, and modifying group settings. ## Access Access group operations through the client: ```rust theme={null} let groups = client.groups(); ``` ## Methods ### query\_info Query group information with caching support. ```rust theme={null} pub async fn query_info(&self, jid: &Jid) -> Result ``` **Parameters:** * `jid` - Group JID (must end with `@g.us`) **Returns:** * `GroupInfo` - Contains participants list and addressing mode **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; let info = client.groups().query_info(&group_jid).await?; println!("Participants: {}", info.participants().len()); println!("Addressing mode: {:?}", info.addressing_mode()); ``` ### get\_participating Get all groups the client is participating in. ```rust theme={null} pub async fn get_participating(&self) -> Result, anyhow::Error> ``` **Returns:** * `HashMap` - Map of group JID strings to metadata **GroupMetadata fields:** * `id: Jid` - Group JID * `subject: String` - Group name * `participants: Vec` - List of participants * `addressing_mode: AddressingMode` - Phone number or LID mode * `creator: Option` - Group creator JID * `creation_time: Option` - Group creation timestamp (Unix seconds) * `subject_time: Option` - Subject modification timestamp (Unix seconds) * `subject_owner: Option` - Subject owner JID * `description: Option` - Group description body text * `description_id: Option` - Description ID (for conflict detection) * `is_locked: bool` - Whether only admins can edit group info * `is_announcement: bool` - Whether only admins can send messages * `ephemeral_expiration: u32` - Disappearing messages timer in seconds (0 = disabled) * `membership_approval: bool` - Whether admin approval is required to join * `member_add_mode: Option` - Who can add members * `member_link_mode: Option` - Who can use invite links * `size: Option` - Total participant count **GroupParticipant fields:** * `jid: Jid` - Participant JID * `phone_number: Option` - Phone number JID (for LID groups) * `is_admin: bool` - Whether participant is an admin **Example:** ```rust theme={null} let groups = client.groups().get_participating().await?; for (jid_str, metadata) in groups { println!("Group: {} ({})", metadata.subject, jid_str); println!(" Participants: {}", metadata.participants.len()); for participant in &metadata.participants { println!(" {} (admin: {})", participant.jid, participant.is_admin); } } ``` ### get\_metadata Get metadata for a specific group. ```rust theme={null} pub async fn get_metadata(&self, jid: &Jid) -> Result ``` **Parameters:** * `jid` - Group JID **Returns:** * `GroupMetadata` - Group metadata (see `get_participating` for fields) **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; let metadata = client.groups().get_metadata(&group_jid).await?; println!("Subject: {}", metadata.subject); println!("Mode: {:?}", metadata.addressing_mode); ``` ### create\_group Create a new group. ```rust theme={null} pub async fn create_group( &self, options: GroupCreateOptions, ) -> Result ``` **Parameters:** * `options: GroupCreateOptions` - Group creation options * `subject: String` - Group name (max 100 characters) * `participants: Vec` - Initial participants * `member_link_mode: Option` - Who can use invite links (default: `AdminLink`) * `member_add_mode: Option` - Who can add members (default: `AllMemberAdd`) * `membership_approval_mode: Option` - Require admin approval (default: `Off`) * `ephemeral_expiration: Option` - Disappearing messages timer in seconds (default: `0`) **Returns:** * `CreateGroupResult` with `gid: Jid` field **Example:** ```rust theme={null} use whatsapp_rust::features::groups::{GroupCreateOptions, GroupParticipantOptions}; let participant1: Jid = "15551234567@s.whatsapp.net".parse()?; let participant2: Jid = "15559876543@s.whatsapp.net".parse()?; let options = GroupCreateOptions::builder() .subject("My New Group") .participants(vec![ GroupParticipantOptions::new(participant1), GroupParticipantOptions::new(participant2), ]) .build(); let result = client.groups().create_group(options).await?; println!("Created group: {}", result.gid); ``` ### set\_subject Change the group name. ```rust theme={null} pub async fn set_subject(&self, jid: &Jid, subject: GroupSubject) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Group JID * `subject` - New group name (max 100 characters) **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; let new_subject = GroupSubject::new("Updated Group Name")?; client.groups().set_subject(&group_jid, new_subject).await?; ``` ### set\_description Set or delete the group description. ```rust theme={null} pub async fn set_description( &self, jid: &Jid, description: Option, prev: Option, ) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Group JID * `description` - New description (max 2048 characters) or `None` to delete * `prev` - Current description ID for conflict detection (pass `None` if unknown) **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; let desc = GroupDescription::new("This is our group chat")?; client.groups().set_description(&group_jid, Some(desc), None).await?; // Delete description client.groups().set_description(&group_jid, None, None).await?; ``` ### leave Leave a group. ```rust theme={null} pub async fn leave(&self, jid: &Jid) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Group JID to leave **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; client.groups().leave(&group_jid).await?; ``` ### add\_participants Add participants to a group. ```rust theme={null} pub async fn add_participants( &self, jid: &Jid, participants: &[Jid], ) -> Result, anyhow::Error> ``` **Parameters:** * `jid` - Group JID * `participants` - Array of participant JIDs to add **Returns:** * `Vec` - Result for each participant **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; let new_members = vec![ "15551234567@s.whatsapp.net".parse()?, "15559876543@s.whatsapp.net".parse()?, ]; let results = client.groups().add_participants(&group_jid, &new_members).await?; for result in results { println!("Added: {:?}", result); } ``` ### remove\_participants Remove participants from a group. ```rust theme={null} pub async fn remove_participants( &self, jid: &Jid, participants: &[Jid], ) -> Result, anyhow::Error> ``` **Parameters:** * `jid` - Group JID * `participants` - Array of participant JIDs to remove **Returns:** * `Vec` - Result for each participant **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; let to_remove = vec!["15551234567@s.whatsapp.net".parse()?]; client.groups().remove_participants(&group_jid, &to_remove).await?; ``` ### promote\_participants Promote participants to admin. ```rust theme={null} pub async fn promote_participants( &self, jid: &Jid, participants: &[Jid], ) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Group JID * `participants` - Array of participant JIDs to promote **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; let to_promote = vec!["15551234567@s.whatsapp.net".parse()?]; client.groups().promote_participants(&group_jid, &to_promote).await?; ``` ### demote\_participants Demote admin participants to regular members. ```rust theme={null} pub async fn demote_participants( &self, jid: &Jid, participants: &[Jid], ) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Group JID * `participants` - Array of admin JIDs to demote **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; let to_demote = vec!["15551234567@s.whatsapp.net".parse()?]; client.groups().demote_participants(&group_jid, &to_demote).await?; ``` ### get\_invite\_link Get or reset the group invite link. ```rust theme={null} pub async fn get_invite_link(&self, jid: &Jid, reset: bool) -> Result ``` **Parameters:** * `jid` - Group JID * `reset` - Whether to reset and generate a new invite link **Returns:** * `String` - Invite link code **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; // Get current invite link let link = client.groups().get_invite_link(&group_jid, false).await?; println!("Invite link: https://chat.whatsapp.com/{}", link); // Reset and get new link let new_link = client.groups().get_invite_link(&group_jid, true).await?; println!("New invite link: https://chat.whatsapp.com/{}", new_link); ``` ### set\_locked Lock or unlock the group so only admins can change group info. ```rust theme={null} pub async fn set_locked(&self, jid: &Jid, locked: bool) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Group JID * `locked` - `true` to lock (only admins edit info), `false` to unlock **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; // Lock group info client.groups().set_locked(&group_jid, true).await?; // Unlock group info client.groups().set_locked(&group_jid, false).await?; ``` ### set\_announce Set announcement mode. When enabled, only admins can send messages. ```rust theme={null} pub async fn set_announce(&self, jid: &Jid, announce: bool) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Group JID * `announce` - `true` to enable (only admins send), `false` to disable **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; // Enable announcement mode client.groups().set_announce(&group_jid, true).await?; // Disable announcement mode client.groups().set_announce(&group_jid, false).await?; ``` ### set\_ephemeral Set the disappearing messages timer on the group. ```rust theme={null} pub async fn set_ephemeral(&self, jid: &Jid, expiration: u32) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Group JID * `expiration` - Timer duration in seconds. Common values: `86400` (24 hours), `604800` (7 days), `7776000` (90 days). Pass `0` to disable. **Example:** ```rust theme={null} let group_jid: Jid = "123456789@g.us".parse()?; // Enable 7-day disappearing messages client.groups().set_ephemeral(&group_jid, 604800).await?; // Disable disappearing messages client.groups().set_ephemeral(&group_jid, 0).await?; ``` ### set\_membership\_approval Set membership approval mode. When enabled, new members must be approved by an admin. ```rust theme={null} pub async fn set_membership_approval( &self, jid: &Jid, mode: MembershipApprovalMode, ) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Group JID * `mode` - `MembershipApprovalMode::On` to require approval, `MembershipApprovalMode::Off` to disable **Example:** ```rust theme={null} use whatsapp_rust::features::groups::MembershipApprovalMode; let group_jid: Jid = "123456789@g.us".parse()?; // Require admin approval client.groups().set_membership_approval(&group_jid, MembershipApprovalMode::On).await?; // Remove approval requirement client.groups().set_membership_approval(&group_jid, MembershipApprovalMode::Off).await?; ``` ## Types ### GroupSubject Validated group name with 100 character limit. ```rust theme={null} impl GroupSubject { pub fn new(subject: impl Into) -> Result pub fn into_string(self) -> String } ``` ### GroupDescription Validated group description with 2048 character limit. ```rust theme={null} impl GroupDescription { pub fn new(description: impl Into) -> Result pub fn into_string(self) -> String } ``` ### MemberAddMode ```rust theme={null} pub enum MemberAddMode { AdminAdd, // Only admins can add members AllMemberAdd, // All members can add } ``` ### MemberLinkMode Controls who can use invite links to join the group. ```rust theme={null} pub enum MemberLinkMode { AdminLink, // Only admins can share invite links AllMemberLink, // All members can share invite links } ``` ### MembershipApprovalMode ```rust theme={null} pub enum MembershipApprovalMode { Off, // No approval required On, // Admin approval required } ``` ### GroupParticipantOptions Options for specifying a participant when creating a group. ```rust theme={null} pub struct GroupParticipantOptions { pub jid: Jid, pub phone_number: Option, pub privacy: Option>, } ``` **Constructors:** * `GroupParticipantOptions::new(jid)` - Create from a JID * `GroupParticipantOptions::from_phone(phone_number)` - Create from a phone number JID * `GroupParticipantOptions::from_lid_and_phone(lid, phone_number)` - Create from a LID and phone number **Example:** ```rust theme={null} use whatsapp_rust::features::groups::GroupParticipantOptions; let participant = GroupParticipantOptions::new("15551234567@s.whatsapp.net".parse()?); ``` ### AddressingMode ```rust theme={null} pub enum AddressingMode { Pn, // Phone number addressing Lid, // LID (privacy) addressing } ``` ## Error Handling All methods return `Result`. Common errors: * Invalid JID format * Network errors * Permission denied (not admin) * Group not found * Subject/description too long * Participant not found ```rust theme={null} match client.groups().set_subject(&group_jid, subject).await { Ok(_) => println!("Subject updated"), Err(e) => eprintln!("Failed to update subject: {}", e), } ``` # HTTP Client Trait Source: https://whatsapp-rust.jlucaso.com/api/http-client HTTP client abstraction and ureq implementation for media operations ## Overview The HTTP client abstraction provides a runtime-agnostic interface for making HTTP requests. It's primarily used for: * Media uploads to WhatsApp servers * Media downloads (with streaming support) * Fetching metadata and authentication tokens The client supports both buffered and streaming responses for efficient handling of large files. ## HttpClient Trait ```rust theme={null} use async_trait::async_trait; #[async_trait] pub trait HttpClient: Send + Sync { /// Executes a given HTTP request and returns the response async fn execute(&self, request: HttpRequest) -> Result; /// Synchronous streaming variant. Returns a reader over the response body /// instead of buffering it all in memory. /// /// Must be called from a blocking context (e.g. inside `spawn_blocking`). fn execute_streaming(&self, request: HttpRequest) -> Result; } ``` ### Methods #### execute Executes an HTTP request and buffers the entire response body. ```rust theme={null} async fn execute(&self, request: HttpRequest) -> Result; ``` **Parameters:** * `request: HttpRequest` - The request to execute **Returns:** * `HttpResponse` with buffered body on success * `anyhow::Error` on failure **Example:** ```rust theme={null} let request = HttpRequest::get("https://api.example.com/data") .with_header("Authorization", "Bearer token"); let response = client.execute(request).await?; println!("Status: {}", response.status_code); println!("Body: {:?}", response.body); ``` #### execute\_streaming Executes an HTTP request and returns a streaming reader over the response body. **Important:** This is a synchronous method that must be called from within `tokio::task::spawn_blocking`. ```rust theme={null} fn execute_streaming(&self, request: HttpRequest) -> Result; ``` **Parameters:** * `request: HttpRequest` - The request to execute **Returns:** * `StreamingHttpResponse` with streaming body reader * `anyhow::Error` on failure or if streaming is not supported **Example:** ```rust theme={null} let http_client = Arc::new(UreqHttpClient::new()); let request = HttpRequest::get("https://mmg.whatsapp.net/large-file.enc"); // Must be called inside spawn_blocking let response = tokio::task::spawn_blocking(move || { http_client.execute_streaming(request) }).await??; // Read in chunks let mut buffer = vec![0u8; 4096]; while let Ok(n) = response.body.read(&mut buffer) { if n == 0 { break; } // Process chunk } ``` ## Data Structures ### HttpRequest Represents an HTTP request with headers and optional body. ```rust theme={null} pub struct HttpRequest { pub url: String, pub method: String, // "GET" or "POST" pub headers: HashMap, pub body: Option>, } ``` #### Constructors ```rust theme={null} // GET request let request = HttpRequest::get("https://example.com/data"); // POST request let request = HttpRequest::post("https://example.com/upload"); ``` #### Builder Methods ```rust theme={null} // Add header let request = HttpRequest::get("https://example.com/data") .with_header("Authorization", "Bearer token") .with_header("Content-Type", "application/json"); // Add body (for POST) let body = b"{\"key\": \"value\"}"; let request = HttpRequest::post("https://example.com/upload") .with_body(body.to_vec()); ``` ### HttpResponse Represents an HTTP response with buffered body. ```rust theme={null} pub struct HttpResponse { pub status_code: u16, pub body: Vec, } ``` #### Methods ```rust theme={null} // Convert body to string let text = response.body_string()?; println!("Response text: {}", text); ``` ### StreamingHttpResponse Represents an HTTP response with streaming body reader. ```rust theme={null} pub struct StreamingHttpResponse { pub status_code: u16, pub body: Box, } ``` **Usage:** ```rust theme={null} use std::io::Read; let mut buffer = vec![0u8; 8192]; loop { match response.body.read(&mut buffer) { Ok(0) => break, // EOF Ok(n) => { // Process n bytes from buffer process_chunk(&buffer[..n]); } Err(e) => return Err(e.into()), } } ``` ## UreqHttpClient The default HTTP client implementation using the `ureq` crate for synchronous HTTP requests. ### Features * **Blocking I/O** - Uses synchronous ureq, wrapped in `tokio::task::spawn_blocking` * **Streaming Support** - Implements efficient streaming downloads * **Simple API** - Minimal configuration required * **Thread-Safe** - Implements `Clone` for easy sharing ### Creating a Client ```rust theme={null} use whatsapp_rust_ureq_http_client::UreqHttpClient; // Basic usage let client = UreqHttpClient::new(); // Or use default let client = UreqHttpClient::default(); ``` ### Usage Examples #### Basic GET Request ```rust theme={null} use whatsapp_rust_ureq_http_client::UreqHttpClient; use wacore::net::HttpRequest; #[tokio::main] async fn main() -> Result<(), Box> { let client = UreqHttpClient::new(); let request = HttpRequest::get("https://api.example.com/data") .with_header("User-Agent", "whatsapp-rust"); let response = client.execute(request).await?; if response.status_code == 200 { println!("Success! Body length: {}", response.body.len()); } Ok(()) } ``` #### POST Request with Body ```rust theme={null} let client = UreqHttpClient::new(); let body = serde_json::to_vec(&json!({ "key": "value", "number": 42 }))?; let request = HttpRequest::post("https://api.example.com/upload") .with_header("Content-Type", "application/json") .with_body(body); let response = client.execute(request).await?; ``` #### Streaming Download ```rust theme={null} use std::fs::File; use std::io::Write; let client = Arc::new(UreqHttpClient::new()); let url = "https://mmg.whatsapp.net/file.enc"; // Must use spawn_blocking for streaming let file_data = tokio::task::spawn_blocking(move || -> Result> { let request = HttpRequest::get(url); let response = client.execute_streaming(request)?; if response.status_code != 200 { return Err(anyhow::anyhow!("HTTP {}", response.status_code)); } let mut buffer = vec![0u8; 65536]; let mut output = Vec::new(); let mut reader = response.body; loop { match reader.read(&mut buffer) { Ok(0) => break, Ok(n) => output.extend_from_slice(&buffer[..n]), Err(e) => return Err(e.into()), } } Ok(output) }).await??; println!("Downloaded {} bytes", file_data.len()); ``` ### Internal Implementation The `execute` method wraps ureq calls in `spawn_blocking`: ```rust theme={null} #[async_trait] impl HttpClient for UreqHttpClient { async fn execute(&self, request: HttpRequest) -> Result { // ureq is blocking, so wrap in spawn_blocking tokio::task::spawn_blocking(move || { let response = match request.method.as_str() { "GET" => { let mut req = ureq::get(&request.url); for (key, value) in &request.headers { req = req.header(key, value); } req.call()? } "POST" => { let mut req = ureq::post(&request.url); for (key, value) in &request.headers { req = req.header(key, value); } if let Some(body) = request.body { req.send(&body[..])? } else { req.send(&[])? } } method => { return Err(anyhow::anyhow!("Unsupported HTTP method: {}", method)); } }; let status_code = response.status().as_u16(); let body_bytes = response.into_body().read_to_vec()?; Ok(HttpResponse { status_code, body: body_bytes, }) }) .await? } } ``` The `execute_streaming` method is synchronous (no `spawn_blocking`) because it's called FROM within a blocking context: ```rust theme={null} fn execute_streaming(&self, request: HttpRequest) -> Result { let response = match request.method.as_str() { "GET" => { let mut req = ureq::get(&request.url); for (key, value) in &request.headers { req = req.header(key, value); } req.call()? } method => { return Err(anyhow::anyhow!( "Streaming only supports GET, got: {}", method )); } }; let status_code = response.status().as_u16(); let reader = response.into_body().into_reader(); Ok(StreamingHttpResponse { status_code, body: Box::new(reader), }) } ``` ## Implementing Custom HTTP Clients You can implement custom HTTP clients for different runtimes or requirements. ### Example: Reqwest Client (Async) ```rust theme={null} use async_trait::async_trait; use wacore::net::{HttpClient, HttpRequest, HttpResponse}; #[derive(Clone)] pub struct ReqwestHttpClient { client: reqwest::Client, } impl ReqwestHttpClient { pub fn new() -> Self { Self { client: reqwest::Client::new(), } } } #[async_trait] impl HttpClient for ReqwestHttpClient { async fn execute(&self, request: HttpRequest) -> Result { let mut req = match request.method.as_str() { "GET" => self.client.get(&request.url), "POST" => self.client.post(&request.url), method => return Err(anyhow::anyhow!("Unsupported method: {}", method)), }; // Add headers for (key, value) in request.headers { req = req.header(key, value); } // Add body for POST if let Some(body) = request.body { req = req.body(body); } let response = req.send().await?; let status_code = response.status().as_u16(); let body = response.bytes().await?.to_vec(); Ok(HttpResponse { status_code, body }) } // Note: Streaming not implemented for this example } ``` ### Example: Mock Client for Testing ```rust theme={null} use async_trait::async_trait; use std::collections::HashMap; pub struct MockHttpClient { responses: HashMap, } impl MockHttpClient { pub fn new() -> Self { Self { responses: HashMap::new(), } } pub fn with_response(mut self, url: &str, response: HttpResponse) -> Self { self.responses.insert(url.to_string(), response); self } } #[async_trait] impl HttpClient for MockHttpClient { async fn execute(&self, request: HttpRequest) -> Result { self.responses .get(&request.url) .cloned() .ok_or_else(|| anyhow::anyhow!("No mock response for {}", request.url)) } } // Usage in tests: let client = MockHttpClient::new() .with_response( "https://api.example.com/test", HttpResponse { status_code: 200, body: b"test data".to_vec(), }, ); ``` ## Usage with Bot builder ```rust theme={null} use whatsapp_rust::Bot; use whatsapp_rust_ureq_http_client::UreqHttpClient; use std::sync::Arc; #[tokio::main] async fn main() -> Result<(), Box> { let http_client = UreqHttpClient::new(); let mut bot = Bot::builder() .with_backend(backend) .with_transport_factory(transport_factory) .with_http_client(http_client) .on_event(|event, client| async move { /* handle events */ }) .build() .await?; Ok(()) } ``` ## Best Practices 1. **Blocking Operations** - Always wrap blocking HTTP libraries in `tokio::task::spawn_blocking` 2. **Streaming for Large Files** - Use `execute_streaming` for media downloads to avoid buffering 3. **Error Handling** - Return descriptive errors with context 4. **Timeouts** - Implement request timeouts for reliability 5. **Retries** - Consider retry logic for transient failures 6. **Connection Pooling** - Reuse connections when possible ## Media Operations The HTTP client is primarily used for media operations in whatsapp-rust: ### Media Upload Flow The client manages media connections internally. Use the high-level `upload` method instead of building requests manually: ```rust theme={null} use whatsapp_rust::download::MediaType; // Upload handles encryption, auth, and CDN host selection automatically let upload_response = client.upload(media_bytes, MediaType::Image).await?; ``` Under the hood, the client uses the HTTP client to: 1. Fetch media connection credentials from WhatsApp servers 2. Encrypt the media with AES-256-CBC 3. Upload to the CDN with proper auth headers 4. Parse the response for `direct_path` and file hashes ### Media Download Flow ```rust theme={null} // 1. Extract media info from message let media_info = extract_media_info(&message)?; // 2. Build download URL let url = format!("https://mmg.whatsapp.net{}", media_info.direct_path); // 3. Download with streaming (in spawn_blocking) let file_data = tokio::task::spawn_blocking(move || { let request = HttpRequest::get(&url); let response = http_client.execute_streaming(request)?; // Read and decrypt chunks decrypt_media_stream(response.body, &media_key, &expected_sha256) }).await??; ``` ## Testing ### Unit Test Example ```rust theme={null} #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_http_get() { let client = UreqHttpClient::new(); let request = HttpRequest::get("https://httpbin.org/get") .with_header("User-Agent", "test"); let response = client.execute(request).await.unwrap(); assert_eq!(response.status_code, 200); } #[tokio::test] async fn test_http_post() { let client = UreqHttpClient::new(); let body = b"test data"; let request = HttpRequest::post("https://httpbin.org/post") .with_body(body.to_vec()); let response = client.execute(request).await.unwrap(); assert_eq!(response.status_code, 200); } } ``` ## See Also * [Storage Traits](/api/store) - Storage backend abstraction * [Transport Trait](/api/transport) - Network transport abstraction * [Client API](/api/client) - Main client interface * [Media Handling](/guides/media-handling) - Guide to sending and receiving media # MEX (GraphQL) Source: https://whatsapp-rust.jlucaso.com/api/mex Execute GraphQL queries and mutations via Meta Exchange The `Mex` feature provides access to WhatsApp's Meta Exchange (MEX) GraphQL API, enabling queries and mutations for advanced operations. ## Access Access MEX operations through the client: ```rust theme={null} let mex = client.mex(); ``` ## Methods ### query Execute a GraphQL query. ```rust theme={null} pub async fn query(&self, request: MexRequest<'_>) -> Result ``` **Parameters:** * `request` - A `MexRequest` containing the document ID and variables **Returns:** * `MexResponse` - The GraphQL response with data and/or errors **Example:** ```rust theme={null} use serde_json::json; use whatsapp_rust::MexRequest; let request = MexRequest { doc_id: "29829202653362039", variables: json!({ "user_ids": ["15551234567@s.whatsapp.net"] }), }; let response = client.mex().query(request).await?; if response.has_data() { let data = response.data.unwrap(); println!("Result: {:?}", data); } ``` ### mutate Execute a GraphQL mutation. ```rust theme={null} pub async fn mutate(&self, request: MexRequest<'_>) -> Result ``` **Parameters:** * `request` - A `MexRequest` containing the document ID and variables **Returns:** * `MexResponse` - The GraphQL response with data and/or errors **Example:** ```rust theme={null} use serde_json::json; use whatsapp_rust::MexRequest; let request = MexRequest { doc_id: "mutation_doc_id_here", variables: json!({ "input": { "field": "value" } }), }; let response = client.mex().mutate(request).await?; if let Some(errors) = &response.errors { for error in errors { eprintln!("Error: {}", error.message); } } ``` ## Types ### MexRequest Request structure for GraphQL operations. ```rust theme={null} pub struct MexRequest<'a> { /// GraphQL document ID pub doc_id: &'a str, /// Query variables as JSON pub variables: serde_json::Value, } ``` ### MexResponse Response from a GraphQL operation. ```rust theme={null} pub struct MexResponse { /// Response data (if successful) pub data: Option, /// List of errors (if any) pub errors: Option>, } ``` **Methods:** * `has_data()` - Returns `true` if response contains data * `has_errors()` - Returns `true` if response contains errors * `fatal_error()` - Returns the first fatal error, if any ### MexGraphQLError GraphQL error structure. ```rust theme={null} pub struct MexGraphQLError { /// Error message pub message: String, /// Error extensions with additional details pub extensions: Option, } ``` **Methods:** * `error_code()` - Returns the error code if available * `is_fatal()` - Returns `true` if this is a fatal error ### MexErrorExtensions Additional error metadata. ```rust theme={null} pub struct MexErrorExtensions { /// Numeric error code pub error_code: Option, /// Whether this is a summary error pub is_summary: Option, /// Whether the operation can be retried pub is_retryable: Option, /// Error severity level pub severity: Option, } ``` ## Error handling The `MexError` enum covers all possible error cases: ```rust theme={null} pub enum MexError { /// Payload parsing error PayloadParsing(String), /// Extension error with code and message ExtensionError { code: i32, message: String }, /// IQ request failed Request(IqError), /// JSON serialization/deserialization error Json(serde_json::Error), } ``` **Example:** ```rust theme={null} match client.mex().query(request).await { Ok(response) => { if let Some(fatal) = response.fatal_error() { eprintln!("Fatal error: {} (code: {:?})", fatal.message, fatal.error_code() ); } else if response.has_data() { println!("Success: {:?}", response.data); } } Err(MexError::ExtensionError { code, message }) => { eprintln!("MEX error {}: {}", code, message); } Err(e) => eprintln!("Request failed: {}", e), } ``` ## Response handling ### Check for data ```rust theme={null} let response = client.mex().query(request).await?; if response.has_data() { let data = response.data.unwrap(); // Process data } ``` ### Handle non-fatal errors ```rust theme={null} let response = client.mex().query(request).await?; if response.has_errors() { for error in response.errors.as_ref().unwrap() { if !error.is_fatal() { // Log warning but continue eprintln!("Warning: {}", error.message); } } } ``` ### Extract typed data ```rust theme={null} use serde::Deserialize; #[derive(Deserialize)] struct UserResult { jid: String, country_code: String, } let response = client.mex().query(request).await?; if let Some(data) = response.data { let users: Vec = serde_json::from_value( data["xwa2_fetch_wa_users"].clone() )?; for user in users { println!("User {} from {}", user.jid, user.country_code); } } ``` # Presence Source: https://whatsapp-rust.jlucaso.com/api/presence Online/offline status and presence subscription operations The `Presence` trait provides methods for managing your online/offline status and subscribing to contact presence updates. ## Access Access presence operations through the client: ```rust theme={null} let presence = client.presence(); ``` ## Methods ### set Set your presence status (online or offline). ```rust theme={null} pub async fn set(&self, status: PresenceStatus) -> Result<(), PresenceError> ``` **Parameters:** * `status: PresenceStatus` - Either `Available` (online) or `Unavailable` (offline) **Requirements:** * Push name must be set before sending presence * Returns error if push name is empty **Example:** ```rust theme={null} use whatsapp_rust::features::presence::PresenceStatus; // Set status to online client.presence().set(PresenceStatus::Available).await?; // Set status to offline client.presence().set(PresenceStatus::Unavailable).await?; ``` ### set\_available Convenience method to set status to available (online). ```rust theme={null} pub async fn set_available(&self) -> Result<(), PresenceError> ``` **Example:** ```rust theme={null} client.presence().set_available().await?; println!("Now online"); ``` ### set\_unavailable Convenience method to set status to unavailable (offline). ```rust theme={null} pub async fn set_unavailable(&self) -> Result<(), PresenceError> ``` **Example:** ```rust theme={null} client.presence().set_unavailable().await?; println!("Now offline"); ``` ### subscribe Subscribe to a contact's presence updates. ```rust theme={null} pub async fn subscribe(&self, jid: &Jid) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Contact JID to subscribe to **Behavior:** * Sends a `` stanza * Automatically includes TC token if available for the contact * Tracks the subscription internally so it can be restored on reconnect * Used to receive notifications when the contact goes online/offline **Example:** ```rust theme={null} let contact_jid: Jid = "15551234567@s.whatsapp.net".parse()?; client.presence().subscribe(&contact_jid).await?; println!("Subscribed to {}'s presence", contact_jid); ``` ### unsubscribe Unsubscribe from a contact's presence updates. ```rust theme={null} pub async fn unsubscribe(&self, jid: &Jid) -> Result<(), anyhow::Error> ``` **Parameters:** * `jid` - Contact JID to unsubscribe from **Behavior:** * Sends a `` stanza * Removes the contact from the internal subscription tracker * You will no longer receive presence updates for this contact **Example:** ```rust theme={null} let contact_jid: Jid = "15551234567@s.whatsapp.net".parse()?; client.presence().unsubscribe(&contact_jid).await?; println!("Unsubscribed from {}'s presence", contact_jid); ``` ## PresenceStatus Enum ```rust theme={null} pub enum PresenceStatus { Available, // Online Unavailable, // Offline } ``` **Methods:** * `as_str()` - Returns `"available"` or `"unavailable"` **Conversion:** ```rust theme={null} let status = PresenceStatus::Available; assert_eq!(status.as_str(), "available"); ``` ## Push Name Requirement WhatsApp requires a push name (display name) to be set before sending presence updates. This matches WhatsApp Web behavior. **Error example:** ```rust theme={null} use whatsapp_rust::features::presence::PresenceError; // If push name not set match client.presence().set_available().await { Ok(_) => println!("Presence set"), Err(PresenceError::PushNameEmpty) => { eprintln!("Cannot send presence without a push name set"); } Err(e) => eprintln!("Other error: {}", e), } ``` The push name is typically set during the pairing/connection process from app state sync. ## Wire Format ### Setting Presence ```xml theme={null} ``` ### Subscribing to Presence ```xml theme={null} ``` ### Unsubscribing from Presence ```xml theme={null} ``` ## TC Token Handling When subscribing to presence, the library automatically: * Looks up TC token for the target JID * Includes token as child node if available * Skips token if not found (non-error) This matches WhatsApp Web's privacy gating behavior. ## Subscription Tracking The library automatically tracks which contacts you have subscribed to. This enables automatic re-subscription after a reconnect, so you don't lose presence updates when the connection drops. ### How it works * Calling `subscribe(jid)` adds the contact to an internal tracked set * Calling `unsubscribe(jid)` removes the contact from the tracked set * Duplicate subscriptions are deduplicated automatically * On reconnect, the library re-subscribes to all tracked contacts ### Automatic re-subscription on reconnect When the client reconnects after a connection drop, it automatically calls `resubscribe_presence_subscriptions()` to restore all tracked presence subscriptions. This happens transparently — you don't need to manually re-subscribe after a reconnect. The re-subscription process includes safety checks: * Bails out early if the connection generation changes (a new reconnect occurred) * Skips re-subscription if the client is no longer connected This matches WhatsApp Web behavior, which re-subscribes to all active presence subscriptions after reconnecting. ## Behavior Notes ### Available (Online) When setting status to `Available`, the library automatically: 1. Validates push name is set 2. Sends unified session (internal protocol requirement) 3. Broadcasts presence stanza with push name ### Unavailable (Offline) When setting status to `Unavailable`: 1. Validates push name is set 2. Broadcasts unavailable presence Note: This marks you as offline but doesn't disconnect the client. ## PresenceError The `set`, `set_available`, and `set_unavailable` methods return `Result<(), PresenceError>`: ```rust theme={null} #[derive(Debug, Error)] pub enum PresenceError { #[error("cannot send presence without a push name set")] PushNameEmpty, #[error(transparent)] Other(#[from] anyhow::Error), } ``` **Variants:** * `PushNameEmpty` - Push name must be set before sending presence * `Other` - Wraps any other error (network, connection, etc.) ## Error handling ```rust theme={null} use whatsapp_rust::features::presence::PresenceError; match client.presence().set_available().await { Ok(_) => println!("Successfully set to online"), Err(PresenceError::PushNameEmpty) => { eprintln!("Need to set push name first"); } Err(e) => eprintln!("Unexpected error: {}", e), } ``` The `subscribe` and `unsubscribe` methods return `Result<(), anyhow::Error>`. ## Complete Example ```rust theme={null} use whatsapp_rust::features::presence::PresenceStatus; // Set yourself online client.presence().set_available().await?; // Subscribe to contacts' presence let contacts: Vec = vec![ "15551111111@s.whatsapp.net".parse()?, "15552222222@s.whatsapp.net".parse()?, ]; for contact in &contacts { client.presence().subscribe(contact).await?; println!("Subscribed to {}", contact); } // Do work while online... // If the connection drops, tracked subscriptions are // automatically re-subscribed on reconnect. // Unsubscribe from a specific contact client.presence().unsubscribe(&contacts[0]).await?; println!("Unsubscribed from {}", contacts[0]); // Set yourself offline when done client.presence().set_unavailable().await?; ``` ## Receiving Presence Updates After subscribing to a contact's presence, you'll receive presence events through the event handler. See the [Events](/concepts/events) documentation for details on handling incoming presence updates. # Profile Source: https://whatsapp-rust.jlucaso.com/api/profile Manage your own profile - push name, status text, and profile picture The `Profile` feature provides methods for managing your own account's display name, status text (about), and profile picture. ## Access Access profile operations through the client (requires `Arc`): ```rust theme={null} let profile = client.profile(); ``` ## Methods ### set\_push\_name Set your display name (push name). ```rust theme={null} pub async fn set_push_name(&self, name: &str) -> Result<()> ``` **Parameters:** * `name` - The new display name (cannot be empty) Updates the local device store, sends a presence stanza with the new name, and propagates the change via app state sync for cross-device synchronization. **Example:** ```rust theme={null} client.profile().set_push_name("My Bot").await?; ``` The push name change takes effect immediately via presence, but app state sync may fail if keys aren't available yet (e.g., right after pairing before initial sync completes). ### set\_status\_text Set your status text (about). ```rust theme={null} pub async fn set_status_text(&self, text: &str) -> Result<()> ``` **Parameters:** * `text` - The new status text Sets the profile "About" text. This is different from ephemeral text status updates. **Example:** ```rust theme={null} client.profile().set_status_text("Available 24/7").await?; ``` ### set\_profile\_picture Set your profile picture. ```rust theme={null} pub async fn set_profile_picture( &self, image_data: Vec ) -> Result ``` **Parameters:** * `image_data` - JPEG image bytes **Returns:** * `SetProfilePictureResponse` - Contains the new picture ID The image should already be properly sized/cropped by the caller. WhatsApp typically uses 640x640 images. **Example:** ```rust theme={null} use std::fs; let image_bytes = fs::read("profile.jpg")?; let response = client.profile().set_profile_picture(image_bytes).await?; println!("New picture ID: {:?}", response.id); ``` The image must be a valid JPEG. Other formats are not supported. ### remove\_profile\_picture Remove your profile picture. ```rust theme={null} pub async fn remove_profile_picture(&self) -> Result ``` **Returns:** * `SetProfilePictureResponse` - Confirmation of removal **Example:** ```rust theme={null} client.profile().remove_profile_picture().await?; ``` ## Types ### SetProfilePictureResponse Response from profile picture operations. ```rust theme={null} pub struct SetProfilePictureResponse { /// The new picture ID (or None if removed) pub id: Option, } ``` ## Complete example ```rust theme={null} use whatsapp_rust::Client; use std::sync::Arc; use std::fs; async fn setup_profile(client: &Arc) -> anyhow::Result<()> { // Set display name client.profile().set_push_name("My WhatsApp Bot").await?; // Set status text client.profile().set_status_text("Automated responses").await?; // Set profile picture let image_bytes = fs::read("bot_avatar.jpg")?; let response = client.profile().set_profile_picture(image_bytes).await?; if let Some(id) = response.id { println!("Profile picture updated: {}", id); } Ok(()) } ``` ## Error handling All methods return `Result`. Common errors: * Empty push name * Invalid image format * Network errors * Not authenticated ```rust theme={null} match client.profile().set_push_name("").await { Ok(_) => println!("Name updated"), Err(e) => eprintln!("Failed: {}", e), // "Push name cannot be empty" } ``` ## See also * [Contacts](/api/contacts) - Get profile pictures for other users * [Presence](/api/presence) - Set online/offline status # receipt Source: https://whatsapp-rust.jlucaso.com/api/receipt Send read receipts, delivery receipts, and played receipts ## mark\_as\_read Send read receipts for one or more messages. Read receipts inform the sender that you've read their message(s). For group messages, you must pass the original sender's JID as the `sender` parameter. ```rust theme={null} pub async fn mark_as_read( &self, chat: &Jid, sender: Option<&Jid>, message_ids: Vec, ) -> Result<(), anyhow::Error> ``` Chat JID where the messages were received. Can be: * Direct message: `15551234567@s.whatsapp.net` * Group: `120363040237990503@g.us` Message sender JID. Required for group messages, `None` for direct messages. * For DMs: Pass `None` * For groups: Pass the JID of the user who sent the message(s) List of message IDs to mark as read. Can be a single ID or multiple IDs. If empty, this function returns immediately without sending anything. ### Example: Mark DM as Read ```rust theme={null} use wacore_binary::jid::Jid; let chat_jid: Jid = "15551234567@s.whatsapp.net".parse()?; client.mark_as_read( &chat_jid, None, // No sender for DMs vec!["MESSAGE_ID_123".to_string()] ).await?; ``` ### Example: Mark Group Message as Read ```rust theme={null} let group_jid: Jid = "120363040237990503@g.us".parse()?; let sender_jid: Jid = "15551234567@s.whatsapp.net".parse()?; client.mark_as_read( &group_jid, Some(&sender_jid), // Must specify sender in groups vec!["MESSAGE_ID_456".to_string()] ).await?; ``` ### Example: Mark Multiple Messages as Read ```rust theme={null} let message_ids = vec![ "MSG_1".to_string(), "MSG_2".to_string(), "MSG_3".to_string(), ]; client.mark_as_read(&chat_jid, None, message_ids).await?; ``` Read receipts are **not sent automatically** by the library. You must explicitly call `mark_as_read()` when you want to notify the sender that messages have been read. *** ## send\_delivery\_receipt (Internal) Sends a delivery receipt to the sender of a message. This is an internal method called automatically by the library when messages are received. You typically don't need to call this directly. ```rust theme={null} pub(crate) async fn send_delivery_receipt( &self, info: &MessageInfo ) ``` Message metadata containing: * `id` - Message ID * `source.chat` - Chat JID * `source.sender` - Sender JID * `source.is_from_me` - Whether this is your own message * `source.is_group` - Whether this is a group message ### Behavior Delivery receipts are automatically sent for all incoming messages **except**: * Your own messages (`is_from_me = true`) * Messages without an ID * Status broadcast messages (`status@broadcast`) For group messages, the receipt includes a `participant` attribute identifying the sender. Delivery receipts are sent automatically. Unlike other receipt types (e.g., `type="read"`, `type="played"`), delivery receipts have **no `type` attribute** on the wire — delivery is the implicit default. The library omits the `type` attribute from ack responses to delivery receipts accordingly, since including an explicit `type="delivery"` would cause `` disconnections from the server. This is different from read receipts (type=`"read"`), which you send manually with `mark_as_read()`. ### Wire format Internally, delivery receipts use `jid_attr()` for the `to` and `participant` attributes, storing JIDs directly without string conversion for better performance: ```rust theme={null} let mut builder = NodeBuilder::new("receipt") .attr("id", &info.id) .jid_attr("to", info.source.chat.clone()); if info.category == "peer" { builder = builder.attr("type", "peer_msg"); } if info.source.is_group { builder = builder.jid_attr("participant", info.source.sender.clone()); } let receipt_node = builder.build(); ``` Read receipts (`mark_as_read`) batch multiple message IDs using a `` child node with `` elements: ```rust theme={null} let mut builder = NodeBuilder::new("receipt") .attr("to", chat.to_string()) .attr("type", "read") .attr("id", &message_ids[0]) .attr("t", ×tamp); if let Some(sender) = sender { builder = builder.attr("participant", sender.to_string()); } // Additional message IDs beyond the first if message_ids.len() > 1 { let items: Vec = message_ids[1..] .iter() .map(|id| NodeBuilder::new("item").attr("id", id).build()) .collect(); builder = builder.children(vec![ NodeBuilder::new("list").children(items).build() ]); } ``` *** ## Receipt Types WhatsApp supports multiple receipt types: ```rust theme={null} pub enum ReceiptType { Delivered, // Message delivered to device Sender, // Sender receipt Retry, // Decryption retry request EncRekeyRetry, // VoIP call encryption re-keying retry Read, // Message read by recipient ReadSelf, // Message read on another device Played, // Media played by recipient PlayedSelf, // Media played on another device ServerError, // Server error Inactive, // Inactive participant PeerMsg, // Peer message HistorySync, // History sync Other(String), // Unknown receipt type } ``` Delivery receipt (type=`""`). Confirms message was delivered to the recipient's device. Sent automatically by the library. Read receipt (type=`"read"`). Confirms message was read by the recipient. Sent manually via `mark_as_read()`. Read receipt from your own device (type=`"read-self"`). Received when you read a message on another device. Played receipt (type=`"played"`). Confirms media (audio/video) was played by the recipient. Played receipt from your own device (type=`"played-self"`). Received when you play media on another device. Retry receipt (type=`"retry"`). Recipient failed to decrypt the message and is requesting a retry. Automatically handled by the library. VoIP call encryption re-keying retry receipt (type=`"enc_rekey_retry"`). Sent when a peer fails to decrypt VoIP call encryption data and needs the sender to re-key. Uses an `` child element (with `call-creator`, `call-id`, `count` attributes) instead of the standard `` child. Automatically handled by the library. Sender receipt (type=`"sender"`). Acknowledges message was sent. Server error receipt (type=`"server-error"`). Message delivery failed on server. *** ## Receipt Events You can listen for receipt events to track message delivery and read status using the Bot event handler: ```rust theme={null} use whatsapp_rust::types::events::Event; let mut bot = Bot::builder() .with_backend(backend) .with_transport_factory(transport_factory) .with_http_client(http_client) .on_event(|event, client| async move { if let Event::Receipt(receipt) = event { println!("Receipt type: {:?}", receipt.r#type); println!("From: {}", receipt.source.sender); println!("Message IDs: {:?}", receipt.message_ids); match receipt.r#type { ReceiptType::Delivered => { println!("Message delivered"); } ReceiptType::Read => { println!("Message read"); } ReceiptType::Played => { println!("Media played"); } _ => {} } } }) .build() .await?; ``` ### Receipt Event Structure ```rust theme={null} pub struct Receipt { pub message_ids: Vec, pub source: MessageSource, pub timestamp: DateTime, pub r#type: ReceiptType, pub message_sender: Jid, } ``` List of message IDs this receipt applies to. Usually contains a single ID, but can have multiple. Source information: * `chat` - Chat JID where the receipt originated * `sender` - JID of the user who sent the receipt * `is_group` - Whether this is from a group When the receipt was received (local time) Type of receipt (Delivered, Read, Played, etc.) JID of the user who sent the receipt (same as `source.sender`) *** ## Message Tracking Example Track message delivery and read status: ```rust theme={null} use std::collections::HashMap; use whatsapp_rust::types::events::{Event, ReceiptType}; #[derive(Default)] struct MessageTracker { delivered: HashMap, read: HashMap, } impl MessageTracker { fn track_receipt(&mut self, receipt: &Receipt) { for msg_id in &receipt.message_ids { match receipt.r#type { ReceiptType::Delivered => { self.delivered.insert(msg_id.clone(), true); } ReceiptType::Read => { self.read.insert(msg_id.clone(), true); } _ => {} } } } fn is_delivered(&self, msg_id: &str) -> bool { self.delivered.get(msg_id).copied().unwrap_or(false) } fn is_read(&self, msg_id: &str) -> bool { self.read.get(msg_id).copied().unwrap_or(false) } } let tracker = Arc::new(Mutex::new(MessageTracker::default())); // Use within Bot event handler let tracker_clone = tracker.clone(); let mut bot = Bot::builder() // ... configure backend, transport, http_client ... .on_event(move |event, _client| { let tracker = tracker_clone.clone(); async move { if let Event::Receipt(receipt) = event { tracker.lock().await.track_receipt(&receipt); } } }) .build() .await?; ``` *** ## Played Receipts (Media) For media messages (audio, video), you can send played receipts to indicate the media was played: The library does not currently expose a public API for sending played receipts. This feature may be added in a future version. Played receipts follow the same pattern as read receipts but use `type="played"`: ```xml theme={null} ``` You can listen for incoming played receipts via the `Event::Receipt` event with `ReceiptType::Played`. *** ## Best Practices ### Read Receipt Privacy Respect user privacy settings. If you're building a client, consider adding a setting to disable read receipts. ```rust theme={null} struct Settings { send_read_receipts: bool, } if settings.send_read_receipts { client.mark_as_read(&chat_jid, sender, message_ids).await?; } ``` ### Batching Multiple Receipts Send read receipts for multiple messages at once to reduce network overhead: ```rust theme={null} let mut pending_receipts: Vec = Vec::new(); // Collect message IDs pending_receipts.push(msg_id_1); pending_receipts.push(msg_id_2); pending_receipts.push(msg_id_3); // Send batch if !pending_receipts.is_empty() { client.mark_as_read(&chat_jid, None, pending_receipts).await?; } ``` ### Group Message Receipts Always include the sender JID for group messages: ```rust theme={null} if message_info.source.is_group { client.mark_as_read( &message_info.source.chat, Some(&message_info.source.sender), // Required for groups vec![message_info.id.clone()] ).await?; } else { client.mark_as_read( &message_info.source.chat, None, // No sender for DMs vec![message_info.id.clone()] ).await?; } ``` # send Source: https://whatsapp-rust.jlucaso.com/api/send Send messages, configure send options, and revoke messages ## send\_message Send a message to a chat. ```rust theme={null} pub async fn send_message( &self, to: Jid, message: wa::Message, ) -> Result ``` Recipient JID. Can be: * Direct message: `15551234567@s.whatsapp.net` * Group: `120363040237990503@g.us` * Newsletter: `120363999999999999@newsletter` Protobuf message to send. Set one of the message fields: * `conversation` - Plain text message * `extended_text_message` - Text with formatting/links * `image_message` - Image with caption * `video_message` - Video with caption * `document_message` - Document/file * `audio_message` - Audio/voice note * `sticker_message` - Sticker * `location_message` - GPS location * `contact_message` - Contact card Unique message ID generated for this message. Use this ID to track delivery receipts and edit/revoke the message. ### Example: Text Message ```rust theme={null} use waproto::whatsapp as wa; let message = wa::Message { conversation: Some("Hello, world!".to_string()), ..Default::default() }; let message_id = client.send_message( "15551234567@s.whatsapp.net".parse()?, message ).await?; println!("Message sent with ID: {}", message_id); ``` ### Example: Image with Caption ```rust theme={null} use waproto::whatsapp as wa; // First upload the image let upload_result = client.upload(image_bytes, MediaType::Image).await?; let message = wa::Message { image_message: Some(Box::new(wa::message::ImageMessage { url: Some(upload_result.url), direct_path: Some(upload_result.direct_path), media_key: Some(upload_result.media_key), file_enc_sha256: Some(upload_result.file_enc_sha256), file_sha256: Some(upload_result.file_sha256), file_length: Some(upload_result.file_length), caption: Some("Check out this image!".to_string()), mimetype: Some("image/jpeg".to_string()), ..Default::default() })), ..Default::default() }; let message_id = client.send_message(chat_jid, message).await?; ``` *** ## send\_message\_with\_options Send a message with additional customization options. ```rust theme={null} pub async fn send_message_with_options( &self, to: Jid, message: wa::Message, options: SendOptions, ) -> Result ``` Recipient JID Protobuf message to send Additional send options (see below) Unique message ID ### SendOptions Options for customizing message sending behavior. ```rust theme={null} pub struct SendOptions { /// Extra XML nodes to add to the message stanza pub extra_stanza_nodes: Vec, } ``` Additional XML nodes to include in the message stanza. Used for advanced protocol features like quoted replies, mentions, or custom metadata. ### Example: Send with Custom Options ```rust theme={null} use wacore_binary::builder::NodeBuilder; let options = SendOptions { extra_stanza_nodes: vec![ NodeBuilder::new("custom-tag") .attr("key", "value") .build() ], }; let message_id = client.send_message_with_options( chat_jid, message, options ).await?; ``` *** ## revoke\_message Delete a message for everyone in the chat (revoke). This sends a revoke protocol message that removes the message for all participants. The message will show as "This message was deleted" for recipients. ```rust theme={null} pub async fn revoke_message( &self, to: Jid, message_id: impl Into, revoke_type: RevokeType, ) -> Result<(), anyhow::Error> ``` Chat JID (direct message or group) ID of the message to delete (from `send_message` return value) Who is revoking the message: * `RevokeType::Sender` - Delete your own message * `RevokeType::Admin { original_sender }` - Admin deleting another user's message in a group ### RevokeType Specifies who is revoking (deleting) the message. ```rust theme={null} pub enum RevokeType { /// The message sender deleting their own message Sender, /// A group admin deleting another user's message /// `original_sender` is the JID of the user who sent the message Admin { original_sender: Jid }, } ``` Default variant. Use when deleting your own message. Works in both DMs and groups. Use when a group admin is deleting another user's message. Only valid in groups. Requires `original_sender` JID. ### Example: Revoke Own Message ```rust theme={null} // Send a message let message_id = client.send_message( chat_jid, message ).await?; // Delete it (sender revoke) client.revoke_message( chat_jid, &message_id, RevokeType::Sender ).await?; ``` ### Example: Admin Revoke in Group ```rust theme={null} use wacore_binary::jid::Jid; // Admin deleting another user's message let original_sender: Jid = "15551234567@s.whatsapp.net".parse()?; client.revoke_message( group_jid, &message_id, RevokeType::Admin { original_sender } ).await?; ``` Admin revoke is only valid for group chats. Attempting to use it in a direct message will return an error. *** ## Message Types The `wa::Message` protobuf supports various message types. Set exactly one of these fields: ### Text Messages Simple text message without formatting Text with formatting, links, quoted replies, or mentions Key fields: * `text` - Message text * `contextInfo` - Quoted message, mentions * `previewType` - Link preview behavior ### Media Messages Image with optional caption. Upload the image first using `client.upload()`, then populate: * `url`, `direct_path`, `media_key`, `file_enc_sha256`, `file_sha256`, `file_length` * `caption` - Image caption * `mimetype` - e.g., `"image/jpeg"` Video with optional caption. Same upload pattern as images. Audio file or voice note: * `ptt` - Set to `true` for voice notes (Push-To-Talk) * `mimetype` - e.g., `"audio/ogg; codecs=opus"` Document/file with metadata: * `file_name` - Original filename * `mimetype` - File MIME type * `caption` - Optional description Sticker image (WebP format) ### Other Messages GPS location with latitude, longitude, and optional name/address Contact card with vCard data Multiple contact cards Real-time location sharing Emoji reaction to another message ### Example: Extended Text with Quote ```rust theme={null} use waproto::whatsapp as wa; let message = wa::Message { extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage { text: Some("Reply to your message".to_string()), context_info: Some(wa::ContextInfo { stanza_id: Some(quoted_message_id), participant: Some(quoted_sender_jid.to_string()), quoted_message: Some(Box::new(quoted_message)), ..Default::default() }), ..Default::default() })), ..Default::default() }; ``` # Status Source: https://whatsapp-rust.jlucaso.com/api/status Post and manage WhatsApp status/story updates The `Status` feature provides APIs for posting text, image, and video status updates, as well as revoking previously sent statuses. ## Access Access status operations through the client: ```rust theme={null} let status = client.status(); ``` ## Methods ### send\_text Send a text status update with background color and font style. ```rust theme={null} pub async fn send_text( &self, text: &str, background_argb: u32, font: i32, recipients: Vec, options: StatusSendOptions, ) -> Result ``` **Parameters:** * `text` - Status text content * `background_argb` - Background color as ARGB (e.g., `0xFF1E6E4F`) * `font` - Font style index (0-4) * `recipients` - List of recipient JIDs * `options` - Privacy and delivery options **Returns:** * Message ID of the sent status **Example:** ```rust theme={null} use whatsapp_rust::{Jid, StatusSendOptions}; let recipients = vec![ Jid::pn("15551234567"), Jid::pn("15559876543"), ]; let message_id = client.status() .send_text( "Hello from Rust!", 0xFF1E6E4F, // Green background 0, // Default font recipients, StatusSendOptions::default(), ) .await?; println!("Status sent: {}", message_id); ``` ### send\_image Send an image status update. ```rust theme={null} pub async fn send_image( &self, upload: &UploadResponse, thumbnail: Vec, caption: Option<&str>, recipients: Vec, options: StatusSendOptions, ) -> Result ``` **Parameters:** * `upload` - Upload response from `client.upload()` * `thumbnail` - JPEG thumbnail bytes * `caption` - Optional caption text * `recipients` - List of recipient JIDs * `options` - Privacy options **Example:** ```rust theme={null} use wacore::download::MediaType; // Upload the image first let image_data = std::fs::read("photo.jpg")?; let upload = client.upload(image_data, MediaType::Image).await?; // Create thumbnail (simplified - use proper JPEG encoding) let thumbnail = create_thumbnail(&image_data)?; let recipients = vec![Jid::pn("15551234567")]; let message_id = client.status() .send_image( &upload, thumbnail, Some("Check this out!"), recipients, StatusSendOptions::default(), ) .await?; ``` ### send\_video Send a video status update. ```rust theme={null} pub async fn send_video( &self, upload: &UploadResponse, thumbnail: Vec, duration_seconds: u32, caption: Option<&str>, recipients: Vec, options: StatusSendOptions, ) -> Result ``` **Parameters:** * `upload` - Upload response from `client.upload()` * `thumbnail` - JPEG thumbnail bytes * `duration_seconds` - Video duration * `caption` - Optional caption text * `recipients` - List of recipient JIDs * `options` - Privacy options **Example:** ```rust theme={null} use wacore::download::MediaType; let video_data = std::fs::read("video.mp4")?; let upload = client.upload(video_data, MediaType::Video).await?; let thumbnail = extract_video_thumbnail(&video_data)?; let recipients = vec![Jid::pn("15551234567")]; let message_id = client.status() .send_video( &upload, thumbnail, 30, // 30 seconds None, recipients, StatusSendOptions::default(), ) .await?; ``` ### send\_raw Send a custom message type as a status update. ```rust theme={null} pub async fn send_raw( &self, message: wa::Message, recipients: Vec, options: StatusSendOptions, ) -> Result ``` **Example:** ```rust theme={null} use waproto::whatsapp as wa; let message = wa::Message { extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage { text: Some("Custom message".to_string()), ..Default::default() })), ..Default::default() }; let message_id = client.status() .send_raw(message, recipients, StatusSendOptions::default()) .await?; ``` ### revoke Delete a previously sent status update. ```rust theme={null} pub async fn revoke( &self, message_id: impl Into, recipients: Vec, options: StatusSendOptions, ) -> Result ``` **Parameters:** * `message_id` - ID of the status to revoke * `recipients` - Same recipients the status was sent to * `options` - Privacy options **Example:** ```rust theme={null} // Send a status let message_id = client.status() .send_text("Temporary status", 0xFF000000, 0, recipients.clone(), Default::default()) .await?; // Later, revoke it client.status() .revoke(message_id, recipients, Default::default()) .await?; ``` ## Types ### StatusPrivacySetting Privacy setting for status delivery. ```rust theme={null} pub enum StatusPrivacySetting { /// Send to all contacts in address book (default) Contacts, /// Send only to contacts in an allow list AllowList, /// Send to all contacts except those in a deny list DenyList, } ``` ### StatusSendOptions Options for sending status updates. ```rust theme={null} pub struct StatusSendOptions { /// Privacy setting for this status pub privacy: StatusPrivacySetting, } ``` **Example:** ```rust theme={null} use whatsapp_rust::{StatusSendOptions, StatusPrivacySetting}; // Send to all contacts let options = StatusSendOptions::default(); // Send to allow list only let options = StatusSendOptions { privacy: StatusPrivacySetting::AllowList, }; ``` ## Font styles WhatsApp Web supports 5 font styles (0-4): | Index | Style | | ----- | ---------------------- | | 0 | Default (sans-serif) | | 1 | Serif | | 2 | Typewriter (monospace) | | 3 | Bold script | | 4 | Condensed | ## Background colors Background colors use ARGB format (`0xAARRGGBB`): ```rust theme={null} // Solid green let green = 0xFF1E6E4F_u32; // Solid blue let blue = 0xFF1A73E8_u32; // Solid red let red = 0xFFD93025_u32; // Semi-transparent black let overlay = 0x80000000_u32; ``` ## Recipient management Recipients should be JIDs of users who can see the status: ```rust theme={null} // Single recipient let recipients = vec![Jid::pn("15551234567")]; // Multiple recipients let recipients = vec![ Jid::pn("15551234567"), Jid::pn("15559876543"), "15557654321@s.whatsapp.net".parse()?, ]; ``` The `recipients` list should match your privacy settings. When revoking a status, use the same recipients list that was used when posting. # Storage Traits Source: https://whatsapp-rust.jlucaso.com/api/store Storage backend traits and SQLite implementation for persistent state ## Overview whatsapp-rust uses a trait-based storage system to persist device state, cryptographic keys, and protocol metadata. The storage layer is split into four domain-specific traits: * **SignalStore** - Signal protocol cryptographic operations (identity keys, sessions, pre-keys, sender keys) * **AppSyncStore** - WhatsApp app state synchronization (sync keys, versions, mutation MACs) * **ProtocolStore** - WhatsApp protocol alignment (SKDM tracking, LID-PN mapping, device registry) * **DeviceStore** - Device persistence operations All four traits are combined into the `Backend` trait for convenience. ## The Backend Trait Any type implementing all four domain traits automatically implements `Backend`: ```rust theme={null} pub trait Backend: SignalStore + AppSyncStore + ProtocolStore + DeviceStore + Send + Sync {} impl Backend for T where T: SignalStore + AppSyncStore + ProtocolStore + DeviceStore + Send + Sync {} ``` ## SignalStore Trait Handles Signal protocol cryptographic storage for end-to-end encryption. ### Identity Operations ```rust theme={null} /// Store an identity key for a remote address async fn put_identity(&self, address: &str, key: [u8; 32]) -> Result<()>; /// Load an identity key for a remote address async fn load_identity(&self, address: &str) -> Result>>; /// Delete an identity key async fn delete_identity(&self, address: &str) -> Result<()>; ``` ### Session Operations ```rust theme={null} /// Get an encrypted session for an address async fn get_session(&self, address: &str) -> Result>>; /// Store an encrypted session async fn put_session(&self, address: &str, session: &[u8]) -> Result<()>; /// Delete a session async fn delete_session(&self, address: &str) -> Result<()>; /// Check if a session exists (default implementation uses get_session) async fn has_session(&self, address: &str) -> Result; ``` ### PreKey Operations ```rust theme={null} /// Store a pre-key async fn store_prekey(&self, id: u32, record: &[u8], uploaded: bool) -> Result<()>; /// Store multiple pre-keys in a single batch operation (default loops over store_prekey) async fn store_prekeys_batch(&self, keys: &[(u32, Vec)], uploaded: bool) -> Result<()>; /// Load a pre-key by ID async fn load_prekey(&self, id: u32) -> Result>>; /// Remove a pre-key async fn remove_prekey(&self, id: u32) -> Result<()>; /// Get the highest pre-key ID currently stored async fn get_max_prekey_id(&self) -> Result; ``` ### Signed PreKey Operations ```rust theme={null} /// Store a signed pre-key async fn store_signed_prekey(&self, id: u32, record: &[u8]) -> Result<()>; /// Load a signed pre-key by ID async fn load_signed_prekey(&self, id: u32) -> Result>>; /// Load all signed pre-keys (returns id, record pairs) async fn load_all_signed_prekeys(&self) -> Result)>>; /// Remove a signed pre-key async fn remove_signed_prekey(&self, id: u32) -> Result<()>; ``` ### Sender Key Operations For group messaging encryption: ```rust theme={null} /// Store a sender key for group messaging async fn put_sender_key(&self, address: &str, record: &[u8]) -> Result<()>; /// Get a sender key async fn get_sender_key(&self, address: &str) -> Result>>; /// Delete a sender key async fn delete_sender_key(&self, address: &str) -> Result<()>; ``` ## AppSyncStore Trait Handles WhatsApp app state synchronization storage. ### Sync Key Operations ```rust theme={null} /// Get an app state sync key by ID async fn get_sync_key(&self, key_id: &[u8]) -> Result>; /// Set an app state sync key async fn set_sync_key(&self, key_id: &[u8], key: AppStateSyncKey) -> Result<()>; /// Get the latest sync key ID async fn get_latest_sync_key_id(&self) -> Result>>; ``` ### Version Tracking ```rust theme={null} /// Get the app state version for a collection async fn get_version(&self, name: &str) -> Result; /// Set the app state version for a collection async fn set_version(&self, name: &str, state: HashState) -> Result<()>; ``` ### Mutation MAC Operations ```rust theme={null} /// Store mutation MACs for a version async fn put_mutation_macs( &self, name: &str, version: u64, mutations: &[AppStateMutationMAC], ) -> Result<()>; /// Get a mutation MAC by index async fn get_mutation_mac(&self, name: &str, index_mac: &[u8]) -> Result>>; /// Delete mutation MACs by their index MACs async fn delete_mutation_macs(&self, name: &str, index_macs: &[Vec]) -> Result<()>; ``` ## ProtocolStore Trait Handles WhatsApp protocol alignment and tracking. ### SKDM Tracking Tracks which devices have received Sender Key Distribution Messages in groups: ```rust theme={null} /// Get device JIDs that have received SKDM for a group async fn get_skdm_recipients(&self, group_jid: &str) -> Result>; /// Record devices that have received SKDM for a group async fn add_skdm_recipients(&self, group_jid: &str, device_jids: &[Jid]) -> Result<()>; /// Clear SKDM recipients for a group (call when sender key is rotated) async fn clear_skdm_recipients(&self, group_jid: &str) -> Result<()>; ``` ### LID-PN Mapping Manages mappings between LID (Locally Indexed Device) and phone numbers: ```rust theme={null} /// Get a mapping by LID async fn get_lid_mapping(&self, lid: &str) -> Result>; /// Get a mapping by phone number (returns the most recent LID) async fn get_pn_mapping(&self, phone: &str) -> Result>; /// Store or update a LID-PN mapping async fn put_lid_mapping(&self, entry: &LidPnMappingEntry) -> Result<()>; /// Get all LID-PN mappings (for cache warm-up) async fn get_all_lid_mappings(&self) -> Result>; ``` ### Base Key Collision Detection ```rust theme={null} /// Save the base key for a session address during retry collision detection async fn save_base_key(&self, address: &str, message_id: &str, base_key: &[u8]) -> Result<()>; /// Check if the current session has the same base key as the saved one async fn has_same_base_key( &self, address: &str, message_id: &str, current_base_key: &[u8], ) -> Result; /// Delete a base key entry async fn delete_base_key(&self, address: &str, message_id: &str) -> Result<()>; ``` ### Device Registry ```rust theme={null} /// Update the device list for a user (called after usync responses) async fn update_device_list(&self, record: DeviceListRecord) -> Result<()>; /// Get all known devices for a user async fn get_devices(&self, user: &str) -> Result>; ``` ### Sender Key Status Lazy deletion tracking for sender keys: ```rust theme={null} /// Mark a participant's sender key as needing regeneration for a group async fn mark_forget_sender_key(&self, group_jid: &str, participant: &str) -> Result<()>; /// Get participants that need fresh SKDM (marked for forget) /// Consumes the marks (deletes them after reading) async fn consume_forget_marks(&self, group_jid: &str) -> Result>; ``` ### TcToken Storage Trusted contact privacy tokens: ```rust theme={null} /// Get a trusted contact token for a JID (stored under LID) async fn get_tc_token(&self, jid: &str) -> Result>; /// Store or update a trusted contact token for a JID async fn put_tc_token(&self, jid: &str, entry: &TcTokenEntry) -> Result<()>; /// Delete a trusted contact token for a JID async fn delete_tc_token(&self, jid: &str) -> Result<()>; /// Get all JIDs that have stored tc tokens async fn get_all_tc_token_jids(&self) -> Result>; /// Delete tc tokens with token_timestamp older than cutoff (returns count deleted) async fn delete_expired_tc_tokens(&self, cutoff_timestamp: i64) -> Result; ``` ### Sent message store Persists sent message payloads for retry handling. Matches WhatsApp Web's `getMessageTable` pattern where retry receipts look up the original message from storage. ```rust theme={null} /// Store a sent message's serialized payload for retry handling. /// Called after each send_message(); the payload is the protobuf-encoded Message. async fn store_sent_message( &self, chat_jid: &str, message_id: &str, payload: &[u8], ) -> Result<()>; /// Retrieve and delete a sent message (atomic take). Returns serialized payload. /// Called when a retry receipt arrives; consuming prevents double-retry. async fn take_sent_message( &self, chat_jid: &str, message_id: &str, ) -> Result>>; /// Delete sent messages older than cutoff (unix timestamp seconds). /// Returns count deleted. async fn delete_expired_sent_messages( &self, cutoff_timestamp: i64, ) -> Result; ``` The `take_sent_message` method is an atomic read-and-delete operation. Once a message payload is taken for retry, it is removed from storage to prevent double-retry. For status broadcasts where multiple devices may retry, the client re-adds the message after taking it. ## DeviceStore Trait Handles device data persistence: ```rust theme={null} /// Save device data async fn save(&self, device: &Device) -> Result<()>; /// Load device data async fn load(&self) -> Result>; /// Check if a device exists async fn exists(&self) -> Result; /// Create a new device row and return its generated device_id async fn create(&self) -> Result; /// Create a snapshot of the database state /// Optional: label with name, save extra_content (e.g. failing message) async fn snapshot_db(&self, name: &str, extra_content: Option<&[u8]>) -> Result<()>; ``` ## SqliteStore implementation The default storage implementation using SQLite with Diesel ORM. SQLite is bundled by default — you don't need it installed on your system. ### Bundled SQLite The `whatsapp-rust-sqlite-storage` crate enables the `bundled-sqlite` feature by default, which compiles SQLite from source and statically links it. To use a system-installed SQLite instead: ```toml Cargo.toml theme={null} [dependencies] whatsapp-rust-sqlite-storage = { version = "0.3", default-features = false } ``` ### Creating a store ```rust theme={null} use whatsapp_rust::store::SqliteStore; // Basic usage - creates/opens database at path let store = SqliteStore::new("whatsapp.db").await?; // With device_id for multi-device support let store = SqliteStore::new_for_device("whatsapp.db", 1).await?; // Using sqlite:// URL format let store = SqliteStore::new("sqlite://path/to/db.sqlite").await?; ``` ### Features * **Connection pooling** - Uses Diesel r2d2 with pool size of 2 * **WAL mode** - Write-Ahead Logging for better concurrency * **Automatic migrations** - Runs embedded migrations on startup * **Semaphore-based locking** - Prevents concurrent writes * **Retry logic** - Automatic retry with exponential backoff for locked database * **Multi-device support** - Single database can store multiple device sessions ### Database Configuration SqliteStore automatically configures connections with: ```sql theme={null} PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 30000; PRAGMA synchronous = NORMAL; PRAGMA cache_size = 512; PRAGMA temp_store = memory; PRAGMA foreign_keys = ON; ``` ### Usage Example ```rust theme={null} use whatsapp_rust_sqlite_storage::SqliteStore; use whatsapp_rust::Bot; use std::sync::Arc; #[tokio::main] async fn main() -> Result<(), Box> { // Create store let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); // Use with Bot builder let mut bot = Bot::builder() .with_backend(backend.clone()) .with_transport_factory(transport_factory) .with_http_client(http_client) .on_event(|event, client| async move { /* handle events */ }) .build() .await?; // Store implements all traits // SignalStore backend.put_identity("address@s.whatsapp.net", [0u8; 32]).await?; let identity = backend.load_identity("address@s.whatsapp.net").await?; // AppSyncStore let version = backend.get_version("regular").await?; // ProtocolStore let devices = backend.get_skdm_recipients("group@g.us").await?; // DeviceStore if backend.exists().await? { let device = backend.load().await?; } Ok(()) } ``` ## CacheStore Trait The `CacheStore` trait enables pluggable cache backends for the client's data caches. By default, caches use in-process moka; implementing this trait lets you use Redis, Memcached, or any other external cache. **Location:** `wacore/src/store/cache.rs` ```rust theme={null} #[async_trait] pub trait CacheStore: Send + Sync + 'static { /// Retrieve a cached value by namespace and key. async fn get(&self, namespace: &str, key: &str) -> anyhow::Result>>; /// Store a value with an optional TTL. /// When `ttl` is `None`, the entry persists until explicitly deleted. async fn set( &self, namespace: &str, key: &str, value: &[u8], ttl: Option, ) -> anyhow::Result<()>; /// Delete a single key from the given namespace. async fn delete(&self, namespace: &str, key: &str) -> anyhow::Result<()>; /// Delete all keys in a namespace. async fn clear(&self, namespace: &str) -> anyhow::Result<()>; /// Approximate entry count (diagnostics only). Default returns 0. async fn entry_count(&self, _namespace: &str) -> anyhow::Result { Ok(0) } } ``` ### Namespaces Each logical cache uses a unique namespace string. Implementations should partition keys by namespace (e.g., prefix as `{namespace}:{key}` in Redis). | Namespace | Cache | Description | | ------------------- | ----------------------- | ----------------------------------- | | `"group"` | `group_cache` | Group metadata | | `"device"` | `device_cache` | Per-user device lists | | `"device_registry"` | `device_registry_cache` | Device registry entries | | `"lid_pn_by_lid"` | `lid_pn_cache` | LID-to-phone bidirectional mappings | ### Error handling Cache operations are best-effort. The client treats read failures as cache misses and logs warnings on write failures. Implementations should still return errors for observability. ### CacheStores configuration ```rust theme={null} pub struct CacheStores { pub group_cache: Option>, pub device_cache: Option>, pub device_registry_cache: Option>, pub lid_pn_cache: Option>, } ``` Set individual caches or use `CacheStores::all(store)` to route all pluggable caches to the same backend: ```rust theme={null} use whatsapp_rust::{CacheConfig, CacheStores}; let redis = Arc::new(MyRedisCacheStore::new("redis://localhost:6379")); let config = CacheConfig { cache_stores: CacheStores::all(redis), ..Default::default() }; ``` See [Custom backends — cache store](/guides/custom-backends#custom-cache-store) for a full implementation example. ## Implementing Custom Storage To implement a custom storage backend: 1. Implement all four domain traits 2. The `Backend` trait is automatically implemented 3. All methods must be `async` and thread-safe (`Send + Sync`) ### Example: Redis Store ```rust theme={null} use async_trait::async_trait; use redis::aio::ConnectionManager; use wacore::store::traits::*; use wacore::store::error::Result; pub struct RedisStore { client: ConnectionManager, device_id: i32, } impl RedisStore { pub async fn new(redis_url: &str) -> Result { let client = redis::Client::open(redis_url) .map_err(|e| StoreError::Connection(e.to_string()))?; let conn = client.get_connection_manager().await .map_err(|e| StoreError::Connection(e.to_string()))?; Ok(Self { client: conn, device_id: 1, }) } } #[async_trait] impl SignalStore for RedisStore { async fn put_identity(&self, address: &str, key: [u8; 32]) -> Result<()> { let mut conn = self.client.clone(); let key_name = format!("identity:{}:{}", self.device_id, address); redis::cmd("SET") .arg(key_name) .arg(&key[..]) .query_async(&mut conn) .await .map_err(|e| StoreError::Database(e.to_string()))?; Ok(()) } async fn load_identity(&self, address: &str) -> Result>> { let mut conn = self.client.clone(); let key_name = format!("identity:{}:{}", self.device_id, address); let result: Option> = redis::cmd("GET") .arg(key_name) .query_async(&mut conn) .await .map_err(|e| StoreError::Database(e.to_string()))?; Ok(result) } // Implement remaining SignalStore methods... } #[async_trait] impl AppSyncStore for RedisStore { // Implement all AppSyncStore methods... } #[async_trait] impl ProtocolStore for RedisStore { // Implement all ProtocolStore methods... } #[async_trait] impl DeviceStore for RedisStore { // Implement all DeviceStore methods... } // Backend is automatically implemented! ``` ### Best Practices 1. **Thread Safety** - Use `Arc` for shared state, `Mutex` for mutable state 2. **Error Handling** - Convert backend errors to `StoreError` variants 3. **Transactions** - Use database transactions for atomic operations 4. **Retries** - Implement retry logic for transient failures 5. **Connection Pooling** - Reuse connections when possible 6. **Blocking Operations** - Wrap blocking I/O in `tokio::task::spawn_blocking` ## Data Structures ### AppStateSyncKey ```rust theme={null} pub struct AppStateSyncKey { pub key_data: Vec, pub fingerprint: Vec, pub timestamp: i64, } ``` ### LidPnMappingEntry ```rust theme={null} pub struct LidPnMappingEntry { pub lid: String, // LID user part pub phone_number: String, // Phone number user part pub created_at: i64, // Unix timestamp pub updated_at: i64, // Unix timestamp pub learning_source: String, // e.g. "usync", "peer_pn_message" } ``` ### TcTokenEntry ```rust theme={null} pub struct TcTokenEntry { pub token: Vec, // Raw token bytes pub token_timestamp: i64, // When token was received pub sender_timestamp: Option, // When we sent our token } ``` ### DeviceListRecord ```rust theme={null} pub struct DeviceListRecord { pub user: String, // User part of JID pub devices: Vec, // Known devices pub timestamp: i64, // Last update timestamp pub phash: Option, // Participant hash from usync } pub struct DeviceInfo { pub device_id: u32, // 0 = primary, 1+ = companions pub key_index: Option, // Key index if known } ``` ## Error Handling All storage operations return `Result` from `wacore::store::error`: ```rust theme={null} pub enum StoreError { Connection(String), // Connection failures Database(String), // Database operation errors Migration(String), // Migration errors Serialization(String), // Serialization/deserialization errors NotFound, // Resource not found } pub type Result = std::result::Result; ``` ## See Also * [Transport Trait](/api/transport) - Network transport abstraction * [HTTP Client Trait](/api/http-client) - HTTP client abstraction * [Client API](/api/client) - Main client interface # TC Token Source: https://whatsapp-rust.jlucaso.com/api/tctoken Manage trusted contact privacy tokens The `TcToken` feature provides APIs for issuing and managing trusted contact privacy tokens (TC tokens). These tokens are used for privacy-gated operations like fetching profile pictures. ## Access Access TC token operations through the client: ```rust theme={null} let tc_token = client.tc_token(); ``` ## Methods ### issue\_tokens Issue privacy tokens for specified contacts. ```rust theme={null} pub async fn issue_tokens(&self, jids: &[Jid]) -> Result, IqError> ``` **Parameters:** * `jids` - Array of JIDs (should be LID JIDs) to issue tokens for **Returns:** * `Vec` - List of received tokens **Example:** ```rust theme={null} let jids = vec![ "100000000000001@lid".parse()?, "100000000000002@lid".parse()?, ]; let tokens = client.tc_token().issue_tokens(&jids).await?; for token in &tokens { println!("Token for {}: {} bytes", token.jid, token.token.len()); println!("Timestamp: {}", token.timestamp); } ``` Issued tokens are automatically stored in the backend and used for subsequent operations like profile picture fetching. ### prune\_expired Remove expired tokens from storage. ```rust theme={null} pub async fn prune_expired(&self) -> Result ``` **Returns:** * Number of tokens deleted **Example:** ```rust theme={null} let deleted = client.tc_token().prune_expired().await?; println!("Pruned {} expired tokens", deleted); ``` Tokens expire after 28 days. Call this periodically to clean up storage. ### get Get a stored token for a specific JID. ```rust theme={null} pub async fn get(&self, jid: &str) -> Result, anyhow::Error> ``` **Parameters:** * `jid` - User portion of the JID (without domain) **Returns:** * `Option` - The stored token entry, if found **Example:** ```rust theme={null} if let Some(entry) = client.tc_token().get("100000000000001").await? { println!("Token timestamp: {}", entry.token_timestamp); println!("Token size: {} bytes", entry.token.len()); if let Some(sender_ts) = entry.sender_timestamp { println!("Issued at: {}", sender_ts); } } ``` ### get\_all\_jids Get all JIDs that have stored tokens. ```rust theme={null} pub async fn get_all_jids(&self) -> Result, anyhow::Error> ``` **Returns:** * List of JID user portions with stored tokens **Example:** ```rust theme={null} let jids = client.tc_token().get_all_jids().await?; println!("Tokens stored for {} contacts", jids.len()); for jid in jids { println!(" - {}", jid); } ``` ## Types ### ReceivedTcToken Token received from the server. ```rust theme={null} pub struct ReceivedTcToken { /// JID the token is for pub jid: Jid, /// Binary token data pub token: Vec, /// Server timestamp pub timestamp: i64, } ``` ### TcTokenEntry Stored token entry. ```rust theme={null} pub struct TcTokenEntry { /// Binary token data pub token: Vec, /// Token timestamp from server pub token_timestamp: i64, /// Timestamp when we issued/received this token pub sender_timestamp: Option, } ``` ## Automatic usage The library automatically uses TC tokens when making privacy-gated requests: ```rust theme={null} // TC tokens are automatically included when fetching profile pictures let picture = client.contacts().get_profile_picture(&jid, true).await?; ``` You typically don't need to manage tokens manually unless: * Pre-issuing tokens for a batch of contacts * Pruning expired tokens to save storage * Debugging token-related issues ## Token lifecycle ```mermaid theme={null} sequenceDiagram participant App participant Client participant Backend participant Server App->>Client: issue_tokens([jid1, jid2]) Client->>Server: IQ request Server-->>Client: Tokens with timestamps Client->>Backend: Store tokens Client-->>App: Vec Note over Client,Backend: Later, when fetching profile... App->>Client: get_profile_picture(jid1) Client->>Backend: Get token for jid1 Backend-->>Client: TcTokenEntry Client->>Server: Request with token Server-->>Client: Profile picture Client-->>App: ProfilePicture ``` ## Expiration TC tokens expire after 28 days. The library provides utilities for managing expiration: ```rust theme={null} use wacore::iq::tctoken::tc_token_expiration_cutoff; // Get the cutoff timestamp for expired tokens let cutoff = tc_token_expiration_cutoff(); println!("Tokens before {} are expired", cutoff); // Prune expired tokens let deleted = client.tc_token().prune_expired().await?; ``` ## Best practices 1. **Pre-issue tokens** for contacts you frequently interact with 2. **Prune periodically** to keep storage clean 3. **Don't over-issue** - tokens are automatically issued when needed 4. **Use LID JIDs** when issuing tokens for privacy features ```rust theme={null} // Good: Issue tokens for frequent contacts on startup async fn initialize_tokens(client: &Client, contacts: &[Jid]) -> anyhow::Result<()> { // Filter to LID JIDs only let lid_jids: Vec<_> = contacts.iter() .filter(|j| j.is_lid()) .cloned() .collect(); if !lid_jids.is_empty() { client.tc_token().issue_tokens(&lid_jids).await?; } // Prune old tokens client.tc_token().prune_expired().await?; Ok(()) } ``` # Transport Trait Source: https://whatsapp-rust.jlucaso.com/api/transport Network transport abstraction and WebSocket implementation ## Overview The transport layer provides a runtime-agnostic abstraction for network connections. It handles raw byte transmission without knowledge of WhatsApp's protocol framing. The transport system consists of two main traits: * **Transport** - Represents an active connection for sending/receiving raw bytes * **TransportFactory** - Creates new transport instances and event streams ## Transport Trait The `Transport` trait represents an active network connection as a simple byte pipe. ```rust theme={null} use async_trait::async_trait; #[async_trait] pub trait Transport: Send + Sync { /// Sends raw data to the server async fn send(&self, data: Vec) -> Result<(), anyhow::Error>; /// Closes the connection async fn disconnect(&self); } ``` ### Methods #### send Sends raw bytes through the transport. The caller is responsible for any protocol framing. ```rust theme={null} async fn send(&self, data: Vec) -> Result<(), anyhow::Error>; ``` **Parameters:** * `data` - Raw bytes to send **Returns:** * `Ok(())` on success * `Err(anyhow::Error)` on failure **Example:** ```rust theme={null} let data = vec![1, 2, 3, 4]; transport.send(data).await?; ``` #### disconnect Gracefully closes the connection. ```rust theme={null} async fn disconnect(&self); ``` **Example:** ```rust theme={null} transport.disconnect().await; ``` ## TransportFactory Trait Creates new transport instances and associated event streams. ```rust theme={null} #[async_trait] pub trait TransportFactory: Send + Sync { /// Creates a new transport and returns it along with a stream of events async fn create_transport( &self, ) -> Result<(Arc, async_channel::Receiver), anyhow::Error>; } ``` ### Methods #### create\_transport Establishes a new connection and returns both the transport handle and an event receiver. ```rust theme={null} async fn create_transport( &self, ) -> Result<(Arc, async_channel::Receiver), anyhow::Error>; ``` **Returns:** * `Arc` - The transport instance for sending data * `async_channel::Receiver` - Stream of transport events **Example:** ```rust theme={null} let factory = TokioWebSocketTransportFactory::new(); let (transport, events) = factory.create_transport().await?; // Use transport to send data transport.send(data).await?; // Listen for events while let Ok(event) = events.recv().await { match event { TransportEvent::Connected => println!("Connected"), TransportEvent::DataReceived(bytes) => println!("Received {} bytes", bytes.len()), TransportEvent::Disconnected => break, } } ``` ## TransportEvent Events produced by the transport layer: ```rust theme={null} pub enum TransportEvent { /// The transport has successfully connected Connected, /// Raw data has been received from the server DataReceived(Bytes), /// The connection was lost Disconnected, } ``` ### Event Types #### Connected Emitted immediately after successful connection establishment. ```rust theme={null} TransportEvent::Connected ``` #### DataReceived Emitted when raw data is received from the server. ```rust theme={null} TransportEvent::DataReceived(bytes) ``` **Fields:** * `bytes: Bytes` - Raw data received (from the `bytes` crate) #### Disconnected Emitted when the connection is closed (gracefully or due to error). ```rust theme={null} TransportEvent::Disconnected ``` ## TokioWebSocketTransport The default implementation using tokio-websockets for async WebSocket connections. ### Features * **Async I/O** - Built on Tokio runtime * **TLS Support** - Uses rustls with webpki-roots for certificate validation * **Split Architecture** - Separate read/write paths for efficiency * **Automatic Reconnection** - Handled by higher-level Client code * **Development Mode** - Optional `danger-skip-tls-verify` feature ### Creating a Transport Factory ```rust theme={null} use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; // Default - connects to WhatsApp Web let factory = TokioWebSocketTransportFactory::new(); ``` ### Usage with Bot builder ```rust theme={null} use whatsapp_rust::Bot; use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; #[tokio::main] async fn main() -> Result<(), Box> { let factory = TokioWebSocketTransportFactory::new(); let mut bot = Bot::builder() .with_backend(backend) .with_transport_factory(factory) .with_http_client(http_client) .on_event(|event, client| async move { /* handle events */ }) .build() .await?; Ok(()) } ``` ### TLS Configuration By default, TokioWebSocketTransport validates TLS certificates using webpki-roots: ```rust theme={null} let mut root_store = rustls::RootCertStore::empty(); root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); let config = rustls::ClientConfig::builder() .with_root_certificates(root_store) .with_no_client_auth(); ``` ### Development Mode (Skip TLS Verification) **WARNING: Only use for development/testing!** Enable the `danger-skip-tls-verify` feature to disable certificate verification: ```toml theme={null} [dependencies] whatsapp-rust-tokio-transport = { version = "*", features = ["danger-skip-tls-verify"] } ``` This allows connecting through MITM proxies or self-signed certificates. ### Connection Settings The default WebSocket URL is: ```rust theme={null} pub const WHATSAPP_WEB_WS_URL: &str = "wss://web.whatsapp.com/ws/chat"; ``` The `Client` wraps `create_transport()` in a **20-second timeout** matching WhatsApp Web's connect timeout. The transport itself does not enforce this timeout — it is applied at the client layer. If you use a custom transport, be aware that the client will abort the connection attempt after 20 seconds regardless of the transport's own timeout behavior. ### Internal Architecture TokioWebSocketTransport splits the WebSocket into separate read/write paths: ```rust theme={null} let (sink, stream) = websocket.split(); // Sink - wrapped in Arc for sending let ws_sink: Arc>> = Arc::new(Mutex::new(Some(sink))); // Stream - moved to read_pump task tokio::task::spawn(read_pump(stream, event_tx)); ``` The read pump continuously processes incoming messages: ```rust theme={null} async fn read_pump(mut stream: WsStream, event_tx: async_channel::Sender) { loop { match stream.next().await { Some(Ok(msg)) if msg.is_binary() => { let payload = msg.into_payload(); event_tx.send(TransportEvent::DataReceived(Bytes::from(payload))).await; } Some(Ok(msg)) if msg.is_close() => break, Some(Err(e)) => break, None => break, } } event_tx.send(TransportEvent::Disconnected).await; } ``` ## Implementing Custom Transports You can implement custom transports for different runtimes or protocols. ### Example: Mock Transport for Testing ```rust theme={null} use async_trait::async_trait; use std::sync::Arc; use wacore::net::{Transport, TransportEvent, TransportFactory}; /// A mock transport that does nothing pub struct MockTransport; #[async_trait] impl Transport for MockTransport { async fn send(&self, _data: Vec) -> Result<(), anyhow::Error> { // Silently succeed Ok(()) } async fn disconnect(&self) { // Nothing to do } } /// Factory for creating mock transports pub struct MockTransportFactory; impl MockTransportFactory { pub fn new() -> Self { Self } } #[async_trait] impl TransportFactory for MockTransportFactory { async fn create_transport( &self, ) -> Result<(Arc, async_channel::Receiver), anyhow::Error> { let (_tx, rx) = async_channel::bounded(1); Ok((Arc::new(MockTransport), rx)) } } ``` ### Example: TCP Transport (No TLS) ```rust theme={null} use async_trait::async_trait; use tokio::net::TcpStream; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use std::sync::Arc; use tokio::sync::Mutex; pub struct TcpTransport { writer: Arc>>, } #[async_trait] impl Transport for TcpTransport { async fn send(&self, data: Vec) -> Result<(), anyhow::Error> { let mut writer = self.writer.lock().await; writer.write_all(&data).await?; Ok(()) } async fn disconnect(&self) { // TCP disconnect handled by drop } } pub struct TcpTransportFactory { address: String, } impl TcpTransportFactory { pub fn new(address: impl Into) -> Self { Self { address: address.into(), } } } #[async_trait] impl TransportFactory for TcpTransportFactory { async fn create_transport( &self, ) -> Result<(Arc, async_channel::Receiver), anyhow::Error> { let stream = TcpStream::connect(&self.address).await?; let (reader, writer) = tokio::io::split(stream); let (event_tx, event_rx) = async_channel::bounded(100); let transport = Arc::new(TcpTransport { writer: Arc::new(Mutex::new(writer)), }); // Spawn read task tokio::task::spawn(async move { let mut reader = reader; let mut buf = vec![0u8; 4096]; event_tx.send(TransportEvent::Connected).await.ok(); loop { match reader.read(&mut buf).await { Ok(0) => break, Ok(n) => { let data = bytes::Bytes::copy_from_slice(&buf[..n]); if event_tx.send(TransportEvent::DataReceived(data)).await.is_err() { break; } } Err(_) => break, } } event_tx.send(TransportEvent::Disconnected).await.ok(); }); Ok((transport, event_rx)) } } ``` ### Best Practices 1. **Thread Safety** - Transport must be `Send + Sync` 2. **Error Handling** - Return descriptive errors from `send()` 3. **Graceful Shutdown** - Implement proper cleanup in `disconnect()` 4. **Event Channel Size** - Use bounded channels with reasonable capacity 5. **Read Task** - Spawn a separate task for receiving data 6. **Resource Cleanup** - Ensure sockets/resources are closed on drop ## Testing Transports ### Unit Test Example ```rust theme={null} #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_mock_transport() { let factory = MockTransportFactory::new(); let (transport, events) = factory.create_transport().await.unwrap(); // Should succeed without error transport.send(vec![1, 2, 3]).await.unwrap(); // Disconnect should be no-op transport.disconnect().await; } #[tokio::test] async fn test_websocket_transport() { let factory = TokioWebSocketTransportFactory::new(); let (transport, mut events) = factory.create_transport().await.unwrap(); // Should receive Connected event match events.recv().await { Ok(TransportEvent::Connected) => {}, other => panic!("Expected Connected, got {:?}", other), } transport.disconnect().await; } } ``` ## See Also * [Storage Traits](/api/store) - Storage backend abstraction * [HTTP Client Trait](/api/http-client) - HTTP client abstraction * [Client API](/api/client) - Main client interface # upload Source: https://whatsapp-rust.jlucaso.com/api/upload Upload and encrypt media for sending in messages ## upload Upload media to WhatsApp's CDN with automatic encryption. ```rust theme={null} pub async fn upload( &self, data: Vec, media_type: MediaType ) -> Result ``` Raw media bytes to upload. The data is automatically encrypted using AES-256-CBC before uploading. Type of media being uploaded. Determines the CDN endpoint and encryption keys: * `MediaType::Image` - Images and stickers * `MediaType::Video` - Video files * `MediaType::Audio` - Audio files and voice notes * `MediaType::Document` - Documents and other files * `MediaType::Sticker` - Sticker images * `MediaType::LinkThumbnail` - Link preview thumbnails Contains all metadata needed to include the media in a message: ```rust theme={null} pub struct UploadResponse { pub url: String, pub direct_path: String, pub media_key: Vec, pub file_enc_sha256: Vec, pub file_sha256: Vec, pub file_length: u64, } ``` Full CDN URL where the encrypted file was uploaded CDN path component (e.g., `/v/t62.7118-24/12345_67890`). Used for downloads. 32-byte encryption key. Required for the recipient to decrypt the media. SHA-256 hash of the encrypted file. Used for integrity verification during download. SHA-256 hash of the original (decrypted) file. Used for final validation after decryption. Original file size in bytes (before encryption) ### Example: Upload and Send Image ```rust theme={null} use wacore::download::MediaType; use waproto::whatsapp as wa; use std::fs; // Read image file let image_bytes = fs::read("photo.jpg")?; // Upload image to WhatsApp CDN let upload = client.upload(image_bytes, MediaType::Image).await?; // Create image message with upload metadata let message = wa::Message { image_message: Some(Box::new(wa::message::ImageMessage { url: Some(upload.url), direct_path: Some(upload.direct_path), media_key: Some(upload.media_key), file_enc_sha256: Some(upload.file_enc_sha256), file_sha256: Some(upload.file_sha256), file_length: Some(upload.file_length), caption: Some("Check out this photo!".to_string()), mimetype: Some("image/jpeg".to_string()), ..Default::default() })), ..Default::default() }; // Send the message let message_id = client.send_message(chat_jid, message).await?; println!("Image sent: {}", message_id); ``` ### Example: Upload Video with Progress ```rust theme={null} use wacore::download::MediaType; use std::fs; let video_bytes = fs::read("video.mp4")?; println!("Uploading {} bytes...", video_bytes.len()); let upload = client.upload(video_bytes, MediaType::Video).await?; println!("Upload complete!"); println!(" URL: {}", upload.url); println!(" Path: {}", upload.direct_path); // Use upload metadata in VideoMessage... ``` ### Example: Upload Document ```rust theme={null} use wacore::download::MediaType; use waproto::whatsapp as wa; use std::fs; use std::path::Path; let doc_path = Path::new("report.pdf"); let doc_bytes = fs::read(doc_path)?; let filename = doc_path.file_name() .and_then(|n| n.to_str()) .unwrap_or("document.pdf"); let upload = client.upload(doc_bytes, MediaType::Document).await?; let message = wa::Message { document_message: Some(Box::new(wa::message::DocumentMessage { url: Some(upload.url), direct_path: Some(upload.direct_path), media_key: Some(upload.media_key), file_enc_sha256: Some(upload.file_enc_sha256), file_sha256: Some(upload.file_sha256), file_length: Some(upload.file_length), file_name: Some(filename.to_string()), mimetype: Some("application/pdf".to_string()), ..Default::default() })), ..Default::default() }; let message_id = client.send_message(chat_jid, message).await?; ``` ### Example: Upload Audio (Voice Note) ```rust theme={null} use wacore::download::MediaType; use waproto::whatsapp as wa; let audio_bytes = fs::read("voice.ogg")?; let upload = client.upload(audio_bytes, MediaType::Audio).await?; let message = wa::Message { audio_message: Some(Box::new(wa::message::AudioMessage { url: Some(upload.url), direct_path: Some(upload.direct_path), media_key: Some(upload.media_key), file_enc_sha256: Some(upload.file_enc_sha256), file_sha256: Some(upload.file_sha256), file_length: Some(upload.file_length), mimetype: Some("audio/ogg; codecs=opus".to_string()), ptt: Some(true), // Mark as Push-To-Talk (voice note) ..Default::default() })), ..Default::default() }; let message_id = client.send_message(chat_jid, message).await?; ``` *** ## Resumable uploads For files 5 MiB (5,242,880 bytes) or larger, the client automatically probes the CDN to check for a previous partial or complete upload of the same file before sending the full payload. ### Resume check flow 1. A `POST` request is sent to the upload URL with `?resume=1` appended 2. The server responds with one of three states: * **Complete** — the file already exists on the CDN. The existing `url` and `direct_path` are returned immediately with no upload * **Resume** — a partial upload exists. The client resumes from the given `byte_offset`, appending `&file_offset={offset}` to the URL and sending only the remaining bytes * **Not found** — no previous upload exists. A full upload proceeds normally The resume check is non-fatal. If the probe request itself fails (network error, unexpected response), the client silently falls back to a full upload. No special handling is needed in your code. ### Resume check endpoint ``` POST https://{media_host}/mms/{type}/{token}?auth={auth}&token={token}&resume=1 Origin: https://web.whatsapp.com ``` ### Resumed upload endpoint When a partial upload is detected at byte offset `N`: ``` POST https://{media_host}/mms/{type}/{token}?auth={auth}&token={token}&file_offset=N Content-Type: application/octet-stream Body: [encrypted bytes from offset N onward] ``` *** ## Media Encryption All media uploaded to WhatsApp is end-to-end encrypted before transmission: ### Encryption Process 1. **Generate keys**: Create random 32-byte `media_key` 2. **Derive keys**: Use HKDF-SHA256 with media type info string to derive: * 16-byte IV (initialization vector) * 32-byte cipher key * 32-byte MAC key 3. **Encrypt**: Apply AES-256-CBC with PKCS7 padding 4. **Compute MAC**: HMAC-SHA256 over IV + ciphertext, append first 10 bytes 5. **Upload**: POST encrypted bytes to WhatsApp CDN 6. **Return metadata**: `media_key`, hashes, and CDN path for message ### Key Derivation Each media type uses a specific HKDF info string: ```rust theme={null} MediaType::Image → "WhatsApp Image Keys" MediaType::Video → "WhatsApp Video Keys" MediaType::Audio → "WhatsApp Audio Keys" MediaType::Document → "WhatsApp Document Keys" MediaType::Sticker → "WhatsApp Image Keys" MediaType::LinkThumbnail → "WhatsApp Link Thumbnail Keys" ``` The `media_key` is shared with recipients through the encrypted message, allowing them to decrypt the media. ### Encryption vs Decryption The encryption process is the inverse of download decryption: | Upload (Encryption) | Download (Decryption) | | ------------------------------ | --------------------------------- | | Generate `media_key` | Receive `media_key` in message | | Derive IV, cipher key, MAC key | Derive same keys from `media_key` | | Encrypt with AES-256-CBC | Decrypt with AES-256-CBC | | Append HMAC-SHA256 (10 bytes) | Verify HMAC-SHA256 | | Upload to CDN | Download from CDN | *** ## MediaType Specifies the type of media for encryption and CDN routing. ```rust theme={null} pub enum MediaType { Image, Video, Audio, Document, History, AppState, Sticker, StickerPack, LinkThumbnail, } ``` JPEG, PNG, or other image formats. Uses `"image"` MMS endpoint and `"WhatsApp Image Keys"` for HKDF. MP4 or other video formats. Uses `"video"` MMS endpoint and `"WhatsApp Video Keys"` for HKDF. Audio files and voice notes. Uses `"audio"` MMS endpoint and `"WhatsApp Audio Keys"` for HKDF. PDF, DOCX, ZIP, and other document formats. Uses `"document"` MMS endpoint and `"WhatsApp Document Keys"` for HKDF. Sticker images (WebP format). Uses `"image"` MMS endpoint and `"WhatsApp Image Keys"` for HKDF (same as images). History sync data. Uses `"md-msg-hist"` MMS endpoint and `"WhatsApp History Keys"` for HKDF. App state sync data. Uses `"md-app-state"` MMS endpoint and `"WhatsApp App State Keys"` for HKDF. Sticker pack metadata. Uses `"sticker-pack"` MMS endpoint. Link preview thumbnails. Uses `"thumbnail-link"` MMS endpoint. *** ## Upload Endpoint The upload endpoint is constructed as: ``` https://{media_host}/mms/{mms_type}/{token}?auth={auth}&token={token} ``` Where: * `{media_host}` — CDN hostname from media connection (primary hosts tried first) * `{mms_type}` — Media type endpoint (e.g., `"image"`, `"video"`, `"document"`) * `{token}` — Base64url-encoded `file_enc_sha256` * `{auth}` — Media connection auth token Optional query parameters (added automatically for resumable uploads): * `resume=1` — probe for existing upload (resume check request) * `file_offset={N}` — resume upload from byte offset `N` The request uses: * **Method**: `POST` * **Content-Type**: `application/octet-stream` * **Origin**: `https://web.whatsapp.com` * **Body**: Encrypted media bytes (or remaining bytes when resuming) The upload automatically handles media connection refresh. If the connection is expired, it's renewed before uploading. If the CDN returns HTTP 401 or 403, the client invalidates the cached credentials, fetches a fresh auth token, and retries the upload once. *** ## Error Handling ### Automatic retry on auth errors The upload method automatically retries when WhatsApp's CDN returns HTTP 401 or 403. On an auth error, the client invalidates the cached media connection, fetches fresh credentials from WhatsApp's servers, and retries the upload once. If the retry also fails with an auth error, the error is returned to the caller. For non-auth HTTP errors (such as 500), the client tries the next available CDN host without refreshing credentials. Hosts are tried in priority order — primary hosts first, then fallback hosts. ### Common upload errors CDN returned error status. Auth errors (401/403) are retried automatically with fresh credentials. Other errors are tried against alternate CDN hosts. ```rust theme={null} match client.upload(data, media_type).await { Err(e) if e.to_string().contains("Upload failed") => { eprintln!("CDN error: {}", e); // Retry or use fallback } Err(e) => eprintln!("Other error: {}", e), Ok(upload) => { /* Success */ } } ``` Media connection has no available CDN hosts. ```rust theme={null} Err(anyhow!("No media hosts")) ``` Failed to encrypt media (rare, usually indicates invalid input). ```rust theme={null} match client.upload(data, media_type).await { Err(e) if e.to_string().contains("encrypt") => { eprintln!("Encryption error: {}", e); } _ => {} } ``` ### Example: Upload with additional retry Auth errors (401/403) are retried automatically. For other transient failures, you can add your own retry logic: ```rust theme={null} use std::time::Duration; use tokio::time::sleep; let mut attempts = 0; let max_attempts = 3; let upload = loop { attempts += 1; match client.upload(data.clone(), MediaType::Image).await { Ok(upload) => break upload, Err(e) if attempts < max_attempts => { eprintln!("Upload attempt {} failed: {}", attempts, e); sleep(Duration::from_secs(2)).await; continue; } Err(e) => return Err(e), } }; println!("Upload succeeded after {} attempt(s)", attempts); ``` # wacore Source: https://whatsapp-rust.jlucaso.com/api/wacore Platform-agnostic WhatsApp protocol implementation ## Overview `wacore` is the core WhatsApp protocol implementation for whatsapp-rust. It's designed to be **platform-agnostic** with no runtime dependencies on Tokio or specific databases, making it portable across different async runtimes and storage backends. ```toml theme={null} [dependencies] wacore = "0.3" ``` ## Philosophy wacore contains all the core logic for: * Binary protocol encoding/decoding * Cryptographic primitives (AES-GCM, Signal Protocol) * IQ protocol types and specifications * State management traits * Message builders and parsers It has **no runtime dependencies** on Tokio or databases. The main `whatsapp-rust` crate integrates wacore with Tokio for async operations and Diesel for persistence. ## Key Exports ### Re-exported Crates ```rust theme={null} pub use aes_gcm; // AES-GCM encryption pub use wacore_appstate as appstate; // App state sync pub use wacore_noise as noise; // Noise Protocol pub use wacore_libsignal as libsignal; // Signal Protocol ``` ### Derive Macros ```rust theme={null} pub use wacore_derive::{EmptyNode, ProtocolNode, StringEnum}; ``` * **`EmptyNode`** - For protocol nodes with only a tag (no attributes) * **`ProtocolNode`** - For protocol nodes with string attributes * **`StringEnum`** - For enums with string representations See [Binary Protocol](/advanced/binary-protocol) for usage examples. ### Framing ```rust theme={null} pub use wacore_noise::framing; // WebSocket frame encoding/decoding ``` ## Core Modules ### Protocol & Binary Type-safe protocol node builders and parsers XML utilities for protocol nodes ### Cryptography Signal Protocol implementation for E2E encryption Noise Protocol XX for handshake encryption ### IQ Protocol ```rust theme={null} pub mod iq; ``` Type-safe IQ request/response specifications: * **`blocklist`** - Block/unblock contacts * **`chatstate`** - Typing indicators, presence * **`contacts`** - Contact synchronization * **`dirty`** - Dirty bit checking * **`groups`** - Group management operations * **`keepalive`** - Connection keepalive * **`mediaconn`** - Media server connections * **`mex`** - Message Extension queries * **`passive`** - Passive IQ handling * **`prekeys`** - Prekey distribution * **`privacy`** - Privacy settings * **`props`** - Server properties * **`spam_report`** - Spam reporting * **`tctoken`** - Temporary client tokens * **`usync`** - User synchronization See [Architecture](/concepts/architecture) for the IQ protocol pattern. ### Message Handling Message encryption and decryption Message sending logic Media download and decryption Media encryption and upload preparation ### State Management ```rust theme={null} pub mod store; ``` * **`Device`** - Core device state structure * **`DeviceCommand`** - State mutation commands * **`traits`** - Backend trait definitions (`Backend`, `SessionStore`, etc.) See [State Management](/advanced/state-management) for the command pattern. ### Connection & Pairing Noise Protocol handshake QR code pairing Phone number pairing Network utilities ### Specialized Features App state synchronization (contacts, settings) Message history synchronization User device list synchronization Signal Protocol prekey generation ### Utilities * **`client`** - Client context traits * **`ib`** - Identity byte utilities * **`proto_helpers`** - Protobuf conversion helpers * **`reporting_token`** - Reporting token generation * **`request`** - Request building utilities * **`stanza`** - Common stanza builders * **`types`** - Common type definitions (JID, events, messages) * **`version`** - WhatsApp version constants ## Submodule Packages wacore is split into several workspace crates: ### wacore-binary **Location:** `wacore/binary` Binary protocol encoding/decoding using WhatsApp's custom format. ```rust theme={null} use wacore_binary::{ jid::Jid, node::Node, builder::NodeBuilder, marshal::{marshal, unmarshal_ref}, }; let node = NodeBuilder::new("message") .attr("type", "text") .attr("to", "15551234567@s.whatsapp.net") .build(); let bytes = marshal(&node)?; let decoded = unmarshal_ref(&bytes)?; ``` **Key exports:** * `jid::Jid` - WhatsApp JID (Jabber ID) parsing * `node::{Node, NodeRef, NodeValue}` - Protocol node types * `builder::NodeBuilder` - Fluent node builder with `attr()`, `jid_attr()`, `children()`, `bytes()`, `string_content()`, and `apply_content()` chaining methods * `marshal::*` - Binary marshaling functions * `attrs::AttrParser` - Attribute parsing utilities * `token` - Token dictionary ### wacore-libsignal **Location:** `wacore/libsignal` Signal Protocol implementation for end-to-end encryption. ```rust theme={null} use wacore::libsignal::{ core::SessionCipher, protocol::{PreKeyBundle, PublicKey}, store::SessionStore, }; ``` **Key modules:** * `core` - Core session cipher logic * `crypto` - Cryptographic primitives (HKDF, HMAC, AES) * `protocol` - Protocol message types * `store` - Store trait definitions See [Signal Protocol](/advanced/signal-protocol) for encryption details. ### wacore-noise **Location:** `wacore/noise` Noise Protocol XX implementation for handshake encryption. ```rust theme={null} use wacore::noise::{ NoiseHandshake, HandshakeUtils, build_handshake_header, }; ``` **Key exports:** * `NoiseState` - Generic Noise XX state machine * `NoiseHandshake` - WhatsApp-specific handshake wrapper * `HandshakeUtils` - Protocol message building/parsing * `framing` - WebSocket frame encoding * `build_edge_routing_preintro` - Edge routing helper ### wacore-appstate **Location:** `wacore/appstate` App state synchronization for contacts, settings, and metadata. ```rust theme={null} use wacore::appstate::{ process_snapshot, process_patch, expand_app_state_keys, Mutation, }; ``` **Key exports:** * `process_snapshot` - Process full state snapshots * `process_patch` - Apply incremental patches * `Mutation` - State mutation records * `LTHash` - LTHash implementation for integrity * `expand_app_state_keys` - Key derivation ### wacore-derive **Location:** `wacore/derive` Procedural macros for protocol node generation. ```rust theme={null} use wacore::{EmptyNode, ProtocolNode, StringEnum}; #[derive(EmptyNode)] #[protocol(tag = "participants")] pub struct ParticipantsRequest; #[derive(ProtocolNode)] #[protocol(tag = "query")] pub struct QueryRequest { #[attr(name = "request", default = "interactive")] pub request_type: String, } #[derive(StringEnum)] pub enum Action { #[str = "block"] Block, #[str = "unblock"] Unblock, } ``` ## Usage in Main Library The main `whatsapp-rust` crate uses wacore modules throughout: ```rust theme={null} // Binary protocol use wacore_binary::jid::Jid; use wacore_binary::builder::NodeBuilder; // Signal Protocol use wacore::libsignal::store::SessionStore; use wacore::libsignal::protocol::PreKeyBundle; // App state use wacore::appstate::{ process_snapshot, Mutation, }; // IQ specs use wacore::iq::privacy as privacy_settings; // Store traits use wacore::store::traits::Backend; // Protobuf helpers use wacore::proto_helpers; ``` ## Design Principles ### Platform-Agnostic No dependencies on: * Tokio or any async runtime * Specific database implementations * File system operations This allows the main library to choose: * **Runtime:** Tokio (current), async-std, smol, etc. * **Storage:** SQLite (current), PostgreSQL, in-memory, etc. * **Transport:** WebSocket (current), custom protocols ### Type Safety Strong typing throughout: * `Jid` for WhatsApp identifiers * `Node` for protocol messages * Validated newtypes (e.g., `GroupSubject` with length limits) * Enum variants with `StringEnum` for protocol values ### Zero-Copy Where Possible * `NodeRef` for borrowed node parsing * `AttrParserRef` for attribute iteration * `marshal_ref` for encoding without cloning ## Next Steps Protocol Buffers message definitions Type-safe protocol node pattern IqSpec request/response pairing Device state and commands # waproto Source: https://whatsapp-rust.jlucaso.com/api/waproto Protocol Buffers definitions for WhatsApp messages ## Overview `waproto` contains the Protocol Buffers definitions for all WhatsApp message types. It's auto-generated from `whatsapp.proto` using `prost` and provides strongly-typed Rust structs for working with WhatsApp's binary protocol. ```toml theme={null} [dependencies] waproto = "0.3" ``` ## Structure ``` waproto/ ├── src/ │ ├── lib.rs # Module definition │ └── whatsapp.rs # Generated protobuf code (15,020 lines) ├── build.rs # prost-build script └── whatsapp.proto # Source protobuf definitions ``` ## Usage All protobuf types are under the `waproto::whatsapp` module: ```rust theme={null} use waproto::whatsapp as wa; let message = wa::Message { conversation: Some("Hello, World!".to_string()), ..Default::default() }; ``` ## Key Message Types ### Core Message Types #### Message The main message container used for all WhatsApp messages. ```rust theme={null} pub struct Message { // Text message pub conversation: Option, // Media messages pub image_message: Option>, pub video_message: Option>, pub audio_message: Option>, pub document_message: Option>, pub sticker_message: Option>, // Rich messages pub extended_text_message: Option>, pub interactive_message: Option>, pub template_message: Option>, pub buttons_message: Option>, pub list_message: Option>, // Group messages pub sender_key_distribution_message: Option>, // System messages pub protocol_message: Option>, pub ephemeral_message: Option>, pub view_once_message: Option>, pub view_once_message_v2: Option>, // Reactions and interactions pub reaction_message: Option>, pub edit_message: Option>, pub keep_in_chat_message: Option>, // AI/Bot messages pub ai_rich_response_message: Option>, pub bot_feedback_message: Option>, // Metadata pub message_context_info: Option>, // ... and many more } ``` **Usage in main library:** ```rust theme={null} use waproto::whatsapp as wa; // Construct a text message let msg = wa::Message { conversation: Some("Hello!".to_string()), ..Default::default() }; // Construct an image message let img = wa::Message { image_message: Some(Box::new(wa::message::ImageMessage { url: Some(media_url), media_key: Some(media_key.to_vec()), file_sha256: Some(file_sha256.to_vec()), file_enc_sha256: Some(file_enc_sha256.to_vec()), caption: Some("Check this out!".to_string()), ..Default::default() })), ..Default::default() }; ``` #### MessageKey Identifies a specific message in a conversation. ```rust theme={null} pub struct MessageKey { pub remote_jid: Option, // Chat JID pub from_me: Option, // Sent by me? pub id: Option, // Message ID pub participant: Option, // Group participant JID } ``` ### Media Messages #### ImageMessage ```rust theme={null} pub struct ImageMessage { pub url: Option, pub mimetype: Option, pub caption: Option, pub file_sha256: Option>, pub file_length: Option, pub height: Option, pub width: Option, pub media_key: Option>, pub file_enc_sha256: Option>, pub jpeg_thumbnail: Option>, pub context_info: Option, // ... } ``` #### VideoMessage ```rust theme={null} pub struct VideoMessage { pub url: Option, pub mimetype: Option, pub caption: Option, pub file_sha256: Option>, pub file_length: Option, pub seconds: Option, pub media_key: Option>, pub file_enc_sha256: Option>, pub jpeg_thumbnail: Option>, pub gif_playback: Option, // ... } ``` #### AudioMessage ```rust theme={null} pub struct AudioMessage { pub url: Option, pub mimetype: Option, pub file_sha256: Option>, pub file_length: Option, pub seconds: Option, pub ptt: Option, // Push-to-talk (voice note) pub media_key: Option>, pub file_enc_sha256: Option>, // ... } ``` #### DocumentMessage ```rust theme={null} pub struct DocumentMessage { pub url: Option, pub mimetype: Option, pub title: Option, pub file_sha256: Option>, pub file_length: Option, pub page_count: Option, pub media_key: Option>, pub file_enc_sha256: Option>, pub file_name: Option, pub jpeg_thumbnail: Option>, // ... } ``` #### StickerMessage ```rust theme={null} pub struct StickerMessage { pub url: Option, pub file_sha256: Option>, pub file_enc_sha256: Option>, pub media_key: Option>, pub mimetype: Option, pub height: Option, pub width: Option, pub is_animated: Option, // ... } ``` ### Rich Content Messages #### ExtendedTextMessage Text with formatting, links, and quoted messages. ```rust theme={null} pub struct ExtendedTextMessage { pub text: Option, pub matched_text: Option, pub canonical_url: Option, pub description: Option, pub title: Option, pub thumbnail: Option>, pub context_info: Option, // ... } ``` #### InteractiveMessage Buttons, lists, and other interactive elements. ```rust theme={null} pub struct InteractiveMessage { pub header: Option, pub body: Option, pub footer: Option, pub context_info: Option, // One of: pub native_flow_message: Option, pub shop_storefront_message: Option, // ... } ``` #### ButtonsMessage ```rust theme={null} pub struct ButtonsMessage { pub content_text: Option, pub footer_text: Option, pub context_info: Option, pub buttons: Vec, pub header_type: Option, // ... } ``` #### ListMessage ```rust theme={null} pub struct ListMessage { pub title: Option, pub description: Option, pub button_text: Option, pub list_type: Option, pub sections: Vec, pub context_info: Option, // ... } ``` ### Encryption Messages #### SenderKeyDistributionMessage Used for group message encryption. ```rust theme={null} pub struct SenderKeyDistributionMessage { pub group_id: Option, pub axolotl_sender_key_distribution_message: Option>, } ``` #### PreKeySignalMessage Used for establishing 1:1 encryption. ```rust theme={null} pub struct PreKeySignalMessage { // Signal Protocol encrypted message } ``` ### System & Protocol Messages #### ProtocolMessage For protocol-level operations. ```rust theme={null} pub struct ProtocolMessage { pub key: Option, pub r#type: Option, // DELETE, REVOKE, etc. pub ephemeral_expiration: Option, pub ephemeral_setting_timestamp: Option, // ... } ``` **Common types:** * `DELETE` - Delete message for everyone * `REVOKE` - Revoke sent message * `EPHEMERAL_SETTING` - Ephemeral message setting #### ReactionMessage ```rust theme={null} pub struct ReactionMessage { pub key: Option, pub text: Option, // Emoji pub group_by_key: Option, pub sender_timestamp_ms: Option, pub unread: Option, } ``` #### EditMessage ```rust theme={null} pub struct EditMessage { pub key: Option, pub message: Option>, // New message content pub timestamp_ms: Option, } ``` ### AI & Bot Messages #### AiRichResponseMessage ```rust theme={null} pub struct AiRichResponseMessage { pub r#type: Option, pub sub_messages: Vec, // ... } ``` #### BotFeedbackMessage ```rust theme={null} pub struct BotFeedbackMessage { pub message_key: Option, pub kind: Option, // Thumbs up/down pub text: Option, // ... } ``` ### Metadata & Context #### MessageContextInfo ```rust theme={null} pub struct MessageContextInfo { pub device_list_metadata: Option, pub device_list_metadata_version: Option, pub message_secret: Option>, pub padding: Option>, pub message_add_on_duration_in_secs: Option, // ... } ``` #### ContextInfo Quoted messages, mentions, and forwarding info. ```rust theme={null} pub struct ContextInfo { pub stanza_id: Option, pub participant: Option, pub quoted_message: Option>, pub remote_jid: Option, pub mentioned_jid: Vec, pub conversion_source: Option, pub forwarding_score: Option, pub is_forwarded: Option, // ... } ``` ## Device & Identity Types ### ADV Messages Account Device Verification messages. ```rust theme={null} pub struct AdvDeviceIdentity { pub raw_id: Option, pub timestamp: Option, pub key_index: Option, pub account_type: Option, pub device_type: Option, } pub struct AdvSignedDeviceIdentity { pub details: Option>, pub account_signature_key: Option>, pub account_signature: Option>, pub device_signature: Option>, } pub struct AdvKeyIndexList { pub raw_id: Option, pub timestamp: Option, pub current_index: Option, pub valid_indexes: Vec, pub account_type: Option, } ``` ### Signal Protocol Structures ```rust theme={null} pub struct PreKeyRecordStructure { pub id: Option, pub public_key: Option>, pub private_key: Option>, } pub struct SignedPreKeyRecordStructure { pub id: Option, pub public_key: Option>, pub private_key: Option>, pub signature: Option>, pub timestamp: Option, } ``` ## Handshake & Connection Types ### HandshakeMessage Used during initial connection handshake. ```rust theme={null} pub struct HandshakeMessage { pub client_hello: Option, pub server_hello: Option, pub client_finish: Option, } ``` ### ClientPayload Device and client information during pairing. ```rust theme={null} pub struct ClientPayload { pub username: Option, pub passive: Option, pub user_agent: Option, pub web_info: Option, pub push_name: Option, pub session_id: Option, pub short_connect: Option, pub connect_type: Option, pub connect_reason: Option, // ... } ``` ## History Sync Types ### HistorySyncNotification ```rust theme={null} pub struct HistorySyncNotification { pub file_sha256: Option>, pub file_length: Option, pub media_key: Option>, pub file_enc_sha256: Option>, pub direct_path: Option, pub sync_type: Option, pub chunk_order: Option, pub original_message_id: Option, } ``` ### HistorySync ```rust theme={null} pub struct HistorySync { pub sync_type: Option, pub conversations: Vec, pub status_v3_messages: Vec, pub chunk_order: Option, pub progress: Option, // ... } ``` ## Media Reference Types ### ExternalBlobReference References to uploaded media files. ```rust theme={null} pub struct ExternalBlobReference { pub media_key: Option>, pub direct_path: Option, pub handle: Option, pub file_size_bytes: Option, pub file_sha256: Option>, pub file_enc_sha256: Option>, } ``` ## App State Types ### SyncActionValue App state synchronization actions. ```rust theme={null} pub struct SyncActionValue { pub timestamp: Option, // One of many action types: pub contact_action: Option, pub mute_action: Option, pub pin_action: Option, pub security_notification_setting: Option, pub push_name_setting: Option, pub archive_chat_action: Option, pub delete_chat_action: Option, pub star_action: Option, // ... and many more } ``` ## Enums waproto includes many enum types (as i32 values with const definitions): ```rust theme={null} // Message types pub mod protocol_message { pub enum Type { Revoke = 0, EphemeralSetting = 3, EphemeralSyncResponse = 4, HistorySyncNotification = 5, AppStateSyncKeyShare = 6, AppStateSyncKeyRequest = 7, Delete = 8, // ... } } // Media types pub mod video_message { pub enum Attribution { None = 0, Giphy = 1, Tenor = 2, } } ``` ## Serde Support All generated types include `serde::Serialize` and `serde::Deserialize`: ```rust theme={null} #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Message { // ... } ``` This allows JSON serialization for debugging: ```rust theme={null} let json = serde_json::to_string_pretty(&message)?; println!("Message: {}", json); ``` ## Code Generation The protobuf code is auto-generated but checked into version control. **To regenerate:** ```bash theme={null} GENERATE_PROTO=1 cargo build -p waproto ``` This runs `build.rs` which uses `prost-build` to compile `whatsapp.proto` into `whatsapp.rs`. ## Usage Examples ### Constructing Messages ```rust theme={null} use waproto::whatsapp as wa; // Text message let text = wa::Message { conversation: Some("Hello!".to_string()), ..Default::default() }; // Image with caption let image = wa::Message { image_message: Some(Box::new(wa::message::ImageMessage { url: Some(media_url), media_key: Some(media_key.to_vec()), file_sha256: Some(file_sha256.to_vec()), file_enc_sha256: Some(file_enc_sha256.to_vec()), caption: Some("Nice photo!".to_string()), jpeg_thumbnail: Some(thumbnail), ..Default::default() })), ..Default::default() }; // Quoted reply let reply = wa::Message { extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage { text: Some("Great question!".to_string()), context_info: Some(Box::new(wa::message::ContextInfo { stanza_id: Some(original_message_id), participant: Some(original_sender), quoted_message: Some(Box::new(original_message)), ..Default::default() })), ..Default::default() })), ..Default::default() }; ``` ### Pattern Matching ```rust theme={null} match &proto_msg { wa::Message { conversation: Some(text), .. } => { println!("Text: {}", text); } wa::Message { image_message: Some(img), .. } => { println!("Image from: {}", img.url.as_ref().unwrap()); } wa::Message { video_message: Some(vid), .. } => { println!("Video: {} seconds", vid.seconds.unwrap_or(0)); } _ => println!("Other message type"), } ``` ### Media Downloads See [Media Handling](/guides/media-handling) for complete examples. ```rust theme={null} use waproto::whatsapp as wa; use wacore::download::{Downloadable, MediaType}; if let Some(img) = &message.image_message { // Use the Downloadable trait to download and decrypt let data = client.download(img).await?; } ``` ## WhatsApp Version The protobuf definitions are based on: ```rust theme={null} // This file is @generated by prost-build. // WhatsApp Version: 2.3000.1031424117 ``` This version is automatically included in the generated file header. ## Relationship with wacore wacore provides utilities for working with waproto messages: * **`proto_helpers`** - Conversion between protobuf and internal types * **`download`** - `Downloadable` trait for media messages * **`upload`** - Media encryption for upload * **`messages`** - Message encryption/decryption * **`send`** - Message building and sending Example: ```rust theme={null} use wacore::proto_helpers; use waproto::whatsapp as wa; // Convert internal JID to protobuf MessageKey let key = proto_helpers::message_key(&jid, &message_id, from_me); // Build delete message let delete = wa::Message { protocol_message: Some(wa::ProtocolMessage { key: Some(key), r#type: Some(wa::protocol_message::Type::Delete as i32), ..Default::default() }), ..Default::default() }; ``` ## Next Steps Platform-agnostic protocol implementation Sending and receiving messages Working with media uploads and downloads End-to-end encryption details # Architecture Source: https://whatsapp-rust.jlucaso.com/concepts/architecture Understanding the whatsapp-rust project structure, modules, and workspace layout ## Overview WhatsApp-Rust is a high-performance, async Rust library for the WhatsApp Web API. The project follows a modular, layered architecture that separates protocol concerns from runtime concerns, enabling platform-agnostic core logic with pluggable backends. ## Workspace Structure The project is organized as a Cargo workspace with multiple crates: ``` whatsapp-rust/ ├── src/ # Main client library ├── wacore/ # Platform-agnostic core │ ├── binary/ # WhatsApp binary protocol │ ├── libsignal/ # Signal Protocol implementation │ ├── appstate/ # App state management │ ├── noise/ # Noise Protocol handshake │ └── derive/ # Derive macros ├── waproto/ # Protocol Buffers definitions ├── storages/sqlite-storage/ # SQLite backend ├── transports/tokio-transport/ # Tokio WebSocket transport └── http_clients/ureq-client/ # HTTP client for media ``` ## Three Main Crates ### wacore - Platform-Agnostic Core **Location:** `wacore/` **Purpose:** Contains core logic for the WhatsApp binary protocol, cryptography primitives, IQ protocol types, and state management traits. **Key Features:** * **No runtime dependencies** on Tokio or specific databases * Pure protocol implementation * Cryptographic operations (Signal Protocol, Noise Protocol) * Type-safe protocol node builders **Key Modules:** ```rust theme={null} wacore/ ├── binary/ // Binary protocol encoding/decoding ├── libsignal/ // E2E encryption ├── noise/ // Noise Protocol handshake ├── appstate/ // App state sync protocol ├── iq/ // Type-safe IQ protocol types ├── protocol.rs // ProtocolNode trait ├── types/ │ ├── events.rs // Event definitions │ └── message.rs // Message types └── store/ ├── traits.rs // Storage trait definitions └── device.rs // Device state model ``` ### waproto - Protocol Buffers **Location:** `waproto/` **Purpose:** Houses WhatsApp's Protocol Buffers definitions compiled to Rust structs. **Build Process:** ```rust theme={null} // build.rs uses prost to compile .proto files prost_build::compile_protos(&["src/whatsapp.proto"], &["src/"])?; ``` **Generated Types:** * `Message` - All message types * `WebMessageInfo` - Message metadata * `HistorySync` - Chat history * `SyncActionValue` - App state mutations ### whatsapp-rust - Main Client **Location:** `src/` **Purpose:** Integrates `wacore` with the Tokio runtime, provides high-level client API, and manages storage. **Key Features:** * Asynchronous operations with Tokio * SQLite persistence (pluggable) * Event bus system * Feature modules (groups, media, etc.) ## Key Components ### Client **Location:** `src/client.rs` **Purpose:** Orchestrates connection lifecycle, event bus, and high-level operations. ```rust theme={null} pub struct Client { pub(crate) core: wacore::client::CoreClient, pub(crate) persistence_manager: Arc, pub(crate) media_conn: Arc>>, pub(crate) noise_socket: Arc>>>, // ... connection state, caches, locks } ``` **Responsibilities:** * Connection management * Request/response routing * Event dispatching * Session management ### PersistenceManager **Location:** `src/store/persistence_manager.rs` **Purpose:** Manages all state changes and persistence. ```rust theme={null} pub struct PersistenceManager { device: Arc>, backend: Arc, dirty: Arc, save_notify: Arc, } ``` **Critical Pattern:** * Never modify `Device` state directly * Use `DeviceCommand` + `process_command()` * For read-only: `get_device_snapshot()` ### Signal Protocol **Location:** `wacore/libsignal/` & `src/store/signal*.rs` **Purpose:** End-to-end encryption via Signal Protocol implementation. **Features:** * Double Ratchet algorithm * Pre-key bundles * Session management * Sender keys for groups ### Socket & Handshake **Location:** `src/socket/`, `src/handshake.rs` **Purpose:** WebSocket connection and Noise Protocol handshake. **Flow:** 1. WebSocket connection 2. Noise handshake (XX pattern) 3. Encrypted frame exchange ## Module Interactions ```mermaid theme={null} graph TB A[Client API] --> B[Client] B --> C[NoiseSocket] B --> D[PersistenceManager] B --> E[Event Bus] C --> F[Transport] D --> G[Backend Storage] D --> H[Device State] B --> I[Signal Protocol] I --> G B --> J[wacore] J --> K[Protocol Types] J --> L[Crypto] ``` ## Layer Responsibilities ### wacore Layer (Platform-Agnostic) * Protocol logic * State traits * Cryptographic helpers * Data models **Example: IQ Protocol** ```rust theme={null} // wacore/src/iq/groups.rs pub struct GroupQueryIq { group_jid: Jid, } impl IqSpec for GroupQueryIq { type Response = GroupInfoResponse; fn build_iq(&self) -> InfoQuery<'static> { /* ... */ } fn parse_response(&self, response: &Node) -> Result { /* ... */ } } ``` ### whatsapp-rust Layer (Runtime) * Runtime orchestration * Storage integration * User-facing API **Example: Feature API** ```rust theme={null} // src/features/groups.rs impl<'a> Groups<'a> { pub async fn get_metadata(&self, jid: &Jid) -> Result { // Use wacore IqSpec for protocol self.client.execute(GroupQueryIq::new(jid)?).await } } ``` ## Protocol Entry Points ### Incoming Messages **Flow:** `src/message.rs` → Signal decryption → Event dispatch ```rust theme={null} // src/message.rs pub async fn handle_message(client: &Arc, node: &Node) { // 1. Extract encrypted message // 2. Decrypt via Signal Protocol // 3. Dispatch Event::Message } ``` ### Outgoing Messages **Flow:** `src/send.rs` → Signal encryption → Socket send ```rust theme={null} // src/send.rs pub async fn send_message(client: &Arc, msg: &Message) { // 1. Encrypt via Signal Protocol // 2. Build protocol node // 3. Send via NoiseSocket } ``` ### Socket Communication **Flow:** `src/socket/` → Noise framing → Transport ```rust theme={null} // src/socket/mod.rs impl NoiseSocket { pub async fn send_node(&self, node: Node) -> Result<()> { // 1. Marshal to binary // 2. Encrypt with Noise // 3. Frame and send } } ``` ## Connection Lifecycle ### Auto-Reconnection The client implements robust reconnection handling with stream error awareness: ```rust theme={null} // Client fields for connection and reconnection management is_connected: Arc, // Lock-free connection state (Acquire/Release) pub enable_auto_reconnect: Arc, // Toggle auto-reconnect pub auto_reconnect_errors: Arc, // Error count for backoff pub(crate) expected_disconnect: Arc, // Expected vs unexpected pub(crate) connection_generation: Arc, // Detect stale tasks ``` The `is_connected` field uses an `AtomicBool` to track whether the noise socket is established. This avoids a TOCTOU race that previously occurred when `try_lock()` on the noise socket mutex failed under contention, causing false-negative connection checks and silent ack drops. **Connection timeout:** Both the transport connection and version fetch are wrapped in a 20-second timeout (`TRANSPORT_CONNECT_TIMEOUT`), matching WhatsApp Web's MQTT and DGW defaults. This prevents dead networks from blocking on the OS TCP SYN timeout (\~60-75s). Both operations run in parallel via `tokio::join!`. **Reconnection flow:** 1. Connection lost → `cleanup_connection_state()` 2. Check `enable_auto_reconnect` → exit if disabled (401, 409, 516 disable this) 3. Check `expected_disconnect` → immediate reconnect if expected (e.g., 515) 4. Calculate Fibonacci backoff delay (1s, 1s, 2s, 3s, 5s, 8s... max 900s with +/-10% jitter) 5. Wait → attempt reconnection (with 20s connect timeout) **Stream error behavior:** * **401 (unauthorized)**: Disables auto-reconnect, emits `LoggedOut` * **409 (conflict)**: Disables auto-reconnect, emits `StreamReplaced` * **429 (rate limited)**: Adds 5 extra Fibonacci steps to backoff, then reconnects * **515 (expected)**: Immediate reconnect without backoff * **516 (device removed)**: Disables auto-reconnect, emits `LoggedOut` ### Keepalive Loop Monitors connection health with periodic pings, matching WhatsApp Web's keepalive protocol: ```rust theme={null} const KEEP_ALIVE_INTERVAL_MIN: Duration = Duration::from_secs(15); const KEEP_ALIVE_INTERVAL_MAX: Duration = Duration::from_secs(30); const KEEP_ALIVE_RESPONSE_DEADLINE: Duration = Duration::from_secs(20); const DEAD_SOCKET_TIME: Duration = Duration::from_secs(20); ``` **Behavior:** * Sends ping every 15-30 seconds (randomized, matching WA Web's `15 * (1 + random())`) * Skips ping if data was received within the minimum interval (connection proven alive) * Sends ping *before* dead-socket check to prevent false-positive reconnects on idle-but-healthy connections * Waits up to 20s for response * Detects dead socket if no data received for 20s after a send, triggering immediate reconnection * Fatal errors (`Socket`, `Disconnected`, `NotConnected`, `InternalChannelClosed`) cause the keepalive loop to exit immediately * Error classification is exhaustive and compile-time enforced — adding a new error variant without handling it causes a build failure See [WebSocket & Noise Protocol - Keepalive](/advanced/websocket-handling#keepalive-and-dead-socket-detection) for detailed keepalive internals. ### Offline sync When reconnecting, the client tracks offline message sync progress: ```rust theme={null} pub(crate) struct OfflineSyncMetrics { pub active: AtomicBool, pub total_messages: AtomicUsize, pub processed_messages: AtomicUsize, pub start_time: Mutex>, } ``` **Sync flow:** 1. Receive `` → start tracking, reset counters 2. Process messages with `offline` attribute → increment counter 3. Receive `` → sync complete 4. Emit `OfflineSyncCompleted` event **Timeout fallback:** If the server advertises offline messages via `offline_preview` but never sends the end marker (``), the client applies a 60-second timeout (matching WhatsApp Web's `OFFLINE_STANZA_TIMEOUT_MS`). On timeout, the client logs a warning, marks sync as complete, and resumes normal operation so startup is not blocked indefinitely. **Concurrency gating:** During offline sync, the client restricts message processing to a single concurrent task (1 semaphore permit) to preserve ordering. Once sync completes — either by the server end marker, all expected items arriving, or timeout — the semaphore is expanded to 64 permits, switching to parallel message processing. **State reset:** On reconnect or cleanup, all offline sync state is reset (counters, timing, and the semaphore is replaced with a fresh single-permit instance) so stale state does not leak into the next connection attempt. ## History Sync Pipeline History sync transfers chat history from the phone to the linked device. The pipeline is designed for minimal RAM usage through a multi-layered zero-copy strategy. ### Processing flow ```mermaid theme={null} graph LR A[Phone uploads
compressed blob] --> B[Download &
stream-decrypt] B --> C[Decompress
zlib] C --> D[Manual protobuf
field walking] D --> E[Bounded channel
cap=4] E --> F[LazyConversation
zero-copy] F --> G[Event dispatch] ``` ### RAM optimization layers 1. **Heuristic pre-allocation with `compressed_size_hint`** — the decompression buffer is pre-allocated using a 4x multiplier on the compressed blob's `file_length` (clamped to 256 bytes – 8 MiB). When the notification provides `file_length`, this avoids repeated `Vec` reallocation during decompression. The hint comes from the decrypted (but still compressed) blob size, which is a better estimate than the encrypted size that includes MAC/padding overhead 2. **Immediate drop of compressed data** — after decompression, the compressed input is dropped so peak memory equals `max(compressed, decompressed)` rather than both combined 3. **Hand-rolled protobuf parser** — instead of decoding the entire `HistorySync` message tree (which allocates every nested message), the core walks varint tags manually and only extracts field 2 (conversations) and field 7 (pushnames) 4. **`Bytes` zero-copy slicing** — decompressed data is wrapped in a reference-counted `Bytes` buffer; each conversation is extracted as `buf.slice(pos..end)`, which is an Arc refcount increment with no per-conversation heap allocation 5. **Bounded channel streaming** — a `tokio::sync::mpsc::channel::(4)` streams conversation bytes from the blocking parser thread to the async event dispatcher, providing backpressure with only \~4 conversations in-flight 6. **`LazyConversation` wrapper** — raw `Bytes` are wrapped without parsing; protobuf decoding only happens if the event handler calls `.get()` or `.conversation()`. Each clone parses independently (plain `OnceLock`, not `Arc`-wrapped) since the common case is a single handler. On first parse, embedded messages are immediately cleared and shrunk to reclaim memory 7. **Compile-time callback elimination** — when no event handlers are registered, the callback is `None`, causing the parser to skip conversation extraction entirely at the protobuf level ### Skip mode For bots that don't need chat history, `skip_history_sync()` sends a receipt so the phone stops retrying uploads but downloads nothing. See [Bot - History Sync](/api/bot#history-sync). ## Concurrency Patterns ### Per-Chat Message Queues Prevents race conditions where a later message is processed before the PreKey message: ```rust theme={null} pub(crate) message_queues: Cache>>, ``` ### Per-Device Session Locks Prevents concurrent Signal protocol operations on the same session. Keys are protocol address strings generated by `to_protocol_address_string()` (format: `user[:device]@server.0`), which builds the key in a single allocation: ```rust theme={null} pub(crate) session_locks: Cache>>, // Key examples: "5511999887766@c.us.0", "123456789:33@lid.0" ``` ### Background Saver Periodic persistence with dirty flag optimization: ```rust theme={null} impl PersistenceManager { pub fn run_background_saver(self: Arc, interval: Duration) { tokio::spawn(async move { loop { // Wait for notification or interval self.save_to_disk().await; } }); } } ``` ## Feature Organization **Location:** `src/features/` ``` features/ ├── mod.rs // Feature exports ├── blocking.rs // Block/unblock contacts ├── chat_actions.rs // Archive, pin, mute, star ├── chatstate.rs // Typing indicators ├── contacts.rs // Contact operations ├── groups.rs // Group management ├── mex.rs // GraphQL MEX queries ├── presence.rs // Presence updates ├── profile.rs // Profile management ├── status.rs // Status updates └── tctoken.rs // Trusted contact tokens ``` Media upload and download operations are in `src/download.rs` and `src/upload.rs` as separate top-level modules. **Pattern:** Features are accessed through accessor methods on `Client`: ```rust theme={null} // Access features through the client let metadata = client.groups().get_metadata(&group_jid).await?; let result = client.groups().create_group(options).await?; client.presence().set_available().await?; ``` ## State Management Flow ```mermaid theme={null} sequenceDiagram participant User participant Client participant PM as PersistenceManager participant Device participant Backend User->>Client: Operation Client->>PM: process_command(DeviceCommand) PM->>Device: modify_device() PM->>PM: Set dirty flag PM->>PM: Notify saver Note over PM: Background task PM->>Backend: save() Backend->>Backend: Persist to disk ``` ## Best Practices ### State Management ```rust ✅ Correct theme={null} // Use DeviceCommand for state changes client.persistence_manager .process_command(DeviceCommand::SetPushName(name)) .await; ``` ```rust ❌ Wrong theme={null} // Never modify Device directly let mut device = client.device.write().await; device.push_name = name; // DON'T DO THIS ``` ### Async Operations ```rust ✅ Correct theme={null} // Wrap blocking I/O in spawn_blocking let result = tokio::task::spawn_blocking(move || { // Heavy crypto or blocking HTTP expensive_operation() }).await?; ``` ```rust ❌ Wrong theme={null} // Never block the async runtime let result = expensive_operation(); // Stalls all tasks ``` ### Error Handling ```rust theme={null} use thiserror::Error; use anyhow::Result; #[derive(Debug, Error)] pub enum SocketError { #[error("connection closed")] Closed, #[error("encryption failed: {0}")] Encryption(String), } // Use anyhow::Result for functions with multiple error types pub async fn complex_operation() -> Result<()> { // Automatically converts errors with ? socket_operation()?; storage_operation()?; Ok(()) } ``` ## Related Sections Learn about QR code and pair code flows Understand the event system and handlers Explore storage backends and state management Build your first WhatsApp bot # Authentication Source: https://whatsapp-rust.jlucaso.com/concepts/authentication QR code and pair code authentication flows in whatsapp-rust ## Overview WhatsApp-Rust supports two authentication methods for linking companion devices: 1. **QR Code Pairing** - Scan a QR code with your phone 2. **Pair Code (Phone Number Linking)** - Enter an 8-character code on your phone Both methods use the Noise Protocol for secure key exchange and can run concurrently - whichever completes first wins. ## Authentication Flow ```mermaid theme={null} sequenceDiagram participant Companion as Companion Device
(Your App) participant WA as WhatsApp Server participant Phone as Primary Device
(Phone) Companion->>WA: Connect (Noise Handshake) WA->>Companion: pair-device (QR refs) Companion->>Companion: Generate QR codes Companion->>Phone: Display QR / Pair Code Phone->>WA: Scan QR / Enter Code WA->>Companion: pair-success Companion->>Companion: Sign identity Companion->>WA: pair-device-sign WA->>Companion: Connected! ``` ## QR Code Pairing ### How It Works **Location:** `src/pair.rs`, `wacore/src/pair.rs` 1. **Server sends pairing refs:** After connection, server sends `pair-device` with multiple refs 2. **Generate QR codes:** Each ref becomes a QR code containing device keys 3. **QR rotation:** First code valid for 60s, subsequent codes for 20s each 4. **Phone scans:** User scans QR with WhatsApp > Linked Devices 5. **Crypto handshake:** Noise-based key exchange establishes trust 6. **Completion:** Server sends `pair-success`, device signs identity ### QR Code Contents ```rust theme={null} // src/pair.rs pub fn make_qr_data(store: &Device, ref_str: String) -> String { let device_state = DeviceState { identity_key: store.identity_key.clone(), noise_key: store.noise_key.clone(), adv_secret_key: store.adv_secret_key, }; PairUtils::make_qr_data(&device_state, ref_str) } ``` **QR Format:** `ref,noise_pub,identity_pub,adv_secret` * `ref`: Pairing reference from server * `noise_pub`: Static Noise public key (32 bytes, base64) * `identity_pub`: Signal identity public key (32 bytes, base64) * `adv_secret`: Advertisement secret key (32 bytes, base64) ### Implementation ```rust theme={null} use whatsapp_rust::bot::Bot; use whatsapp_rust::store::SqliteStore; use wacore::types::events::Event; #[tokio::main] async fn main() -> Result<(), Box> { let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); let mut bot = Bot::builder() .with_backend(backend) .on_event(|event, _client| async move { match event { Event::PairingQrCode { code, timeout } => { println!("Scan this QR code (valid for {}s):", timeout.as_secs()); println!("{}", code); } Event::PairSuccess(info) => { println!("✅ Paired as {}", info.id); } _ => {} } }) .build() .await?; bot.run().await?.await?; Ok(()) } ``` ### QR Code Events **Event:** `Event::PairingQrCode` ```rust theme={null} // wacore/src/types/events.rs Event::PairingQrCode { code: String, // ASCII art QR or data string timeout: Duration, // Validity duration (60s first, 20s subsequent) } ``` **Generated in:** `src/pair.rs:63-97` ```rust theme={null} for code in codes_clone { let timeout = if is_first { is_first = false; Duration::from_secs(60) } else { Duration::from_secs(20) }; client.core.event_bus.dispatch(&Event::PairingQrCode { code, timeout }); tokio::select! { _ = tokio::time::sleep(timeout) => {} _ = stop_rx_clone.changed() => { info!("Pairing complete. Stopping QR rotation."); return; } } } ``` ## Pair Code (Phone Number Linking) ### How It Works **Location:** `src/pair_code.rs`, `wacore/src/pair_code.rs` 1. **Generate code:** 8-character Crockford Base32 code 2. **Stage 1 - Hello:** Send phone number + encrypted ephemeral key 3. **Server response:** Returns pairing reference 4. **User enters code:** On phone: WhatsApp > Linked Devices > Link with phone number 5. **Stage 2 - Finish:** Phone confirms, companion sends key bundle 6. **Completion:** Server sends `pair-success` ### Pair Code Format **Alphabet:** Crockford Base32 (excludes 0, I, O, U) ``` 123456789ABCDEFGHJKLMNPQRSTVWXYZ ``` **Length:** Exactly 8 characters **Example:** `ABCD1234`, `MYCODE12` ### Implementation #### Random Code ```rust theme={null} use whatsapp_rust::pair_code::PairCodeOptions; let options = PairCodeOptions { phone_number: "15551234567".to_string(), show_push_notification: true, ..Default::default() }; let code = client.pair_with_code(options).await?; println!("Enter this code on your phone: {}", code); ``` #### Custom Code ```rust theme={null} let options = PairCodeOptions { phone_number: "15551234567".to_string(), custom_code: Some("MYCODE12".to_string()), ..Default::default() }; let code = client.pair_with_code(options).await?; assert_eq!(code, "MYCODE12"); ``` ### Pair Code Options ```rust theme={null} // wacore/src/pair_code.rs pub struct PairCodeOptions { /// Phone number in international format (e.g., "15551234567") /// Non-digit characters are automatically stripped pub phone_number: String, /// Whether to show a push notification on the phone pub show_push_notification: bool, /// Custom 8-character code (must be valid Crockford Base32) /// If None, a random code is generated pub custom_code: Option, /// Platform identifier (default: Chrome) pub platform_id: PlatformId, /// Platform display name (default: "Chrome (Linux)") pub platform_display: String, } ``` ### Pair Code Events **Event:** `Event::PairingCode` ```rust theme={null} // wacore/src/types/events.rs Event::PairingCode { code: String, // The 8-character pairing code timeout: Duration, // Validity (~180 seconds) } ``` **Generated in:** `src/pair_code.rs:215-219` ```rust theme={null} self.core.event_bus.dispatch(&Event::PairingCode { code: code.clone(), timeout: PairCodeUtils::code_validity(), }); ``` ### Two-Stage Flow #### Stage 1: Hello **Purpose:** Register phone number and encrypted ephemeral key ```rust theme={null} // src/pair_code.rs:165-174 let iq_content = PairCodeUtils::build_companion_hello_iq( &phone_number, &noise_static_pub, &wrapped_ephemeral, options.platform_id, &options.platform_display, options.show_push_notification, req_id.clone(), ); ``` **Response:** Pairing reference ```rust theme={null} let pairing_ref = PairCodeUtils::parse_companion_hello_response(&response) .ok_or(PairCodeError::MissingPairingRef)?; ``` #### Stage 2: Finish **Trigger:** `link_code_companion_reg` notification from server **Handling:** `src/pair_code.rs:229-376` ```rust theme={null} pub(crate) async fn handle_pair_code_notification(client: &Arc, node: &Node) -> bool { // 1. Extract primary's wrapped ephemeral pub (80 bytes) // 2. Extract primary's identity pub (32 bytes) // 3. Decrypt primary's ephemeral key (expensive PBKDF2) // 4. Prepare encrypted key bundle // 5. Send companion_finish IQ } ``` ## Cryptography ### Noise Protocol Handshake **Pattern:** Noise XX (mutual authentication) ```rust theme={null} // wacore/noise/ pub struct NoiseHandshake { initiator_static: KeyPair, ephemeral: KeyPair, // ... Noise state machine } ``` **Flow:** 1. Initiator → Responder: ephemeral pub 2. Responder → Initiator: ephemeral pub, static pub, encrypted payload 3. Initiator → Responder: static pub, encrypted payload ### Key Derivation **For QR Code:** ```rust theme={null} // Direct key exchange - keys in QR code ``` **For Pair Code:** ```rust theme={null} // wacore/src/pair_code.rs // Expensive PBKDF2 operation (wrapped in spawn_blocking) let wrapped_ephemeral = tokio::task::spawn_blocking(move || { PairCodeUtils::encrypt_ephemeral_pub(&ephemeral_pub, &code_clone) }).await?; ``` **Parameters:** * **Algorithm:** AES-256-CBC * **KDF:** PBKDF2-HMAC-SHA256 * **Iterations:** 2^16 (65,536) * **Salt:** 16 random bytes * **IV:** 16 random bytes ### Signal Protocol Setup **After pairing:** 1. Server sends signed device identity 2. Companion verifies signature 3. Identity keys exchanged 4. Pre-keys registered ```rust theme={null} // src/pair.rs:188-209 let result = PairUtils::do_pair_crypto(&device_state, &device_identity_bytes); match result { Ok((self_signed_identity_bytes, key_index)) => { // Store device JID, LID, account info client.persistence_manager .process_command(DeviceCommand::SetId(Some(jid.clone()))) .await; client.persistence_manager .process_command(DeviceCommand::SetAccount(Some(signed_identity))) .await; } Err(e) => { // Send error to server } } ``` ## Concurrent Pairing Both methods can run simultaneously: ```rust theme={null} // Start QR code (automatic on connection) bot.run().await?; // Also start pair code in parallel let code = client.pair_with_code(options).await?; ``` **State Management:** ```rust theme={null} // src/client.rs pub(crate) pairing_cancellation_tx: Mutex>>, pub(crate) pair_code_state: Mutex, ``` **Cancellation:** ```rust theme={null} // src/pair.rs:120-127 async fn handle_pair_success(...) { // Cancel QR code rotation if active if let Some(tx) = client.pairing_cancellation_tx.lock().await.take() { let _ = tx.send(()); } // Clear pair code state if active *client.pair_code_state.lock().await = PairCodeState::Completed; } ``` ## Success Events ### PairSuccess ```rust theme={null} // wacore/src/types/events.rs #[derive(Debug, Clone, Serialize)] pub struct PairSuccess { pub id: Jid, // Device JID (e.g., "15551234567.0:1@s.whatsapp.net") pub lid: Jid, // LID JID (e.g., "100000012345678.0:1@lid") pub business_name: String, // Push name / business name pub platform: String, // Platform identifier } Event::PairSuccess(PairSuccess { id, lid, business_name, platform }) ``` ### PairError ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct PairError { pub id: Jid, pub lid: Jid, pub business_name: String, pub platform: String, pub error: String, // Error description } Event::PairError(PairError { /* ... */ }) ``` ## Error Handling ### QR Code Errors ```rust theme={null} // Handled internally, retries with new QR codes // If all QR codes expire, disconnects: info!("All QR codes for this session have expired."); client.disconnect().await; ``` ### Pair Code Errors ```rust theme={null} use wacore::pair_code::PairCodeError; match client.pair_with_code(options).await { Ok(code) => println!("Code: {}", code), Err(PairCodeError::PhoneNumberRequired) => { eprintln!("Phone number is required"); } Err(PairCodeError::PhoneNumberTooShort) => { eprintln!("Phone number must be at least 7 digits"); } Err(PairCodeError::PhoneNumberNotInternational) => { eprintln!("Phone number must not start with 0 (use international format)"); } Err(PairCodeError::InvalidCustomCode) => { eprintln!("Custom code must be 8 valid Crockford Base32 characters"); } Err(PairCodeError::MissingPairingRef) => { eprintln!("Server did not return a pairing reference"); } Err(PairCodeError::NotWaiting) => { eprintln!("No pending pair code request"); } Err(PairCodeError::InvalidWrappedData { expected, got }) => { eprintln!("Invalid wrapped data: expected {} bytes, got {}", expected, got); } Err(PairCodeError::CryptoError(msg)) => { eprintln!("Crypto error during pairing: {}", msg); } Err(PairCodeError::RequestFailed(msg)) => { eprintln!("Pairing request failed: {}", msg); } } ``` ## Session Persistence ### After Successful Pairing **State saved to storage:** * Device JID (Phone Number) * LID (Long-term Identifier) * Identity keys * Noise keys * Registration ID * Push name **Next connection:** ```rust theme={null} // No pairing needed - automatic reconnection let bot = Bot::builder() .with_backend(backend) .build() .await?; bot.run().await?; // Uses saved session ``` ### Logout ```rust theme={null} // Clear session data client.logout().await?; // Event emitted: Event::LoggedOut(LoggedOut { on_connect: false, reason: ConnectFailureReason::LoggedOut, }) ``` ## Best Practices ### Phone Number Format ```rust ✅ Correct theme={null} let options = PairCodeOptions { phone_number: "15551234567".to_string(), // International format // Non-digits automatically stripped: // phone_number: "+1-555-123-4567".to_string(), ..Default::default() }; ``` ```rust ❌ Wrong theme={null} let options = PairCodeOptions { phone_number: "0551234567".to_string(), // Don't start with 0 phone_number: "5551234567".to_string(), // Missing country code ..Default::default() }; ``` ### Event Handling ```rust theme={null} .on_event(|event, client| async move { match event { Event::PairingQrCode { code, timeout } => { // Display QR to user println!("Valid for: {}s", timeout.as_secs()); } Event::PairingCode { code, timeout } => { // Display code to user println!("Enter {} on your phone", code); } Event::PairSuccess(info) => { // Save success notification println!("Paired: {}", info.id); } Event::PairError(err) => { // Handle error eprintln!("Pairing failed: {}", err.error); } _ => {} } }) ``` ### Concurrent Usage ```rust theme={null} // Both methods active - whichever completes first wins tokio::spawn(async move { if let Ok(code) = client.pair_with_code(options).await { println!("Pair code: {}", code); } }); // QR codes automatically generated and rotated bot.run().await?; ``` ## Related Sections Understand the project structure Learn about all event types Explore session persistence Build your first bot # Events Source: https://whatsapp-rust.jlucaso.com/concepts/events Event system, event handlers, and Event enum types in whatsapp-rust ## Overview WhatsApp-Rust uses an event-driven architecture where the client emits events for all WhatsApp protocol interactions. Your application subscribes to these events to handle messages, connection changes, and notifications. ## Event System Architecture ### CoreEventBus **Location:** `wacore/src/types/events.rs` ```rust theme={null} #[derive(Default, Clone)] pub struct CoreEventBus { handlers: Arc>>>, } impl CoreEventBus { pub fn dispatch(&self, event: &Event) { for handler in self.handlers.read().expect("...").iter() { handler.handle_event(event); } } pub fn has_handlers(&self) -> bool { !self.handlers.read().expect("...").is_empty() } } ``` **Features:** * Thread-safe event dispatching * Multiple handlers supported * Clone-cheap with `Arc` ### EventHandler Trait ```rust theme={null} pub trait EventHandler: Send + Sync { fn handle_event(&self, event: &Event); } ``` **Implementation:** ```rust theme={null} struct MyHandler; impl EventHandler for MyHandler { fn handle_event(&self, event: &Event) { match event { Event::Message(msg, info) => { println!("Message from {}: {:?}", info.source.sender, msg); } _ => {} } } } client.core.event_bus.add_handler(Arc::new(MyHandler)); ``` ## Event Enum **Location:** `wacore/src/types/events.rs:292-351` ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub enum Event { // Connection Connected(Connected), Disconnected(Disconnected), StreamReplaced(StreamReplaced), StreamError(StreamError), ConnectFailure(ConnectFailure), TemporaryBan(TemporaryBan), // Pairing PairingQrCode { code: String, timeout: Duration }, PairingCode { code: String, timeout: Duration }, PairSuccess(PairSuccess), PairError(PairError), QrScannedWithoutMultidevice(QrScannedWithoutMultidevice), ClientOutdated(ClientOutdated), LoggedOut(LoggedOut), // Messages Message(Box, MessageInfo), Receipt(Receipt), UndecryptableMessage(UndecryptableMessage), Notification(Node), // Presence ChatPresence(ChatPresenceUpdate), Presence(PresenceUpdate), // User Updates PictureUpdate(PictureUpdate), UserAboutUpdate(UserAboutUpdate), PushNameUpdate(PushNameUpdate), SelfPushNameUpdated(SelfPushNameUpdated), // Group Updates JoinedGroup(LazyConversation), GroupUpdate(GroupUpdate), // Contact Updates ContactUpdated(ContactUpdated), ContactNumberChanged(ContactNumberChanged), ContactSyncRequested(ContactSyncRequested), ContactUpdate(ContactUpdate), // Chat State PinUpdate(PinUpdate), MuteUpdate(MuteUpdate), ArchiveUpdate(ArchiveUpdate), StarUpdate(StarUpdate), MarkChatAsReadUpdate(MarkChatAsReadUpdate), // History Sync HistorySync(HistorySync), OfflineSyncPreview(OfflineSyncPreview), OfflineSyncCompleted(OfflineSyncCompleted), // Device Updates DeviceListUpdate(DeviceListUpdate), BusinessStatusUpdate(BusinessStatusUpdate), // Notification Updates DisappearingModeChanged(DisappearingModeChanged), } ``` ## Connection Events ### Connected **Emitted:** After successful connection and authentication ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct Connected; Event::Connected(Connected) ``` **Usage:** ```rust theme={null} Event::Connected(_) => { println!("✅ Connected to WhatsApp"); // Safe to send messages now } ``` ### Disconnected **Emitted:** When connection is lost ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct Disconnected; Event::Disconnected(Disconnected) ``` **Behavior:** Client automatically attempts reconnection ### ConnectFailure **Emitted:** When connection fails with a specific reason ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct ConnectFailure { pub reason: ConnectFailureReason, pub message: String, pub raw: Option, } #[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize)] pub enum ConnectFailureReason { Generic, // 400 LoggedOut, // 401 TempBanned, // 402 MainDeviceGone, // 403 UnknownLogout, // 406 ClientOutdated, // 405 BadUserAgent, // 409 CatExpired, // 413 CatInvalid, // 414 NotFound, // 415 ClientUnknown, // 418 InternalServerError, // 500 Experimental, // 501 ServiceUnavailable, // 503 Unknown(i32), } ``` **Helper methods:** ```rust theme={null} if reason.is_logged_out() { // Clear session and re-pair } if reason.should_reconnect() { // Retry connection } ``` ### TemporaryBan **Emitted:** When account is temporarily banned ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct TemporaryBan { pub code: TempBanReason, pub expire: Duration, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub enum TempBanReason { SentToTooManyPeople, // 101 BlockedByUsers, // 102 CreatedTooManyGroups, // 103 SentTooManySameMessage, // 104 BroadcastList, // 106 Unknown(i32), } ``` **Usage:** ```rust theme={null} Event::TemporaryBan(ban) => { eprintln!("Banned: {} (expires in {:?})", ban.code, ban.expire); } ``` ### StreamReplaced **Emitted:** When another device connects with the same credentials (stream error code 409 or ``) ```rust theme={null} Event::StreamReplaced(_) => { println!("⚠️ Another instance connected - disconnecting"); // Auto-reconnect is disabled — reconnecting would displace the other client } ``` **Behavior:** Auto-reconnect is disabled. The client stops permanently. ### LoggedOut **Emitted:** When the session is invalidated by the server (stream error code 401 or 516) ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct LoggedOut { pub on_connect: bool, pub reason: ConnectFailureReason, } ``` **Usage:** ```rust theme={null} Event::LoggedOut(logout) => { eprintln!("Logged out (reason: {:?})", logout.reason); // Session is invalid — you must re-pair the device // Auto-reconnect is disabled } ``` **Behavior:** Auto-reconnect is disabled. The application must re-pair the device to establish a new session. ### StreamError **Emitted:** For unrecognized stream error codes (codes not matching 401, 409, 429, 503, 515, or 516) ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct StreamError { pub code: String, pub raw: Option, } ``` **Usage:** ```rust theme={null} Event::StreamError(err) => { eprintln!("Unknown stream error: {} (raw: {:?})", err.code, err.raw); } ``` Recognized stream error codes emit specific events instead of `StreamError`: * **401** → `LoggedOut` (session invalidated) * **409** → `StreamReplaced` (another client connected) * **429** → No event emitted (reconnects with extended backoff) * **503** → No event emitted (reconnects with normal backoff) * **515** → No event emitted (immediate reconnect, e.g., after pairing) * **516** → `LoggedOut` (device removed) ## Pairing Events ### PairingQrCode **Emitted:** For each QR code in rotation ```rust theme={null} Event::PairingQrCode { code: String, // ASCII art QR or data string timeout: Duration, // 60s first, 20s subsequent } ``` **Example:** ```rust theme={null} Event::PairingQrCode { code, timeout } => { println!("Scan this QR (valid {}s):", timeout.as_secs()); println!("{}", code); } ``` ### PairingCode **Emitted:** When pair code is generated ```rust theme={null} Event::PairingCode { code: String, // 8-character code timeout: Duration, // ~180 seconds } ``` **Example:** ```rust theme={null} Event::PairingCode { code, .. } => { println!("Enter {} on your phone", code); } ``` ### PairSuccess **Emitted:** When pairing completes successfully ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct PairSuccess { pub id: Jid, pub lid: Jid, pub business_name: String, pub platform: String, } ``` **Example:** ```rust theme={null} Event::PairSuccess(info) => { println!("✅ Paired as {}", info.id); println!("LID: {}", info.lid); println!("Name: {}", info.business_name); } ``` ### PairError **Emitted:** When pairing fails ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct PairError { pub id: Jid, pub lid: Jid, pub business_name: String, pub platform: String, pub error: String, } ``` ## Message Events ### Message **Emitted:** For all incoming messages (text, media, etc.) ```rust theme={null} Event::Message(Box, MessageInfo) ``` **MessageInfo structure:** ```rust theme={null} #[derive(Debug, Clone, Default, Serialize)] pub struct MessageInfo { pub source: MessageSource, pub id: MessageId, pub server_id: MessageServerId, pub r#type: String, pub push_name: String, pub timestamp: DateTime, pub category: String, pub multicast: bool, pub media_type: String, pub edit: EditAttribute, pub bot_info: Option, pub meta_info: MsgMetaInfo, pub verified_name: Option, pub device_sent_meta: Option, } #[derive(Debug, Clone, Default, Serialize)] pub struct MessageSource { pub chat: Jid, // Where it was sent (group or DM) pub sender: Jid, // Who sent the message pub is_from_me: bool, pub is_group: bool, pub addressing_mode: Option, pub sender_alt: Option, pub recipient_alt: Option, pub broadcast_list_owner: Option, pub recipient: Option, } ``` **Example:** ```rust theme={null} use waproto::whatsapp as wa; Event::Message(msg, info) => { println!("From: {} in {}", info.source.sender, info.source.chat); // Text message if let Some(text) = &msg.conversation { println!("Text: {}", text); } // Extended text (with link preview, quoted message, etc.) if let Some(ext) = &msg.extended_text_message { println!("Text: {}", ext.text.as_deref().unwrap_or("")); if let Some(context) = &ext.context_info { if let Some(quoted) = &context.quoted_message { println!("Quoted: {:?}", quoted); } } } // Image message if let Some(img) = &msg.image_message { println!("Image: {} ({}x{})", img.caption.as_deref().unwrap_or(""), img.width.unwrap_or(0), img.height.unwrap_or(0) ); } // Video, audio, document, sticker, etc. // See waproto::whatsapp::Message for all types } ``` ### Receipt **Emitted:** For delivery/read/played receipts ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct Receipt { pub source: MessageSource, pub message_ids: Vec, pub timestamp: DateTime, pub r#type: ReceiptType, pub message_sender: Jid, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub enum ReceiptType { Delivered, Sender, Retry, EncRekeyRetry, Read, ReadSelf, Played, PlayedSelf, ServerError, Inactive, PeerMsg, HistorySync, Other(String), } ``` **Example:** ```rust theme={null} Event::Receipt(receipt) => { match receipt.r#type { ReceiptType::Read => { println!("✓✓ Read by {}", receipt.source.sender); } ReceiptType::Delivered => { println!("✓ Delivered to {}", receipt.source.sender); } _ => {} } } ``` ### UndecryptableMessage **Emitted:** When a message cannot be decrypted ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct UndecryptableMessage { pub info: MessageInfo, pub is_unavailable: bool, pub unavailable_type: UnavailableType, pub decrypt_fail_mode: DecryptFailMode, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub enum UnavailableType { Unknown, ViewOnce, // View-once media already viewed } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub enum DecryptFailMode { Show, // Show placeholder in chat Hide, // Hide from chat } ``` **Example:** ```rust theme={null} Event::UndecryptableMessage(undec) => { if matches!(undec.unavailable_type, UnavailableType::ViewOnce) { println!("View-once message already consumed"); } else { eprintln!("Failed to decrypt message from {}", undec.info.source.sender); } } ``` ## Presence Events ### ChatPresence **Emitted:** For typing indicators and recording states ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct ChatPresenceUpdate { pub source: MessageSource, pub state: ChatPresence, pub media: ChatPresenceMedia, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub enum ChatPresence { Composing, Paused, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub enum ChatPresenceMedia { Text, Audio, } ``` **Example:** ```rust theme={null} Event::ChatPresence(update) => { match (update.state, update.media) { (ChatPresence::Composing, ChatPresenceMedia::Text) => { println!("{} is typing...", update.source.sender); } (ChatPresence::Composing, ChatPresenceMedia::Audio) => { println!("{} is recording audio...", update.source.sender); } (ChatPresence::Paused, _) => { println!("{} stopped typing", update.source.sender); } } } ``` ### Presence **Emitted:** For online/offline status and last seen ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct PresenceUpdate { pub from: Jid, pub unavailable: bool, pub last_seen: Option>, } ``` **Example:** ```rust theme={null} Event::Presence(update) => { if update.unavailable { println!("{} is offline", update.from); if let Some(last_seen) = update.last_seen { println!("Last seen: {}", last_seen); } } else { println!("{} is online", update.from); } } ``` ## User Update Events ### PictureUpdate **Emitted:** When a user changes their profile picture ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct PictureUpdate { pub jid: Jid, pub author: Option, pub timestamp: DateTime, pub removed: bool, pub picture_id: Option, } ``` **Fields:** * `jid` - The JID whose picture changed (user or group) * `author` - The user who made the change. Present for group picture changes (the admin who changed it). `None` for personal picture updates. * `removed` - Whether the picture was removed (`true`) or set/updated (`false`) * `picture_id` - The server-assigned picture ID. `None` for deletions. ### UserAboutUpdate **Emitted:** When a user changes their status/about ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct UserAboutUpdate { pub jid: Jid, pub status: String, pub timestamp: DateTime, } ``` ### PushNameUpdate **Emitted:** When a contact changes their display name ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct PushNameUpdate { pub jid: Jid, pub message: Box, pub old_push_name: String, pub new_push_name: String, } ``` ### SelfPushNameUpdated **Emitted:** When your own push name is updated ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct SelfPushNameUpdated { pub from_server: bool, pub old_name: String, pub new_name: String, } ``` ## Group Events ### JoinedGroup **Emitted:** When added to a group, and for each conversation received during history sync ```rust theme={null} Event::JoinedGroup(LazyConversation) ``` **LazyConversation:** Lazily-parsed conversation data. This event is used both for real-time group join notifications and as the per-conversation delivery mechanism for history sync. During history sync, the pipeline streams each conversation through a bounded channel and dispatches it as `Event::JoinedGroup(LazyConversation)`. ```rust theme={null} Event::JoinedGroup(lazy_conv) => { if let Some(conv) = lazy_conv.get() { println!("Conversation: {}", conv.id); } } ``` ### GroupUpdate **Emitted:** For each action in a group notification (subject changes, participant changes, settings updates, etc.). A single notification may produce multiple `GroupUpdate` events. ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct GroupUpdate { pub group_jid: Jid, pub participant: Option, pub participant_pn: Option, pub timestamp: DateTime, pub is_lid_addressing_mode: bool, pub action: GroupNotificationAction, } ``` **Fields:** * `group_jid` - The group this update applies to * `participant` - The admin/user who triggered the change * `participant_pn` - Phone number JID of the participant (for LID-addressed groups) * `is_lid_addressing_mode` - Whether the group uses LID addressing mode * `action` - The specific group notification action (subject change, participant add/remove/promote/demote, description change, etc.) **Example:** ```rust theme={null} Event::GroupUpdate(update) => { println!("Group {} updated by {:?}: {:?}", update.group_jid, update.participant, update.action); } ``` ## Contact Notification Events These events are emitted from `` stanzas sent by the server. They are distinct from `ContactUpdate`, which comes from app-state sync mutations. ### ContactUpdated **Emitted:** When a contact's profile changes (server notification) ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct ContactUpdated { pub jid: Jid, pub timestamp: DateTime, } ``` **Wire format:** `` When you receive this event, you should invalidate any cached presence or profile picture data for the contact. WhatsApp Web resets its `PresenceCollection` and refreshes the profile picture thumbnail on this event. **Example:** ```rust theme={null} Event::ContactUpdated(update) => { println!("Contact {} profile changed at {}", update.jid, update.timestamp); // Invalidate cached presence/profile data // Re-fetch profile picture if needed } ``` ### ContactNumberChanged **Emitted:** When a contact changes their phone number ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct ContactNumberChanged { /// Old phone number JID. pub old_jid: Jid, /// New phone number JID. pub new_jid: Jid, /// Old LID (if provided by server). pub old_lid: Option, /// New LID (if provided by server). pub new_lid: Option, pub timestamp: DateTime, } ``` **Wire format:** `` The library automatically creates LID-PN mappings when LID attributes are present (`old_lid→old_jid` and `new_lid→new_jid`). WhatsApp Web generates a system notification message in both the old and new chats. **Example:** ```rust theme={null} Event::ContactNumberChanged(change) => { println!("Contact changed number: {} -> {}", change.old_jid, change.new_jid); if let Some(new_lid) = &change.new_lid { println!("New LID: {}", new_lid); } // Update your contact records to use the new JID } ``` ### ContactSyncRequested **Emitted:** When the server requests a full contact re-sync ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct ContactSyncRequested { /// If present, only sync contacts modified after this timestamp. pub after: Option>, pub timestamp: DateTime, } ``` **Wire format:** `` **Example:** ```rust theme={null} Event::ContactSyncRequested(sync) => { if let Some(after) = sync.after { println!("Server requests contact sync for changes after {}", after); } else { println!("Server requests full contact sync"); } } ``` The server may also send `` and `` child actions in contacts notifications for lightweight roster changes. These are acknowledged automatically and do not emit events. ## Chat State Events ### PinUpdate **Emitted:** When a chat is pinned/unpinned ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct PinUpdate { pub jid: Jid, pub timestamp: DateTime, pub action: Box, pub from_full_sync: bool, } ``` ### MuteUpdate **Emitted:** When a chat is muted/unmuted ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct MuteUpdate { pub jid: Jid, pub timestamp: DateTime, pub action: Box, pub from_full_sync: bool, } ``` ### ArchiveUpdate **Emitted:** When a chat is archived/unarchived ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct ArchiveUpdate { pub jid: Jid, pub timestamp: DateTime, pub action: Box, pub from_full_sync: bool, } ``` ### StarUpdate **Emitted:** When a message is starred or unstarred ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct StarUpdate { pub chat_jid: Jid, pub participant_jid: Option, pub message_id: String, pub from_me: bool, pub timestamp: DateTime, pub action: Box, pub from_full_sync: bool, } ``` **Fields:** * `chat_jid` - The chat containing the starred message * `participant_jid` - The sender of the message (only for group messages from others; `None` for self-authored or 1-on-1 messages) * `message_id` - The ID of the starred/unstarred message * `from_me` - Whether the starred message was sent by you **Example:** ```rust theme={null} Event::StarUpdate(update) => { println!("Message {} in {} was {}starred", update.message_id, update.chat_jid, if update.action.starred.unwrap_or(false) { "" } else { "un" } ); } ``` ### MarkChatAsReadUpdate **Emitted:** When a chat is marked as read ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct MarkChatAsReadUpdate { pub jid: Jid, pub timestamp: DateTime, pub action: Box, pub from_full_sync: bool, } ``` ## History Sync Events ### HistorySync **Emitted:** For chat history synchronization ```rust theme={null} Event::HistorySync(HistorySync) ``` **HistorySync:** Protobuf message containing chat history History sync data is processed through a RAM-optimized pipeline. The raw protobuf is parsed with a hand-rolled field walker (not a full decode) and conversations are streamed through a bounded channel (capacity 4) as zero-copy `Bytes` slices. If no event handlers are registered, conversation extraction is skipped entirely at the protobuf level. ### OfflineSyncPreview **Emitted:** Preview of pending offline sync data when reconnecting ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct OfflineSyncPreview { pub total: i32, pub app_data_changes: i32, pub messages: i32, pub notifications: i32, pub receipts: i32, } ``` **Example:** ```rust theme={null} Event::OfflineSyncPreview(preview) => { println!("Syncing {} items ({} messages, {} notifications)", preview.total, preview.messages, preview.notifications); } ``` ### OfflineSyncCompleted **Emitted:** When offline sync completes after reconnection ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct OfflineSyncCompleted { pub count: i32, } ``` **Example:** ```rust theme={null} Event::OfflineSyncCompleted(sync) => { println!("Offline sync completed: {} items processed", sync.count); } ``` Offline sync happens automatically when the client reconnects after being disconnected. The client tracks progress internally and emits these events to notify your application of sync status. If the server does not complete offline sync within 60 seconds, the client forces completion via a timeout fallback — `OfflineSyncCompleted` is still emitted with the count of items processed so far. This prevents startup from blocking indefinitely. ## Device Events ### DeviceListUpdate **Emitted:** When a user's device list changes ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct DeviceListUpdate { pub user: Jid, pub lid_user: Option, pub update_type: DeviceListUpdateType, pub devices: Vec, pub key_index: Option, pub contact_hash: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub enum DeviceListUpdateType { Add, Remove, Update, } ``` ### BusinessStatusUpdate **Emitted:** When a business account status changes ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct BusinessStatusUpdate { pub jid: Jid, pub update_type: BusinessUpdateType, pub timestamp: i64, pub target_jid: Option, pub hash: Option, pub verified_name: Option, pub product_ids: Vec, pub collection_ids: Vec, pub subscriptions: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub enum BusinessUpdateType { RemovedAsBusiness, VerifiedNameChanged, ProfileUpdated, ProductsUpdated, CollectionsUpdated, SubscriptionsUpdated, Unknown, } ``` ## Notification Events ### DisappearingModeChanged **Emitted:** When a contact changes their default disappearing messages setting. Sent by the server as a `` stanza. ```rust theme={null} #[derive(Debug, Clone, Serialize)] pub struct DisappearingModeChanged { pub from: Jid, pub duration: u32, pub setting_timestamp: u64, } ``` **Fields:** * `from` - The contact whose disappearing messages setting changed * `duration` - New duration in seconds (`0` = disabled, `86400` = 24 hours, `604800` = 7 days, etc.) * `setting_timestamp` - Unix timestamp (seconds) when the setting was changed You should only apply this update if `setting_timestamp` is newer than your previously stored value for this contact. This prevents out-of-order updates from overwriting newer settings. **Example:** ```rust theme={null} Event::DisappearingModeChanged(change) => { let status = if change.duration == 0 { "disabled".to_string() } else { format!("enabled ({}s)", change.duration) }; println!("Contact {} changed disappearing messages: {}", change.from, status); } ``` ## Event Handler Patterns ### Bot Builder Pattern ```rust theme={null} use whatsapp_rust::bot::Bot; use wacore::types::events::Event; let mut bot = Bot::builder() .with_backend(backend) .on_event(|event, client| async move { match event { Event::Message(msg, info) => { // Handle message } Event::Connected(_) => { // Handle connection } _ => {} } }) .build() .await?; ``` ### Multiple Handlers ```rust theme={null} struct MessageHandler; impl EventHandler for MessageHandler { fn handle_event(&self, event: &Event) { if let Event::Message(msg, info) = event { // Handle messages } } } struct ConnectionHandler; impl EventHandler for ConnectionHandler { fn handle_event(&self, event: &Event) { match event { Event::Connected(_) => { /* ... */ } Event::Disconnected(_) => { /* ... */ } _ => {} } } } client.core.event_bus.add_handler(Arc::new(MessageHandler)); client.core.event_bus.add_handler(Arc::new(ConnectionHandler)); ``` ### Async Event Handlers ```rust theme={null} use tokio::sync::mpsc; let (tx, mut rx) = mpsc::unbounded_channel(); struct AsyncHandler { tx: mpsc::UnboundedSender, } impl EventHandler for AsyncHandler { fn handle_event(&self, event: &Event) { let _ = self.tx.send(event.clone()); } } client.core.event_bus.add_handler(Arc::new(AsyncHandler { tx })); // Process events asynchronously tokio::spawn(async move { while let Some(event) = rx.recv().await { // Async processing } }); ``` ## Performance Optimization ### LazyConversation **Purpose:** Avoid parsing large protobuf messages unless needed. This is a key part of the history sync RAM optimization — raw conversation bytes flow through the pipeline as zero-copy `Bytes` slices, and protobuf decoding only happens if your code actually accesses the data. ```rust theme={null} pub struct LazyConversation { raw_bytes: Bytes, // Zero-copy reference-counted bytes parsed: OnceLock, // Parsed on first access } impl LazyConversation { /// Creates from owned bytes (converts to Bytes internally) pub fn new(raw_bytes: Vec) -> Self; /// True zero-copy from existing Bytes (used by history sync pipeline) pub fn from_bytes(raw_bytes: Bytes) -> Self; /// Parse on first access, returns None if conversation ID is empty pub fn get(&self) -> Option<&wa::Conversation>; /// Parse on first access, panics on failure pub fn conversation(&self) -> &wa::Conversation; } ``` **Memory optimization:** On first parse, the `messages` field is immediately cleared and shrunk (`messages.clear()` + `messages.shrink_to_fit()`) because history sync conversations embed full `WebMessageInfo` arrays that can be very large. This prevents parsed conversations from retaining message data in memory. **Cloning:** `Bytes` is reference-counted so cloning the raw data is O(1). However, `parsed` uses a plain `OnceLock` (not `Arc`), so each clone gets its own parse cache and parses independently. This is acceptable because parsing is idempotent and the common case is a single handler. If multi-handler parsing cost becomes an issue, wrapping `parsed` in `Arc>` is a straightforward upgrade. **Usage:** ```rust theme={null} Event::JoinedGroup(lazy_conv) => { // No parsing cost unless you access it if interested_in_group() { if let Some(conv) = lazy_conv.get() { // Parse happens here (only once, then cached) process_group(conv); } } // If you never call .get(), the raw bytes are never decoded } ``` ### SharedData **Purpose:** Cheap cloning of large event data ```rust theme={null} // wacore/src/types/events.rs:14-39 pub struct SharedData(pub Arc); impl std::ops::Deref for SharedData { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } ``` **Usage:** ```rust theme={null} let shared = SharedData::new(expensive_data); let clone1 = shared.clone(); // O(1) - just increments Arc counter let clone2 = shared.clone(); // O(1) ``` ## Best Practices ### Event Filtering ```rust theme={null} .on_event(|event, client| async move { // Only handle events you care about match event { Event::Message(msg, info) if !info.source.is_from_me => { // Only handle messages from others } Event::Message(msg, info) if info.is_group => { // Only handle group messages } _ => {} } }) ``` ### Error Handling ```rust theme={null} .on_event(|event, client| async move { if let Err(e) = handle_event(event, client).await { eprintln!("Event handler error: {}", e); } }) async fn handle_event(event: &Event, client: Arc) -> Result<()> { match event { Event::Message(msg, info) => { process_message(msg, info, client).await? } _ => {} } Ok(()) } ``` ### Spawning Tasks ```rust theme={null} .on_event(|event, client| async move { match event { Event::Message(msg, info) => { let client = client.clone(); let msg = msg.clone(); let info = info.clone(); tokio::spawn(async move { // Process in background process_message(&msg, &info, &client).await; }); } _ => {} } }) ``` ## Related Sections Understand the event bus system Learn about pairing events Sending and receiving messages Complete client API reference # Storage Source: https://whatsapp-rust.jlucaso.com/concepts/storage Storage backends, PersistenceManager, and state management in whatsapp-rust ## Overview WhatsApp-Rust uses a layered storage architecture with pluggable backends. The `PersistenceManager` manages all state changes, while the `Backend` trait defines storage operations for device data, Signal protocol keys, app state sync, and protocol-specific data. ## Architecture ```mermaid theme={null} graph TB A[Client] --> B[PersistenceManager] B --> C[Device State] B --> D[Backend Trait] D --> E[SignalStore] D --> F[AppSyncStore] D --> G[ProtocolStore] D --> H[DeviceStore] E --> I[SqliteStore] F --> I G --> I H --> I I --> J[SQLite Database] ``` ## PersistenceManager **Location:** `src/store/persistence_manager.rs` ### Purpose Manages all device state changes and persistence operations. Acts as the gatekeeper for state modifications. ### Structure ```rust theme={null} pub struct PersistenceManager { device: Arc>, backend: Arc, dirty: Arc, save_notify: Arc, } ``` **Fields:** * `device`: In-memory device state (protected by RwLock) * `backend`: Storage backend implementation * `dirty`: Flag indicating unsaved changes * `save_notify`: Notification channel for background saver ### Initialization ```rust theme={null} impl PersistenceManager { pub async fn new(backend: Arc) -> Result { // Ensure device row exists let exists = backend.exists().await?; if !exists { backend.create().await?; } // Load existing data or create new let device_data = backend.load().await?; let device = if let Some(data) = device_data { let mut dev = Device::new(backend.clone()); dev.load_from_serializable(data); dev } else { Device::new(backend.clone()) }; Ok(Self { device: Arc::new(RwLock::new(device)), backend, dirty: Arc::new(AtomicBool::new(false)), save_notify: Arc::new(Notify::new()), }) } } ``` ### Key Methods #### get\_device\_snapshot **Purpose:** Read-only access to device state ```rust theme={null} pub async fn get_device_snapshot(&self) -> Device { self.device.read().await.clone() } ``` **Usage:** ```rust theme={null} let device = client.persistence_manager.get_device_snapshot().await; println!("Device ID: {:?}", device.pn); println!("Push Name: {}", device.push_name); ``` #### modify\_device **Purpose:** Modify device state with automatic dirty tracking ```rust theme={null} pub async fn modify_device(&self, modifier: F) -> R where F: FnOnce(&mut Device) -> R, { let mut device_guard = self.device.write().await; let result = modifier(&mut device_guard); self.dirty.store(true, Ordering::Relaxed); self.save_notify.notify_one(); result } ``` **Usage:** ```rust theme={null} client.persistence_manager.modify_device(|device| { device.push_name = "New Name".to_string(); }).await; ``` #### process\_command **Purpose:** Apply state changes via `DeviceCommand` ```rust theme={null} pub async fn process_command(&self, command: DeviceCommand) { self.modify_device(|device| { apply_command_to_device(device, command); }).await; } ``` **Usage:** ```rust theme={null} client.persistence_manager .process_command(DeviceCommand::SetPushName("New Name".to_string())) .await; ``` ### Background Saver **Purpose:** Periodically persist dirty state to disk ```rust theme={null} pub fn run_background_saver(self: Arc, interval: Duration) { tokio::spawn(async move { loop { tokio::select! { _ = self.save_notify.notified() => { debug!("Save notification received."); } _ = sleep(interval) => {} } if let Err(e) = self.save_to_disk().await { error!("Error saving device state: {e}"); } } }); } async fn save_to_disk(&self) -> Result<(), StoreError> { if self.dirty.swap(false, Ordering::AcqRel) { let device_guard = self.device.read().await; let serializable_device = device_guard.to_serializable(); drop(device_guard); self.backend.save(&serializable_device).await?; } Ok(()) } ``` **Behavior:** * Wakes up when notified or after interval * Only saves if dirty flag is set * Uses optimistic locking (dirty flag) **Start background saver:** ```rust theme={null} let persistence_manager = Arc::new(PersistenceManager::new(backend).await?); persistence_manager.clone().run_background_saver(Duration::from_secs(5)); ``` ## Backend Trait **Location:** `wacore/src/store/traits.rs` ### Overview The `Backend` trait is automatically implemented for any type that implements all four domain-specific traits: ```rust theme={null} pub trait Backend: SignalStore + AppSyncStore + ProtocolStore + DeviceStore + Send + Sync {} ``` ### Domain Traits ## SignalStore **Purpose:** Signal protocol cryptographic operations ```rust theme={null} #[async_trait] pub trait SignalStore: Send + Sync { // Identity Operations async fn put_identity(&self, address: &str, key: [u8; 32]) -> Result<()>; async fn load_identity(&self, address: &str) -> Result>>; async fn delete_identity(&self, address: &str) -> Result<()>; // Session Operations async fn get_session(&self, address: &str) -> Result>>; async fn put_session(&self, address: &str, session: &[u8]) -> Result<()>; async fn delete_session(&self, address: &str) -> Result<()>; async fn has_session(&self, address: &str) -> Result; // PreKey Operations async fn store_prekey(&self, id: u32, record: &[u8], uploaded: bool) -> Result<()>; async fn store_prekeys_batch(&self, keys: &[(u32, Vec)], uploaded: bool) -> Result<()>; async fn load_prekey(&self, id: u32) -> Result>>; async fn remove_prekey(&self, id: u32) -> Result<()>; async fn get_max_prekey_id(&self) -> Result; // Signed PreKey Operations async fn store_signed_prekey(&self, id: u32, record: &[u8]) -> Result<()>; async fn load_signed_prekey(&self, id: u32) -> Result>>; async fn load_all_signed_prekeys(&self) -> Result)>>; async fn remove_signed_prekey(&self, id: u32) -> Result<()>; // Sender Key Operations (for groups) async fn put_sender_key(&self, address: &str, record: &[u8]) -> Result<()>; async fn get_sender_key(&self, address: &str) -> Result>>; async fn delete_sender_key(&self, address: &str) -> Result<()>; } ``` **Usage Example:** ```rust theme={null} // Store identity key for a contact backend.put_identity( "15551234567@s.whatsapp.net:0", identity_key ).await?; // Load session for decryption if let Some(session) = backend.get_session("15551234567@s.whatsapp.net:0").await? { // Decrypt message using session } ``` ### SignalStoreCache **Location:** `src/store/signal_cache.rs` The `SignalStoreCache` provides an in-memory cache layer for Signal protocol state, matching WhatsApp Web's `SignalStoreCache` implementation. All crypto operations read and write through this cache, with database writes deferred to explicit `flush()` calls. ```rust theme={null} pub struct SignalStoreCache { sessions: Mutex, identities: Mutex, sender_keys: Mutex, } // Internal per-domain state struct StoreState { cache: HashMap, Option>>, // None = known-absent dirty: HashSet>, // Modified keys pending flush deleted: HashSet>, // Deleted keys pending flush } ``` **Key features:** * **Deferred writes**: Changes are accumulated in memory and batch-written on `flush()` * **Negative caching**: Known-absent keys are cached as `None` to avoid repeated DB lookups * **Independent locking**: Sessions, identities, and sender keys each have their own mutex * **O(1) reads**: Values stored as `Arc<[u8]>` for cheap cloning (reference count bump) * **O(1) key cloning**: Keys stored as `Arc` so cloning a key (needed for both cache and dirty/deleted sets) is a refcount bump instead of a heap allocation * **Single-allocation keys**: Session lock keys use `to_protocol_address_string()` (format: `user[:device]@server.0`) which builds the key string in one allocation, avoiding the two-allocation overhead of constructing a `ProtocolAddress` then calling `.to_string()`. See [Signal Protocol performance](/advanced/signal-protocol#single-allocation-session-lock-keys) for details **Cache operations:** ```rust theme={null} // Read (loads from backend if not cached) let session = cache.get_session(address, &backend).await?; // Write (marks as dirty, doesn't hit backend) cache.put_session(address, &data).await; // Delete (marks for deletion on flush) cache.delete_session(address).await; // Persist all dirty state to backend cache.flush(&backend).await?; // Clear cache (on disconnect/reconnect) cache.clear().await; ``` **Flush behavior:** * Acquires all three mutexes to ensure consistency * Only clears dirty tracking after ALL writes succeed * On failure, dirty state is preserved for retry on next flush ## AppSyncStore **Purpose:** WhatsApp app state synchronization ```rust theme={null} #[async_trait] pub trait AppSyncStore: Send + Sync { // Sync Keys async fn get_sync_key(&self, key_id: &[u8]) -> Result>; async fn set_sync_key(&self, key_id: &[u8], key: AppStateSyncKey) -> Result<()>; // Version Tracking async fn get_version(&self, name: &str) -> Result; async fn set_version(&self, name: &str, state: HashState) -> Result<()>; // Mutation MACs async fn put_mutation_macs( &self, name: &str, version: u64, mutations: &[AppStateMutationMAC], ) -> Result<()>; async fn get_mutation_mac(&self, name: &str, index_mac: &[u8]) -> Result>>; async fn delete_mutation_macs(&self, name: &str, index_macs: &[Vec]) -> Result<()>; async fn get_latest_sync_key_id(&self) -> Result>>; } ``` **AppStateSyncKey Structure:** ```rust theme={null} #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct AppStateSyncKey { pub key_data: Vec, pub fingerprint: Vec, pub timestamp: i64, } ``` **Collections:** * `critical_block` - Blocked contacts, push names * `regular_high` - Mute settings, starred messages, contact info * `regular_low` - Archive settings, pin settings * `regular` - Other chat settings ## ProtocolStore **Purpose:** WhatsApp Web protocol-specific storage ```rust theme={null} #[async_trait] pub trait ProtocolStore: Send + Sync { // SKDM Tracking (Sender Key Distribution Messages) async fn get_skdm_recipients(&self, group_jid: &str) -> Result>; async fn add_skdm_recipients(&self, group_jid: &str, device_jids: &[Jid]) -> Result<()>; async fn clear_skdm_recipients(&self, group_jid: &str) -> Result<()>; // LID-PN Mapping (Long-term ID to Phone Number) async fn get_lid_mapping(&self, lid: &str) -> Result>; async fn get_pn_mapping(&self, phone: &str) -> Result>; async fn put_lid_mapping(&self, entry: &LidPnMappingEntry) -> Result<()>; async fn get_all_lid_mappings(&self) -> Result>; // Base Key Collision Detection async fn save_base_key(&self, address: &str, message_id: &str, base_key: &[u8]) -> Result<()>; async fn has_same_base_key(&self, address: &str, message_id: &str, current_base_key: &[u8]) -> Result; async fn delete_base_key(&self, address: &str, message_id: &str) -> Result<()>; // Device Registry async fn update_device_list(&self, record: DeviceListRecord) -> Result<()>; async fn get_devices(&self, user: &str) -> Result>; // Sender Key Status (Lazy Deletion) async fn mark_forget_sender_key(&self, group_jid: &str, participant: &str) -> Result<()>; async fn consume_forget_marks(&self, group_jid: &str) -> Result>; // TcToken Storage (Trusted Contact Tokens) async fn get_tc_token(&self, jid: &str) -> Result>; async fn put_tc_token(&self, jid: &str, entry: &TcTokenEntry) -> Result<()>; async fn delete_tc_token(&self, jid: &str) -> Result<()>; async fn get_all_tc_token_jids(&self) -> Result>; async fn delete_expired_tc_tokens(&self, cutoff_timestamp: i64) -> Result; // Sent Message Store (retry support, matches WA Web's getMessageTable) async fn store_sent_message(&self, chat_jid: &str, message_id: &str, payload: &[u8]) -> Result<()>; async fn take_sent_message(&self, chat_jid: &str, message_id: &str) -> Result>>; async fn delete_expired_sent_messages(&self, cutoff_timestamp: i64) -> Result; } ``` **LidPnMappingEntry:** ```rust theme={null} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LidPnMappingEntry { pub lid: String, pub phone_number: String, pub created_at: i64, pub updated_at: i64, pub learning_source: String, } ``` ### LidPnCache **Location:** `src/lid_pn_cache.rs` The `LidPnCache` provides a bounded in-memory cache for LID to phone number mappings, used for Signal address resolution. WhatsApp Web uses LID-based addresses for Signal sessions when available. ```rust theme={null} pub struct LidPnCache { lid_to_entry: Cache, pn_to_entry: Cache, } ``` **Bounds (prevents unbounded memory growth):** * **Max capacity**: 10,000 entries per map * **Time-to-idle TTL**: 1 hour **Bidirectional lookups:** ```rust theme={null} // Get LID for a phone number let lid = cache.get_current_lid("15551234567").await; // Get phone number for a LID let phone = cache.get_phone_number("100000012345678").await; ``` **Timestamp conflict resolution:** When multiple LIDs exist for the same phone number, the entry with the most recent `created_at` timestamp wins for the PN → LID lookup: ```rust theme={null} // Add mapping (only updates PN map if newer timestamp) cache.add(entry).await; ``` **Initialization:** ```rust theme={null} // Warm up cache from persistent storage on client init let entries = backend.get_all_lid_mappings().await?; cache.warm_up(entries).await; ``` **Learning sources:** * `usync` - User sync responses * `peer_pn_message` / `peer_lid_message` - Peer messages * `pairing` - Device pairing * `device_notification` - Device notifications * `blocklist_active` / `blocklist_inactive` - Blocklist operations **DeviceListRecord:** ```rust theme={null} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceListRecord { pub user: String, pub devices: Vec, pub timestamp: i64, pub phash: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceInfo { pub device_id: u32, pub key_index: Option, } ``` **TcTokenEntry:** ```rust theme={null} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TcTokenEntry { pub token: Vec, pub token_timestamp: i64, pub sender_timestamp: Option, } ``` ## DeviceStore **Purpose:** Device data persistence ```rust theme={null} #[async_trait] pub trait DeviceStore: Send + Sync { async fn save(&self, device: &Device) -> Result<()>; async fn load(&self) -> Result>; async fn exists(&self) -> Result; async fn create(&self) -> Result; async fn snapshot_db(&self, _name: &str, _extra_content: Option<&[u8]>) -> Result<()>; } ``` **Device Structure:** ```rust theme={null} // wacore/src/store/device.rs #[derive(Clone, Serialize, Deserialize)] pub struct Device { pub pn: Option, // Phone number JID pub lid: Option, // Long-term identifier pub push_name: String, // Display name pub platform: String, // Platform identifier pub registration_id: u32, // Signal registration ID pub adv_secret_key: [u8; 32], // Advertisement secret pub identity_key: KeyPair, // Signal identity keypair pub noise_key: KeyPair, // Noise protocol keypair pub account: Option, pub next_pre_key_id: u32, // Monotonic counter for prekey IDs // ... more fields } ``` ## SqliteStore implementation **Location:** `storages/sqlite-storage/src/lib.rs` As of v0.3, the `whatsapp-rust-sqlite-storage` crate bundles SQLite by default via the `bundled-sqlite` feature. You no longer need SQLite installed as a system dependency. To link against a system SQLite instead, disable the default features on the crate. ### Database Schema ```sql theme={null} -- Device table CREATE TABLE device ( device_id INTEGER PRIMARY KEY AUTOINCREMENT, data BLOB NOT NULL ); -- Signal protocol tables CREATE TABLE identities ( device_id INTEGER NOT NULL, address TEXT NOT NULL, key BLOB NOT NULL, PRIMARY KEY (device_id, address) ); CREATE TABLE sessions ( device_id INTEGER NOT NULL, address TEXT NOT NULL, record BLOB NOT NULL, PRIMARY KEY (device_id, address) ); CREATE TABLE prekeys ( device_id INTEGER NOT NULL, prekey_id INTEGER NOT NULL, record BLOB NOT NULL, uploaded BOOLEAN NOT NULL, PRIMARY KEY (device_id, prekey_id) ); CREATE TABLE signed_prekeys ( device_id INTEGER NOT NULL, signed_prekey_id INTEGER NOT NULL, record BLOB NOT NULL, PRIMARY KEY (device_id, signed_prekey_id) ); CREATE TABLE sender_keys ( device_id INTEGER NOT NULL, address TEXT NOT NULL, record BLOB NOT NULL, PRIMARY KEY (device_id, address) ); -- App state sync tables CREATE TABLE app_state_sync_keys ( device_id INTEGER NOT NULL, key_id BLOB NOT NULL, key_data BLOB NOT NULL, fingerprint BLOB NOT NULL, timestamp INTEGER NOT NULL, PRIMARY KEY (device_id, key_id) ); CREATE TABLE app_state_versions ( device_id INTEGER NOT NULL, name TEXT NOT NULL, version INTEGER NOT NULL, hash BLOB, PRIMARY KEY (device_id, name) ); CREATE TABLE app_state_mutation_macs ( device_id INTEGER NOT NULL, name TEXT NOT NULL, version INTEGER NOT NULL, index_mac BLOB NOT NULL, value_mac BLOB NOT NULL, PRIMARY KEY (device_id, name, version, index_mac) ); -- Protocol tables CREATE TABLE skdm_recipients ( device_id INTEGER NOT NULL, group_jid TEXT NOT NULL, device_jid TEXT NOT NULL, timestamp INTEGER NOT NULL, PRIMARY KEY (device_id, group_jid, device_jid) ); CREATE TABLE lid_pn_mappings ( device_id INTEGER NOT NULL, lid TEXT NOT NULL, phone_number TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, learning_source TEXT NOT NULL, PRIMARY KEY (device_id, lid) ); CREATE TABLE device_lists ( device_id INTEGER NOT NULL, user TEXT NOT NULL, devices BLOB NOT NULL, timestamp INTEGER NOT NULL, phash TEXT, PRIMARY KEY (device_id, user) ); CREATE TABLE tc_tokens ( device_id INTEGER NOT NULL, jid TEXT NOT NULL, token BLOB NOT NULL, token_timestamp INTEGER NOT NULL, sender_timestamp INTEGER, PRIMARY KEY (device_id, jid) ); -- Sent message store for retry handling CREATE TABLE sent_messages ( chat_jid TEXT NOT NULL, message_id TEXT NOT NULL, payload BLOB NOT NULL, device_id INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), PRIMARY KEY (chat_jid, message_id, device_id) ); CREATE INDEX idx_sent_messages_created ON sent_messages (created_at, device_id); ``` ### Multi-Account Support **Each device has unique `device_id`:** ```rust theme={null} use whatsapp_rust::store::SqliteStore; // Account 1 let backend1 = Arc::new(SqliteStore::new_for_device("whatsapp.db", 1).await?); let pm1 = PersistenceManager::new(backend1).await?; // Account 2 let backend2 = Arc::new(SqliteStore::new_for_device("whatsapp.db", 2).await?); let pm2 = PersistenceManager::new(backend2).await?; ``` **All tables scoped by `device_id`:** ```sql theme={null} SELECT * FROM sessions WHERE device_id = 1 AND address = ?; ``` ## DeviceCommand Pattern **Location:** `src/store/commands.rs`, `wacore/src/store/commands.rs` ### Purpose Provide type-safe, centralized state mutations. ### Command Enum ```rust theme={null} pub enum DeviceCommand { SetId(Option), SetLid(Option), SetPushName(String), SetPlatform(String), SetAccount(Option), SetNextPreKeyId(u32), // ... more commands } ``` ### Command Application ```rust theme={null} pub fn apply_command_to_device(device: &mut Device, command: DeviceCommand) { match command { DeviceCommand::SetId(id) => { device.pn = id; } DeviceCommand::SetLid(lid) => { device.lid = lid; } DeviceCommand::SetPushName(name) => { device.push_name = name; } DeviceCommand::SetPlatform(platform) => { device.platform = platform; } DeviceCommand::SetAccount(account) => { device.account = account; } // ... } } ``` ### Usage ```rust theme={null} // ✅ Correct: Use DeviceCommand client.persistence_manager .process_command(DeviceCommand::SetPushName("New Name".to_string())) .await; // ❌ Wrong: Direct modification let mut device = client.device.write().await; device.push_name = "New Name".to_string(); // DON'T DO THIS ``` ## State Management Best Practices ### Read-Only Access ```rust theme={null} // Cheap snapshot for read-only access let device = client.persistence_manager.get_device_snapshot().await; println!("JID: {:?}", device.pn); ``` ### Modifications ```rust theme={null} // Use process_command for type-safe mutations client.persistence_manager .process_command(DeviceCommand::SetPushName(name)) .await; ``` ### Bulk Operations ```rust theme={null} // Use modify_device for multiple changes client.persistence_manager.modify_device(|device| { device.push_name = "New Name".to_string(); device.platform = "Chrome".to_string(); }).await; ``` ### Critical Errors ```rust theme={null} // Create snapshot for debugging crypto failures client.persistence_manager .create_snapshot("decrypt_failure", Some(&failed_message_bytes)) .await?; ``` ## Custom Backend Implementation ### Example: PostgreSQL Backend ```rust theme={null} use async_trait::async_trait; use wacore::store::traits::*; pub struct PostgresStore { pool: sqlx::PgPool, device_id: i32, } #[async_trait] impl SignalStore for PostgresStore { async fn put_identity(&self, address: &str, key: [u8; 32]) -> Result<()> { sqlx::query( "INSERT INTO identities (device_id, address, key) VALUES ($1, $2, $3) ON CONFLICT (device_id, address) DO UPDATE SET key = $3" ) .bind(self.device_id) .bind(address) .bind(&key[..]) .execute(&self.pool) .await?; Ok(()) } // ... implement other methods } #[async_trait] impl AppSyncStore for PostgresStore { // ... implement methods } #[async_trait] impl ProtocolStore for PostgresStore { // ... implement methods } #[async_trait] impl DeviceStore for PostgresStore { // ... implement methods } // Backend trait automatically implemented ``` ### Usage ```rust theme={null} let backend = Arc::new(PostgresStore::new(pool, device_id)); let pm = PersistenceManager::new(backend).await?; ``` ## Migration & Debugging ### Database Snapshots **Feature flag:** `debug-snapshots` ```toml theme={null} [dependencies] whatsapp-rust = { version = "0.3", features = ["debug-snapshots"] } ``` **Usage:** ```rust theme={null} // Trigger snapshot on critical errors if let Err(e) = decrypt_message(&msg).await { client.persistence_manager .create_snapshot( &format!("decrypt_failure_{}", msg.id), Some(&serialized_message) ) .await?; return Err(e); } ``` **Output:** ``` snapshots/ ├── decrypt_failure_1234567890_whatsapp.db └── decrypt_failure_1234567890_extra.bin ``` ## Pluggable cache store **Location:** `src/cache_store.rs`, `src/cache_config.rs`, `wacore/src/store/cache.rs` ### Overview By default, whatsapp-rust uses in-process [moka](https://github.com/moka-rs/moka) caches for group metadata, device lists, device registry, and LID-PN mappings. The pluggable cache store adapter lets you replace any of these with an external backend (Redis, Memcached, etc.) by implementing the `CacheStore` trait. ```mermaid theme={null} graph TB A[Client] --> B[TypedCache] B -->|Default| C[Moka In-Process] B -->|Custom| D[CacheStore Trait] D --> E[Redis] D --> F[Memcached] D --> G[Your Backend] ``` ### CacheStore trait ```rust theme={null} #[async_trait] pub trait CacheStore: Send + Sync + 'static { async fn get(&self, namespace: &str, key: &str) -> anyhow::Result>>; async fn set(&self, namespace: &str, key: &str, value: &[u8], ttl: Option) -> anyhow::Result<()>; async fn delete(&self, namespace: &str, key: &str) -> anyhow::Result<()>; async fn clear(&self, namespace: &str) -> anyhow::Result<()>; async fn entry_count(&self, namespace: &str) -> anyhow::Result { Ok(0) } } ``` Each logical cache uses a unique namespace string (e.g., `"group"`, `"device"`, `"lid_pn_by_lid"`). Implementations should partition keys by namespace — for example, a Redis implementation might prefix keys as `{namespace}:{key}`. Cache operations are best-effort. The client falls back gracefully when cache reads fail (treats as miss) and logs warnings on write failures. ### TypedCache `TypedCache` is a generic wrapper that dispatches to either moka or a custom `CacheStore` backend. The moka path has zero extra overhead — values are stored in-process without any serialization. The custom-store path serializes values with `serde_json` and keys via `Display`. ```rust theme={null} // Moka path (zero overhead) let cache = TypedCache::from_moka(moka_cache); // Custom store path (serde_json serialization) let cache = TypedCache::from_store(store, "group", Some(Duration::from_secs(3600))); ``` ### CacheStores configuration The `CacheStores` struct controls which caches use custom backends: ```rust theme={null} pub struct CacheStores { pub group_cache: Option>, pub device_cache: Option>, pub device_registry_cache: Option>, pub lid_pn_cache: Option>, } ``` Fields left as `None` keep the default moka behavior. Use `CacheStores::all(store)` to set the same backend for all pluggable caches at once. Coordination caches (`session_locks`, `message_queues`, `message_enqueue_locks`), the signal write-behind cache, and `pdo_pending_requests` always stay in-process — they hold live Rust objects (mutexes, channel senders) that cannot be serialized to an external store. ### Usage with Bot builder ```rust theme={null} use whatsapp_rust::{CacheConfig, CacheStores}; use std::sync::Arc; let redis = Arc::new(MyRedisCacheStore::new("redis://localhost:6379")); let bot = Bot::builder() .with_backend(backend) .with_transport_factory(transport) .with_http_client(http_client) .with_cache_config(CacheConfig { cache_stores: CacheStores { group_cache: Some(redis.clone()), device_cache: Some(redis.clone()), ..Default::default() }, ..Default::default() }) .build() .await?; ``` Or use `CacheStores::all()` to route all pluggable caches to the same backend: ```rust theme={null} let config = CacheConfig { cache_stores: CacheStores::all(redis.clone()), ..Default::default() }; ``` See [Custom backends guide](/guides/custom-backends#custom-cache-store) for a full implementation example. ## Granular cache patching Instead of invalidating a cache entry and re-fetching from the server on every change notification, whatsapp-rust applies granular patches to cached values in place. This eliminates an extra IQ round-trip per update and keeps caches consistent in real time. ### How it works All patching follows the same pattern: read the cached value, mutate it, and write it back. There is no atomic compare-and-swap — the `get` → mutate → `insert` sequence can race with concurrent notifications, but this is acceptable because the cache is best-effort and a full server fetch on the next read corrects any drift. ```rust theme={null} // Pseudocode for the patching pattern if let Some(mut cached) = cache.get(&key).await { cached.apply_change(notification_data); cache.insert(key, cached).await; } // If the key isn't cached, the patch is a no-op — // the next read fetches authoritative state from the server. ``` ### Patched cache domains Granular patching is implemented in three areas: **Device registry** (`src/client/device_registry.rs`) When a device notification arrives, the client patches both the `device_cache` (keyed by JID, stores `Vec`) and `device_registry_cache` (keyed by user string, stores `DeviceListRecord`) simultaneously: * `patch_device_add` — appends a new device JID to the cached list and persists the updated `DeviceListRecord` to the backend store * `patch_device_remove` — removes a device by ID using `retain`, then persists * `patch_device_update` — updates `key_index` on an existing device entry, then persists All three methods iterate over every known PN/LID alias for the user so stale alternate-key entries stay consistent. Device patches also persist to the backend store immediately, so changes survive cache eviction and restarts. When a notification arrives with only a hash (no device list), the client falls back to full invalidation and lets the next access re-fetch from the server. **Group metadata** (`src/handlers/notification.rs`, `src/features/groups.rs`) When participant add/remove notifications arrive, the cached `GroupInfo` is patched in place: * Participant adds use `GroupInfo::add_participants()`, which deduplicates by user and backfills LID-to-PN maps for LID-addressed groups * Participant removes use `GroupInfo::remove_participants()`, which also cleans up both LID-to-PN and PN-to-LID maps bidirectionally * API calls (`client.groups().add_participants()`, `client.groups().remove_participants()`) also patch the cache after a successful server response, filtering to only participants the server accepted (status 200) Group patches are cache-only — they are not persisted to the backend. If the cache entry is evicted, the next `query_info()` call re-fetches from the server. Only `leave()` uses full invalidation. **LID-PN mappings** (`src/lid_pn_cache.rs`) The `LidPnCache` uses timestamp-based conflict resolution when adding new mappings. The PN → entry map only updates if the new entry's `created_at` is newer than or equal to the existing entry's, preventing older stale mappings from overwriting newer ones. ### Patching vs. invalidation summary | Behavior | Granular patch | Invalidate + refetch | | ---------------- | --------------------------------------------------------------------- | ------------------------------------ | | **Network cost** | Zero — applies diff locally | One IQ round-trip per cache miss | | **Latency** | Immediate update | Stale until next access | | **Used when** | Device add/remove/update, group participant changes, LID-PN discovery | Group leave, hash-only device update | | **Atomicity** | Not atomic (can race, corrected on next full fetch) | N/A | | **Persistence** | Device patches persist immediately; group patches are cache-only | Backend remains authoritative | ## Related Sections Understand PersistenceManager's role Learn how session data is persisted Implement your own storage backend Complete storage API reference # Custom Backends Source: https://whatsapp-rust.jlucaso.com/guides/custom-backends Learn how to implement custom storage, transport, and HTTP backends for whatsapp-rust ## Overview This guide covers implementing custom backends for storage, network transport, and HTTP operations in whatsapp-rust. The library uses trait-based abstractions to allow full customization. ## Storage Backend Architecture The storage backend is split into four domain-grouped traits: 1. **SignalStore** - Signal protocol cryptography (identities, sessions, keys) 2. **AppSyncStore** - WhatsApp app state synchronization 3. **ProtocolStore** - WhatsApp Web protocol alignment (SKDM, LID mapping, device registry) 4. **DeviceStore** - Device persistence operations See [Store API reference](/api/store) for the complete trait definitions. ### Backend Trait Any type implementing all four traits automatically implements `Backend`: ```rust theme={null} use wacore::store::traits::*; pub trait Backend: SignalStore + AppSyncStore + ProtocolStore + DeviceStore + Send + Sync {} impl Backend for T where T: SignalStore + AppSyncStore + ProtocolStore + DeviceStore + Send + Sync {} ``` ## Implementing a Custom Storage Backend ### Step 1: Define Your Store ```rust theme={null} use std::sync::Arc; use async_trait::async_trait; use wacore::store::traits::*; use wacore::store::error::{Result, StoreError}; #[derive(Clone)] pub struct MyCustomStore { // Your storage implementation (database connection, file handle, etc.) connection: Arc, } impl MyCustomStore { pub async fn new(connection_string: &str) -> Result { let connection = MyConnection::connect(connection_string).await?; Ok(Self { connection: Arc::new(connection), }) } } ``` ### Step 2: Implement SignalStore Handle Signal protocol cryptographic operations: ```rust theme={null} #[async_trait] impl SignalStore for MyCustomStore { // --- Identity Operations --- async fn put_identity(&self, address: &str, key: [u8; 32]) -> Result<()> { self.connection.execute( "INSERT OR REPLACE INTO identities (address, key) VALUES (?, ?)", &[address, &key[..]], ).await?; Ok(()) } async fn load_identity(&self, address: &str) -> Result>> { let row = self.connection.query_optional( "SELECT key FROM identities WHERE address = ?", &[address], ).await?; Ok(row.map(|r| r.get(0))) } async fn delete_identity(&self, address: &str) -> Result<()> { self.connection.execute( "DELETE FROM identities WHERE address = ?", &[address], ).await?; Ok(()) } // --- Session Operations --- async fn get_session(&self, address: &str) -> Result>> { let row = self.connection.query_optional( "SELECT record FROM sessions WHERE address = ?", &[address], ).await?; Ok(row.map(|r| r.get(0))) } async fn put_session(&self, address: &str, session: &[u8]) -> Result<()> { self.connection.execute( "INSERT OR REPLACE INTO sessions (address, record) VALUES (?, ?)", &[address, session], ).await?; Ok(()) } async fn delete_session(&self, address: &str) -> Result<()> { self.connection.execute( "DELETE FROM sessions WHERE address = ?", &[address], ).await?; Ok(()) } // --- PreKey Operations --- async fn store_prekey(&self, id: u32, record: &[u8], uploaded: bool) -> Result<()> { self.connection.execute( "INSERT OR REPLACE INTO prekeys (id, record, uploaded) VALUES (?, ?, ?)", &[&id, record, &(uploaded as i32)], ).await?; Ok(()) } async fn load_prekey(&self, id: u32) -> Result>> { let row = self.connection.query_optional( "SELECT record FROM prekeys WHERE id = ?", &[&id], ).await?; Ok(row.map(|r| r.get(0))) } async fn remove_prekey(&self, id: u32) -> Result<()> { self.connection.execute( "DELETE FROM prekeys WHERE id = ?", &[&id], ).await?; Ok(()) } // --- Signed PreKey Operations --- async fn store_signed_prekey(&self, id: u32, record: &[u8]) -> Result<()> { self.connection.execute( "INSERT OR REPLACE INTO signed_prekeys (id, record) VALUES (?, ?)", &[&id, record], ).await?; Ok(()) } async fn load_signed_prekey(&self, id: u32) -> Result>> { let row = self.connection.query_optional( "SELECT record FROM signed_prekeys WHERE id = ?", &[&id], ).await?; Ok(row.map(|r| r.get(0))) } async fn load_all_signed_prekeys(&self) -> Result)>> { let rows = self.connection.query( "SELECT id, record FROM signed_prekeys", &[], ).await?; Ok(rows.into_iter().map(|r| (r.get(0), r.get(1))).collect()) } async fn remove_signed_prekey(&self, id: u32) -> Result<()> { self.connection.execute( "DELETE FROM signed_prekeys WHERE id = ?", &[&id], ).await?; Ok(()) } // --- Sender Key Operations --- async fn put_sender_key(&self, address: &str, record: &[u8]) -> Result<()> { self.connection.execute( "INSERT OR REPLACE INTO sender_keys (address, record) VALUES (?, ?)", &[address, record], ).await?; Ok(()) } async fn get_sender_key(&self, address: &str) -> Result>> { let row = self.connection.query_optional( "SELECT record FROM sender_keys WHERE address = ?", &[address], ).await?; Ok(row.map(|r| r.get(0))) } async fn delete_sender_key(&self, address: &str) -> Result<()> { self.connection.execute( "DELETE FROM sender_keys WHERE address = ?", &[address], ).await?; Ok(()) } } ``` See [Store API reference](/api/store#signal-store) for all SignalStore methods. ### Step 3: Implement AppSyncStore Handle WhatsApp app state synchronization: ```rust theme={null} use wacore::appstate::hash::HashState; use wacore_appstate::processor::AppStateMutationMAC; #[async_trait] impl AppSyncStore for MyCustomStore { async fn get_sync_key(&self, key_id: &[u8]) -> Result> { let row = self.connection.query_optional( "SELECT key_data, fingerprint, timestamp FROM app_state_sync_keys WHERE key_id = ?", &[key_id], ).await?; Ok(row.map(|r| AppStateSyncKey { key_data: r.get(0), fingerprint: r.get(1), timestamp: r.get(2), })) } async fn set_sync_key(&self, key_id: &[u8], key: AppStateSyncKey) -> Result<()> { self.connection.execute( "INSERT OR REPLACE INTO app_state_sync_keys (key_id, key_data, fingerprint, timestamp) VALUES (?, ?, ?, ?)", &[key_id, &key.key_data[..], &key.fingerprint[..], &key.timestamp], ).await?; Ok(()) } async fn get_version(&self, name: &str) -> Result { let row = self.connection.query_optional( "SELECT version, hash FROM app_state_versions WHERE name = ?", &[name], ).await?; Ok(row.map(|r| HashState { version: r.get(0), hash: r.get(1), }).unwrap_or_default()) } async fn set_version(&self, name: &str, state: HashState) -> Result<()> { self.connection.execute( "INSERT OR REPLACE INTO app_state_versions (name, version, hash) VALUES (?, ?, ?)", &[name, &state.version, &state.hash[..]], ).await?; Ok(()) } async fn put_mutation_macs( &self, name: &str, version: u64, mutations: &[AppStateMutationMAC], ) -> Result<()> { for mutation in mutations { self.connection.execute( "INSERT INTO mutation_macs (name, version, index_mac, value_mac) VALUES (?, ?, ?, ?)", &[name, &version, &mutation.index_mac[..], &mutation.value_mac[..]], ).await?; } Ok(()) } async fn get_mutation_mac(&self, name: &str, index_mac: &[u8]) -> Result>> { let row = self.connection.query_optional( "SELECT value_mac FROM mutation_macs WHERE name = ? AND index_mac = ?", &[name, index_mac], ).await?; Ok(row.map(|r| r.get(0))) } async fn delete_mutation_macs(&self, name: &str, index_macs: &[Vec]) -> Result<()> { for index_mac in index_macs { self.connection.execute( "DELETE FROM mutation_macs WHERE name = ? AND index_mac = ?", &[name, &index_mac[..]], ).await?; } Ok(()) } } ``` See [Store API reference](/api/store#app-sync-store) for all AppSyncStore methods. ### Step 4: Implement ProtocolStore Handle WhatsApp Web protocol alignment: ```rust theme={null} use wacore_binary::jid::Jid; #[async_trait] impl ProtocolStore for MyCustomStore { // --- SKDM Tracking --- async fn get_skdm_recipients(&self, group_jid: &str) -> Result> { let rows = self.connection.query( "SELECT device_jid FROM skdm_recipients WHERE group_jid = ?", &[group_jid], ).await?; rows.into_iter() .map(|r| { let jid_str: String = r.get(0); jid_str.parse().map_err(|e| StoreError::Parse(format!("{}", e))) }) .collect() } async fn add_skdm_recipients(&self, group_jid: &str, device_jids: &[Jid]) -> Result<()> { for device_jid in device_jids { self.connection.execute( "INSERT OR IGNORE INTO skdm_recipients (group_jid, device_jid) VALUES (?, ?)", &[group_jid, &device_jid.to_string()], ).await?; } Ok(()) } async fn clear_skdm_recipients(&self, group_jid: &str) -> Result<()> { self.connection.execute( "DELETE FROM skdm_recipients WHERE group_jid = ?", &[group_jid], ).await?; Ok(()) } // --- LID-PN Mapping --- async fn get_lid_mapping(&self, lid: &str) -> Result> { let row = self.connection.query_optional( "SELECT lid, phone_number, created_at, updated_at, learning_source FROM lid_pn_mappings WHERE lid = ?", &[lid], ).await?; Ok(row.map(|r| LidPnMappingEntry { lid: r.get(0), phone_number: r.get(1), created_at: r.get(2), updated_at: r.get(3), learning_source: r.get(4), })) } async fn get_pn_mapping(&self, phone: &str) -> Result> { let row = self.connection.query_optional( "SELECT lid, phone_number, created_at, updated_at, learning_source FROM lid_pn_mappings WHERE phone_number = ? ORDER BY updated_at DESC LIMIT 1", &[phone], ).await?; Ok(row.map(|r| LidPnMappingEntry { lid: r.get(0), phone_number: r.get(1), created_at: r.get(2), updated_at: r.get(3), learning_source: r.get(4), })) } async fn put_lid_mapping(&self, entry: &LidPnMappingEntry) -> Result<()> { self.connection.execute( "INSERT OR REPLACE INTO lid_pn_mappings (lid, phone_number, created_at, updated_at, learning_source) VALUES (?, ?, ?, ?, ?)", &[&entry.lid, &entry.phone_number, &entry.created_at, &entry.updated_at, &entry.learning_source], ).await?; Ok(()) } async fn get_all_lid_mappings(&self) -> Result> { let rows = self.connection.query( "SELECT lid, phone_number, created_at, updated_at, learning_source FROM lid_pn_mappings", &[], ).await?; Ok(rows.into_iter().map(|r| LidPnMappingEntry { lid: r.get(0), phone_number: r.get(1), created_at: r.get(2), updated_at: r.get(3), learning_source: r.get(4), }).collect()) } // --- Sent Message Store (retry support) --- async fn store_sent_message( &self, chat_jid: &str, message_id: &str, payload: &[u8], ) -> Result<()> { self.connection.execute( "INSERT OR REPLACE INTO sent_messages (chat_jid, message_id, payload, created_at) VALUES (?, ?, ?, strftime('%s', 'now'))", &[chat_jid, message_id, payload], ).await?; Ok(()) } async fn take_sent_message( &self, chat_jid: &str, message_id: &str, ) -> Result>> { // Atomic read-and-delete let row = self.connection.query_optional( "DELETE FROM sent_messages WHERE chat_jid = ? AND message_id = ? RETURNING payload", &[chat_jid, message_id], ).await?; Ok(row.map(|r| r.get(0))) } async fn delete_expired_sent_messages(&self, cutoff_timestamp: i64) -> Result { let count = self.connection.execute( "DELETE FROM sent_messages WHERE created_at < ?", &[&cutoff_timestamp], ).await?; Ok(count as u32) } // ... Implement remaining methods (base_key, device_list, forget_marks, tc_token) } ``` See [Store API reference](/api/store#protocol-store) for all ProtocolStore methods. ### Step 5: Implement DeviceStore Handle device data persistence: ```rust theme={null} use wacore::store::Device; #[async_trait] impl DeviceStore for MyCustomStore { async fn save(&self, device: &Device) -> Result<()> { // Serialize device data (use your preferred format) let serialized = bincode::serialize(device) .map_err(|e| StoreError::Serialization(e.to_string()))?; self.connection.execute( "UPDATE devices SET data = ? WHERE id = 1", &[&serialized[..]], ).await?; Ok(()) } async fn load(&self) -> Result> { let row = self.connection.query_optional( "SELECT data FROM devices WHERE id = 1", &[], ).await?; match row { Some(r) => { let data: Vec = r.get(0); let device = bincode::deserialize(&data) .map_err(|e| StoreError::Deserialization(e.to_string()))?; Ok(Some(device)) } None => Ok(None), } } async fn exists(&self) -> Result { let row = self.connection.query_optional( "SELECT 1 FROM devices WHERE id = 1", &[], ).await?; Ok(row.is_some()) } async fn create(&self) -> Result { self.connection.execute( "INSERT INTO devices (id, data) VALUES (1, ?)", &[&Vec::::new()], ).await?; Ok(1) } async fn snapshot_db(&self, name: &str, extra_content: Option<&[u8]>) -> Result<()> { // Optional: Implement database snapshotting for debugging Ok(()) } } ``` See [Store API reference](/api/store#device-store) for all DeviceStore methods. ### Using Your Custom Backend ```rust theme={null} use whatsapp_rust::bot::Bot; let backend = Arc::new(MyCustomStore::new("my://connection").await?); let bot = Bot::builder() .with_backend(backend) .with_transport_factory(transport_factory) .with_http_client(http_client) .build() .await?; ``` ## Custom Transport Backend ### Transport Trait Implement the `Transport` trait for custom network transports: ```rust theme={null} use async_trait::async_trait; use wacore::net::Transport; pub struct MyCustomTransport { // Your transport implementation } #[async_trait] impl Transport for MyCustomTransport { async fn send(&self, data: Vec) -> Result<(), anyhow::Error> { // Send raw bytes through your transport Ok(()) } async fn disconnect(&self) { // Close the connection } } ``` See [Transport API reference](/api/transport) for full trait details. ### Transport Factory Implement `TransportFactory` to create transport instances: ```rust theme={null} use wacore::net::{TransportFactory, TransportEvent}; use async_channel::Receiver; use std::sync::Arc; pub struct MyCustomTransportFactory { url: String, } impl MyCustomTransportFactory { pub fn new(url: String) -> Self { Self { url } } } #[async_trait] impl TransportFactory for MyCustomTransportFactory { async fn create_transport( &self, ) -> Result<(Arc, Receiver), anyhow::Error> { // Create transport and event channel let (event_tx, event_rx) = async_channel::bounded(10000); // Connect to server let transport = MyCustomTransport::connect(&self.url).await?; // Spawn read pump task tokio::spawn(read_pump(transport.reader(), event_tx)); // Send Connected event event_tx.send(TransportEvent::Connected).await?; Ok((Arc::new(transport), event_rx)) } } ``` See [Transport API reference](/api/transport#transport-factory) for factory details. ### Transport Events ```rust theme={null} pub enum TransportEvent { Connected, DataReceived(Bytes), Disconnected, } // Read pump example async fn read_pump( mut reader: impl AsyncRead, event_tx: async_channel::Sender, ) { loop { match read_frame(&mut reader).await { Ok(data) => { if event_tx.send(TransportEvent::DataReceived(data)).await.is_err() { break; } } Err(_) => break, } } let _ = event_tx.send(TransportEvent::Disconnected).await; } ``` ## Custom HTTP Client Implement the `HttpClient` trait for custom HTTP operations: ```rust theme={null} use whatsapp_rust::http::{HttpClient, HttpRequest, HttpResponse}; use async_trait::async_trait; pub struct MyCustomHttpClient { // Your HTTP client implementation } #[async_trait] impl HttpClient for MyCustomHttpClient { async fn execute(&self, request: HttpRequest) -> Result { // Execute HTTP request let response = self.send_request(&request).await?; Ok(HttpResponse { status_code: response.status_code, body: response.body, }) } fn execute_streaming(&self, request: HttpRequest) -> Result { // Return a streaming response with a reader Ok(HttpStreamingResponse { status_code: response.status_code, body: Box::new(response.body_reader), }) } fn clone_box(&self) -> Box { Box::new(self.clone()) } } ``` ## SQLite Reference Implementation The library includes a full SQLite implementation you can use as reference: See [Store API reference](/api/store#sqlite-store) for the SQLite implementation details. ### Key Features * Diesel ORM with migrations * Connection pooling (r2d2) * WAL mode for concurrency * Prepared statements for performance * Transaction support * Database snapshotting for debugging ### Using SQLite store ```rust theme={null} use whatsapp_rust::store::SqliteStore; let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); ``` SQLite is bundled by default via the `bundled-sqlite` feature on `whatsapp-rust-sqlite-storage`. No system SQLite installation is required. ## Custom Cache Store The pluggable cache store adapter lets you replace the default in-process moka caches with an external backend like Redis or Memcached. This is useful for sharing cache state across multiple client instances or for deployments where in-process memory is limited. ### The CacheStore trait Implement the `CacheStore` trait from `wacore::store::cache`: ```rust theme={null} use async_trait::async_trait; use std::time::Duration; use wacore::store::cache::CacheStore; pub struct MyRedisCacheStore { client: redis::aio::ConnectionManager, } impl MyRedisCacheStore { pub async fn new(url: &str) -> Self { let client = redis::Client::open(url).unwrap(); let conn = client.get_connection_manager().await.unwrap(); Self { client: conn } } } #[async_trait] impl CacheStore for MyRedisCacheStore { async fn get(&self, namespace: &str, key: &str) -> anyhow::Result>> { let redis_key = format!("{namespace}:{key}"); let result: Option> = redis::cmd("GET") .arg(&redis_key) .query_async(&mut self.client.clone()) .await?; Ok(result) } async fn set( &self, namespace: &str, key: &str, value: &[u8], ttl: Option, ) -> anyhow::Result<()> { let redis_key = format!("{namespace}:{key}"); match ttl { Some(duration) => { redis::cmd("SET") .arg(&redis_key) .arg(value) .arg("EX") .arg(duration.as_secs()) .query_async(&mut self.client.clone()) .await?; } None => { redis::cmd("SET") .arg(&redis_key) .arg(value) .query_async(&mut self.client.clone()) .await?; } } Ok(()) } async fn delete(&self, namespace: &str, key: &str) -> anyhow::Result<()> { let redis_key = format!("{namespace}:{key}"); redis::cmd("DEL") .arg(&redis_key) .query_async(&mut self.client.clone()) .await?; Ok(()) } async fn clear(&self, namespace: &str) -> anyhow::Result<()> { // Use SCAN to find and delete all keys with the namespace prefix let pattern = format!("{namespace}:*"); let mut cursor = 0u64; loop { let (next_cursor, keys): (u64, Vec) = redis::cmd("SCAN") .arg(cursor) .arg("MATCH") .arg(&pattern) .arg("COUNT") .arg(100) .query_async(&mut self.client.clone()) .await?; if !keys.is_empty() { redis::cmd("DEL") .arg(&keys) .query_async(&mut self.client.clone()) .await?; } cursor = next_cursor; if cursor == 0 { break; } } Ok(()) } async fn entry_count(&self, namespace: &str) -> anyhow::Result { // Approximate count using SCAN (expensive — use only for diagnostics) let pattern = format!("{namespace}:*"); let mut count = 0u64; let mut cursor = 0u64; loop { let (next_cursor, keys): (u64, Vec) = redis::cmd("SCAN") .arg(cursor) .arg("MATCH") .arg(&pattern) .arg("COUNT") .arg(100) .query_async(&mut self.client.clone()) .await?; count += keys.len() as u64; cursor = next_cursor; if cursor == 0 { break; } } Ok(count) } } ``` ### Plugging in your cache store Use `CacheStores` to assign your custom backend to specific caches: ```rust theme={null} use whatsapp_rust::{CacheConfig, CacheStores}; use std::sync::Arc; let redis = Arc::new(MyRedisCacheStore::new("redis://localhost:6379").await); // Route only group and device caches to Redis let bot = Bot::builder() .with_backend(backend) .with_transport_factory(transport) .with_http_client(http_client) .with_cache_config(CacheConfig { cache_stores: CacheStores { group_cache: Some(redis.clone()), device_cache: Some(redis.clone()), ..Default::default() }, ..Default::default() }) .build() .await?; ``` Or route all pluggable caches to the same backend: ```rust theme={null} let config = CacheConfig { cache_stores: CacheStores::all(redis.clone()), ..Default::default() }; ``` ### Available namespaces The following namespaces are used internally by the client: | Namespace | Cache | Description | | ------------------- | ----------------------- | ----------------------------------- | | `"group"` | `group_cache` | Group metadata | | `"device"` | `device_cache` | Device lists per user | | `"device_registry"` | `device_registry_cache` | Device registry entries | | `"lid_pn_by_lid"` | `lid_pn_cache` | LID-to-phone bidirectional mappings | ### Design considerations * **Error handling is best-effort.** Cache misses and failures are logged as warnings but don't break the client — it falls back to fetching from the authoritative source. * **Serialization uses `serde_json`.** Values are serialized to JSON bytes on the custom-store path. The moka path has zero serialization overhead. * **TTL is forwarded from `CacheEntryConfig`.** Your implementation receives the same TTL configured in `CacheConfig`. * **Coordination caches cannot be externalized.** Session locks, message queues, and enqueue locks hold live Rust objects (mutexes, channels) and always stay in-process. ## Best Practices ### Use Connection Pooling ```rust theme={null} // ✅ Good: Use connection pool let pool = Pool::new(config)?; let backend = MyCustomStore::new(pool); // ❌ Bad: Create connections per operation let backend = MyCustomStore::new(connection_string); ``` ### Implement Proper Error Handling ```rust theme={null} use wacore::store::error::{Result, StoreError}; async fn load_session(&self, address: &str) -> Result>> { match self.connection.query("SELECT ...", &[address]).await { Ok(row) => Ok(Some(row.get(0))), Err(e) if is_not_found(&e) => Ok(None), Err(e) => Err(StoreError::Database(e.to_string())), } } ``` ### Use Transactions for Batch Operations ```rust theme={null} async fn put_mutation_macs( &self, name: &str, version: u64, mutations: &[AppStateMutationMAC], ) -> Result<()> { let tx = self.connection.begin_transaction().await?; for mutation in mutations { tx.execute("INSERT ...", &[...]).await?; } tx.commit().await?; Ok(()) } ``` ### Implement Database Migrations Use a migration system (e.g., Diesel migrations) to manage schema changes: ```sql theme={null} -- migrations/2024-01-01-000000_create_sessions/up.sql CREATE TABLE IF NOT EXISTS sessions ( address TEXT PRIMARY KEY, record BLOB NOT NULL ); ``` ## Next Steps * [Sending Messages](/guides/sending-messages) - Use your custom backend * [Receiving Messages](/guides/receiving-messages) - Store received messages * [Group Management](/guides/group-management) - Store group metadata # Group Management Source: https://whatsapp-rust.jlucaso.com/guides/group-management Learn how to create groups, manage participants, and update group metadata in whatsapp-rust ## Overview This guide covers group operations including creation, participant management, and metadata updates using the whatsapp-rust library. ## Accessing the Groups API All group operations are accessed through the `groups()` method: ```rust theme={null} use whatsapp_rust::client::Client; let groups = client.groups(); ``` See [Groups API reference](/api/groups) for the full API. ## Creating Groups ### Basic Group Creation ```rust theme={null} use whatsapp_rust::features::groups::{GroupCreateOptions, GroupSubject, GroupParticipantOptions}; use wacore_binary::jid::Jid; let participant1: Jid = "1234567890@s.whatsapp.net".parse()?; let participant2: Jid = "9876543210@s.whatsapp.net".parse()?; let options = GroupCreateOptions { subject: GroupSubject::new("My New Group")?, participants: vec![ GroupParticipantOptions::new(participant1), GroupParticipantOptions::new(participant2), ], ..Default::default() }; let result = client.groups().create_group(options).await?; println!("Group created with JID: {}", result.gid); ``` See [Groups API reference](/api/groups#create_group) for full details. ### Group Creation Options ```rust theme={null} use whatsapp_rust::features::groups::{ MemberAddMode, MemberLinkMode, MembershipApprovalMode, }; let options = GroupCreateOptions { subject: GroupSubject::new("Advanced Group")?, participants: vec![ GroupParticipantOptions::new(participant1), ], // Control who can add members member_add_mode: Some(MemberAddMode::AdminAdd), // Control membership approval requirement membership_approval_mode: Some(MembershipApprovalMode::On), // Control who can share invite links member_link_mode: Some(MemberLinkMode::AdminOnly), // Make it an announcement group (only admins can send messages) announce: Some(true), // Lock the group (prevent non-admins from changing group settings) locked: Some(true), // Make it a parent group (can have linked subgroups) parent: Some(false), ..Default::default() }; let result = client.groups().create_group(options).await?; ``` See [Groups API reference](/api/groups#create_group) for all options. ### Group Constraints ```rust theme={null} use whatsapp_rust::features::groups::*; // Subject: Max 100 characters let subject = GroupSubject::new("A".repeat(101)); assert!(subject.is_err()); // Fails validation // Description: Max 2048 characters let description = GroupDescription::new("B".repeat(2049)); assert!(description.is_err()); // Fails validation // Participants: Max 257 (256 + creator) // GROUP_SIZE_LIMIT = 257 ``` These limits are based on WhatsApp Web's A/B test properties and may change. The library validates against current known limits. ## Querying Group Information ### Get Group Metadata ```rust theme={null} let group_jid: Jid = "120363012345678@g.us".parse()?; let metadata = client.groups().get_metadata(&group_jid).await?; println!("Group: {}", metadata.subject); println!("Participants: {}", metadata.participants.len()); println!("Addressing mode: {:?}", metadata.addressing_mode); for participant in &metadata.participants { println!("- {} (admin: {})", participant.jid, participant.is_admin); if let Some(phone) = &participant.phone_number { println!(" Phone: {}", phone); } } ``` See [Groups API reference](/api/groups#get_metadata) for metadata fields. ### List All Groups ```rust theme={null} use std::collections::HashMap; let groups: HashMap = client .groups() .get_participating() .await?; for (jid, metadata) in groups { println!("{}: {} ({} members)", jid, metadata.subject, metadata.participants.len() ); } ``` See [Groups API reference](/api/groups#get_participating) for details. ### Query Group Info (Internal) For lower-level access with caching: ```rust theme={null} use wacore::client::context::GroupInfo; let info: GroupInfo = client.groups().query_info(&group_jid).await?; println!("Participants: {:?}", info.participants); println!("Addressing mode: {:?}", info.addressing_mode); // Access LID-to-phone mapping if available if let Some(mapping) = info.get_lid_to_pn_map() { for (lid, pn) in mapping { println!("LID {} -> Phone {}", lid, pn); } } ``` See [Groups API reference](/api/groups#query_info) for details. ## Updating Group Metadata ### Change Group Subject ```rust theme={null} let new_subject = GroupSubject::new("Updated Group Name")?; client.groups().set_subject(&group_jid, new_subject).await?; ``` See [Groups API reference](/api/groups#set_subject) for details. ### Set or Delete Group Description ```rust theme={null} use whatsapp_rust::features::groups::GroupDescription; // Set description let description = GroupDescription::new("This is the group description")?; client.groups().set_description( &group_jid, Some(description), None, // prev description ID (optional) ).await?; // Delete description client.groups().set_description( &group_jid, None, None, ).await?; ``` See [Groups API reference](/api/groups#set_description) for details. The `prev` parameter can be used for conflict detection. If provided and the current description ID doesn't match, the operation may fail. Pass `None` if you don't need this check. ## Managing Participants ### Add Participants ```rust theme={null} let new_members = vec![ "1111111111@s.whatsapp.net".parse()?, "2222222222@s.whatsapp.net".parse()?, ]; let responses = client.groups().add_participants( &group_jid, &new_members, ).await?; // Check results for response in responses { match response.code { 200 => println!("Added: {}", response.jid), 403 => println!("Failed to add {} (permission denied)", response.jid), 409 => println!("{} is already in the group", response.jid), _ => println!("Error adding {}: {}", response.jid, response.code), } } ``` See [Groups API reference](/api/groups#add_participants) for response codes. ### Remove Participants ```rust theme={null} let members_to_remove = vec![ "1111111111@s.whatsapp.net".parse()?, ]; let responses = client.groups().remove_participants( &group_jid, &members_to_remove, ).await?; for response in responses { match response.code { 200 => println!("Removed: {}", response.jid), 404 => println!("{} is not in the group", response.jid), _ => println!("Error removing {}: {}", response.jid, response.code), } } ``` See [Groups API reference](/api/groups#remove_participants) for details. ### Promote to Admin ```rust theme={null} let participants = vec!["1234567890@s.whatsapp.net".parse()?]; client.groups().promote_participants( &group_jid, &participants, ).await?; println!("Promoted to admin"); ``` See [Groups API reference](/api/groups#promote_participants) for details. ### Demote Admin ```rust theme={null} let participants = vec!["1234567890@s.whatsapp.net".parse()?]; client.groups().demote_participants( &group_jid, &participants, ).await?; println!("Demoted from admin"); ``` See [Groups API reference](/api/groups#demote_participants) for details. ### Leave Group ```rust theme={null} client.groups().leave(&group_jid).await?; println!("Left the group"); ``` See [Groups API reference](/api/groups#leave) for details. ## Group Invite Links ### Get Invite Link ```rust theme={null} // Get existing invite link let link = client.groups().get_invite_link(&group_jid, false).await?; println!("Invite link: https://chat.whatsapp.com/{}", link); // Reset and get new invite link let new_link = client.groups().get_invite_link(&group_jid, true).await?; println!("New invite link: https://chat.whatsapp.com/{}", new_link); ``` See [Groups API reference](/api/groups#get_invite_link) for details. Resetting the invite link (`reset = true`) invalidates the old link. Anyone with the old link will no longer be able to join. ## Group Settings ### Member Add Mode Control who can add new members: ```rust theme={null} use whatsapp_rust::features::groups::MemberAddMode; pub enum MemberAddMode { AdminAdd, // Only admins can add members AllMemberAdd, // All members can add others } // Set during creation or via group settings update IQ let options = GroupCreateOptions { member_add_mode: Some(MemberAddMode::AdminAdd), ..Default::default() }; ``` ### Membership Approval Mode Require admin approval for new members: ```rust theme={null} use whatsapp_rust::features::groups::MembershipApprovalMode; pub enum MembershipApprovalMode { On, // Require approval Off, // Auto-accept } let options = GroupCreateOptions { membership_approval_mode: Some(MembershipApprovalMode::On), ..Default::default() }; ``` ### Announcement Mode Make it an announcement group (only admins can send messages): ```rust theme={null} let options = GroupCreateOptions { announce: Some(true), ..Default::default() }; ``` ## Addressing Modes Groups can use different addressing modes: ```rust theme={null} use wacore::types::message::AddressingMode; pub enum AddressingMode { Pn, // Phone number (legacy) Lid, // Long ID (new privacy mode) } // Check the group's addressing mode let info = client.groups().query_info(&group_jid).await?; match info.addressing_mode { AddressingMode::Pn => println!("Group uses phone numbers"), AddressingMode::Lid => println!("Group uses LIDs (privacy mode)"), } ``` LID (Long ID) mode provides better privacy by hiding phone numbers. The library automatically handles LID-to-phone-number mapping when needed. ## Participant Response Codes When adding or removing participants, check the response codes: ```rust theme={null} use whatsapp_rust::features::groups::ParticipantChangeResponse; let responses = client.groups().add_participants(&group_jid, &participants).await?; for response in responses { match response.code { 200 => println!("✅ Success"), 403 => println!("❌ Permission denied"), 404 => println!("❌ Not found"), 409 => println!("⚠️ Already in group"), _ => println!("⚠️ Unknown error: {}", response.code), } if let Some(error) = response.error { println!("Error: {}", error); } } ``` ## Error Handling ```rust theme={null} use anyhow::Result; async fn create_group_safely( client: &Client, subject: &str, participants: Vec, ) -> Result { let subject = GroupSubject::new(subject)?; let options = GroupCreateOptions { subject, participants: participants .into_iter() .map(GroupParticipantOptions::new) .collect(), ..Default::default() }; match client.groups().create_group(options).await { Ok(result) => { println!("✅ Group created: {}", result.gid); Ok(result.gid) } Err(e) => { eprintln!("❌ Failed to create group: {:?}", e); Err(e) } } } ``` ## Advanced Usage ### Handling LID Groups For LID-based groups, you may need phone number mappings: ```rust theme={null} let info = client.groups().query_info(&group_jid).await?; if info.addressing_mode == AddressingMode::Lid { // Get LID-to-phone mapping if let Some(mapping) = info.get_lid_to_pn_map() { for (lid, pn_jid) in mapping { println!("LID {} maps to phone {}", lid, pn_jid); } } } ``` ### Participant Options For advanced participant configurations: ```rust theme={null} use whatsapp_rust::features::groups::GroupParticipantOptions; use wacore_binary::jid::Jid; let lid_jid: Jid = "123456789012345:10@lid".parse()?; let phone_jid: Jid = "1234567890@s.whatsapp.net".parse()?; let participant = GroupParticipantOptions::new(lid_jid) .with_phone_number(phone_jid); // The library will automatically resolve phone numbers for LID participants ``` See [Groups API reference](/api/groups) for addressing mode details. ## Best Practices ### Validate Input Before Creating Groups ```rust theme={null} // ✅ Good: Validate before creation let subject = match GroupSubject::new(user_input) { Ok(s) => s, Err(e) => { eprintln!("Invalid subject: {}", e); return Err(e); } }; // ❌ Bad: No validation let subject = GroupSubject::new(user_input)?; // May panic on invalid input ``` ### Check Response Codes Always check participant operation responses: ```rust theme={null} let responses = client.groups().add_participants(&group_jid, &participants).await?; let mut success_count = 0; let mut failed = Vec::new(); for response in responses { if response.code == 200 { success_count += 1; } else { failed.push(response); } } println!("Added {} participants", success_count); if !failed.is_empty() { eprintln!("Failed to add {} participants", failed.len()); } ``` ### Handle Addressing Modes Be aware of LID vs PN addressing: ```rust theme={null} let info = client.groups().query_info(&group_jid).await?; if info.addressing_mode == AddressingMode::Lid { // Handle LID-based group // Phone numbers may not be directly visible } else { // Handle phone-number-based group } ``` ### Cache Group Information The library automatically caches group info: ```rust theme={null} // First call: fetches from server let info = client.groups().query_info(&group_jid).await?; // Second call: uses cache let info = client.groups().query_info(&group_jid).await?; ``` ## Next Steps * [Sending Messages](/guides/sending-messages) - Send messages to groups * [Receiving Messages](/guides/receiving-messages) - Handle group message events * [Custom Backends](/guides/custom-backends) - Store group metadata # Media Handling Source: https://whatsapp-rust.jlucaso.com/guides/media-handling Learn how to upload and download media with encryption in whatsapp-rust ## Overview This guide covers uploading and downloading media (images, videos, audio, documents, stickers) with automatic encryption and decryption. ## Downloading Media ### From Message Objects Download media directly from message types that implement `Downloadable`: ```rust theme={null} use wacore::download::Downloadable; use whatsapp_rust::client::Client; match event { Event::Message(message, info) => { // Image if let Some(img) = &message.image_message { let data = client.download(img.as_ref()).await?; std::fs::write("image.jpg", data)?; } // Video if let Some(video) = &message.video_message { let data = client.download(video.as_ref()).await?; std::fs::write("video.mp4", data)?; } // Audio if let Some(audio) = &message.audio_message { let data = client.download(audio.as_ref()).await?; let ext = if audio.ptt() { "ogg" } else { "mp3" }; std::fs::write(format!("audio.{}", ext), data)?; } // Document if let Some(doc) = &message.document_message { let data = client.download(doc.as_ref()).await?; let filename = doc.file_name.as_deref().unwrap_or("document"); std::fs::write(filename, data)?; } // Sticker if let Some(sticker) = &message.sticker_message { let data = client.download(sticker.as_ref()).await?; std::fs::write("sticker.webp", data)?; } } _ => {} } ``` See [Download API reference](/api/download) for full details. ### Streaming Download For large files, use streaming to avoid memory overhead: ```rust theme={null} use std::fs::File; if let Some(video) = &message.video_message { let file = File::create("video.mp4")?; let file = client.download_to_writer(video.as_ref(), file).await?; println!("Video downloaded with constant memory usage!"); } ``` See [Download API reference](/api/download#download_to_writer) for streaming details. Streaming downloads use \~40KB of memory regardless of file size (8KB buffer + decryption state). The entire HTTP download, decryption, and file write happen in a single blocking thread for optimal performance. ### From Raw Parameters Download media when you have the raw encryption parameters: ```rust theme={null} use wacore::download::MediaType; let data = client.download_from_params( "/v/t62.1234-5/...", // direct_path &media_key, // media_key (32 bytes) &file_sha256, // file_sha256 &file_enc_sha256, // file_enc_sha256 12345, // file_length MediaType::Image, // media_type ).await?; std::fs::write("downloaded.jpg", data)?; ``` See [Download API reference](/api/download#download_from_params) for raw parameter downloads. ### Streaming from Raw Parameters ```rust theme={null} let file = File::create("large_video.mp4")?; let file = client.download_from_params_to_writer( direct_path, &media_key, &file_sha256, &file_enc_sha256, file_length, MediaType::Video, file, ).await?; ``` See [Download API reference](/api/download#download_from_params_to_writer) for streaming from raw parameters. ## Uploading Media ### Basic Upload ```rust theme={null} use wacore::download::MediaType; use whatsapp_rust::upload::UploadResponse; // Read file data let data = std::fs::read("photo.jpg")?; // Upload with automatic encryption let upload: UploadResponse = client.upload(data, MediaType::Image).await?; println!("URL: {}", upload.url); println!("Direct path: {}", upload.direct_path); println!("File SHA256: {:?}", upload.file_sha256); ``` See [Upload API reference](/api/upload) for full details. ### Using Upload Response in Messages ```rust theme={null} use waproto::whatsapp as wa; let upload = client.upload(image_data, MediaType::Image).await?; let message = wa::Message { image_message: Some(Box::new(wa::message::ImageMessage { url: Some(upload.url), direct_path: Some(upload.direct_path), media_key: Some(upload.media_key), file_sha256: Some(upload.file_sha256), file_enc_sha256: Some(upload.file_enc_sha256), file_length: Some(upload.file_length), mimetype: Some("image/jpeg".to_string()), caption: Some("Check out this image!".to_string()), width: Some(1920), height: Some(1080), ..Default::default() })), ..Default::default() }; let message_id = client.send_message(to, message).await?; ``` ### Resumable uploads For files 5 MiB or larger, the client automatically checks whether a previous upload of the same file exists on the CDN before uploading the full payload. This avoids re-uploading data that the server already has. The resume check flow works as follows: 1. The client sends a `?resume=1` probe to the CDN for each host 2. If the server responds with **complete**, the existing URL is returned immediately — no upload occurs 3. If the server responds with a **byte offset**, only the remaining bytes are uploaded using the `file_offset` query parameter 4. If the server responds with **not found** (or the check fails), a full upload proceeds Resumable upload detection is fully transparent. You call `client.upload()` the same way regardless of file size. The resume check is non-fatal — if it fails for any reason, the client falls back to a full upload. ```rust theme={null} // Large files automatically use resumable upload logic let video_bytes = std::fs::read("large_video.mp4")?; // e.g., 50 MiB let upload = client.upload(video_bytes, MediaType::Video).await?; // If a partial upload existed, only the remaining bytes were sent ``` ## Media Types ### Available Media Types ```rust theme={null} pub enum MediaType { Image, Video, Audio, Document, History, AppState, Sticker, StickerPack, LinkThumbnail, } ``` ### Media Type Properties ```rust theme={null} use wacore::download::MediaType; // Get MMS type for upload URLs let mms_type = MediaType::Image.mms_type(); // Returns: "image", "video", "audio", "document" // Get app info string for HKDF key derivation let info = MediaType::Video.app_info(); // Returns: b"WhatsApp Video Keys" ``` ## Encryption Details ### Upload Encryption Media is automatically encrypted before upload: 1. Generate random 32-byte `media_key` 2. Derive encryption keys using HKDF-SHA256: * IV: 16 bytes * Cipher key: 32 bytes * MAC key: 32 bytes 3. Encrypt with AES-256-CBC 4. Append HMAC-SHA256 MAC (10 bytes) 5. Compute SHA256 hashes of plaintext and ciphertext ```rust theme={null} // Encryption happens automatically in upload() let data = std::fs::read("file.pdf")?; let upload = client.upload(data, MediaType::Document).await?; // The UploadResponse contains all encryption metadata assert_eq!(upload.media_key.len(), 32); assert_eq!(upload.file_sha256.len(), 32); assert_eq!(upload.file_enc_sha256.len(), 32); ``` ### Download Decryption Decryption is automatic when downloading: 1. Fetch encrypted data from WhatsApp CDN 2. Verify MAC (last 10 bytes) 3. Decrypt with AES-256-CBC 4. Verify plaintext SHA256 5. Return decrypted data ```rust theme={null} // Decryption happens automatically in download() let data = client.download(image_message.as_ref()).await?; // `data` is already decrypted and verified ``` Never skip SHA256 verification! The library automatically verifies both encrypted and decrypted hashes to ensure data integrity. ## Error Handling ### Download Errors ```rust theme={null} use anyhow::Result; async fn safe_download( client: &Client, downloadable: &dyn Downloadable, ) -> Result> { match client.download(downloadable).await { Ok(data) => { println!("✅ Downloaded {} bytes", data.len()); Ok(data) } Err(e) => { if e.to_string().contains("invalid mac") { eprintln!("❌ Media corrupted (MAC mismatch)"); } else if e.to_string().contains("status") { eprintln!("❌ Download failed (HTTP error)"); } else { eprintln!("❌ Download error: {:?}", e); } Err(e) } } } ``` ### Upload Errors ```rust theme={null} async fn safe_upload( client: &Client, data: Vec, media_type: MediaType, ) -> Result { match client.upload(data, media_type).await { Ok(upload) => { println!("✅ Uploaded to: {}", upload.url); Ok(upload) } Err(e) => { eprintln!("❌ Upload failed: {:?}", e); Err(e) } } } ``` ## Advanced Usage ### Custom Media Processing ```rust theme={null} use image::DynamicImage; // Download and process image if let Some(img) = &message.image_message { let data = client.download(img.as_ref()).await?; // Load with image crate let image = image::load_from_memory(&data)?; // Resize let thumbnail = image.resize(200, 200, image::imageops::FilterType::Lanczos3); // Save thumbnail.save("thumbnail.jpg")?; } ``` ### Streaming with Progress ```rust theme={null} use std::io::{Write, Seek}; use std::sync::{Arc, Mutex}; struct ProgressWriter { inner: W, total: Arc>, } impl Write for ProgressWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { let n = self.inner.write(buf)?; *self.total.lock().unwrap() += n; println!("Downloaded {} bytes", *self.total.lock().unwrap()); Ok(n) } fn flush(&mut self) -> std::io::Result<()> { self.inner.flush() } } impl Seek for ProgressWriter { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.inner.seek(pos) } } let file = File::create("video.mp4")?; let progress_writer = ProgressWriter { inner: file, total: Arc::new(Mutex::new(0)), }; let _file = client.download_to_writer(video.as_ref(), progress_writer).await?; ``` ### Automatic retry and failover Both `download` and `upload` methods automatically retry when WhatsApp's CDN returns certain HTTP errors. The client handles three categories of errors: **Auth errors (401/403):** The client invalidates the cached `MediaConn`, fetches fresh credentials from WhatsApp's servers, and retries the operation once with new auth tokens. **Media not found (404/410):** When a download URL has expired or the media has been moved, the CDN returns 404 or 410. The client treats this the same as an auth error — it invalidates the cached media connection, re-derives download URLs with fresh credentials and hosts, and retries once. This matches WhatsApp Web's `MediaNotFoundError` handling. **Other errors (e.g., 500):** The client tries the next available CDN host without refreshing credentials. Media connections include multiple hosts sorted by priority (primary hosts first, then fallback hosts), and the client iterates through them sequentially. This retry behavior is fully transparent — no changes are needed in your code. Both `download` and `upload` handle auth refresh, URL re-derivation, and host failover automatically, with a maximum of one credential refresh per operation. For streaming downloads (`download_to_writer`), the writer is automatically seeked back to position 0 before each retry so partial writes don't corrupt the output. ### Additional retry logic For transient errors beyond auth failures, you can implement your own retry wrapper: ```rust theme={null} use tokio::time::{sleep, Duration}; async fn download_with_retry( client: &Client, downloadable: &dyn Downloadable, max_retries: u32, ) -> Result> { let mut attempt = 0; loop { match client.download(downloadable).await { Ok(data) => return Ok(data), Err(e) if attempt < max_retries => { attempt += 1; eprintln!("Retry {}/{}: {:?}", attempt, max_retries, e); sleep(Duration::from_secs(2u64.pow(attempt))).await; } Err(e) => return Err(e), } } } ``` ## Media connection ### Automatic management The client automatically manages media connection tokens, including refreshing them when they expire or when the CDN rejects them with 401/403/404/410: ```rust theme={null} // Automatically refreshed when expired or rejected let upload = client.upload(data, MediaType::Image).await?; let download = client.download(downloadable).await?; // No manual token management needed! ``` ### Host types and failover order Each media connection includes multiple CDN hosts with a `host_type` field indicating whether the host is `"primary"` or `"fallback"`. The client sorts hosts with primary hosts first, so uploads and downloads always try the fastest hosts before falling back to alternatives. Each host may also include a `fallback_hostname` for additional resilience. ### Token expiration Media connections have two TTL values: * **`ttl`** — how long the route/host information is valid * **`auth_ttl`** — how long the auth token specifically is valid (may be shorter than `ttl`) The client uses the minimum of these two values to determine when the cached connection has expired. When expired, the next media operation automatically fetches fresh credentials. ### Internal refresh Media connection refresh and invalidation are managed internally by the client. When the CDN rejects a request (401/403/404/410), the client automatically invalidates the cached connection and fetches fresh credentials before retrying. You don't need to manage this manually. ## Best Practices ### Use Streaming for Large Files ```rust theme={null} // ❌ Bad: Loads entire file into memory let data = client.download(video.as_ref()).await?; std::fs::write("video.mp4", data)?; // ✅ Good: Constant memory usage let file = File::create("video.mp4")?; let _file = client.download_to_writer(video.as_ref(), file).await?; ``` ### Verify Media Type Check `mimetype` before processing: ```rust theme={null} if let Some(img) = &message.image_message { if let Some(mimetype) = &img.mimetype { match mimetype.as_str() { "image/jpeg" | "image/png" => { // Process image } _ => { eprintln!("Unsupported image type: {}", mimetype); } } } } ``` ### Handle Missing Fields Media messages may have optional fields: ```rust theme={null} if let Some(doc) = &message.document_message { let filename = doc.file_name.as_deref().unwrap_or("unknown"); let mimetype = doc.mimetype.as_deref().unwrap_or("application/octet-stream"); let size = doc.file_length.unwrap_or(0); println!("Document: {} ({}, {} bytes)", filename, mimetype, size); } ``` ### Set Proper Dimensions For images and videos, include dimensions: ```rust theme={null} use image::GenericImageView; let img = image::open("photo.jpg")?; let (width, height) = img.dimensions(); let message = wa::Message { image_message: Some(Box::new(wa::message::ImageMessage { url: Some(upload.url), direct_path: Some(upload.direct_path), media_key: Some(upload.media_key), file_sha256: Some(upload.file_sha256), file_enc_sha256: Some(upload.file_enc_sha256), file_length: Some(upload.file_length), mimetype: Some("image/jpeg".to_string()), width: Some(width), height: Some(height), ..Default::default() })), ..Default::default() }; ``` ## Next Steps * [Sending Messages](/guides/sending-messages) - Send media with captions and quotes * [Receiving Messages](/guides/receiving-messages) - Handle incoming media * [Custom Backends](/guides/custom-backends) - Implement custom HTTP clients for media operations # Receiving Messages Source: https://whatsapp-rust.jlucaso.com/guides/receiving-messages Learn how to handle incoming messages, decrypt content, and send receipts in whatsapp-rust ## Overview This guide covers message event handling, decryption, and receipt management in whatsapp-rust. ## Event System ### Subscribing to Events Use the Bot API to handle events: ```rust theme={null} use whatsapp_rust::bot::Bot; use wacore::types::events::Event; let bot = Bot::builder() .with_backend(backend) .with_transport_factory(transport_factory) .with_http_client(http_client) .on_event(|event, client| async move { match event { Event::Message(message, info) => { println!("📨 Message from: {}", info.source.sender); println!("💬 Text: {:?}", message.text_content()); } Event::Connected(_) => { println!("✅ Connected!"); } _ => {} } }) .build() .await?; ``` See [Bot API reference](/api/bot#event-handling) for full details. ### Available Events ```rust theme={null} pub enum Event { /// Successfully decrypted message Message(Box, MessageInfo), /// Message that couldn't be decrypted UndecryptableMessage(UndecryptableMessage), /// Client connected to WhatsApp Connected(Connected), /// Client disconnected Disconnected(Disconnected), /// Logged out (session invalidated) LoggedOut(LoggedOut), /// Pairing QR code for scanning PairingQrCode { code: String, timeout: Duration }, /// Receipt (delivery, read, played) Receipt(Receipt), // ... other events } ``` ## Message Structure ### MessageInfo Every message event includes metadata: ```rust theme={null} use crate::types::message::MessageInfo; // Access message metadata println!("Message ID: {}", info.id); println!("Timestamp: {}", info.timestamp); println!("Chat: {}", info.source.chat); println!("Sender: {}", info.source.sender); println!("From me: {}", info.source.is_from_me); // Check if it's a group message if info.source.chat.is_group() { println!("Group message from participant: {}", info.source.sender); } ``` ### Message Content Extraction Use the `MessageExt` trait to extract content: ```rust theme={null} use wacore::proto_helpers::MessageExt; // Get text content (conversation or extended_text_message) if let Some(text) = message.text_content() { println!("Text: {}", text); } // Get media caption if let Some(caption) = message.get_caption() { println!("Caption: {}", caption); } // Get base message (unwrap ephemeral/view-once/edited wrappers) let base = message.get_base_message(); // Check wrapper types if message.is_ephemeral() { println!("This is an ephemeral message"); } if message.is_view_once() { println!("This is a view-once message"); } // Access message context info (thread metadata, bot info, etc.) if let Some(ctx_info) = &message.message_context_info { if let Some(thread_id) = &ctx_info.thread_id { println!("Thread: {}", thread_id); } } ``` See [WAProto API reference](/api/waproto) for the full message type hierarchy. ## Message Types ### Text Messages ```rust theme={null} match event { Event::Message(message, info) => { // Simple text if let Some(text) = &message.conversation { println!("Text: {}", text); } // Extended text (with links, formatting) if let Some(ext) = &message.extended_text_message { if let Some(text) = &ext.text { println!("Extended text: {}", text); } if let Some(url) = &ext.matched_text { println!("Contains link: {}", url); } } } _ => {} } ``` ### Media Messages ```rust theme={null} if let Some(img) = &message.image_message { println!("📷 Image message"); if let Some(caption) = &img.caption { println!("Caption: {}", caption); } // Download the image let data = client.download(img.as_ref()).await?; std::fs::write("image.jpg", data)?; } if let Some(video) = &message.video_message { println!("🎥 Video message"); } if let Some(audio) = &message.audio_message { println!("🎵 Audio message"); if audio.ptt() { println!("This is a voice message"); } } if let Some(doc) = &message.document_message { println!("📄 Document: {}", doc.file_name.as_deref().unwrap_or("unknown")); } if let Some(sticker) = &message.sticker_message { println!("🎨 Sticker"); } ``` See [Media Handling Guide](/guides/media-handling) for download details. ### Reactions ```rust theme={null} if let Some(reaction) = &message.reaction_message { if let Some(emoji) = &reaction.text { println!("👍 Reaction: {}", emoji); } else { println!("Reaction removed"); } if let Some(key) = &reaction.key { println!("Reacted to message: {:?}", key.id); } } ``` ### Quoted Messages ```rust theme={null} if let Some(ext) = &message.extended_text_message { if let Some(context) = &ext.context_info { if let Some(quoted) = &context.quoted_message { println!("💬 This is a reply"); println!("Original message ID: {:?}", context.stanza_id); println!("Original sender: {:?}", context.participant); // Access quoted content if let Some(quoted_text) = quoted.text_content() { println!("Replying to: {}", quoted_text); } } } } ``` ## Message Unwrapping ### DeviceSentMessage handling When you send a message from one device, other devices receive it as a `DeviceSentMessage` wrapper. The library automatically unwraps this and merges `messageContextInfo` from both the outer envelope and inner message: ```rust theme={null} // The library handles this automatically - you receive the inner message directly match event { Event::Message(message, info) => { // If this was originally a DeviceSentMessage, the library has: // 1. Extracted the inner message content // 2. Merged message_context_info from outer + inner // - message_secret: inner value, fallback to outer // - limit_sharing_v2: always from outer // - thread_id: inner if non-empty, otherwise outer // - bot_metadata: inner value, fallback to outer // You can safely access the merged context if let Some(ctx) = &message.message_context_info { println!("Thread: {:?}", ctx.thread_id); } } _ => {} } ``` Self-sent messages synced from your primary device are automatically unwrapped. The `messageContextInfo` is merged following WhatsApp Web's logic, ensuring metadata like thread IDs and bot metadata are preserved correctly. ## Decryption ### Automatic decryption Messages are automatically decrypted by the client: ```rust theme={null} // The Event::Message already contains decrypted content match event { Event::Message(message, info) => { // Message is already decrypted and ready to use println!("Decrypted: {:?}", message.conversation); } _ => {} } ``` ### Undecryptable Messages When decryption fails, you receive an `UndecryptableMessage` event: ```rust theme={null} match event { Event::UndecryptableMessage(undecryptable) => { println!("❌ Could not decrypt message"); println!("Message ID: {}", undecryptable.info.id); println!("From: {}", undecryptable.info.source.sender); // The client automatically sends retry receipts // No manual action needed } _ => {} } ``` The client automatically handles decryption retries using the retry receipt mechanism. Failed messages trigger `Event::UndecryptableMessage`, and the client will request re-encryption from the sender. See [Signal Protocol](/advanced/signal-protocol) and [Events reference](/concepts/events) for more details. ### Decryption Retry Mechanism The library automatically: 1. Detects decryption failures (no session, invalid keys, MAC errors) 2. Sends retry receipts with fresh prekeys 3. Tracks retry count (max 5 attempts) 4. Falls back to PDO (Peer Data Operation) as last resort ```rust theme={null} // Retry reasons (internal, handled automatically) enum RetryReason { NoSession = 1, // No session exists InvalidKey = 2, // Invalid key InvalidKeyId = 3, // PreKey ID not found InvalidMessage = 4, // Invalid format or MAC // ... other reasons } ``` ### Sent message retry (outbound) When a recipient's device cannot decrypt your message, it sends a retry receipt. The client handles this automatically using DB-backed sent message storage: 1. Every `send_message()` persists the serialized message payload to the `sent_messages` database table 2. On retry receipt, the client retrieves the original payload, re-encrypts it for the requesting device, and resends 3. The payload is consumed (deleted) on retrieval to prevent double-retry 4. Expired entries are periodically cleaned up based on `sent_message_ttl_secs` (default: 5 minutes) This matches WhatsApp Web's `getMessageTable` pattern of reading from persistent storage on retry receipt. An optional in-memory L1 cache (`recent_messages` in `CacheConfig`) can be enabled for faster retry lookups. When disabled (default, capacity 0), all retry lookups go directly to the database. See [Bot - Cache Configuration Reference](/api/bot#cache-configuration-reference) for details. ## Receipts ### Automatic Delivery Receipts The client automatically sends delivery receipts for successfully decrypted messages: ```rust theme={null} // Happens automatically after message decryption // No manual action needed ``` See [Receipt API reference](/api/receipt) for full details. ### Sending Read Receipts ```rust theme={null} // Mark a single message as read (DM) client.mark_as_read( &chat_jid, None, // No sender for DMs vec!["msg1".to_string()], ).await?; // Mark multiple messages as read (group) client.mark_as_read( &group_jid, Some(&sender_jid), // Must specify sender in groups vec!["msg1".to_string(), "msg2".to_string(), "msg3".to_string()], ).await?; ``` ### Receipt Events Handle receipt updates from other participants: ```rust theme={null} match event { Event::Receipt(receipt) => { println!("📬 Receipt for: {:?}", receipt.message_ids); match receipt.r#type { ReceiptType::Delivered => println!("Delivered"), ReceiptType::Read => println!("Read"), ReceiptType::ReadSelf => println!("Read on another device"), ReceiptType::Played => println!("Played (voice/video)"), ReceiptType::EncRekeyRetry => println!("VoIP call re-keying retry"), _ => {} } } _ => {} } ``` ## Advanced Usage ### Custom Encryption Handlers For custom encryption types (e.g., `pkmsg`, `msg`, `skmsg`): ```rust theme={null} use whatsapp_rust::client::EncHandler; use async_trait::async_trait; use std::sync::Arc; #[derive(Clone)] struct CustomEncHandler; #[async_trait] impl EncHandler for CustomEncHandler { async fn handle( &self, client: Arc, node: &Node, info: &MessageInfo, ) -> Result<(), anyhow::Error> { // Custom decryption logic println!("Custom encryption type: {:?}", node.attrs().optional_string("type")); Ok(()) } } // Register the handler via BotBuilder let bot = Bot::builder() .with_enc_handler("custom_type", CustomEncHandler) // ... other configuration .build() .await?; ``` See [Client API reference](/api/client) for handler registration details. ### Filtering Messages ```rust theme={null} use wacore_binary::jid::JidExt; .on_event(|event, _client| async move { match event { Event::Message(message, info) => { // Ignore own messages if info.source.is_from_me { return; } // Only handle group messages if !info.source.chat.is_group() { return; } // Only handle text messages if let Some(text) = message.text_content() { println!("Group text: {}", text); } } _ => {} } }) ``` ### Session and Key Management The library automatically manages Signal Protocol sessions: ```rust theme={null} // Sessions are established automatically when: // - Receiving PreKeySignalMessage (pkmsg) // - Receiving SignalMessage (msg) for existing sessions // - Receiving SenderKeyDistributionMessage for groups // No manual session management needed! ``` For advanced cases (identity changes, session cleanup): ```rust theme={null} // The library handles identity changes automatically: // - Detects UntrustedIdentity errors // - Clears old identity // - Retries decryption with new identity // - Preserves old session for in-flight messages ``` See [Signal Protocol](/advanced/signal-protocol) for more on session management. ## Error Handling ```rust theme={null} .on_event(|event, client| async move { match event { Event::Message(message, info) => { // Process message if let Err(e) = process_message(&message, &info, client).await { eprintln!("Error processing message {}: {:?}", info.id, e); } } Event::UndecryptableMessage(undecryptable) => { eprintln!("⚠️ Undecryptable message from {}", undecryptable.info.source.sender); // Client automatically handles retries } _ => {} } }) async fn process_message( message: &wa::Message, info: &MessageInfo, client: Arc, ) -> Result<()> { // Your message processing logic Ok(()) } ``` ## Best Practices ### Use the Bot API for Event Handling The Bot API provides a clean interface for message handling: ```rust theme={null} let bot = Bot::builder() .on_event(|event, client| async move { // Handle events here }) .build() .await?; ``` ### Extract Content with Helper Methods ```rust theme={null} use wacore::proto_helpers::MessageExt; // Use helper methods instead of manual field access let text = message.text_content(); let caption = message.get_caption(); let base = message.get_base_message(); ``` ### Handle All Event Types Always handle critical events: ```rust theme={null} match event { Event::Message(message, info) => { /* ... */ } Event::Connected(_) => { /* Initialize */ } Event::Disconnected(_) => { /* Cleanup */ } Event::LoggedOut(_) => { /* Re-authenticate */ } _ => {} } ``` ### Don't Block Event Handlers Spawn tasks for long-running operations: ```rust theme={null} .on_event(|event, client| async move { match event { Event::Message(message, info) => { // Spawn task for heavy processing let client = client.clone(); tokio::spawn(async move { process_heavy_task(message, info, client).await; }); } _ => {} } }) ``` ## Next Steps * [Sending Messages](/guides/sending-messages) - Send text, reactions, and replies * [Media Handling](/guides/media-handling) - Download and process media * [Group Management](/guides/group-management) - Handle group events # Sending Messages Source: https://whatsapp-rust.jlucaso.com/guides/sending-messages Learn how to send text messages, reactions, quoted replies, and edit messages in whatsapp-rust ## Overview This guide covers sending messages, including text, reactions, quotes, and message editing operations using the whatsapp-rust library. ## Sending Text Messages ### Simple Text Message Use the `conversation` field for plain text messages: ```rust theme={null} use waproto::whatsapp as wa; use wacore_binary::jid::Jid; let to: Jid = "1234567890@s.whatsapp.net".parse()?; let message = wa::Message { conversation: Some("Hello, WhatsApp!".to_string()), ..Default::default() }; let message_id = client.send_message(to, message).await?; println!("Message sent with ID: {}", message_id); ``` ### Extended Text Message For messages with formatting, links, or context (replies/quotes): ```rust theme={null} use waproto::whatsapp::message::ExtendedTextMessage; let message = wa::Message { extended_text_message: Some(Box::new(ExtendedTextMessage { text: Some("Check out this link: https://example.com".to_string()), matched_text: Some("https://example.com".to_string()), title: Some("Example Site".to_string()), description: Some("A sample website".to_string()), ..Default::default() })), ..Default::default() }; let message_id = client.send_message(to, message).await?; ``` ## Quoted Replies ### Replying to a Message Use `build_quote_context` to create a reply: ```rust theme={null} use wacore::proto_helpers::{build_quote_context, MessageExt}; // Assume you received the original message let original_message: wa::Message = /* ... */; let original_message_id = "3EB0ABC123"; let original_sender = "1234567890@s.whatsapp.net"; // Build quote context let context = build_quote_context( original_message_id, original_sender, &original_message, ); // Create reply message let reply = wa::Message { extended_text_message: Some(Box::new(ExtendedTextMessage { text: Some("This is my reply".to_string()), context_info: Some(Box::new(context)), ..Default::default() })), ..Default::default() }; let message_id = client.send_message(to, reply).await?; ``` ### Setting Context on Media Messages You can add quote context to any message type using `set_context_info`: ```rust theme={null} use waproto::whatsapp::message::ImageMessage; let mut reply = wa::Message { image_message: Some(Box::new(ImageMessage { url: Some("https://mmg.whatsapp.net/...".to_string()), mimetype: Some("image/jpeg".to_string()), caption: Some("Here's an image reply".to_string()), // ... other image fields ..Default::default() })), ..Default::default() }; // Build and set context let context = build_quote_context( original_message_id, original_sender, &original_message, ); reply.set_context_info(context); let message_id = client.send_message(to, reply).await?; ``` ## Reactions ### Sending a Reaction Reactions are sent using `ReactionMessage`: ```rust theme={null} use waproto::whatsapp::message::ReactionMessage; let reaction = wa::Message { reaction_message: Some(Box::new(ReactionMessage { key: Some(wa::MessageKey { remote_jid: Some(chat_jid.to_string()), from_me: Some(false), id: Some(message_id_to_react_to.to_string()), participant: Some(sender_jid.to_string()), }), text: Some("👍".to_string()), sender_timestamp_ms: Some( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as i64 ), ..Default::default() })), ..Default::default() }; let message_id = client.send_message(chat_jid, reaction).await?; ``` ### Removing a Reaction Send an empty reaction text to remove: ```rust theme={null} let remove_reaction = wa::Message { reaction_message: Some(Box::new(ReactionMessage { key: Some(wa::MessageKey { remote_jid: Some(chat_jid.to_string()), from_me: Some(false), id: Some(message_id_to_react_to.to_string()), participant: Some(sender_jid.to_string()), }), text: Some("".to_string()), // Empty to remove sender_timestamp_ms: Some( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as i64 ), ..Default::default() })), ..Default::default() }; ``` ## Editing Messages You can edit text messages using `EditedMessage`: ```rust theme={null} use waproto::whatsapp::message::FutureProofMessage; let edited = wa::Message { edited_message: Some(Box::new(FutureProofMessage { message: Some(Box::new(wa::Message { protocol_message: Some(Box::new(wa::message::ProtocolMessage { key: Some(wa::MessageKey { remote_jid: Some(chat_jid.to_string()), from_me: Some(true), id: Some(original_message_id.to_string()), participant: None, }), r#type: Some(wa::message::protocol_message::Type::MessageEdit as i32), edited_message: Some(Box::new(wa::Message { conversation: Some("This is the edited text".to_string()), ..Default::default() })), timestamp_ms: Some( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as i64 ), ..Default::default() })), ..Default::default() })), })), ..Default::default() }; let message_id = client.send_message(chat_jid, edited).await?; ``` Message editing only works for text messages (conversation or extended\_text\_message) sent by you within the last 15 minutes. ## Deleting Messages (Revoke) ### Delete Your Own Message ```rust theme={null} use whatsapp_rust::send::RevokeType; client.revoke_message( chat_jid, message_id, RevokeType::Sender, ).await?; ``` See [Send API reference](/api/send#revoke_message) for full details. ### Admin Delete (Group Only) Group admins can delete messages from other participants: ```rust theme={null} let original_sender: Jid = "1234567890@s.whatsapp.net".parse()?; client.revoke_message( group_jid, message_id, RevokeType::Admin { original_sender }, ).await?; ``` Admin revoke only works in group chats. The `original_sender` must match the JID format (LID or phone number) of the message being deleted. ## Send Options ### Adding Extra Stanza Nodes For advanced use cases, you can include custom XML nodes: ```rust theme={null} use whatsapp_rust::send::SendOptions; use wacore_binary::builder::NodeBuilder; let options = SendOptions { extra_stanza_nodes: vec![ NodeBuilder::new("custom") .attr("key", "value") .build(), ], }; let message_id = client.send_message_with_options( to, message, options, ).await?; ``` See [Send API reference](/api/send#send-options) for full details. ## Message Preparation Helpers ### Preparing Messages for Quoting The `prepare_for_quote` method strips nested context info: ```rust theme={null} use wacore::proto_helpers::MessageExt; let quoted_message = original_message.prepare_for_quote(); let context = wa::ContextInfo { stanza_id: Some(message_id.clone()), participant: Some(sender_jid.to_string()), quoted_message: Some(quoted_message), ..Default::default() }; ``` This ensures: * Nested mentions are stripped * Quote chains are broken (except for bot messages) * Content fields (text, caption, media) are preserved See [WAProto API reference](/api/waproto) for message type details. ## Error Handling ```rust theme={null} use anyhow::Result; async fn send_safe_message( client: &Client, to: Jid, text: &str, ) -> Result { let message = wa::Message { conversation: Some(text.to_string()), ..Default::default() }; match client.send_message(to.clone(), message).await { Ok(id) => { println!("✅ Message sent: {}", id); Ok(id) } Err(e) => { eprintln!("❌ Failed to send: {:?}", e); Err(e) } } } ``` ## Best Practices ### Use the Right Message Type * **Simple text**: Use `conversation` * **Links/formatting**: Use `extended_text_message` * **Replies**: Use `extended_text_message` with `context_info` * **Media**: Use specific media message types with optional captions ### Handle Message IDs ```rust theme={null} // Store message ID for later operations let message_id = client.send_message(to, message).await?; // Use it for reactions, edits, or deletes client.revoke_message(to, &message_id, RevokeType::Sender).await?; ``` ### Quote Context Best Practices * Always use `prepare_for_quote()` to avoid nested quote chains * Use `build_quote_context_with_info` for special message types (newsletters, status) * Preserve original media fields when quoting media messages ## Next Steps * [Receiving Messages](/guides/receiving-messages) - Handle incoming messages and events * [Media Handling](/guides/media-handling) - Upload and download media * [Group Management](/guides/group-management) - Work with group chats # Installation Source: https://whatsapp-rust.jlucaso.com/installation Add whatsapp-rust to your Rust project ## Prerequisites Before installing whatsapp-rust, ensure you have: * **Rust 2024 edition** or later (requires recent stable Rust) * **Cargo** package manager SQLite is bundled by default with the `whatsapp-rust-sqlite-storage` crate, so you don't need to install it separately. If you prefer to link against a system-installed SQLite, disable the default `bundled-sqlite` feature. ## Add to your project Add whatsapp-rust and its required dependencies to your `Cargo.toml`: ```toml Default features theme={null} [dependencies] whatsapp-rust = "0.3" whatsapp-rust-sqlite-storage = "0.3" whatsapp-rust-tokio-transport = "0.3" whatsapp-rust-ureq-http-client = "0.3" wacore = "0.3" waproto = "0.3" tokio = { version = "1.48", features = ["macros", "rt-multi-thread"] } ``` ```toml Minimal setup theme={null} [dependencies] whatsapp-rust = { version = "0.3", default-features = false } whatsapp-rust-sqlite-storage = "0.3" whatsapp-rust-tokio-transport = "0.3" whatsapp-rust-ureq-http-client = "0.3" wacore = "0.3" waproto = "0.3" tokio = { version = "1.48", features = ["macros", "rt-multi-thread"] } ``` ## Feature flags whatsapp-rust supports several optional features: | Feature | Description | Included by default | | ------------------------ | ------------------------------------------------ | ------------------- | | `simd` | SIMD-optimized binary protocol encoding/decoding | ✅ Yes | | `sqlite-storage` | SQLite storage backend | ✅ Yes | | `tokio-transport` | Tokio WebSocket transport | ✅ Yes | | `ureq-client` | Ureq HTTP client | ✅ Yes | | `tokio-native` | Tokio multi-threaded runtime | ✅ Yes | | `signal` | Unix signal handling | ❌ No | | `danger-skip-tls-verify` | Skip TLS verification (unsafe) | ❌ No | | `debug-snapshots` | Debug protocol snapshots | ❌ No | | `debug-diagnostics` | Memory diagnostics API (`MemoryDiagnostics`) | ❌ No | The `whatsapp-rust-sqlite-storage` crate has its own feature flags: | Feature | Description | Included by default | | ---------------- | --------------------------------------------------------------- | ------------------- | | `bundled-sqlite` | Bundles SQLite (compiles from source, no system library needed) | ✅ Yes | To use a system-installed SQLite instead of the bundled version: ```toml Cargo.toml theme={null} [dependencies] whatsapp-rust-sqlite-storage = { version = "0.3", default-features = false } ``` The default features provide everything needed for most use cases. Only customize features if you have specific requirements. ## Custom features example If you want to use only specific features: ```toml Cargo.toml theme={null} [dependencies] whatsapp-rust = { version = "0.3", default-features = false, features = ["sqlite-storage", "tokio-transport"] } ``` ## Verify installation Create a simple test file to verify the installation: ```rust src/main.rs theme={null} use whatsapp_rust::bot::Bot; fn main() { println!("whatsapp-rust installed successfully!"); } ``` Run it with: ```bash theme={null} cargo run ``` If you see "whatsapp-rust installed successfully!", you're ready to move on to the [Quickstart](/quickstart) guide. ## Next steps Build your first WhatsApp bot in minutes # Introduction Source: https://whatsapp-rust.jlucaso.com/introduction A high-performance, async Rust library for the WhatsApp Web API # whatsapp-rust A high-performance, async Rust library for the WhatsApp Web API. Inspired by [whatsmeow](https://github.com/tulir/whatsmeow) (Go) and [Baileys](https://github.com/WhiskeySockets/Baileys) (TypeScript). This is an unofficial, open-source reimplementation. Using custom WhatsApp clients may violate Meta's Terms of Service and could result in account suspension. Use at your own risk. ## Key features ### Authentication * **QR code pairing** - Scan QR code from your phone to authenticate * **Pair code linking** - Link using phone number with 8-digit code * **Persistent sessions** - Automatic reconnection with session management ### Messaging * **End-to-end encryption** - Full Signal Protocol implementation * **One-on-one and group chats** - Send and receive messages in any chat type * **Message editing and reactions** - Edit sent messages and add reactions * **Quoting/replying** - Reply to specific messages with context * **Delivery receipts** - Track delivery, read, and played status ### Media handling * **Upload and download** - Support for images, videos, documents, GIFs, and audio * **Automatic encryption** - Transparent encryption/decryption for all media ### Contacts and groups * **Phone number validation** - Check if phone numbers are on WhatsApp * **Profile management** - Get/set profile pictures, push name, and status text * **Group management** - Create, query, and manage groups with full settings control * **Group listing** - List all groups you're participating in ### Status updates * **Text, image, and video** - Post status updates with background colors and fonts * **Privacy controls** - Send to all contacts, allow lists, or deny lists * **Revocation** - Delete posted status updates ### Presence and chat state * **Online/offline status** - Set your presence status * **Typing indicators** - Show composing, recording, and paused states * **Contact management** - Block and unblock contacts ### Chat organization * **Archive/pin/mute** - Organize chats with archive, pin, and mute actions * **Star messages** - Star and unstar individual messages * **App state sync** - Actions sync across all linked devices ### Architecture * **Modular design** - Pluggable storage, transport, and HTTP clients * **Tokio-powered** - Built on Tokio with a runtime-agnostic core (`wacore`) * **SQLite included** - Default storage backend, easily swappable ## Getting started Add whatsapp-rust to your project with cargo Build your first WhatsApp bot in minutes Understand the modular architecture Implement your own storage or transport layer ## Project structure The library is organized into multiple crates for maximum flexibility: ``` whatsapp-rust/ ├── src/ # Main client library ├── wacore/ # Platform-agnostic core (runtime-agnostic) │ ├── binary/ # WhatsApp binary protocol │ ├── libsignal/ # Signal Protocol implementation │ └── appstate/ # App state management ├── waproto/ # Protocol Buffers definitions ├── storages/sqlite-storage # SQLite backend ├── transports/tokio-transport └── http_clients/ureq-client ``` ## Acknowledgements This project would not be possible without the excellent work of: * [whatsmeow](https://github.com/tulir/whatsmeow) - Go implementation * [Baileys](https://github.com/WhiskeySockets/Baileys) - TypeScript implementation # Quickstart Source: https://whatsapp-rust.jlucaso.com/quickstart Build your first WhatsApp bot in minutes This guide will help you create a simple WhatsApp bot that responds to messages. You'll learn the core concepts and have a working bot by the end. ## Basic example Here's a minimal bot that responds to "ping" messages: ```rust src/main.rs theme={null} use std::sync::Arc; use whatsapp_rust::bot::Bot; use whatsapp_rust::store::SqliteStore; use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; use whatsapp_rust_ureq_http_client::UreqHttpClient; use wacore::types::events::Event; #[tokio::main] async fn main() -> Result<(), Box> { // Initialize storage backend let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); // Build the bot let mut bot = Bot::builder() .with_backend(backend) .with_transport_factory(TokioWebSocketTransportFactory::new()) .with_http_client(UreqHttpClient::new()) .on_event(|event, client| async move { match event { Event::PairingQrCode { code, .. } => { println!("Scan this QR code with WhatsApp:\n{}", code); } Event::Message(msg, info) => { println!("Message from {}: {:?}", info.source.sender, msg); } _ => {} } }) .build() .await?; // Start the bot bot.run().await?.await?; Ok(()) } ``` ## Step-by-step breakdown The bot needs persistent storage for session data, keys, and state: ```rust theme={null} let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); ``` This creates a SQLite database file named `whatsapp.db` in your current directory. The session will persist across restarts. The `Bot::builder()` pattern lets you configure all required components: ```rust theme={null} let mut bot = Bot::builder() .with_backend(backend) .with_transport_factory(TokioWebSocketTransportFactory::new()) .with_http_client(UreqHttpClient::new()) ``` All three components (backend, transport, HTTP client) are required. The builder will return an error if any are missing. Use `.on_event()` to handle incoming events from WhatsApp: ```rust theme={null} .on_event(|event, client| async move { match event { Event::PairingQrCode { code, .. } => { println!("QR Code:\n{}", code); } Event::Message(msg, info) => { // Handle incoming message } Event::Connected(_) => { println!("Connected successfully!"); } _ => {} } }) ``` The event handler receives two parameters: * `event`: The event type (QR code, message, connection status, etc.) * `client`: An `Arc` you can use to send messages or call API methods Build the bot and start the event loop: ```rust theme={null} .build() .await?; bot.run().await?.await?; ``` The double `.await?` is intentional: * First `.await?` starts the bot and returns a `JoinHandle` * Second `.await?` waits for the bot to finish running ## Responding to messages Let's extend the bot to respond to "ping" with "pong": ```rust theme={null} use wacore::proto_helpers::MessageExt; use waproto::whatsapp as wa; .on_event(|event, client| async move { match event { Event::PairingQrCode { code, .. } => { println!("QR Code:\n{}", code); } Event::Message(msg, info) => { // Check if message is a text message saying "ping" if let Some(text) = msg.text_content() { if text == "ping" { // Create reply message let reply = wa::Message { conversation: Some("pong".to_string()), ..Default::default() }; // Send the reply if let Err(e) = client.send_message(info.source.chat, reply).await { eprintln!("Failed to send reply: {}", e); } } } } _ => {} } }) ``` ### Key methods * `msg.text_content()` - Extract text from any message type (conversation, extended text, etc.) * `client.send_message()` - Send a message to a chat * `info.source.chat` - The JID (identifier) of the chat where the message came from * `info.source.sender` - The JID of the user who sent the message ## Authentication methods ### QR code pairing (default) The bot automatically generates QR codes when not authenticated. Scan with your phone to link: ```rust theme={null} Event::PairingQrCode { code, .. } => { println!("Scan this QR code:\n{}", code); } ``` ### Pair code (phone number) Alternatively, link using a phone number and 8-digit code: ```rust theme={null} use whatsapp_rust::pair_code::{PairCodeOptions, PlatformId}; let mut bot = Bot::builder() .with_backend(backend) .with_transport_factory(TokioWebSocketTransportFactory::new()) .with_http_client(UreqHttpClient::new()) .with_pair_code(PairCodeOptions { phone_number: "15551234567".to_string(), show_push_notification: true, custom_code: None, platform_id: PlatformId::Chrome, platform_display: "Chrome (Linux)".to_string(), }) .on_event(|event, client| async move { match event { Event::PairingCode { code, .. } => { println!("Enter this code on your phone: {}", code); } _ => {} } }) .build() .await?; ``` Pair code and QR code authentication run concurrently. Whichever method completes first will be used. ## Running the bot On the first run, the bot will generate a QR code: ```bash theme={null} cargo run ``` Scan the QR code with WhatsApp on your phone: 1. Open WhatsApp on your phone 2. Go to Settings → Linked Devices 3. Tap "Link a Device" 4. Scan the QR code displayed in your terminal After pairing, the session is saved. The bot will automatically reconnect: ```bash theme={null} cargo run ``` You should see: ``` Connected successfully! ``` Send "ping" to your bot from any WhatsApp chat. It should reply with "pong"! ## Complete example with logging Here's a production-ready example with proper logging: ```rust src/main.rs theme={null} use chrono::Local; use log::{error, info}; use std::sync::Arc; use wacore::proto_helpers::MessageExt; use wacore::types::events::Event; use waproto::whatsapp as wa; use whatsapp_rust::bot::Bot; use whatsapp_rust::store::SqliteStore; use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; use whatsapp_rust_ureq_http_client::UreqHttpClient; #[tokio::main] async fn main() -> Result<(), Box> { // Set up logging env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) .format(|buf, record| { use std::io::Write; writeln!( buf, "{} [{}] - {}", Local::now().format("%H:%M:%S"), record.level(), record.args() ) }) .init(); // Initialize backend let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); info!("SQLite backend initialized"); // Build bot let mut bot = Bot::builder() .with_backend(backend) .with_transport_factory(TokioWebSocketTransportFactory::new()) .with_http_client(UreqHttpClient::new()) .on_event(|event, client| async move { match event { Event::PairingQrCode { code, .. } => { println!("\n{}", code); } Event::Message(msg, info) => { if let Some(text) = msg.text_content() { info!("Received: {} from {}", text, info.source.sender); if text == "ping" { let reply = wa::Message { conversation: Some("pong".to_string()), ..Default::default() }; if let Err(e) = client.send_message(info.source.chat, reply).await { error!("Failed to send reply: {}", e); } } } } Event::Connected(_) => { info!("✅ Bot connected successfully!"); } Event::LoggedOut(_) => { error!("❌ Bot was logged out"); } _ => {} } }) .build() .await?; info!("Starting bot..."); bot.run().await?.await?; Ok(()) } ``` ## Configuring log targets whatsapp-rust uses the `log` crate with module-specific targets for fine-grained filtering. You can use `RUST_LOG` to control which components emit log output. ### Available log targets | Target | Description | | ----------------------- | ------------------------------------------------- | | `Client/Keepalive` | Keepalive ping/pong and dead socket detection | | `Client/Recv` | Incoming frame processing (unmarshal, decompress) | | `Client/Send` | Outgoing message encryption and dispatch | | `Client/OfflineSync` | Offline message sync progress and timing | | `Client/AppState` | App state sync (contacts, settings, etc.) | | `Client/AccountSync` | Account-level sync operations | | `Client/PairCode` | Pair code authentication flow | | `Client/Pair` | QR code pairing flow | | `Client/Receipt` | Receipt processing (read, delivered, played) | | `Client/TcToken` | Trusted contact token operations | | `Client/Group` | Group metadata and participant operations | | `Client/Contacts` | Contact sync and lookup | | `Client/Business` | Business profile updates | | `Client/PDO` | Pre-delivery operations and retry | | `Client/IQ` | IQ stanza send/receive | | `Client/Ack` | Stanza acknowledgment | | `Client/Status` | Status/story operations | | `Client/Picture` | Profile picture updates | | `Client/UnifiedSession` | Session establishment | | `Blocking` | Block/unblock operations | | `Chatstate` | Typing indicator events | | `PresenceHandler` | Presence update processing | | `AppState` | App state patch encoding/decoding | | `Bot/PairCode` | Bot-level pair code handling | ### Filtering examples ```bash theme={null} # Show only connection and messaging logs RUST_LOG="Client/Keepalive=debug,Client/Send=debug,Client/Recv=trace" cargo run # Debug offline sync issues RUST_LOG="Client/OfflineSync=debug" cargo run # Quiet mode: only errors and warnings RUST_LOG="warn" cargo run # Verbose: all client internals at debug level RUST_LOG="debug" cargo run ``` During shutdown or disconnect, the client automatically downgrades sync errors from `error` to `debug` level to reduce noise. This means you won't see spurious error logs when the client is intentionally disconnecting. ## Next steps Learn about different message types and how to send them Upload and download images, videos, and documents Create and manage WhatsApp groups Explore all available client methods