vojo/apps/widget-whatsapp/README.md

193 lines
8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# @vojo/widget-whatsapp
Vojo WhatsApp bridge management widget — mounts inside `/bots/whatsapp`
in the Vojo client.
This is **not** a WhatsApp client. It's a control panel for the
mautrix-whatsapp bridge that talks to the bridge's **provisioning HTTP
API** (bridgev2 `/_matrix/provision/v3/*`, exposed by Caddy at
`https://vojo.chat/_provision/whatsapp`). It links the account (QR scan
or an 8-character pairing code typed into the WhatsApp app), shows the
linked account, lists WhatsApp contacts, resolves +phone numbers, and
creates DM portals on demand.
Auth: the widget requests MSC1960 OpenID credentials from the host
(`get_openid` over the widget API; granted by `BotWidgetDriver.askOpenID`
when config.json opts the bot into the `vojo.openid` capability) and
sends them to the bridge as `Authorization: Bearer openid:<token>`. The
OpenID token only proves identity — it is not a Matrix access token.
There is no bot text-command transport and no reply parsing: the legacy
`!wa`-command dialect (`bridge-protocol/`) was deleted when the bridge
API became reachable. The bot control DM still exists (BotShell needs a
room and the «Show chat» fallback), the widget just doesn't read or
write it — it requests **zero** MSC2762 capabilities.
## WhatsApp-specific contract notes
Extracted from mautrix-whatsapp v0.2604.0 (`pkg/connector/login.go`,
`startchat.go`) + mautrix-go v0.27.0 (`bridgev2/matrix/provisioning.go`):
- Login flows: `qr` and `phone`. The phone flow's submit answers with a
`display_and_wait` step of type **`code`** — an `XXXX-XXXX` pairing
code the user types into the WhatsApp app (NOT an SMS OTP entered into
the widget like Telegram's flow).
- The login window is ~160 s: whatsmeow issues ~6 QR tokens (60 s + 5×20 s)
and closes the socket when they run out; the pairing code lives in the
same window.
- There are **no** `.incorrect` retry steps: every rejected input is a
`FI.MAU.WHATSAPP.*` RespError that deletes the login process
server-side. The phone form transparently starts a fresh process on
resubmit.
- Login-liveness semantics differ by bridge generation. mautrix ≤ v0.28.0:
the login process dies with the poll's request context (and there is
**no** `/v3/login/cancel` route — cancelling works by aborting the
long-poll). mautrix ≥ v0.28.1 (`provisioninglogin.go`): the process
lives server-side with a 30-minute TTL, dropped polls reattach, and the
cancel route exists. The widget's wait loop is written for both: it
retries transport-shaped poll failures with backoff (Android freezes
the backgrounded WebView while the user enters the pairing code in
WhatsApp), wakes on return to foreground, and disambiguates a late
404 via whoami (the login may have COMPLETED while frozen).
- Identifiers are phone numbers only (`tel:+…`); no usernames. Contact
ids are bare phone digits (or `lid-`/`bot-` prefixed for
hidden-number/bot peers).
The About card and modal carry the **Meta-ToS risk disclosure**
(`warning.*` keys, amber warn styling, triangle icon) — WhatsApp's terms
of service forbid third-party clients and Meta may ban accounts for it.
This copy is a deliberate product/legal requirement: keep it intact when
restyling.
## Layout
```
src/
├── bootstrap.ts Parse URL params the host appends (matches BotWidgetEmbed.ts)
├── widget-api.ts Inline matrix-widget-api postMessage transport: handshake,
│ theme, MSC1960 get_openid, io.vojo.bot-widget verbs
├── provisioning.ts Typed client for the bridgev2 provisioning API + identifier
│ helpers (wire contract documented in the file header)
├── errors.ts Bridge/WhatsApp errcode → localized copy mapping
├── login.tsx Login flow over the v3 step machine + forms (phone form,
│ QR panel and pairing-code panel with long-poll)
├── contacts.tsx Contacts list, search-as-filter, phone probe, create DM
├── App.tsx Shell: boot/disconnected/connected phases, account view,
│ About modal with the Meta-ToS warning callout
├── ui.tsx Icons, initials avatar, command cards, notices
├── main.tsx Entry: init bootstrap, render App or diagnostic
└── styles.css Telegram-widget stylesheet verbatim + WhatsApp-only
additions (warn card, ToS callout, pairing-code plate)
```
## Local development
**Don't touch the committed `config.json`.** Create `config.local.json` at
the project root once — gitignored, never deployed. The host's Vite dev
server overlays it on top of `/config.json` responses (see
`serveLocalConfigOverlay` in `vite.config.js`); prod builds ignore the
overlay entirely.
Note the overlay merges bot entries **shallowly** — your local
`experience` object replaces the base one wholesale, so it must carry
`provisioningUrl` and `capabilities` too:
```bash
# one-time: install widget deps
cd apps/widget-whatsapp && npm install
# one-time: create config.local.json (gitignored) at the project root
cat > /home/ubuntu/projects/vojo/cinny/config.local.json <<'JSON'
{
"bots": [
{
"id": "whatsapp",
"experience": {
"type": "matrix-widget",
"url": "http://localhost:8083/",
"provisioningUrl": "https://vojo.chat/_provision/whatsapp",
"capabilities": ["vojo.openid"]
}
}
]
}
JSON
```
Run both servers:
```bash
# terminal 1 — widget on :8083 with HMR
cd apps/widget-whatsapp && npm run dev
# terminal 2 — host SPA on :8080
cd /home/ubuntu/projects/vojo/cinny && npm start
```
Open `http://localhost:8080/bots/whatsapp`. Iframe loads cross-origin
from the widget dev server, HMR works, no proxy. The provisioning calls
go straight to the prod bridge API (CORS `*` + per-request bearer auth),
acting on whatever account you're signed in with — same trust model as
the dev client talking to the prod homeserver.
`http://localhost:*` URLs are accepted by the host's URL validator only
in dev builds (`import.meta.env.DEV` branch in
`src/app/features/bots/catalog.ts`); production builds drop the branch
via Vite's dead-code elimination, AND production-only enforces an origin
allowlist (`PROD_WIDGET_ORIGINS`) so prod can never embed `localhost` even
if config.json is poisoned.
## Build
```bash
npm run build
```
Outputs to `apps/widget-whatsapp/dist/`. Deploy by rsyncing `dist/*`
into `~/vojo/widgets/whatsapp/` on the production host (Caddy serves
this via the `widgets.vojo.chat` block) — the VSCode task
`Deploy widgets` does all three widgets.
## Server-side requirements
1. The widget static files at `widgets.vojo.chat/whatsapp/` (Caddy
`handle_path /whatsapp/*` block).
2. The bridge provisioning API exposed at the URL configured in
config.json `experience.provisioningUrl`. Caddy block inside the
`vojo.chat` site, next to the telegram one:
```
handle /_provision/whatsapp/* {
uri replace /_provision/whatsapp /_matrix/provision
reverse_proxy whatsapp-bridge:29318 # host:port from appservice.address
}
```
Path-scoped on purpose: the same bridge listener serves the appservice
transaction endpoints (`/_matrix/app/*`), which must stay internal.
3. `provisioning.shared_secret` in the bridge config must NOT be
`disable` (a ≥16-char secret enables the API; the widget never sees
the secret — it authenticates with per-user OpenID tokens).
## Updating the production /config.json
```json
"experience": {
"type": "matrix-widget",
"url": "https://widgets.vojo.chat/whatsapp/index.html",
"provisioningUrl": "https://vojo.chat/_provision/whatsapp",
"capabilities": ["vojo.openid"]
}
```
## Capability contract
The widget requests **no** MSC2762 capabilities — the handshake replies
with only `org.matrix.msc4039.download_file` (avatar thumbnails). The
privileged surfaces are:
- MSC1960 `get_openid` — granted by `BotWidgetDriver.askOpenID` iff
config.json declares `"capabilities": ["vojo.openid"]` for this bot;
- `io.vojo.bot-widget` side-channel verbs `open-external-url` and
`open-matrix-to` (origin-pinned and validated host-side in
`BotWidgetEmbed.onWidgetMessage`).