Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions .github/workspace-dep-closures.json
Original file line number Diff line number Diff line change
Expand Up @@ -13786,6 +13786,13 @@
"crates/webhook_signature",
"crates/workspace-hack"
],
"whatsapp_fake_server": [
"crates/whatsapp_fake_server",
"crates/whatsmeow",
"crates/whatsmeow_proto",
"crates/whatsmeow_signal",
"crates/workspace-hack"
],
"whatsmeow": [
"crates/whatsmeow",
"crates/whatsmeow_proto",
Expand All @@ -13795,6 +13802,12 @@
"crates/whatsmeow_proto",
"crates/workspace-hack"
],
"whatsmeow_signal": [
"crates/whatsmeow",
"crates/whatsmeow_proto",
"crates/whatsmeow_signal",
"crates/workspace-hack"
],
"work_feed": [
"crates/activity",
"crates/agent",
Expand Down
98 changes: 97 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,10 @@ members = [
"crates/unfurl",
"crates/user_api_key",
"crates/webhook_signature",
"crates/whatsapp_fake_server",
"crates/whatsmeow",
"crates/whatsmeow_proto",
"crates/whatsmeow_signal",
"crates/work_feed",
"crates/worker-rs-otel",
"crates/workspace-hack",
Expand Down Expand Up @@ -286,6 +288,8 @@ jsonwebtoken = { git = "https://github.com/whutchinson98/jsonwebtoken", features
lambda_runtime = "0.13.0"
lazy-regex = "3.6.1"
lazy_static = "1.5.0"
# Signal's curve25519 keys and XEdDSA; see crates/whatsmeow_signal/AGENTS.md.
libsignal-core = { git = "https://github.com/signalapp/libsignal", tag = "v0.105.2" }
livekit-api = { version = "0.4.16", default-features = false, features = [
"access-token",
"rustls-tls-webpki-roots",
Expand Down
52 changes: 52 additions & 0 deletions crates/whatsapp_fake_server/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# whatsapp_fake_server

## What it is

Test support for WhatsApp linked devices: a fake WhatsApp server that the real
whatsmeow port (`crates/whatsmeow`) connects to over a local websocket, and fake
phones (the other people) with real Signal sessions from `whatsmeow_signal`.
Lets the session service and its tests run end to end without WhatsApp.

## How it works

- `FakeWhatsApp::start()` binds a websocket port (noise XX responder, binary XML)
and a plain-HTTP media port on `127.0.0.1`.
- The server's noise certificate chain is signed by a test root key, not
WhatsApp's. Clients must use `test_signal()` (`TestSignal`): libsignal with
WhatsApp's root key swapped for the test root when verifying. Nothing else is
faked; prekey, pairing and message crypto are real.
- `add_user` creates a user with a phone (device 0, a `FakePhone` in process)
that publishes prekeys. `FakePhone::scan_qr` approves a client's QR code; the
client signs, gets the 515 restart and logs in as device 1, 2, ….
- Routing is LID-first like WhatsApp today: groups use `addressing_mode="lid"`,
usync returns LIDs, delivered stanzas carry `participant_pn` / `sender_pn`.
- A message handed to a connection stays unacked until the device sends a
receipt or message ack; if the connection drops it goes back to the offline
queue and is delivered again on the next login, then `<ib><offline/>`.

## Start here

- `src/lib.rs`: the `FakeWhatsApp` API (users, groups, kick, drop connection,
delivery counters, waiters).
- `src/state.rs`: routing, IQ handlers (encrypt, usync, w:g2, w:m), offline queues.
- `src/phone.rs`: fake phones: send/receive with real Signal, QR approval.
- `src/connection.rs`: handshake, login, `conflict replaced`, pairing.

## Checks

- `cargo test -p whatsapp_fake_server`

## Gotchas

- Never point it at real WhatsApp; it only listens on `127.0.0.1`.
- Waiting is event driven (`Shared::wait_for` re-checks on every state change);
don't add sleep loops.
- Media: clients set `ClientConfig::media_base_url` to `media_base_url()`
(plain HTTP); phones upload and download with whatsmeow's own media crypto.
- Unhandled IQs get an empty `result`; add a handler in `State::handle_iq` when
the port starts parsing a response.

## Related

- `crates/whatsmeow/AGENTS.md`, `crates/whatsmeow_signal/AGENTS.md`,
`crates/whatsapp_e2e/AGENTS.md`
1 change: 1 addition & 0 deletions crates/whatsapp_fake_server/CLAUDE.md
32 changes: 32 additions & 0 deletions crates/whatsapp_fake_server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
edition = "2024"
name = "whatsapp_fake_server"
publish = false
version = "0.1.0"
description = "Test support: a fake WhatsApp server and fake phones that speak the real protocol, for testing the whatsmeow port and the session service without WhatsApp."

[dependencies]
aes-gcm = { workspace = true }
async-trait = { workspace = true }
axum = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true }
futures = { workspace = true }
hmac = { workspace = true }
nom = { workspace = true }
prost = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] }
tokio-tungstenite = { workspace = true }
tokio-util = { workspace = true, features = ["rt"] }
tracing = { workspace = true }
whatsmeow = { path = "../whatsmeow", features = ["testing"] }
whatsmeow_signal = { path = "../whatsmeow_signal" }
workspace-hack = { version = "0.1", path = "../workspace-hack" }

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "test-util"] }
Loading
Loading