- bot.New now selects Telegram's test environment with the library's native tgbot.UseTestEnvironment() instead of a token += "/test" hack (functionally identical URL /bot<token>/test/METHOD, but idiomatic) + a bot test asserting the getMe path for both test and prod. - ci.yaml pins TELEGRAM_TEST_ENV=true for the test contour (it IS the test environment) instead of a TEST_TELEGRAM_TEST_ENV variable: removes the confusing double-TEST, telegram-specific, prefixed operator knob and the secret-vs-variable footgun. Prod (Stage 17) leaves it false. - deploy/README.md + PLAN.md updated.
scrabble/platform/telegram — Telegram connector
The Telegram platform side-service. It is the only component that holds the bot
tokens: it runs a Bot API long-poll loop per service language (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.
Service languages (dual bots)
The connector hosts one bot per service language (en, ru) — each its own
token + game channel, configured by the *_EN / *_RU env vars; at least one is
required. The same Telegram user id spans both bots. ValidateInitData tries
each bot's token in turn (none validates ⇒ invalid) and reports which bot validated:
its service_language (persisted by the backend to route the user's later push)
and its supported_languages set (which the UI gates the New Game variant choice
by — en → English, ru → Russian + Эрудит). The user-facing Notify routes by the
recipient's persisted service language; the admin SendToUser / SendToGameChannel
route by an operator-chosen language (unrelated to login).
Responsibilities
- Mini App auth.
ValidateInitDataverifies Telegram Web AppinitData(HMAC under the bot token) and returns the user identity. The gateway calls it during theauth.telegramedge operation, then provisions the session through the backend internal API — so the bot token never leaves this process. - Out-of-app push.
Notifyrenders 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 bot for the request'slanguage(the recipient's service language). The gateway calls it only for a recipient with no live in-app stream and thenotifications_in_app_onlyflag 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 (wired in Stage 10).
SendToUserandSendToGameChannelsend arbitrary text to one user or a game channel through the bot the request selects bylanguage(an operator choice 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 (Stage 11) 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.
Deep-link scheme
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_EN |
— | English bot's API token + initData HMAC secret |
TELEGRAM_BOT_TOKEN_RU |
— | Russian bot's API token + initData HMAC secret (≥ 1 of EN/RU required) |
TELEGRAM_GAME_CHANNEL_ID_EN |
— | English bot's game channel chat id for SendToGameChannel |
TELEGRAM_GAME_CHANNEL_ID_RU |
— | Russian bot's game channel chat id |
TELEGRAM_MINIAPP_URL |
— (required) | Mini App HTTPS origin, shared by all bots (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_EN or _RU
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 lands with Stage 12.
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.