whatsapp-rust 0.3.0

Rust client for WhatsApp Web
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# WhatsApp-Rust Copilot Instructions

You are an expert Rust developer specializing in asynchronous networking, cryptography, and reverse-engineered protocols. Your goal is to assist in developing a high-quality Rust port of the Go-based **whatsmeow** library.

---

## 1. Architecture Overview

The project is split into three main crates:

- **wacore**
  A platform-agnostic library containing core logic for the WhatsApp binary protocol, cryptography primitives, IQ protocol types, and state management traits.
  It has **no runtime dependencies** on Tokio or specific databases.

- **waproto**
  Houses the Protocol Buffers definitions (`whatsapp.proto`). It contains a `build.rs` script that uses **prost** to compile these definitions into Rust structs.

- **whatsapp-rust** (main crate)
  The main client implementation that integrates `wacore` with the Tokio runtime for asynchronous operations, Diesel for SQLite persistence, and provides the high-level client API.

### Key Components

- **Client** (`src/client.rs`): Orchestrates the connection lifecycle, event bus, and high-level operations.
- **PersistenceManager** (`src/store/persistence_manager.rs`): Manages all state.
- **Signal Protocol** (`wacore/libsignal/` & `src/store/signal*.rs`): E2E encryption via our Signal Protocol implementation.
- **Socket & Handshake** (`src/socket/`, `src/handshake.rs`): Handles WebSocket connection and Noise Protocol handshake.

---

## 2. Current Project State & Focus

### Stable Features

- QR code pairing and persistent sessions.
- Connection management and automatic reconnection.
- End-to-End encrypted one-on-one messaging (send/receive).
- End-to-End encrypted group messaging (send/receive).
- Media uploads and downloads (images, videos, documents, etc.), including all necessary encryption and decryption logic.

---

## 3. Critical Patterns & Conventions

- **State Management is Paramount**

  - Never modify Device state directly.
  - Use `DeviceCommand` + `PersistenceManager::process_command()`.
  - For read-only, use `PersistenceManager::get_device_snapshot()`.

- **Asynchronous Code**

  - All I/O uses Tokio. Be mindful of race conditions.
  - Use `Client::chat_locks` to serialize per-chat operations.
  - **All blocking I/O (like `ureq` calls) and heavy CPU-bound tasks (like media encryption) MUST be wrapped in `tokio::task::spawn_blocking` to avoid stalling the async runtime.**

- **Media Handling**

  - Media operations are handled in `src/download.rs` and `src/upload.rs`.
  - The `Downloadable` trait in `wacore/src/download.rs` provides a generic interface for any message type that contains downloadable media.
  - The `MediaConn` struct (`src/mediaconn.rs`) is used to get the current media servers and auth tokens. Always refresh it if it's expired.

- **Error Handling**

  - Use `thiserror` for custom errors (`SocketError`, etc.).
  - Use `anyhow::Error` for functions with multiple failure modes.
  - Avoid `.unwrap()` and `.expect()` outside of tests and unrecoverable logic paths.

- **Comments**
  - Keep comments concise and actionable.
  - Avoid narrating obvious code; prefer short summaries, invariants, or non-obvious behavior.

- **Protocol Implementation**
  - When in doubt, refer to the **whatsmeow** Go library as the source of truth.

---

## 4. Key Files for Understanding

- `src/client.rs`: Central hub of the client.
- `src/store/persistence_manager.rs`: Gatekeeper of all state changes.
- `src/message.rs`: Incoming message decryption pipeline.
- `src/send.rs`: Outgoing message encryption pipeline.
- `src/download.rs`: Media download logic.
- `src/upload.rs`: Media upload logic.
- `src/mediaconn.rs`: Media server connection management.
- `src/features/`: High-level feature APIs (groups, blocking, etc.).
- `wacore/src/iq/`: Type-safe IQ protocol types and specs.
- `waproto/src/whatsapp.proto`: Source of all message structures.
- `docs/captured-js/`: Captured WhatsApp Web JavaScript for reverse engineering.

---

## 5. Feature Implementation Philosophy (WhatsApp Web–based)

When adding a new feature, follow a repeatable flow that mirrors WhatsApp Web behavior while staying aligned with the project’s architecture:

1. **Identify the wire format first**
   - Capture or locate the WhatsApp Web request/response for the feature.
   - Extract the exact stanza structure: tags, attributes, and children.
   - Treat this as the ground truth for what must be sent and parsed.

2. **Map the feature to the right layer**
   - **wacore**: protocol logic, state traits, cryptographic helpers, and data models that must be platform-agnostic.
   - **whatsapp-rust**: runtime orchestration, storage integration, and user-facing API.
   - **waproto**: protobuf structures only (avoid feature logic here).

3. **Build minimal primitives before high-level APIs**
   - Start with the smallest IQ/message builder that can successfully round-trip.
   - Parse and validate the response path before adding options or convenience methods.

