Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 334 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ You are an expert Rust developer specializing in asynchronous networking, crypto
The project is split into three main crates:

- **wacore**
A platform-agnostic library containing the pure, `no_std`-compatible core logic for the WhatsApp binary protocol, cryptography primitives, and state management traits.
It has **no dependencies** on Tokio or specific databases.
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.
Expand All @@ -22,7 +22,7 @@ The project is split into three main crates:

- **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/src/signal/` & `src/store/signal*.rs`): E2E encryption via our Signal Protocol implementation.
- **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.

---
Expand Down Expand Up @@ -79,7 +79,10 @@ The project is split into three main crates:
- `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.

---

Expand Down Expand Up @@ -128,17 +131,343 @@ When adding a new feature, follow a repeatable flow that mirrors WhatsApp Web be

---

## 6. Final Implementation Checks
## 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
```
Comment on lines +394 to +401

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language tag to the fenced block.

markdownlint MD040 flags this block; adding a language keeps the docs lint-clean.

💡 Suggested fix
-```
+```text
 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
-```
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
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
```
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

394-394: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@AGENTS.md` around lines 394 - 401, Update the fenced code block that starts
with the directory listing "wacore/src/iq/" to include a language tag by
changing the opening ``` to ```text (so the block becomes ```text ... ```),
which satisfies markdownlint MD040; leave the closing ``` as-is and ensure no
other characters are added to the block fence.


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)

---

## 7. Debugging Tools
## 9. Debugging Tools

### evcxr - Rust REPL

Expand Down
Loading