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)
- Widget sends
!discord login-qr. - Bridge replies with an
m.imageevent whosebodyis a Discord remoteauth URL (https://discord.com/ra/<token>). The host driver stripsurl/file/infoso the widget never touches the uploaded PNG bytes — it re-encodes the URL into an SVG QR matrix client-side viaqrcode-generator. - 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.
- Bridge redacts the
m.imageevent after a successful scan and sendsSuccessfully logged in as @<username>. - Widget fires
!discord pingto 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→ disconnectedYou're logged in as @x (\`)` → connectedYou 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.
~/vojo/caddy/Caddyfile— append to the existingwidgets.vojo.chat { … }block, beside the Telegramhandle_path:handle_path /discord/* { root * /var/www/widgets/discord try_files {path} /index.html file_server }mkdir -p ~/vojo/widgets/discord(placeholder so the bind-mount has something to serve), thendocker compose up -d caddy(orreload).- Verify directly:
curl -I https://widgets.vojo.chat/discord/index.htmlshould return 200 and theContent-Security-Policyheader.
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.