4. **Keep state changes behind the PersistenceManager**
   - If the feature touches device or chat state, use `DeviceCommand` and `PersistenceManager::process_command()`.
   - For read access, use `get_device_snapshot()`.

5. **Confirm concurrency requirements**
   - Network I/O stays async.
   - Blocking or heavy CPU work goes into `tokio::task::spawn_blocking`.
   - Use `Client::chat_locks` to serialize per-chat operations when needed.

6. **Add ergonomic API last**
   - Once the protocol is stable, add ergonomic Rust builders, enums, and result types.
   - Expose them via `src/features/mod.rs`.

7. **Test and verify**
   - Run `cargo fmt`, `cargo clippy --all-targets`, and `cargo test --all`.
   - Use logging to compare with WhatsApp Web traffic where applicable.

### Quick Structure Guide

- **Protocol entry points**: `src/send.rs`, `src/message.rs`, `src/socket/`, `src/handshake.rs`
- **Feature modules**: `src/features/`
- **State + storage**: `src/store/` + `PersistenceManager`
- **Core protocol & crypto**: `wacore/`
- **Protobufs**: `waproto/`

---

## 6. Type-Safe Protocol Node Architecture

All protocol stanza builders should use the declarative, type-safe pattern defined in `wacore/src/iq/`. This architecture provides compile-time safety, validation, and clear separation between request building and response parsing.

### Core Traits

#### `ProtocolNode` (`wacore/src/protocol.rs`)

Maps Rust structs to WhatsApp protocol nodes:

```rust
pub trait ProtocolNode: Sized {
    fn tag(&self) -> &'static str;
    fn into_node(self) -> Node;
    fn try_from_node(node: &Node) -> Result<Self>;
}
```

#### `IqSpec` (`wacore/src/iq/spec.rs`)

Pairs IQ requests with their typed responses:

```rust
pub trait IqSpec {
    type Response;
    fn build_iq(&self) -> InfoQuery<'static>;
    fn parse_response(&self, response: &Node) -> Result<Self::Response>;
}
```

### Implementation Pattern

1. **Define request struct with `ProtocolNode`**:

```rust
#[derive(Debug, Clone)]
pub struct GroupQueryRequest {
    pub request_type: String,
}

impl ProtocolNode for GroupQueryRequest {
    fn tag(&self) -> &'static str { "query" }
    fn into_node(self) -> Node {
        NodeBuilder::new("query")
            .attr("request", &self.request_type)
            .build()
    }
    fn try_from_node(node: &Node) -> Result<Self> { /* ... */ }
}
```

2. **Define response struct with `ProtocolNode`**:

```rust
pub struct GroupInfoResponse {
    pub id: Jid,
    pub subject: GroupSubject,
    pub addressing_mode: AddressingMode,
    pub participants: Vec<GroupParticipantResponse>,
}

impl ProtocolNode for GroupInfoResponse {
    fn tag(&self) -> &'static str { "group" }
    fn try_from_node(node: &Node) -> Result<Self> { /* parse from XML */ }
    fn into_node(self) -> Node { /* ... */ }
}
```

3. **Create IqSpec implementation**:

```rust
pub struct GroupQueryIq {
    group_jid: Jid,
}

impl GroupQueryIq {
    pub fn new(group_jid: &Jid) -> Self {
        Self { group_jid: group_jid.clone() }
    }
}

impl IqSpec for GroupQueryIq {
    type Response = GroupInfoResponse;

    fn build_iq(&self) -> InfoQuery<'static> {
        InfoQuery::get(
            GROUP_IQ_NAMESPACE,
            self.group_jid.clone(),
            Some(NodeContent::Nodes(vec![
                GroupQueryRequest::default().into_node()
            ])),
        )
    }

    fn parse_response(&self, response: &Node) -> Result<Self::Response> {
        GroupInfoResponse::try_from_node(response)
    }
}
```

4. **Use in feature code** (`src/features/`):

```rust
// Use client.execute() for simplified IQ handling
let group_response = self.client.execute(GroupQueryIq::new(&jid)).await?;
```

### Validated Newtypes

Use newtypes to enforce protocol constraints at compile time:

```rust
/// Group subject with WhatsApp's 100 character limit.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GroupSubject(String);

impl GroupSubject {
    pub fn new(subject: impl Into<String>) -> Result<Self, anyhow::Error> {
        let s = subject.into();
        if s.len() > GROUP_SUBJECT_MAX_LENGTH {
            return Err(anyhow!("subject exceeds {} chars", GROUP_SUBJECT_MAX_LENGTH));
        }
        Ok(Self(s))
    }
}
```

