vojo/apps/widget-discord/README.md

6.8 KiB

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

# 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:

# 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

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

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:

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.