Stage 9: Telegram integration (connector side-service, Mini App, out-of-app push)
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 12s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s

New platform/telegram connector (own container, bot token only there):
- go-telegram/bot long-poll loop: /start deep-links + Mini App launch button.
- gRPC API pkg/proto/telegram/v1 (Telegram service): ValidateInitData, Notify
  (renders a localized message + deep-link button), SendToUser/SendToGameChannel
  (admin, wired in Stage 10). Generic methods are platform-agnostic (external_id).
- Bot API base override for Telegram's test environment; Dockerfile + compose
  (VPN sidecar, no public ingress); README.

Gateway:
- initData validation relocated from the gateway into the connector; the gateway
  calls ValidateInitData over gRPC (GATEWAY_CONNECTOR_ADDR), drops the bot token,
  and deletes internal/auth.
- Out-of-app push: runPushPump routes events whose recipient has no live in-app
  stream to connector.Notify, gated by /internal/push-target + the in-app-only
  flag (race-free de-dup); HasSubscribers added to the push hub.

Backend:
- Migration 00007 accounts.notifications_in_app_only (default true) + jetgen.
- ProvisionTelegram seeds a new account's language/display name from the launch
  fields; IdentityExternalID reverse lookup; /internal/push-target handler.

UI:
- Telegram Mini App launch: detect initData, apply themeParams, authTelegram,
  route the deep-link start_param (g/i/f); /telegram/ guard redirects outside
  Telegram. Vite relative base + telegram-web-app.js. In-app-only profile toggle;
  share-to-Telegram link for a friend code. Vitest + Playwright coverage.

