Skip to content

feat(telegram): add bot + mini app MVP for voice calls#158

Draft
yagudaev wants to merge 6 commits into
mainfrom
feature/telegram-miniapp
Draft

feat(telegram): add bot + mini app MVP for voice calls#158
yagudaev wants to merge 6 commits into
mainfrom
feature/telegram-miniapp

Conversation

@yagudaev

@yagudaev yagudaev commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Telegram Mini App (telegram-miniapp workspace): Vite + TS static webview, AudioWorklet on its own thread (emitted as a separate asset for iOS WKWebView), reuses desktop's audio pipeline. Launched via t.me/<bot>/<shortname> URL that Telegram auto-renders as a "Launch" pill.
  • Relay auth: new POST /auth/telegram validates Telegram initData HMAC and mints short-lived signed tickets. session.config accepts tickets alongside the existing RELAY_API_KEY. Desktop/mobile paths unchanged.
  • Agent-name transcripts: mini app shows the persona name (e.g. "Pam:") via ?startapp=<Name> from the agent's /call reply, falling back to the bot's first_name from getMe.
  • Restart UX: ticket cached in-memory for 4 min so tap-end-tap-start skips the auth round-trip; initData freshness window is 1 h (matches standard Telegram practice).

OpenClaw integration

OpenClaw (openclaw/openclaw) owns the bot's long-poll connection, so a sidecar bot process on the same token isn't viable. This PR removes the originally-planned telegram-bot workspace entirely. The /call UX instead flows through two small OpenClaw-side changes (made outside this repo, on the Hetzner gateway):

  1. Behavior — add a permanent agent instruction: when the user sends /call, reply with https://t.me/<bot>/<shortname>?startapp=<AgentName> and nothing else. Telegram renders the URL as a Launch pill.
  2. Menu entry — add call to channels.telegram.customCommands in the OpenClaw config, then restart the gateway to push setMyCommands.

Note: OpenClaw's inline-keyboard builder currently filters out web_app buttons (only callback_data is supported). Using a plain-URL pill sidesteps that limitation.

Architecture

Static mini app served publicly over HTTPS; per-user relay (bot + mini app are shared). The mini app reads ?relay=<url> baked into the Web App URL registered with @BotFather, POSTs initData to that relay to get a ticket, and opens a WSS session — same protocol as desktop/mobile.

Security notes

  • initData HMAC uses timing-safe compare; freshness window 1 h.
  • Tickets: HMAC-SHA256 over {tgUid, exp} with RELAY_API_KEY as secret; 5 min TTL; raw relay key never leaves the server.
  • CORS scoped narrowly to /auth/telegram only.
  • No secrets in the diff; test fixtures use obviously-fake placeholder tokens.

Test plan

  • Unit: relay-server/test/test-telegram-auth.ts (HMAC freshness, tamper rejection, ticket roundtrip)
  • Integration: relay-server/test/test-telegram-integration.ts (spawns relay, exercises /auth/telegram + WS handshake + CORS preflight + legacy-key regression)
  • Mini app builds clean with AudioWorklet as separate asset
  • Manual end-to-end from iOS Telegram: BotFather-registered mini app, /call via OpenClaw, Launch pill → mini app opens → voice conversation works with agent-name transcripts
  • Manual: end call / restart within the same webview open now works (no 401)

Follow-ups (out of MVP)

  • Multi-bot relay: accept a list of bot tokens (TELEGRAM_BOT_TOKENS=t1,t2,t3) so one relay can serve Pam + Holly + Buffet. Currently one bot per relay instance.
  • Upstream PR to OpenClaw adding web_app button support to extensions/telegram/src/inline-keyboard.ts for nicer UX than the plain-URL pill.
  • Per-user billing/quotas at the relay layer.
  • Handling of Telegram backgrounding / incoming-call interruptions.

🤖 Generated with Claude Code

Comment thread relay-server/src/index.ts Fixed
Comment thread relay-server/src/telegram-auth.ts Dismissed
Comment thread relay-server/src/telegram-auth.ts Fixed
Comment thread relay-server/test/test-telegram-integration.ts Fixed
Comment thread relay-server/test/test-telegram-integration.ts Fixed
@yagudaev yagudaev force-pushed the feature/telegram-miniapp branch from 216655a to 7b11b3b Compare April 19, 2026 04:33
Comment thread relay-server/src/index.ts Fixed
Comment thread relay-server/src/index.ts Dismissed
Comment thread relay-server/src/index.ts

const app = express()

app.use(express.json({ limit: "32kb" }))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

big enough for ai conversations?

yagudaev and others added 6 commits April 23, 2026 22:08
Two new workspaces share a single deployment; the relay stays per-user.
The bot stores each user's relay URL and opens a mini app webview that
talks to their relay directly, reusing the existing Gemini session
pipeline with a new initData-based auth path.

- relay-server: POST /auth/telegram validates Telegram initData HMAC
  (timing-safe) and mints short-lived signed tickets; session.config
  accepts tickets alongside the legacy RELAY_API_KEY. No existing
  clients change.
- telegram-bot: grammY bot with /call, /setrelay, /getrelay, /forget;
  per-user relay URLs in better-sqlite3; menu button registered.
- telegram-miniapp: static Vite + TS webview reusing desktop's
  audio-engine (AudioWorklet emitted as a separate asset for iOS
  WKWebView compatibility); vanilla port of useRealtime without React.

Tests: 9 auth unit + 9 relay integration (spawns relay, exercises
full auth + WS handshake + CORS + tamper/replay rejection) + 10 bot
store; mini app builds clean. Browser-level E2E verified via
Playwright against a live Gemini session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Show agent name in transcripts via ?startapp=<Name> (from agent's /call
  reply); falls back to bot first_name from /auth/telegram response.
- Cache auth ticket in-memory for 4 min to skip re-auth on quick restarts.
- Bump initData freshness window 5min -> 1h (fixes 401 on restart after
  idle; matches standard Telegram practice).
- Remove telegram-bot workspace: OpenClaw owns the bot's long-poll, so a
  sidecar grammY bot isn't viable. /call now flows through OpenClaw agent
  instructions + customCommands; mini app is launched via plain
  t.me/<bot>/<shortname> URL that Telegram auto-renders as a pill.
- Vite preview allowedHosts for .ts.net (Tailscale Funnel).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- rate-limit /auth/telegram at 30 req/min/IP to block token-guess floods
- swap Math.random() for randomBytes() in integration test key
- simplify toBase64Url trailing-= strip to avoid polynomial regex warning
- document HMAC-as-MAC (not password hash) with suppression comment
- ship telegram-miniapp/CLAUDE.md docs

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- new Starlight page covering architecture, OpenClaw setup,
  BotFather flow, ticket auth, Funnel dev, gotchas
- sidebar + landing grid updated
- test-telegram-auth: update assertion to match 1h freshness window
  (caught by codex review)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yagudaev yagudaev force-pushed the feature/telegram-miniapp branch from 2dab677 to af9a764 Compare April 24, 2026 05:09
@yagudaev yagudaev marked this pull request as draft April 24, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants