vojo/apps/widget-discord/README.md

193 lines
6.8 KiB
Markdown

# @vojo/widget-discord
Vojo Discord bridge management widget — mounts inside `/bots/discord`
in the Vojo client. Mirrors the Telegram widget contract; protocol
specifics differ because mautrix-discord runs on the **legacy** mautrix
command framework, not bridgev2 (the Discord bridge had not yet been
ported to v2 as of January 2026 — see
https://mau.fi/blog/2026-01-mautrix-release/).
This is **not** a Discord client. It's a small panel that drives the
mautrix-discord bridge bot (`@discordbot:vojo.chat`) by sending text
commands in the control DM and rendering the bot's text replies. It
ships QR-only login (the Discord token-login flow stays accessible via
chat-fallback for power users).
## Layout
```
src/
├── bootstrap.ts Parse URL params (matches BotWidgetEmbed.ts)
├── widget-api.ts Inline matrix-widget-api postMessage transport
├── App.tsx UI: status pill, QR panel, logout / reconnect cards, transcript
├── main.tsx Entry: bootstrap + render
├── state.ts LoginState reducer + hydrate-from-timeline
├── styles.css Theme-aware CSS variables (Dawn palette)
├── i18n/ Tiny RU/EN dictionary harness
└── bridge-protocol/
├── types.ts LoginEvent + ParsableEvent types
├── parser.ts Dialect dispatch shim
└── dialects/
└── legacy_v076.ts mautrix-discord v0.7.6 wording
```
## Login flow (QR only)
1. Widget sends `!discord login-qr`.
2. Bridge replies with an `m.image` event whose `body` is a Discord
remoteauth URL (`https://discord.com/ra/<token>`). The host driver
strips `url`/`file`/`info` so the widget never touches the uploaded
PNG bytes — it re-encodes the URL into an SVG QR matrix client-side
via `qrcode-generator`.
3. The user scans the QR with the **Discord mobile app** (Settings →
Devices → Scan QR Code). Discord's remoteauth gateway requires the
mobile app — desktop Discord and the browser cannot scan.
4. Bridge redacts the `m.image` event after a successful scan and sends
`Successfully logged in as @<username>`.
5. Widget fires `!discord ping` to pick up the discord snowflake for
the connected pill.
If Discord asks for a CAPTCHA, the bridge replies with the standard
error line plus a hint about token-login. The widget surfaces an amber
warning suggesting the user retry later or use chat-fallback.
## Status probe
Discord's legacy command system has no `list-logins` API; status is
queried via `!discord ping`. The four reply variants map to four UI
states:
- `You're not logged in` → disconnected
- `You're logged in as @x (\`<id>\`)` → connected
- `You have a Discord token stored, but are not connected for some reason 🤔` → connected_dead (token_stored)
- `You're logged in, but the Discord connection seems to be dead 💥` → connected_dead (connection_dead)
`connected_dead` exposes a «Переподключиться» card that sends
`!discord reconnect`. `disconnect` is recognised for chat-fallback
typists but never sent by the widget.
## Local development
Same overlay mechanism as the Telegram widget — create
`config.local.json` at the project root (gitignored) with a `bots[]`
entry overriding the discord widget's `experience.url` to your local
dev server:
```bash
# one-time: install widget deps
cd apps/widget-discord && npm install
# config.local.json (gitignored) at the project root
cat > /home/ubuntu/projects/vojo/cinny/config.local.json <<'JSON'
{
"bots": [
{
"id": "discord",
"experience": {
"type": "matrix-widget",
"url": "http://localhost:8082/",
"commandPrefix": "!discord"
}
}
]
}
JSON
```
`http://localhost:*` URLs pass the host's URL validator only in dev
builds — see `src/app/features/bots/catalog.ts` `import.meta.env.DEV`
branch. Production builds drop the branch via Vite's dead-code
elimination AND enforce an origin allowlist (`PROD_WIDGET_ORIGINS`).
Run both servers:
```bash
# terminal 1 — widget on :8082 with HMR
cd apps/widget-discord && npm run dev
# terminal 2 — host SPA on :8080
cd /home/ubuntu/projects/vojo/cinny && npm start
```
Open `http://localhost:8080/bots/discord`. The Telegram widget on :8081
can run in parallel with no port conflict.
## Build
```bash
npm run build
```
Outputs to `apps/widget-discord/dist/`. Deploy by rsyncing `dist/*` into
`~/vojo/widgets/discord/` on the production host (Caddy serves this via
the `widgets.vojo.chat` block).
## Hosting (server-side, runbook)
Pre-requisite: `widgets.vojo.chat` already exists for the Telegram
widget — only the Caddy `widgets.vojo.chat` block needs a new
`handle_path` and the docker host needs a new directory.
1. `~/vojo/caddy/Caddyfile` — append to the existing
`widgets.vojo.chat { … }` block, beside the Telegram `handle_path`:
```
handle_path /discord/* {
root * /var/www/widgets/discord
try_files {path} /index.html
file_server
}
```
2. `mkdir -p ~/vojo/widgets/discord` (placeholder so the bind-mount has
something to serve), then `docker compose up -d caddy` (or `reload`).
3. Verify directly:
`curl -I https://widgets.vojo.chat/discord/index.html` should
return 200 and the `Content-Security-Policy` header.
## Adding the discord bridge to docker-compose
```yaml
discord-bridge:
image: dock.mau.dev/mautrix/discord:v0.7.6
restart: unless-stopped
volumes:
- ./mautrix-discord:/data
```
Then `~/vojo/synapse/homeserver.yaml` needs the discord registration
file added to `app_service_config_files`:
```yaml
app_service_config_files:
- /data/telegram-registration.yaml
- /data/discord-registration.yaml
```
The bridge's `command_prefix` defaults to `!discord` — keep it that
way so it matches the widget's `experience.commandPrefix`. If you
override it in `mautrix-discord/config.yaml`, mirror the override in
`/config.json`.
## Capacitor (Android)
`capacitor.config.ts` already allow-navigates `widgets.vojo.chat` for
the Telegram widget; no further change needed.
## Capability contract
The widget requests EXACTLY this set (matches the host's
`BotWidgetDriver.getBotWidgetCapabilities`):
```
org.matrix.msc2762.timeline:<roomId>
org.matrix.msc2762.send.event:m.room.message#m.text
org.matrix.msc2762.receive.event:m.room.message#m.text
org.matrix.msc2762.receive.event:m.room.message#m.notice
org.matrix.msc2762.receive.event:m.room.message#m.image
org.matrix.msc2762.receive.event:m.room.redaction
org.matrix.msc2762.receive.state_event:m.room.member
```
`m.image` is the QR carrier; `m.room.redaction` signals the bridge
consumed the QR after a successful scan. The host sanitizer strips
`url`/`file`/`info` from `m.image` content, so only the QR URL string
inside `body` survives the boundary.