Wire/docs/CI: fbs Profile/UpdateProfileRequest gain notifications_in_app_only
(Go + TS); go.work uses ./platform/telegram; go-unit.yaml covers it; PLAN,
ARCHITECTURE, FUNCTIONAL (+ru), UI_DESIGN, READMEs updated.
This commit is contained in:
Ilia Denisov
2026-06-04 01:42:54 +02:00
parent 1012fb47a0
commit cf66ed7e26
86 changed files with 3624 additions and 372 deletions
+95 -6
View File
@@ -42,7 +42,7 @@ independent (see ARCHITECTURE §9.1).
| 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | **done** |
| 7 | UI — playable slice + UX polish (Svelte+Vite, board, lobby, chat, hint/word-check, i18n) | **done** |
| 8 | UI — social/account/history (friends, blocks, invitations, profile edit, stats, history/GCG) | **done** |
| 9 | Telegram integration (bot side-service, deep-link, push) | todo |
| 9 | Telegram integration (bot side-service, deep-link, push) | **done** |
| 10 | Admin & dictionary ops (complaint review, version reload) | todo |
| 11 | Account linking & merge | todo |
| 12 | Polish (observability, perf with evidence, deploy) | todo |
@@ -605,6 +605,94 @@ Open details: deployment target/host; dashboards; load expectations.
OS and can't be forced to match, and a select also avoids the iOS "clear" button
that would empty a time field.
- **Stage 9** (interview + implementation):
- **Connector as its own container** (interview): the Telegram side-service is a
standalone module `platform/telegram` (binary `cmd/telegram`) holding the bot
token **only there**; gateway and backend reach it by **unauthenticated gRPC**
on the trusted internal network, and it egresses to `api.telegram.org` through a
**VPN sidecar** (`deploy/docker-compose.yml`, mirroring `../15-puzzle`). Bot
library **`github.com/go-telegram/bot`** (one new dep), **long-poll** updates.
- **initData validation moved off the gateway** (interview): the gateway's HMAC
validator was **relocated** into the connector (`internal/initdata`, now also
returning `language_code`); the gateway calls `connector.ValidateInitData` over
gRPC during `auth.telegram`. The hop is negligible (loopback gRPC, once per
login). `GATEWAY_TELEGRAM_BOT_TOKEN` is gone; `GATEWAY_CONNECTOR_ADDR` replaces
it. The `gateway/internal/auth` package was deleted.
- **Connector gRPC API** (`pkg/proto/telegram/v1`, service `Telegram`): the
generic methods are **platform-agnostic**, keyed by the identity `external_id`
(so a future VK/MAX connector reuses them); only `ValidateInitData` is
Telegram-specific. Methods: `ValidateInitData`, **`Notify`** (the out-of-app push
— renders a localized message + a Mini App deep-link button from the FlatBuffers
payload), `SendToUser` and `SendToGameChannel` (arbitrary admin messages — built
and unit-tested now, **wired to the admin surface in Stage 10**; the game channel
id lives only in connector config).
- **Push = fallback, gateway-routed, de-dup by presence** (interview): the gateway
already consumes the firehose and knows in-app presence (`push.Hub.HasSubscribers`),
so it decides in-app vs out-of-app **atomically**: for a recipient with **no live
in-app stream** it fetches a new backend `/internal/push-target`
(`{external_id, language, notifications_in_app_only}`) and calls `connector.Notify`
only when they have a Telegram identity and have **not** set the new flag. Push
set: `your_turn`, `nudge`, `match_found`, and the `notify` sub-kinds `invitation`/
`friend_request` (the connector skips the rest). Delivery runs in a goroutine so a
slow connector never stalls the firehose; best-effort (no cursor resume — single
instance, §10).
- **Profile flag `notifications_in_app_only`** (interview, **default true** → push
is **opt-in**): migration `00007` (+ jetgen), threaded through
`account.Profile`/`UpdateProfile`, the REST DTOs, the fbs `Profile`/
`UpdateProfileRequest` (default `true` in the schema so an unset field reads
conservatively), and a Profile-screen toggle. Flagged at review: the channel is
silent until a user turns it off.
- **Language seeding from the platform** (discharges the Stage 8 forward-note):
`account.ProvisionTelegram` seeds a **brand-new** account's `preferred_language`
from the Telegram `language_code` and its display name from `first_name`/
`username` (existing accounts untouched); the UI's `adoptSession` already adopts
the server language when the user has not locked a locale, so no extra UI seeding
was needed. The gateway forwards the fields from `ValidateInitData`.
- **Mini App = `/telegram/` + guard** (interview): the gateway serves the one SPA
build under `/telegram/` (Vite **relative base**; the hash router is
path-agnostic). The UI detects a Telegram launch by `Telegram.WebApp.initData`,
applies `themeParams`, authenticates via the existing `auth.telegram` op (UI
`authTelegram` codec/client/transport/mock added), and routes the deep-link
`start_param` (`g`/`i`/`f` → game / lobby-invitation / friend-code redeem). On the
`/telegram/` path **without** initData it redirects to the site root. The official
`telegram-web-app.js` loads from `index.html` (harmless outside Telegram).
- **Deep-link scheme** (shared Go `platform/telegram/internal/deeplink` ↔ TS
`ui/src/lib/deeplink.ts`): `g<game uuid>` / `i<invitation uuid>` / `f<6-digit
code>` / empty = lobby. A friend-code **share-to-Telegram** link is shown when
`VITE_TELEGRAM_LINK` is configured (**partially discharges TODO-5**; QR still
open). The `Notify` button and the bot `/start` reply both wrap the payload as
`<MiniAppURL>?startapp=<payload>`.
- **Test environment** (interview nuance): the Bot API base is overridable for
Telegram's test environment — `TELEGRAM_TEST_ENV=true` suffixes the token with
`/test` so the client hits `/bot<token>/test/METHOD` (`TELEGRAM_API_BASE_URL`
overrides the host for a mock/self-hosted server).
- **Deploy groundwork** (interview): `platform/telegram/Dockerfile` (builds the
connector standalone — drops backend/gateway and the solver replace from a copy
of `go.work`, validated with `docker build`) + the connector-scoped compose with
the VPN sidecar; a root `.dockerignore`. **No public ingress** for the connector
(long-poll + sidecar egress); the host reverse proxy routes only to the gateway
port, which serves the Mini App. The full multi-service deploy is **Stage 12**.
- **Wire/codegen/CI**: new proto `pkg/proto/telegram/v1` (committed Go); fbs
`Profile`/`UpdateProfileRequest` gained `notifications_in_app_only` (committed Go
+ TS). `go.work` gains `use ./platform/telegram`; deps via `go mod edit` +
`go work sync` (no-tidy). `go-unit.yaml` gained the `platform/**` path filter and
builds/vets/tests `./platform/telegram/...`. UI grows to ~86 KB gzip JS (budget
100 KB). The connector's unit tests use an httptest fake Bot API; a Playwright
smoke drives the Mini App launch + guard with an injected `window.Telegram`.
- **Stage 10 forward-note**: the admin surface will wire `connector.SendToUser`/
`SendToGameChannel` (backend gains its own connector client) for operator
broadcasts to a user and the game channel.
- **Verification-time fixes** (caught by the CI gate): (1) the gateway transcode
dropped `notifications_in_app_only` in four places (`ProfileResp`, `encodeProfile`,
`profileUpdateHandler`, the `UpdateProfile` body) so the toggle never reached the
backend — fixed, with a round-trip transcode test added. (2) The e2e suite was made
**hermetic** (a shared `ui/e2e/fixtures.ts` blocks the real `telegram-web-app.js`):
the render-blocking CDN `<script>` hung every page load on the CI runner, where
telegram.org is unreachable, timing out all non-Telegram specs. (3) A pre-existing
time-of-day flake in `TestTimeoutSweep` (the default 00:0007:00 away window made
the sweeper skip when CI ran with `now-1h` inside it) was made deterministic by
clearing the test account's away window.
## Deferred TODOs (cross-stage)
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable,
@@ -637,11 +725,12 @@ Open details: deployment target/host; dashboards; load expectations.
value)` table so the UI stops duplicating it, and optionally moving tile exchange to
letter **indices** end-to-end. Caveat (as for the dictionaries, TODO-2): the wire table
must stay pinned to the same `rules.Alphabet` the engine uses, or indices drift.
- **TODO-5 — QR / deep-link friend codes (owner's idea, Stage 8).** The one-time
friend code is entered by hand today. Once the Telegram/native deep-link scheme
exists (Stage 9), wrap a code in a deep link and render it as a QR so a friend can
add you by scanning rather than typing. The code semantics (12 h TTL, single use,
one active per issuer) stay as-is; only the delivery changes.
- **TODO-5 — QR friend codes (owner's idea, Stage 8).** *Partially done in Stage 9:*
the deep-link scheme now exists (`f<code>`, shared Go ↔ TS), the bot redeems it on
launch, and the UI shows a **share-to-Telegram** link for an issued code when
`VITE_TELEGRAM_LINK` is configured. **Still open:** render the link as a **QR** so a
friend can add you by scanning rather than tapping/typing. The code semantics
(12 h TTL, single use, one active per issuer) stay as-is; only the delivery changes.
- **TODO-6 — smart default for the friend-game "game type" (owner's idea, Stage 8).**
The play-with-friends form has no preselected variant today (an empty, required
pick). Default it from the player's history (the variant they play most, from