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
+38 -13
View File
@@ -20,16 +20,17 @@ type SessionResp struct {
// ProfileResp is an account's own profile.
type ProfileResp struct {
UserID string `json:"user_id"`
DisplayName string `json:"display_name"`
PreferredLanguage string `json:"preferred_language"`
TimeZone string `json:"time_zone"`
AwayStart string `json:"away_start"`
AwayEnd string `json:"away_end"`
HintBalance int `json:"hint_balance"`
BlockChat bool `json:"block_chat"`
BlockFriendRequests bool `json:"block_friend_requests"`
IsGuest bool `json:"is_guest"`
UserID string `json:"user_id"`
DisplayName string `json:"display_name"`
PreferredLanguage string `json:"preferred_language"`
TimeZone string `json:"time_zone"`
AwayStart string `json:"away_start"`
AwayEnd string `json:"away_end"`
HintBalance int `json:"hint_balance"`
BlockChat bool `json:"block_chat"`
BlockFriendRequests bool `json:"block_friend_requests"`
IsGuest bool `json:"is_guest"`
NotificationsInAppOnly bool `json:"notifications_in_app_only"`
}
// TileJSON is one placed tile, used in both play requests and move responses.
@@ -109,11 +110,35 @@ type ChatResp struct {
CreatedAtUnix int64 `json:"created_at_unix"`
}
// TelegramAuth provisions/finds the Telegram account and mints a session.
func (c *Client) TelegramAuth(ctx context.Context, externalID string) (SessionResp, error) {
// TelegramAuth provisions/finds the Telegram account and mints a session, seeding a
// brand-new account's display name and language from the validated launch fields.
func (c *Client) TelegramAuth(ctx context.Context, externalID, languageCode, username, firstName string) (SessionResp, error) {
var out SessionResp
err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/telegram", "", "",
map[string]string{"external_id": externalID}, &out)
map[string]string{
"external_id": externalID,
"language_code": languageCode,
"username": username,
"first_name": firstName,
}, &out)
return out, err
}
// PushTargetResp is a recipient's out-of-app push routing data: their Telegram
// external_id (empty when they have no Telegram identity), preferred language, and
// whether they confined notifications to the in-app stream.
type PushTargetResp struct {
ExternalID string `json:"external_id"`
Language string `json:"language"`
NotificationsInAppOnly bool `json:"notifications_in_app_only"`
}
// PushTarget resolves a user id to their out-of-app Telegram routing data (the
// gateway uses it to decide whether to deliver an event over platform push).
func (c *Client) PushTarget(ctx context.Context, userID string) (PushTargetResp, error) {
var out PushTargetResp
err := c.do(ctx, http.MethodPost, "/api/v1/internal/push-target", "", "",
map[string]string{"user_id": userID}, &out)
return out, err
}
+8 -7
View File
@@ -215,13 +215,14 @@ func (c *Client) ListInvitations(ctx context.Context, userID string) (Invitation
func (c *Client) UpdateProfile(ctx context.Context, userID string, p ProfileResp) (ProfileResp, error) {
var out ProfileResp
body := map[string]any{
"display_name": p.DisplayName,
"preferred_language": p.PreferredLanguage,
"time_zone": p.TimeZone,
"away_start": p.AwayStart,
"away_end": p.AwayEnd,
"block_chat": p.BlockChat,
"block_friend_requests": p.BlockFriendRequests,
"display_name": p.DisplayName,
"preferred_language": p.PreferredLanguage,
"time_zone": p.TimeZone,
"away_start": p.AwayStart,
"away_end": p.AwayEnd,
"block_chat": p.BlockChat,
"block_friend_requests": p.BlockFriendRequests,
"notifications_in_app_only": p.NotificationsInAppOnly,
}
err := c.do(ctx, http.MethodPut, "/api/v1/user/profile", userID, "", body, &out)
return out, err