whatsapp-rust 0.6.0

Rust client for WhatsApp Web
Documentation
# Type-Safe Protocol Node Architecture

All protocol stanza builders 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>;
}
```

## Derive Macros (Recommended)

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`)

For enums where the default should not be the first variant, use `#[string_default]`.

### Legacy Declarative Macros

Prefer derive macros for new code. Declarative macros in `wacore/src/protocol.rs` (`define_empty_node!`, `define_simple_node!`) are also available but considered legacy.

## Implementation Pattern

1. Define request struct with `ProtocolNode` (or derive macro)
2. Define response struct with `ProtocolNode`
3. Create `IqSpec` implementation pairing them
4. Use `client.execute(Spec::new(&jid)).await?` in feature code

See `wacore/src/iq/groups.rs` and `wacore/src/iq/blocklist.rs` for complete examples.

## IQ Executor

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

```rust
// Single call replaces manual build + send + parse
let response = client.execute(GroupQueryIq::new(&jid)).await?;
```

**API Design Note**: IqSpec constructors should take `&Jid` (not `Jid`) to avoid forcing callers to clone.

## Node Parsing Helpers

Use helpers from `wacore/src/iq/node.rs`:

```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")?;
    let name = optional_attr(node, "name");
    let jid = optional_jid(node, "jid")?;
    let child = required_child(node, "group")?;
}
```

## Validated Newtypes

Use newtypes to enforce protocol constraints at compile time. See `GroupSubject` in `wacore/src/iq/groups.rs` for examples.

Constants from WhatsApp Web A/B props:
- `GROUP_SUBJECT_MAX_LENGTH`: 100 characters
- `GROUP_DESCRIPTION_MAX_LENGTH`: 2048 characters
- `GROUP_SIZE_LIMIT`: 257 participants

## File Organization

```text
wacore/src/iq/
├── mod.rs          # Re-exports
├── spec.rs         # IqSpec trait definition
├── node.rs         # Helper functions
├── groups.rs       # Group types + IqSpec impls
└── blocklist.rs    # Blocklist types + IqSpec impls
```

Each feature file contains: constants, enums (`StringEnum`), request/response structs (`ProtocolNode`), `IqSpec` impls, and unit tests.

## Noise Handshake Patterns

Three Noise patterns coexist, mirroring WA Web's `WAWebOpenChatSocket`:

| Pattern        | When                                                          | State machine               | Cost                              |
| -------------- | ------------------------------------------------------------- | --------------------------- | --------------------------------- |
| **XX**         | First connect / pairing / forced fallback                     | `XxHandshakeState`          | 1.5 RTT                           |
| **IK**         | Reconnect with valid cached `serverStaticPub`                 | `IkHandshakeState`          | 1 RTT, ships 0-RTT login payload  |
| **XXfallback** | Server rejects in-flight IK (reply has `static != null`)      | `XxFallbackHandshakeState`  | 1 RTT (reuses already-sent eph.)  |

### Selection (`src/handshake.rs::select_pattern`)

```text
ik_failures >= 1  ───────────────────────────────────────► XX
no cached server_cert_chain ─────────────────────────────► XX
leaf.not_after < now OR intermediate.not_after < now ────► XX
otherwise ──────────────────────────────────────────────► IK with leaf.key
```

The counter `Client.ik_handshake_failures: AtomicU32` is per-process and
not persisted (matches WA Web's `K = 0` reset on process start).

### Invalidation policy

| Error                                              | `ik_handshake_failures` | `server_cert_chain`                              |
| -------------------------------------------------- | ----------------------- | ------------------------------------------------ |
| Transient (timeout, disconnect, transport)         | unchanged               | unchanged                                        |
| Crypto-fatal during IK (cert MAC, decrypt, proto)  | `+= 1`                  | cleared via `DeviceCommand::ClearServerCertChain`|
| XX or XX-fallback failure                          | unchanged               | unchanged (XX never reads the cache)             |
| Any successful handshake                           | reset to `0`            | repopulated (XX, XX-fallback) or kept (IK Continue)|

Distinguishing transient from crypto-fatal is via `HandshakeError::is_transient()`
and `HandshakeError::is_crypto_fatal()`. Getting the classification wrong leads
to either oscillating back to XX needlessly or looping on a stale cache.

### Persisted state (`Device.server_cert_chain`)

`CachedServerCertChain { intermediate, leaf }` with each cert reduced to
`{ key: [u8; 32], not_before: i64, not_after: i64 }`. Mirrors the
storage layout WA Web uses in `PrefsInfoStore.js:setCertificateChain` —
only those fields end up on disk.

`verify_server_cert` checks structural shape, the issuer-serial pin, the
chain link, and that `leaf.key` matches the decrypted Noise static.
Ed25519 signature verification against `WA_CERT_PUB_KEY` is intentionally
skipped (would break the e2e mock server). Same posture as whatsmeow.

### Logs (matching WA Web's `[socket]` lines)

```text
[socket] doFullHandshake: openChatSocket send hello
[socket] resumeNoiseHandshake started
[socket] resumeNoiseHandshake send hello
[socket] resumeNoiseHandshake rcv hello
[socket] resumeNoiseHandshake deriving secrets
[socket] resumeNoiseHandshake failed: serverStaticCiphertext not null —
  doFallbackHandshake continuing handshake with given server hello
[socket] continueFullHandshakeCore client finish and deriving secrets
```