Files
scrabble-game/platform/telegram
Ilia Denisov 57c778f9b2
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
feat(telegram,game): single bot + per-user variant preferences
Collapse the two per-language Telegram bots into one unified bot and
replace language-based variant gating with explicit per-user variant
preferences.

- Telegram: one bot; drop service_language and the supported_languages
  set everywhere (DB, account, auth, FlatBuffers Session wire, gateway,
  connector proto). The single bot renders chat and out-of-app push in
  the recipient's preferred_language; remove the game-language push
  routing override (notify Intent.Language / push Event.language).
- Preferences: new accounts.variant_preferences (text[], DB default
  {erudit_ru}, CHECK non-empty + subset of the three variants). Gates
  the New Game picker, vs-AI and the friend invitation the player
  creates, enforced server-side (HTTP 400 otherwise); an invited friend
  may still accept any variant. Edited on the Settings screen; variants
  are Erudit-first everywhere.
- Admin: drop the per-bot language selectors (broadcast / send-to-user)
  and the feedback channel_lang column/field.
- Env/CI: collapse TELEGRAM_BOT_TOKEN_{EN,RU}, TELEGRAM_GAME_CHANNEL_ID_{EN,RU},
  VITE_TELEGRAM_LINK{,_EN,_RU} and VITE_TELEGRAM_GAME_CHANNEL_NAME_{EN,RU}
  to single unsuffixed names; drop GATEWAY_DEFAULT_SUPPORTED_LANGUAGES.
- Docs updated (ARCHITECTURE, FUNCTIONAL + _ru, platform/telegram, gateway,
  backend, ui, UI_DESIGN, PRERELEASE).

The migration squash is deferred to a follow-up PR.
2026-06-20 14:23:25 +02:00
..

scrabble/platform/telegram — Telegram connector

The Telegram platform side-service. It is the only component that holds the bot token: it runs a Bot API long-poll loop (Mini App launch + deep-links) and serves the connector gRPC API that the gateway and backend call over the trusted internal network. See docs/ARCHITECTURE.md §1/§3/§10/§12.

Single bot

The connector hosts one unified bot — one token plus one optional game channel, configured by TELEGRAM_BOT_TOKEN and TELEGRAM_GAME_CHANNEL_ID. ValidateInitData validates initData against that single token (it does not validate ⇒ invalid) and returns only the Telegram user identity — there is no per-bot service language and no supported-languages set. Every message the bot sends is rendered in the recipient's interface language: the user-facing Notify renders in the request's language (the recipient's interface language); the admin SendToUser / SendToGameChannel render in an operator-chosen language. No call routes between bots — there is only one.

Responsibilities

  • Mini App auth. ValidateInitData verifies Telegram Web App initData (HMAC under the bot token) and returns the user identity. The gateway calls it during the auth.telegram edge operation, then provisions the session through the backend internal API — so the bot token never leaves this process.
  • Out-of-app push. Notify renders a backend push event (your_turn, nudge, match_found, and the invitation / friend_request notify sub-kinds) into a localized message with a Mini App launch button and sends it through the single bot, rendered in the request's language (the recipient's interface language). The gateway calls it only for a recipient with no live in-app stream and the notifications_in_app_only flag off, so the platform push never duplicates in-app delivery.
  • Bot chat. /start <payload> (and the chat menu button) reply with a Mini App launch button; a deep-link payload routes the launch to a game / invitation / friend code.
  • Admin messaging. SendToUser and SendToGameChannel send arbitrary text to one user or a game channel through the single bot, rendered in the language the operator chooses in the admin console.

The generic methods (Notify, SendToUser, SendToGameChannel) address a recipient by the identity external_id (as in the backend identities table), so a future VK / MAX connector can implement the same service; only ValidateInitData is Telegram-specific.

gRPC API

pkg/proto/telegram/v1, service Telegram: ValidateInitData, ValidateLoginWidget, Notify, SendToUser, SendToGameChannel. Generated Go is committed under pkg. ValidateLoginWidget verifies Telegram Login Widget web sign-in data — HMAC under SHA-256(bot_token), distinct from initData (internal/loginwidget) — for attaching a Telegram identity to an account from a browser.

Shared verbatim with the UI (ui/src/lib/deeplink.ts). A Mini App start parameter is a one-character kind prefix plus a value:

Parameter Destination
g<game uuid> open that game
i<invitation uuid> open that invitation
f<6-digit code> redeem that friend code
empty / unknown the lobby

The bot turns a /start <payload> or a notification target into a launch-button URL <MiniAppURL>?startapp=<payload>.

Configuration

Env var Default Meaning
TELEGRAM_BOT_TOKEN — (required) The bot's API token + initData HMAC secret
TELEGRAM_GAME_CHANNEL_ID The bot's game channel chat id for SendToGameChannel
TELEGRAM_MINIAPP_URL — (required) Mini App HTTPS origin (BotFather-registered)
TELEGRAM_GRPC_ADDR :9091 connector gRPC listen address
TELEGRAM_API_BASE_URL https://api.telegram.org Bot API host override (mock / self-hosted)
TELEGRAM_TEST_ENV false route to the Bot API test environment (/bot<token>/test/METHOD)
TELEGRAM_LOG_LEVEL info zap log level
TELEGRAM_SERVICE_NAME scrabble-telegram OpenTelemetry service.name
TELEGRAM_OTEL_TRACES_EXPORTER none none, stdout or otlp (gRPC; endpoint from OTEL_EXPORTER_OTLP_*)
TELEGRAM_OTEL_METRICS_EXPORTER none none, stdout or otlp

The test environment is selected by TELEGRAM_TEST_ENV=true, which suffixes the Bot API path with /test (the connector appends it to the token, since the client builds <host>/bot<token>/<method>).

Build, test, run

go build ./platform/telegram/...
go test ./platform/telegram/...        # unit tests use an httptest fake Bot API
go run ./platform/telegram/cmd/telegram # needs a real TELEGRAM_BOT_TOKEN

Deploy

The connector runs in its own container with the bot token held only there and all egress through a VPN sidecar (deploy/docker-compose.yml, mirroring ../../15-puzzle). It needs no public ingress — it long-polls Telegram and answers internal gRPC at telegram:9091 on the shared edge network. The host reverse proxy routes public traffic to the gateway port only, which serves the Mini App under /telegram/. The full multi-service deploy is deploy/docker-compose.yml.

A real end-to-end Telegram smoke needs a BotFather bot, its token, a public HTTPS Mini App origin, and the connector container; the unit tests cover the wire format, templates, deep-links and the gRPC handlers without a live bot.