# @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.
