feat(telegram,game): single bot + per-user variant preferences
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
Collapse the two per-language Telegram bots into one unified bot and
replace language-based variant gating with explicit per-user variant
preferences.
- Telegram: one bot; drop service_language and the supported_languages
set everywhere (DB, account, auth, FlatBuffers Session wire, gateway,
connector proto). The single bot renders chat and out-of-app push in
the recipient's preferred_language; remove the game-language push
routing override (notify Intent.Language / push Event.language).
- Preferences: new accounts.variant_preferences (text[], DB default
{erudit_ru}, CHECK non-empty + subset of the three variants). Gates
the New Game picker, vs-AI and the friend invitation the player
creates, enforced server-side (HTTP 400 otherwise); an invited friend
may still accept any variant. Edited on the Settings screen; variants
are Erudit-first everywhere.
- Admin: drop the per-bot language selectors (broadcast / send-to-user)
and the feedback channel_lang column/field.
- Env/CI: collapse TELEGRAM_BOT_TOKEN_{EN,RU}, TELEGRAM_GAME_CHANNEL_ID_{EN,RU},
VITE_TELEGRAM_LINK{,_EN,_RU} and VITE_TELEGRAM_GAME_CHANNEL_NAME_{EN,RU}
to single unsuffixed names; drop GATEWAY_DEFAULT_SUPPORTED_LANGUAGES.
- Docs updated (ARCHITECTURE, FUNCTIONAL + _ru, platform/telegram, gateway,
backend, ui, UI_DESIGN, PRERELEASE).
The migration squash is deferred to a follow-up PR.
This commit is contained in:
@@ -77,7 +77,6 @@ connector (`ValidateLoginWidget`) and forward the trusted `external_id`. These
|
||||
| `GATEWAY_BACKEND_TIMEOUT` | `5s` | per backend REST call |
|
||||
| `GATEWAY_ADMIN_USER` / `GATEWAY_ADMIN_PASSWORD` | unset | enable + guard the admin console at `/_gm` |
|
||||
| `GATEWAY_CONNECTOR_ADDR` | unset | Telegram connector gRPC address (enables initData validation + out-of-app push) |
|
||||
| `GATEWAY_DEFAULT_SUPPORTED_LANGUAGES` | `en,ru` | New Game variant gating set placed on the Session for non-platform logins (web / email / guest); a deployment may narrow it |
|
||||
| `GATEWAY_SESSION_TTL` | `10m` | cached session lifetime |
|
||||
| `GATEWAY_SESSION_CACHE_MAX` | `50000` | cached session cap |
|
||||
| `GATEWAY_PUSH_HEARTBEAT_INTERVAL` | `10s` | live-stream keep-alive (an immediate heartbeat also fires on open, under the ~15s edge idle timeout) |
|
||||
|
||||
@@ -126,7 +126,7 @@ func run(ctx context.Context, cfg config.Config, logger *zap.Logger) error {
|
||||
logger.Info("admin console disabled (set GATEWAY_ADMIN_USER and GATEWAY_ADMIN_PASSWORD)")
|
||||
}
|
||||
|
||||
registry := transcode.NewRegistry(backend, validator, cfg.DefaultSupportedLanguages...)
|
||||
registry := transcode.NewRegistry(backend, validator)
|
||||
edge := connectsrv.NewServer(connectsrv.Deps{
|
||||
Registry: registry,
|
||||
Sessions: sessions,
|
||||
@@ -258,7 +258,7 @@ func runPushPump(ctx context.Context, backend *backendclient.Client, hub *push.H
|
||||
// deliver the event over the platform push channel. Done in a goroutine
|
||||
// so a slow connector never stalls the in-app firehose.
|
||||
if conn != nil && connector.OutOfAppKind(ev.GetKind()) && !hub.HasSubscribers(ev.GetUserId()) {
|
||||
go deliverOutOfApp(ctx, backend, conn, ev.GetUserId(), ev.GetKind(), ev.GetPayload(), ev.GetLanguage(), logger)
|
||||
go deliverOutOfApp(ctx, backend, conn, ev.GetUserId(), ev.GetKind(), ev.GetPayload(), logger)
|
||||
}
|
||||
}
|
||||
if !sleep(ctx, pushReconnectDelay) {
|
||||
@@ -271,7 +271,7 @@ func runPushPump(ctx context.Context, backend *backendclient.Client, hub *push.H
|
||||
// Telegram identity and have not confined notifications to the app, asks the
|
||||
// connector to deliver the event. It is best-effort: every failure is logged and
|
||||
// dropped (the in-app stream remains the primary channel).
|
||||
func deliverOutOfApp(ctx context.Context, backend *backendclient.Client, conn *connector.Client, userID, kind string, payload []byte, gameLang string, logger *zap.Logger) {
|
||||
func deliverOutOfApp(ctx context.Context, backend *backendclient.Client, conn *connector.Client, userID, kind string, payload []byte, logger *zap.Logger) {
|
||||
target, err := backend.PushTarget(ctx, userID)
|
||||
if err != nil {
|
||||
logger.Warn("push target lookup failed", zap.String("user_id", userID), zap.Error(err))
|
||||
@@ -280,13 +280,8 @@ func deliverOutOfApp(ctx context.Context, backend *backendclient.Client, conn *c
|
||||
if !connector.DeliverToTarget(target.ExternalID, target.NotificationsInAppOnly) {
|
||||
return
|
||||
}
|
||||
// A game event carries its own language, so the push comes from the game's bot rather than
|
||||
// the recipient's last-login bot; other events fall back to the service language.
|
||||
lang := target.Language
|
||||
if gameLang != "" {
|
||||
lang = gameLang
|
||||
}
|
||||
if _, err := conn.Notify(ctx, target.ExternalID, kind, payload, lang); err != nil {
|
||||
// The single bot renders the message in the recipient's interface language.
|
||||
if _, err := conn.Notify(ctx, target.ExternalID, kind, payload, target.Language); err != nil {
|
||||
logger.Warn("out-of-app notify failed", zap.String("kind", kind), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,26 +14,26 @@ import (
|
||||
|
||||
// SessionResp is the credential minted by an auth operation.
|
||||
type SessionResp struct {
|
||||
Token string `json:"token"`
|
||||
UserID string `json:"user_id"`
|
||||
IsGuest bool `json:"is_guest"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ServiceLanguage string `json:"service_language"`
|
||||
Token string `json:"token"`
|
||||
UserID string `json:"user_id"`
|
||||
IsGuest bool `json:"is_guest"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
NotificationsInAppOnly bool `json:"notifications_in_app_only"`
|
||||
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"`
|
||||
VariantPreferences []string `json:"variant_preferences"`
|
||||
// Banner is the advertising-banner block, present only for a viewer eligible to
|
||||
// see the banner. The gateway forwards it verbatim into the Profile payload.
|
||||
Banner *BannerResp `json:"banner,omitempty"`
|
||||
@@ -184,18 +184,15 @@ type ChatResp struct {
|
||||
}
|
||||
|
||||
// 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 and
|
||||
// recording the validating bot's serviceLanguage (which routes the account's later
|
||||
// out-of-app push).
|
||||
func (c *Client) TelegramAuth(ctx context.Context, externalID, languageCode, username, firstName, serviceLanguage string) (SessionResp, error) {
|
||||
// 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,
|
||||
"language_code": languageCode,
|
||||
"username": username,
|
||||
"first_name": firstName,
|
||||
"service_language": serviceLanguage,
|
||||
"external_id": externalID,
|
||||
"language_code": languageCode,
|
||||
"username": username,
|
||||
"first_name": firstName,
|
||||
}, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -287,6 +287,7 @@ func (c *Client) UpdateProfile(ctx context.Context, userID string, p ProfileResp
|
||||
"block_chat": p.BlockChat,
|
||||
"block_friend_requests": p.BlockFriendRequests,
|
||||
"notifications_in_app_only": p.NotificationsInAppOnly,
|
||||
"variant_preferences": p.VariantPreferences,
|
||||
}
|
||||
err := c.do(ctx, http.MethodPut, "/api/v1/user/profile", userID, "", body, &out)
|
||||
return out, err
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pkgtel "scrabble/pkg/telemetry"
|
||||
@@ -33,11 +32,6 @@ type Config struct {
|
||||
// gateway calls it to validate Mini App initData and to deliver out-of-app push.
|
||||
// Empty disables the telegram auth path and the out-of-app push channel.
|
||||
ConnectorAddr string
|
||||
// DefaultSupportedLanguages is the New Game variant gating set put on the Session
|
||||
// for non-platform logins (web / email / guest), which carry no service container
|
||||
// to declare one. The UI offers only variants in this set (en -> English; ru ->
|
||||
// Russian + Эрудит). Defaults to all of them; a deployment may narrow it.
|
||||
DefaultSupportedLanguages []string
|
||||
// SessionTTL bounds how long a resolved session stays cached; SessionCacheMax
|
||||
// caps the number of cached sessions.
|
||||
SessionTTL time.Duration
|
||||
@@ -85,13 +79,6 @@ const (
|
||||
// yet small enough to stop a cheap memory-amplification upload.
|
||||
const DefaultMaxBodyBytes = 1 << 20
|
||||
|
||||
// supportedLanguages is the set of game languages a service may declare for the
|
||||
// New Game variant gating; defaultSupportedLanguages is the non-platform fallback.
|
||||
var (
|
||||
supportedLanguages = map[string]bool{"en": true, "ru": true}
|
||||
defaultSupportedLanguages = []string{"en", "ru"}
|
||||
)
|
||||
|
||||
// DefaultRateLimit returns the built-in anti-abuse limits.
|
||||
func DefaultRateLimit() RateLimitConfig {
|
||||
return RateLimitConfig{
|
||||
@@ -141,9 +128,6 @@ func Load() (Config, error) {
|
||||
if c.MaxBodyBytes, err = envInt("GATEWAY_MAX_BODY_BYTES", DefaultMaxBodyBytes); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if c.DefaultSupportedLanguages, err = envLanguages("GATEWAY_DEFAULT_SUPPORTED_LANGUAGES", defaultSupportedLanguages); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := c.validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
@@ -190,31 +174,6 @@ func envOr(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// envLanguages parses a comma-separated language list (e.g. "en,ru") from the
|
||||
// environment variable named key, returning fallback when it is unset. Every entry
|
||||
// must be a supported language and the result must be non-empty.
|
||||
func envLanguages(key string, fallback []string) ([]string, error) {
|
||||
raw := strings.TrimSpace(os.Getenv(key))
|
||||
if raw == "" {
|
||||
return fallback, nil
|
||||
}
|
||||
var out []string
|
||||
for part := range strings.SplitSeq(raw, ",") {
|
||||
lang := strings.ToLower(strings.TrimSpace(part))
|
||||
if lang == "" {
|
||||
continue
|
||||
}
|
||||
if !supportedLanguages[lang] {
|
||||
return nil, fmt.Errorf("config: %s: unsupported language %q", key, lang)
|
||||
}
|
||||
out = append(out, lang)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("config: %s must list at least one language", key)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// envInt parses the environment variable named key as an int, returning fallback
|
||||
// when it is unset and an error when it is set but malformed.
|
||||
func envInt(key string, fallback int) (int, error) {
|
||||
|
||||
@@ -27,18 +27,13 @@ var ErrInvalidInitData = errors.New("connector: invalid telegram init data")
|
||||
// rejects the Login Widget data (a gRPC InvalidArgument).
|
||||
var ErrInvalidLoginWidget = errors.New("connector: invalid telegram login widget data")
|
||||
|
||||
// User is a validated Mini App identity. ServiceLanguage is the validating bot's
|
||||
// language tag (en/ru), persisted to route the user's out-of-app push back through
|
||||
// the right bot; SupportedLanguages is that bot's set of offered game languages,
|
||||
// which the UI gates the New Game variant choice by. Both are empty for a Login
|
||||
// Widget validation (it carries no bot language).
|
||||
// User is a validated Mini App identity. LanguageCode seeds a brand-new account's
|
||||
// preferred (interface) language; it is empty for a Login Widget validation.
|
||||
type User struct {
|
||||
ExternalID string
|
||||
Username string
|
||||
FirstName string
|
||||
LanguageCode string
|
||||
ServiceLanguage string
|
||||
SupportedLanguages []string
|
||||
ExternalID string
|
||||
Username string
|
||||
FirstName string
|
||||
LanguageCode string
|
||||
}
|
||||
|
||||
// Client wraps the connector's Telegram gRPC service.
|
||||
@@ -73,12 +68,10 @@ func (c *Client) ValidateInitData(ctx context.Context, initData string) (User, e
|
||||
return User{}, err
|
||||
}
|
||||
return User{
|
||||
ExternalID: resp.GetExternalId(),
|
||||
Username: resp.GetUsername(),
|
||||
FirstName: resp.GetFirstName(),
|
||||
LanguageCode: resp.GetLanguageCode(),
|
||||
ServiceLanguage: resp.GetServiceLanguage(),
|
||||
SupportedLanguages: resp.GetSupportedLanguages(),
|
||||
ExternalID: resp.GetExternalId(),
|
||||
Username: resp.GetUsername(),
|
||||
FirstName: resp.GetFirstName(),
|
||||
LanguageCode: resp.GetLanguageCode(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,38 +13,17 @@ import (
|
||||
// created before the table that references it, and no two tables/vectors are
|
||||
// under construction at once.
|
||||
|
||||
// buildSupportedLanguagesVector creates the Session.supported_languages [string]
|
||||
// vector from langs. FlatBuffers is built bottom-up, so the caller must invoke this
|
||||
// (which itself creates the element strings) before SessionStart and with no table
|
||||
// under construction.
|
||||
func buildSupportedLanguagesVector(b *flatbuffers.Builder, langs []string) flatbuffers.UOffsetT {
|
||||
offsets := make([]flatbuffers.UOffsetT, len(langs))
|
||||
for i, lang := range langs {
|
||||
offsets[i] = b.CreateString(lang)
|
||||
}
|
||||
fb.SessionStartSupportedLanguagesVector(b, len(langs))
|
||||
for i := len(offsets) - 1; i >= 0; i-- {
|
||||
b.PrependUOffsetT(offsets[i])
|
||||
}
|
||||
return b.EndVector(len(langs))
|
||||
}
|
||||
|
||||
// encodeSession builds a Session payload. supportedLangs is the service's set of
|
||||
// offered game languages, which the UI gates the New Game variant choice by.
|
||||
func encodeSession(s backendclient.SessionResp, supportedLangs []string) []byte {
|
||||
// encodeSession builds a Session payload.
|
||||
func encodeSession(s backendclient.SessionResp) []byte {
|
||||
b := flatbuffers.NewBuilder(128)
|
||||
token := b.CreateString(s.Token)
|
||||
uid := b.CreateString(s.UserID)
|
||||
name := b.CreateString(s.DisplayName)
|
||||
svcLang := b.CreateString(s.ServiceLanguage)
|
||||
langs := buildSupportedLanguagesVector(b, supportedLangs)
|
||||
fb.SessionStart(b)
|
||||
fb.SessionAddToken(b, token)
|
||||
fb.SessionAddUserId(b, uid)
|
||||
fb.SessionAddIsGuest(b, s.IsGuest)
|
||||
fb.SessionAddDisplayName(b, name)
|
||||
fb.SessionAddSupportedLanguages(b, langs)
|
||||
fb.SessionAddServiceLanguage(b, svcLang)
|
||||
b.Finish(fb.SessionEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
@@ -74,6 +53,7 @@ func encodeProfile(p backendclient.ProfileResp) []byte {
|
||||
if p.Banner != nil {
|
||||
banner = encodeBanner(b, *p.Banner)
|
||||
}
|
||||
prefs := buildStringVector(b, p.VariantPreferences, fb.ProfileStartVariantPreferencesVector)
|
||||
fb.ProfileStart(b)
|
||||
fb.ProfileAddUserId(b, uid)
|
||||
fb.ProfileAddDisplayName(b, name)
|
||||
@@ -86,6 +66,7 @@ func encodeProfile(p backendclient.ProfileResp) []byte {
|
||||
fb.ProfileAddAwayStart(b, awayStart)
|
||||
fb.ProfileAddAwayEnd(b, awayEnd)
|
||||
fb.ProfileAddNotificationsInAppOnly(b, p.NotificationsInAppOnly)
|
||||
fb.ProfileAddVariantPreferences(b, prefs)
|
||||
if p.Banner != nil {
|
||||
fb.ProfileAddBanner(b, banner)
|
||||
}
|
||||
@@ -147,10 +128,8 @@ func encodeBlockStatus(s backendclient.BlockStatusResp) []byte {
|
||||
|
||||
// encodeLinkResult builds a LinkResult payload. A switched-session token
|
||||
// (a guest initiator whose durable counterpart won) is carried as a nested Session
|
||||
// for the client to adopt; it is omitted otherwise. supportedLangs is the variant
|
||||
// gating set for that switched session — the link flows run on the web, so it is the
|
||||
// gateway's default (non-platform) set.
|
||||
func encodeLinkResult(r backendclient.LinkResultResp, supportedLangs []string) []byte {
|
||||
// for the client to adopt; it is omitted otherwise.
|
||||
func encodeLinkResult(r backendclient.LinkResultResp) []byte {
|
||||
b := flatbuffers.NewBuilder(256)
|
||||
status := b.CreateString(r.Status)
|
||||
secID := b.CreateString(r.SecondaryUserID)
|
||||
@@ -161,13 +140,11 @@ func encodeLinkResult(r backendclient.LinkResultResp, supportedLangs []string) [
|
||||
token := b.CreateString(r.Token)
|
||||
uid := b.CreateString(r.Profile.UserID)
|
||||
name := b.CreateString(r.Profile.DisplayName)
|
||||
langs := buildSupportedLanguagesVector(b, supportedLangs)
|
||||
fb.SessionStart(b)
|
||||
fb.SessionAddToken(b, token)
|
||||
fb.SessionAddUserId(b, uid)
|
||||
fb.SessionAddIsGuest(b, r.Profile.IsGuest)
|
||||
fb.SessionAddDisplayName(b, name)
|
||||
fb.SessionAddSupportedLanguages(b, langs)
|
||||
sess = fb.SessionEnd(b)
|
||||
}
|
||||
fb.LinkResultStart(b)
|
||||
|
||||
@@ -89,20 +89,14 @@ 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.
|
||||
// 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"}
|
||||
}
|
||||
func NewRegistry(backend *backendclient.Client, tg TelegramValidator) *Registry {
|
||||
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, defaultLanguages)}
|
||||
r.ops[MsgAuthGuest] = Op{Handler: authGuestHandler(backend)}
|
||||
r.ops[MsgAuthEmailReq] = Op{Handler: authEmailRequestHandler(backend), Email: true}
|
||||
r.ops[MsgAuthEmailLogin] = Op{Handler: authEmailLoginHandler(backend, defaultLanguages), Email: true}
|
||||
r.ops[MsgAuthEmailLogin] = Op{Handler: authEmailLoginHandler(backend), Email: true}
|
||||
r.ops[MsgProfileGet] = Op{Handler: profileHandler(backend), Auth: true}
|
||||
r.ops[MsgBlockStatus] = Op{Handler: blockStatusHandler(backend), Auth: true}
|
||||
r.ops[MsgGameSubmitPlay] = Op{Handler: submitPlayHandler(backend), Auth: true}
|
||||
@@ -130,7 +124,7 @@ func NewRegistry(backend *backendclient.Client, tg TelegramValidator, defaultLan
|
||||
r.ops[MsgFeedbackGet] = Op{Handler: feedbackGetHandler(backend), Auth: true}
|
||||
r.ops[MsgFeedbackUnread] = Op{Handler: feedbackUnreadHandler(backend), Auth: true}
|
||||
registerSocialOps(r, backend)
|
||||
registerLinkOps(r, backend, tg, defaultLanguages)
|
||||
registerLinkOps(r, backend, tg)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -164,21 +158,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, user.ServiceLanguage)
|
||||
sess, err := backend.TelegramAuth(ctx, user.ExternalID, user.LanguageCode, user.Username, user.FirstName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeSession(sess, user.SupportedLanguages), nil
|
||||
return encodeSession(sess), nil
|
||||
}
|
||||
}
|
||||
|
||||
func authGuestHandler(backend *backendclient.Client, supportedLangs []string) Handler {
|
||||
func authGuestHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, _ Request) ([]byte, error) {
|
||||
sess, err := backend.GuestAuth(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeSession(sess, supportedLangs), nil
|
||||
return encodeSession(sess), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,14 +186,14 @@ func authEmailRequestHandler(backend *backendclient.Client) Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func authEmailLoginHandler(backend *backendclient.Client, supportedLangs []string) Handler {
|
||||
func authEmailLoginHandler(backend *backendclient.Client) 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, supportedLangs), nil
|
||||
return encodeSession(sess), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,13 @@ const (
|
||||
|
||||
// registerLinkOps 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 registerLinkOps(r *Registry, backend *backendclient.Client, tg TelegramValidator, supportedLangs []string) {
|
||||
func registerLinkOps(r *Registry, backend *backendclient.Client, tg TelegramValidator) {
|
||||
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}
|
||||
r.ops[MsgLinkEmailConfirm] = Op{Handler: linkEmailConfirmHandler(backend), Auth: true, Email: true}
|
||||
r.ops[MsgLinkEmailMerge] = Op{Handler: linkEmailMergeHandler(backend), 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}
|
||||
r.ops[MsgLinkTelegram] = Op{Handler: linkTelegramHandler(backend, tg, false), Auth: true}
|
||||
r.ops[MsgLinkTelegramMerge] = Op{Handler: linkTelegramHandler(backend, tg, true), Auth: true}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,31 +42,31 @@ func linkEmailRequestHandler(backend *backendclient.Client) Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func linkEmailConfirmHandler(backend *backendclient.Client, supportedLangs []string) Handler {
|
||||
func linkEmailConfirmHandler(backend *backendclient.Client) 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
|
||||
return encodeLinkResult(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func linkEmailMergeHandler(backend *backendclient.Client, supportedLangs []string) Handler {
|
||||
func linkEmailMergeHandler(backend *backendclient.Client) 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
|
||||
return encodeLinkResult(res), 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 {
|
||||
func linkTelegramHandler(backend *backendclient.Client, tg TelegramValidator, merge bool) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsLinkTelegramRequest(req.Payload, 0)
|
||||
user, err := tg.ValidateLoginWidget(ctx, string(in.Data()))
|
||||
@@ -84,6 +82,6 @@ func linkTelegramHandler(backend *backendclient.Client, tg TelegramValidator, me
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeLinkResult(res, supportedLangs), nil
|
||||
return encodeLinkResult(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +246,10 @@ func invitationCancelHandler(backend *backendclient.Client) Handler {
|
||||
func profileUpdateHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsUpdateProfileRequest(req.Payload, 0)
|
||||
prefs := make([]string, in.VariantPreferencesLength())
|
||||
for i := range prefs {
|
||||
prefs[i] = string(in.VariantPreferences(i))
|
||||
}
|
||||
p := backendclient.ProfileResp{
|
||||
DisplayName: string(in.DisplayName()),
|
||||
PreferredLanguage: string(in.PreferredLanguage()),
|
||||
@@ -255,6 +259,7 @@ func profileUpdateHandler(backend *backendclient.Client) Handler {
|
||||
BlockChat: in.BlockChat(),
|
||||
BlockFriendRequests: in.BlockFriendRequests(),
|
||||
NotificationsInAppOnly: in.NotificationsInAppOnly(),
|
||||
VariantPreferences: prefs,
|
||||
}
|
||||
out, err := backend.UpdateProfile(ctx, req.UserID, p)
|
||||
if err != nil {
|
||||
|
||||
@@ -43,11 +43,11 @@ func TestTelegramAuthForwardsSeedFields(t *testing.T) {
|
||||
t.Errorf("unexpected path %q", r.URL.Path)
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&gotBody)
|
||||
_, _ = w.Write([]byte(`{"token":"tok-tg","user_id":"u-tg","is_guest":false,"display_name":"Иван","service_language":"ru"}`))
|
||||
_, _ = w.Write([]byte(`{"token":"tok-tg","user_id":"u-tg","is_guest":false,"display_name":"Иван"}`))
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
v := fakeValidator{user: connector.User{ExternalID: "42", Username: "neo", FirstName: "Иван", LanguageCode: "ru", ServiceLanguage: "ru", SupportedLanguages: []string{"ru"}}}
|
||||
v := fakeValidator{user: connector.User{ExternalID: "42", Username: "neo", FirstName: "Иван", LanguageCode: "ru"}}
|
||||
reg := transcode.NewRegistry(backend, v)
|
||||
op, ok := reg.Lookup(transcode.MsgAuthTelegram)
|
||||
if !ok {
|
||||
@@ -62,48 +62,9 @@ func TestTelegramAuthForwardsSeedFields(t *testing.T) {
|
||||
if string(sess.Token()) != "tok-tg" || string(sess.UserId()) != "u-tg" {
|
||||
t.Fatalf("session decoded wrong: token=%q user=%q", sess.Token(), sess.UserId())
|
||||
}
|
||||
// The validating bot's supported languages ride the Session so the UI gates the
|
||||
// New Game variant choice (here: ru -> Russian + Эрудит only).
|
||||
if got := sessionLanguages(sess); len(got) != 1 || got[0] != "ru" {
|
||||
t.Errorf("session supported_languages = %v, want [ru]", got)
|
||||
}
|
||||
// The bot's service language rides the Session so the UI can build a share link to
|
||||
// the same bot.
|
||||
if got := string(sess.ServiceLanguage()); got != "ru" {
|
||||
t.Errorf("session service_language = %q, want ru", got)
|
||||
}
|
||||
// The validated launch fields are forwarded so the backend can seed a new account;
|
||||
// service_language is recorded to route the account's later out-of-app push.
|
||||
if gotBody["external_id"] != "42" || gotBody["language_code"] != "ru" || gotBody["first_name"] != "Иван" || gotBody["service_language"] != "ru" {
|
||||
t.Errorf("forwarded body = %+v, want external_id=42 language_code=ru first_name=Иван service_language=ru", gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
// sessionLanguages reads the supported_languages vector off a decoded Session.
|
||||
func sessionLanguages(s *fb.Session) []string {
|
||||
out := make([]string, s.SupportedLanguagesLength())
|
||||
for i := range out {
|
||||
out[i] = string(s.SupportedLanguages(i))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestGuestAuthSeedsDefaultLanguages confirms a non-platform (guest) session carries
|
||||
// the gateway's default supported-languages set, so the web client is ungated.
|
||||
func TestGuestAuthSeedsDefaultLanguages(t *testing.T) {
|
||||
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"token":"tok-g","user_id":"u-g","is_guest":true,"display_name":"Guest"}`))
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
reg := transcode.NewRegistry(backend, nil, "en", "ru")
|
||||
op, _ := reg.Lookup(transcode.MsgAuthGuest)
|
||||
payload, err := op.Handler(context.Background(), transcode.Request{})
|
||||
if err != nil {
|
||||
t.Fatalf("handler: %v", err)
|
||||
}
|
||||
if got := sessionLanguages(fb.GetRootAsSession(payload, 0)); len(got) != 2 || got[0] != "en" || got[1] != "ru" {
|
||||
t.Errorf("guest session supported_languages = %v, want [en ru]", got)
|
||||
// The validated launch fields are forwarded so the backend can seed a new account.
|
||||
if gotBody["external_id"] != "42" || gotBody["language_code"] != "ru" || gotBody["first_name"] != "Иван" {
|
||||
t.Errorf("forwarded body = %+v, want external_id=42 language_code=ru first_name=Иван", gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user