193 lines
6.8 KiB
Markdown
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.
|