Files
scrabble-game/gateway/internal/transcode/transcode_link.go
T
Ilia Denisov e9f836db87
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
Stage 15: dual Telegram bots & language-gated variants
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.
2026-06-05 09:35:53 +02:00

90 lines
3.6 KiB
Go

package transcode
import (
"context"
"scrabble/gateway/internal/backendclient"
fb "scrabble/pkg/fbs/scrabblefb"
)
// Stage 11 account linking & merge message types. The email ops carry the costly-
// email rate flag; the telegram ops validate Login Widget data through the
// connector (registered only when the connector is configured). All are
// authenticated. The merge ops are the explicit irreversible step, gated in the UI
// after a merge_required confirm.
const (
MsgLinkEmailRequest = "link.email.request"
MsgLinkEmailConfirm = "link.email.confirm"
MsgLinkEmailMerge = "link.email.merge"
MsgLinkTelegram = "link.telegram.confirm"
MsgLinkTelegramMerge = "link.telegram.merge"
)
// registerStage11 adds the linking & merge operations. The telegram ops need the
// connector's Login Widget validator, so they are registered only when tg is set.
// supportedLangs is the variant gating set for a switched link session (the link
// flows run on the web, so the gateway default set).
func registerStage11(r *Registry, backend *backendclient.Client, tg TelegramValidator, supportedLangs []string) {
r.ops[MsgLinkEmailRequest] = Op{Handler: linkEmailRequestHandler(backend), Auth: true, Email: true}
r.ops[MsgLinkEmailConfirm] = Op{Handler: linkEmailConfirmHandler(backend, supportedLangs), Auth: true, Email: true}
r.ops[MsgLinkEmailMerge] = Op{Handler: linkEmailMergeHandler(backend, supportedLangs), Auth: true, Email: true}
if tg != nil {
r.ops[MsgLinkTelegram] = Op{Handler: linkTelegramHandler(backend, tg, false, supportedLangs), Auth: true}
r.ops[MsgLinkTelegramMerge] = Op{Handler: linkTelegramHandler(backend, tg, true, supportedLangs), Auth: true}
}
}
func linkEmailRequestHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsLinkEmailRequest(req.Payload, 0)
if err := backend.LinkEmailRequest(ctx, req.UserID, string(in.Email())); err != nil {
return nil, err
}
return encodeAck(true), nil
}
}
func linkEmailConfirmHandler(backend *backendclient.Client, supportedLangs []string) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsLinkEmailConfirm(req.Payload, 0)
res, err := backend.LinkEmailConfirm(ctx, req.UserID, string(in.Email()), string(in.Code()))
if err != nil {
return nil, err
}
return encodeLinkResult(res, supportedLangs), nil
}
}
func linkEmailMergeHandler(backend *backendclient.Client, supportedLangs []string) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsLinkEmailConfirm(req.Payload, 0)
res, err := backend.LinkEmailMerge(ctx, req.UserID, string(in.Email()), string(in.Code()))
if err != nil {
return nil, err
}
return encodeLinkResult(res, supportedLangs), nil
}
}
// linkTelegramHandler validates Login Widget data via the connector and then calls
// the backend's link or merge endpoint with the trusted Telegram external id.
func linkTelegramHandler(backend *backendclient.Client, tg TelegramValidator, merge bool, supportedLangs []string) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsLinkTelegramRequest(req.Payload, 0)
user, err := tg.ValidateLoginWidget(ctx, string(in.Data()))
if err != nil {
return nil, err
}
var res backendclient.LinkResultResp
if merge {
res, err = backend.LinkTelegramMerge(ctx, req.UserID, user.ExternalID)
} else {
res, err = backend.LinkTelegram(ctx, req.UserID, user.ExternalID)
}
if err != nil {
return nil, err
}
return encodeLinkResult(res, supportedLangs), nil
}
}