vojo/apps/widget-telegram
2026-05-05 01:02:36 +03:00
..
src feat(bots-telegram): land QR-code login flow rendered client-side from m.image body via qrcode-generator with bridge-race-tolerant state machine 2026-05-05 01:02:36 +03:00
.gitignore feat(bots-telegram): land Phase 3 widget scaffold with Dawn UI, dev config overlay, and prod origin allowlist 2026-05-02 13:22:25 +03:00
index.html feat(bots-telegram): land Phase 3 widget scaffold with Dawn UI, dev config overlay, and prod origin allowlist 2026-05-02 13:22:25 +03:00
package-lock.json feat(bots-telegram): land QR-code login flow rendered client-side from m.image body via qrcode-generator with bridge-race-tolerant state machine 2026-05-05 01:02:36 +03:00
package.json feat(bots-telegram): land QR-code login flow rendered client-side from m.image body via qrcode-generator with bridge-race-tolerant state machine 2026-05-05 01:02:36 +03:00
README.md feat(bots-telegram): land Phase 3 widget scaffold with Dawn UI, dev config overlay, and prod origin allowlist 2026-05-02 13:22:25 +03:00
tsconfig.json feat(bots-telegram): land Phase 3 widget scaffold with Dawn UI, dev config overlay, and prod origin allowlist 2026-05-02 13:22:25 +03:00
vite.config.ts feat(bots-telegram): land Phase 3 widget scaffold with Dawn UI, dev config overlay, and prod origin allowlist 2026-05-02 13:22:25 +03:00

@vojo/widget-telegram

Vojo Telegram bridge management widget — mounts inside /bots/telegram in the Vojo client. See docs/plans/bots_tab.md Phase 3 for product context and the matrix-widget-api contract.

This is not a Telegram client. It's a small panel that drives the mautrix-telegram bridge bot (@telegrambot:vojo.chat) by sending text commands in the control DM and rendering the bot's text replies. M11 ships only the bootstrap + a ping button to verify the host handshake.

Layout

src/
├── bootstrap.ts    Parse URL params the host appends (matches BotWidgetEmbed.ts)
├── widget-api.ts   Inline matrix-widget-api postMessage transport (no SDK)
├── App.tsx         UI: bootstrap card, action buttons, transcript pane
├── main.tsx        Entry: init bootstrap, render App or diagnostic
└── styles.css      Theme-aware CSS variables

Local development

Don't touch the committed config.json. Create config.local.json at the project root once — gitignored, never deployed. The host's Vite dev server overlays it on top of /config.json responses (see serveLocalConfigOverlay in vite.config.js); prod builds ignore the overlay entirely.

# one-time: install widget deps
cd apps/widget-telegram && npm install

# one-time: create config.local.json (gitignored) at the project root
cat > /home/ubuntu/projects/vojo/cinny/config.local.json <<'JSON'
{
  "bots": [
    {
      "id": "telegram",
      "experience": {
        "type": "matrix-widget",
        "url": "http://localhost:8081/"
      }
    }
  ]
}
JSON

The overlay merges bots[] by id, so just { id, experience } is enough — base bot's mxid and name are preserved. Top-level fields not present in config.local.json are inherited from config.json.

Run both servers:

# terminal 1 — widget on :8081 with HMR
cd apps/widget-telegram && npm run dev

# terminal 2 — host SPA on :8080
cd /home/ubuntu/projects/vojo/cinny && npm start

Open http://localhost:8080/bots/telegram. Iframe loads cross-origin from the widget dev server, HMR works, no proxy.

http://localhost:* URLs are accepted by the host's URL validator only in dev builds (import.meta.env.DEV branch in src/app/features/bots/catalog.ts); production builds drop the branch via Vite's dead-code elimination, AND production-only enforces an origin allowlist (PROD_WIDGET_ORIGINS) so prod can never embed localhost even if config.json is poisoned.

Deploy is unchanged. config.local.json is gitignored, never shipped. You don't need to revert anything before Deploy to vojo.chat — there is nothing in tracked files that points at localhost.

Standalone preview of the widget bundle (no host, useful for visual iteration):

cd apps/widget-telegram
npm run dev      # vite dev server on :8081 — shows missing-params banner
                 # without host, expected.
npm run preview  # serves the production build from dist/

Build

npm run build

Outputs to apps/widget-telegram/dist/. Deploy by rsyncing dist/* into ~/vojo/widgets/telegram/ on the production host (Caddy serves this via the widgets.vojo.chat block). One parent ~/vojo/widgets/ directory hosts every bot widget — adding a second one is mkdir ~/vojo/widgets/<slug>/ plus a Caddy block, no docker-compose edit.

Hosting (server-side, runbook)

  1. DNS: widgets.vojo.chat A/AAAA → server. Verify with dig.
  2. ~/vojo/docker-compose.yml — Caddy volumes: adds (one parent mount, future widgets reuse it):
    - ./widgets:/var/www/widgets
    
  3. ~/vojo/caddy/Caddyfile — append:
    widgets.vojo.chat {
        encode zstd gzip
    
        header {
            Content-Security-Policy "frame-ancestors https://vojo.chat https://localhost"
            X-Content-Type-Options "nosniff"
            Referrer-Policy "no-referrer"
            Cache-Control "no-cache, no-store, must-revalidate"
            -Server
        }
    
        handle_path /telegram/* {
            root * /var/www/widgets/telegram
            try_files {path} /index.html
            file_server
        }
    
        handle {
            respond "Not Found" 404
        }
    }
    
  4. mkdir -p ~/vojo/widgets/telegram (placeholder so cert provisioning has something to serve), then docker compose up -d caddy to apply.
  5. Verify directly: curl -I https://widgets.vojo.chat/telegram/index.html should return 200 and the Content-Security-Policy header.

Updating the production /config.json

Once the widget is live at https://widgets.vojo.chat/telegram/index.html, add to the host repo's config.json:

"experience": {
  "type": "matrix-widget",
  "url": "https://widgets.vojo.chat/telegram/index.html"
}

Capacitor (Android)

capacitor.config.ts already has a placeholder. Uncomment and set:

server: { allowNavigation: ['widgets.vojo.chat'] }

Without this, Android's WebView hijacks the cross-origin iframe URL into Intent.ACTION_VIEW and the iframe stays blank. Rebuild the APK after.

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.state_event:m.room.member

Anything else is silently dropped by the host. To extend the surface, update BotWidgetDriver.ts upstream — that requires a security review per Phase 2 plan §M9.