Constants from WhatsApp Web A/B props (`wacore/src/iq/groups.rs`):
- `GROUP_SUBJECT_MAX_LENGTH`: 100 characters
- `GROUP_DESCRIPTION_MAX_LENGTH`: 2048 characters
- `GROUP_SIZE_LIMIT`: 257 participants

### Strongly Typed Enums

Replace stringly-typed attributes with enums using the `StringEnum` derive macro:

```rust
use wacore::StringEnum;

#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
pub enum MemberAddMode {
    #[str = "admin_add"]
    AdminAdd,
    #[str = "all_member_add"]
    AllMemberAdd,
}

// Automatically generates:
// - as_str() -> &'static str
// - Display impl
// - TryFrom<&str> impl
// - Default impl (first variant, or use #[string_default])
```

For enums where the default should not be the first variant:

```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
pub enum MembershipApprovalMode {
    #[string_default]  // Mark this as default
    #[str = "off"]
    Off,
    #[str = "on"]
    On,
}
```

### Derive Macros (Recommended)

For simple nodes and enums, use the derive macros from `wacore-derive` (re-exported via `wacore`):

```rust
use wacore::{ProtocolNode, EmptyNode, StringEnum};

// Empty node (tag only)
#[derive(EmptyNode)]
#[protocol(tag = "participants")]
pub struct ParticipantsRequest;

// Node with string attributes
#[derive(ProtocolNode)]
#[protocol(tag = "query")]
pub struct QueryRequest {
    #[attr(name = "request", default = "interactive")]
    pub request_type: String,
}

// Enum with string representations
#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
pub enum BlocklistAction {
    #[str = "block"]
    Block,
    #[str = "unblock"]
    Unblock,
}
```

**Available derive macros:**
- `EmptyNode` - For nodes with only a tag (no attributes)
- `ProtocolNode` - For nodes with string attributes
- `StringEnum` - For enums with string representations (generates `as_str()`, `Display`, `TryFrom<&str>`, `Default`)

**Benefits over manual implementations:**
- Better IDE support (autocomplete, go-to-definition)
- Clearer error messages from the compiler
- Standard Rust derive pattern
- Less boilerplate code

### Declarative Macros (Legacy)

> **Note**: Prefer derive macros (`EmptyNode`, `ProtocolNode`, `StringEnum`) for new code.

For quick one-off definitions, declarative macros in `wacore/src/protocol.rs` are also available:

```rust
// Empty node
define_empty_node!(
    /// Wire format: `<participants/>`
    pub struct ParticipantsRequest("participants")
);

// Node with attributes
define_simple_node! {
    /// Wire format: `<query request="interactive"/>`
    pub struct QueryRequest("query") {
        #[attr("request")]
        pub request_type: String = "interactive",
    }
}
```

### Generic IQ Executor

Use `Client::execute()` for simplified IQ request/response handling:

```rust
// Before: manual build + send + parse
let spec = GroupQueryIq::new(&jid);
let resp_node = client.send_iq(spec.build_iq()).await?;
let response = spec.parse_response(&resp_node)?;

// After: single execute() call
let response = client.execute(GroupQueryIq::new(&jid)).await?;
```

**API Design Note**: IqSpec constructors should take `&Jid` instead of `Jid` to avoid forcing callers to clone. The clone happens inside the constructor:

```rust
impl UpdateBlocklistSpec {
    pub fn block(jid: &Jid) -> Self {
        Self { request: BlocklistItemRequest::block(jid) }
    }
}

// Caller doesn't need to clone
client.execute(UpdateBlocklistSpec::block(&jid)).await?;
```

### File Organization

```
wacore/src/iq/
├── mod.rs          # Re-exports
├── spec.rs         # IqSpec trait definition
├── node.rs         # Helper functions (required_child, required_attr, optional_attr)
├── groups.rs       # Group types, enums, newtypes, ProtocolNode & IqSpec impls
└── blocklist.rs    # Blocklist types, ProtocolNode & IqSpec impls
```

Each feature file (e.g., `groups.rs`, `blocklist.rs`) contains:
- Constants (namespaces, limits)
- Enums with `StringEnum` derive
- Request/Response structs with `ProtocolNode` impl
- `IqSpec` implementations pairing requests with responses
- Unit tests

### Node Parsing Helpers

Use helper functions from `wacore/src/iq/node.rs` for consistent parsing:

```rust
use crate::iq::node::{required_child, required_attr, optional_attr, optional_jid};

fn try_from_node(node: &Node) -> Result<Self> {
    let id = required_attr(node, "id")?;           // Error if missing
    let name = optional_attr(node, "name");         // Returns Option<&str>
    let jid = optional_jid(node, "jid")?;           // Returns Result<Option<Jid>>
    let child = required_child(node, "group")?;     // Error if missing
    // ...
}
```

### Benefits

