# @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/`). 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 @`. 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 (\`\`)` → 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: 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.