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
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:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pkgtel "scrabble/pkg/telemetry"
|
||||
@@ -32,6 +33,11 @@ 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
|
||||
@@ -71,6 +77,13 @@ const (
|
||||
defaultServiceName = "scrabble-gateway"
|
||||
)
|
||||
|
||||
// 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{
|
||||
@@ -113,6 +126,9 @@ func Load() (Config, error) {
|
||||
if c.PushHeartbeatInterval, err = envDuration("GATEWAY_PUSH_HEARTBEAT_INTERVAL", defaultPushHeartbeatInterval); 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
|
||||
}
|
||||
@@ -156,6 +172,31 @@ 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) {
|
||||
|
||||
Reference in New Issue
Block a user