| Aspect | Before (Imperative) | After (Type-Safe) |
|--------|---------------------|-------------------|
| Attribute names | Raw strings, typo-prone | Compile-time checked |
| Validation | Runtime, easy to forget | Enforced via newtypes |
| Request/Response | Disconnected functions | Paired via `IqSpec` |
| Wire format | Scattered in builders | Documented on types |
| Refactoring | Find-and-replace | Compiler-assisted |

---

## 7. Reverse Engineering Reference

The `docs/captured-js/` directory contains captured WhatsApp Web JavaScript files. Use these to verify protocol implementations:

```bash
# Search for blocklist-related code
grep -r "blocklist" docs/captured-js/*.js

# Find specific IQ namespace usage
grep -r "xmlns.*blocklist\|xmlns.*w:g2" docs/captured-js/*.js
```

**Key patterns to look for:**
- `xmlns: "namespace"` - IQ namespaces
- `action: "value"` - Action attributes
- `smax("tag", { attrs })` - Node construction
- Module names like `WASmaxOutBlocklists*` - Outgoing request builders
- Module names like `WASmaxInBlocklists*` - Incoming response parsers

---

## 8. Final Implementation Checks

Before finalizing a feature/fix, always run:

- **Format**: `cargo fmt`
- **Lint**: `cargo clippy --all-targets`
- **Test**: `cargo test --all`
- **Review**: `coderabbit review --prompt-only` (if available)

---

## 9. Debugging Tools

### evcxr - Rust REPL

For interactive debugging and quick code exploration, use `evcxr`:

```bash
# Install (use binstall for faster installation)
cargo binstall evcxr_repl -y

# Run from project root
evcxr
```

**Use cases:**

- **Decode binary protocol data**: Inspect nibble-encoded values, hex strings, or protocol buffers
- **Test encoding/decoding logic**: Quickly verify transformations without full compile cycles
- **Explore data structures**: Inspect how structs serialize/deserialize
- **Prototype algorithms**: Test Signal protocol operations or crypto functions

### Using Project Crates in evcxr

You can import local crates using the `:dep` command with relative paths. Note that package names use hyphens, but Rust imports use underscores:

```rust
// Add dependencies (run from project root)
:dep wacore-binary = { path = "wacore/binary" }
:dep hex = "0.4"

// Import modules
use wacore_binary::jid::Jid;
use wacore_binary::marshal::{marshal, unmarshal_ref};
use wacore_binary::builder::NodeBuilder;
```

**Important**: evcxr processes each line independently. For multi-line code with local variables, wrap in a block:

```rust
{
    let jid: Jid = "100000000000001.1:75@lid".parse().unwrap();
    println!("User: {}, Device: {}, Is LID: {}", jid.user, jid.device, jid.is_lid());
}
```

### Example: Decoding Binary Protocol Data

```rust
:dep wacore-binary = { path = "wacore/binary" }
:dep hex = "0.4"
use wacore_binary::marshal::unmarshal_ref;

{
    let data = hex::decode("f80f4c1a...").unwrap();
    let node = unmarshal_ref(&data).unwrap();
    println!("Tag: {}", node.tag);
    for (k, v) in node.attrs.iter() { println!("  {}: {}", k, v); }
}
```

### Example: Building and Marshaling Nodes

```rust
:dep wacore-binary = { path = "wacore/binary" }
use wacore_binary::builder::NodeBuilder;
use wacore_binary::marshal::marshal;

{
    let node = NodeBuilder::new("message")
        .attr("type", "text")
        .attr("to", "15551234567@s.whatsapp.net")
        .build();
    println!("{:?}", node);
    let bytes = marshal(&node).unwrap();
    println!("Marshaled: {:02x?}", bytes);
}
```

### Example: Decoding Nibble-Encoded Data

WhatsApp binary protocol uses nibble encoding for numeric strings. Each byte contains two digits (0-9), with 0xF as terminator for odd-length strings:

```rust
fn decode_nibbles(hex: &str) -> String {
    let mut result = String::new();
    for i in (0..hex.len()).step_by(2) {
        let byte = u8::from_str_radix(&hex[i..i+2], 16).unwrap();
        let high = byte >> 4;
        let low = byte & 0x0f;
        if high < 10 { result.push(('0' as u8 + high) as char); }
        if low < 10 { result.push(('0' as u8 + low) as char); }
        else if low == 0x0f { break; } // terminator
    }
    result
}

fn encode_nibbles(s: &str) -> String {
    let mut result = String::new();
    let bytes: Vec<u8> = s.bytes().map(|b| b - b'0').collect();
    for chunk in bytes.chunks(2) {
        let high = chunk[0];
        let low = if chunk.len() > 1 { chunk[1] } else { 0x0f };
        result.push_str(&format!("{:x}{:x}", high, low));
    }
    result
}

decode_nibbles("100000000000001f") // -> "100000000000001"
encode_nibbles("100000000000001")  // -> "100000000000001f"
```