Stage 15: dual Telegram bots & language-gated variants
Tests · Go / test (push) Successful in 9s
Tests · Integration / integration (push) Successful in 10s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 8s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s

Service-agnostic refinement of the owner's idea: the sign-in service returns a
set of supported game languages with the user identity, and the lobby gates the
New Game variant choice by it (en -> English; ru -> Russian + Эрудит).

- Connector hosts two bots in one container (one per service language, each its
  own token + game channel; the same telegram_id spans both). ValidateInitData
  tries each token and returns the validating bot's service_language +
  supported_languages. Per-language config (TELEGRAM_BOT_TOKEN_EN/_RU, channels).
- supported_languages rides the Session (fbs, session-scoped, not persisted); the
  UI offers only the matching variants on New Game — gating only the START of a
  new game (auto-match + friend invite), not accept/open/play; backend does not
  enforce.
- service_language persisted (accounts.service_language, migration 00010, written
  every login, last-login-wins) and routes the user-facing Notify push back
  through the right bot (push-target coalesces with preferred_language).
- Admin SendToUser/SendToGameChannel gain an operator-chosen language selector in
  the console (unrelated to ValidateInitData).
- Non-Telegram logins carry the gateway default set
  (GATEWAY_DEFAULT_SUPPORTED_LANGUAGES, all variants).

Wire (committed regen): ValidateInitDataResponse +service_language
+supported_languages; Session +supported_languages; SendToUser/SendToGameChannel
+language. Docs (ARCHITECTURE/FUNCTIONAL/_ru/READMEs) + PLAN updated; stage marked done.
This commit is contained in:
Ilia Denisov
2026-06-05 09:35:53 +02:00
parent 23b5c3b5cc
commit e9f836db87
45 changed files with 1010 additions and 267 deletions
+16 -10
View File
@@ -75,14 +75,20 @@ type TelegramValidator interface {
// NewRegistry builds the slice's message-type catalog over the backend client.
// The Telegram auth op is registered only when a validator is supplied (the
// connector is configured); otherwise auth.telegram is simply unknown.
func NewRegistry(backend *backendclient.Client, tg TelegramValidator) *Registry {
// defaultLanguages is the New Game variant gating set placed on the Session for
// non-platform logins (web / email / guest) and on a switched link session; an
// empty argument falls back to all languages (matching the config default).
func NewRegistry(backend *backendclient.Client, tg TelegramValidator, defaultLanguages ...string) *Registry {
if len(defaultLanguages) == 0 {
defaultLanguages = []string{"en", "ru"}
}
r := &Registry{ops: make(map[string]Op)}
if tg != nil {
r.ops[MsgAuthTelegram] = Op{Handler: authTelegramHandler(backend, tg)}
}
r.ops[MsgAuthGuest] = Op{Handler: authGuestHandler(backend)}
r.ops[MsgAuthGuest] = Op{Handler: authGuestHandler(backend, defaultLanguages)}
r.ops[MsgAuthEmailReq] = Op{Handler: authEmailRequestHandler(backend), Email: true}
r.ops[MsgAuthEmailLogin] = Op{Handler: authEmailLoginHandler(backend), Email: true}
r.ops[MsgAuthEmailLogin] = Op{Handler: authEmailLoginHandler(backend, defaultLanguages), Email: true}
r.ops[MsgProfileGet] = Op{Handler: profileHandler(backend), Auth: true}
r.ops[MsgGameSubmitPlay] = Op{Handler: submitPlayHandler(backend), Auth: true}
r.ops[MsgGameState] = Op{Handler: gameStateHandler(backend), Auth: true}
@@ -101,7 +107,7 @@ func NewRegistry(backend *backendclient.Client, tg TelegramValidator) *Registry
r.ops[MsgChatList] = Op{Handler: chatListHandler(backend), Auth: true}
r.ops[MsgChatNudge] = Op{Handler: nudgeHandler(backend), Auth: true}
registerStage8(r, backend)
registerStage11(r, backend, tg)
registerStage11(r, backend, tg, defaultLanguages)
return r
}
@@ -135,21 +141,21 @@ func authTelegramHandler(backend *backendclient.Client, tg TelegramValidator) Ha
if err != nil {
return nil, err
}
sess, err := backend.TelegramAuth(ctx, user.ExternalID, user.LanguageCode, user.Username, user.FirstName)
sess, err := backend.TelegramAuth(ctx, user.ExternalID, user.LanguageCode, user.Username, user.FirstName, user.ServiceLanguage)
if err != nil {
return nil, err
}
return encodeSession(sess), nil
return encodeSession(sess, user.SupportedLanguages), nil
}
}
func authGuestHandler(backend *backendclient.Client) Handler {
func authGuestHandler(backend *backendclient.Client, supportedLangs []string) Handler {
return func(ctx context.Context, _ Request) ([]byte, error) {
sess, err := backend.GuestAuth(ctx)
if err != nil {
return nil, err
}
return encodeSession(sess), nil
return encodeSession(sess, supportedLangs), nil
}
}
@@ -163,14 +169,14 @@ func authEmailRequestHandler(backend *backendclient.Client) Handler {
}
}
func authEmailLoginHandler(backend *backendclient.Client) Handler {
func authEmailLoginHandler(backend *backendclient.Client, supportedLangs []string) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsEmailLoginRequest(req.Payload, 0)
sess, err := backend.EmailLogin(ctx, string(in.Email()), string(in.Code()))
if err != nil {
return nil, err
}
return encodeSession(sess), nil
return encodeSession(sess, supportedLangs), nil
}
}