feat(telegram): promo deep-link seeds English Scrabble for new users
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 50s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 50s
The promo bot button carries a configurable variant-seed start-param (default verudit_ru-scrabble_en). The gateway parses start_param from the validated initData and forwards it; the backend, on first contact only, seeds the new account variant_preferences from it (English Scrabble alongside the default Erudit). No schema change (the scrabble_en CHECK is already in the baseline) and the gateway<->backend REST field is additive, so the rolling deploy is safe in either order. TELEGRAM_PROMO_START_PARAM configures the payload (empty forwards the user own /start payload). Covered by account unit tests, a gateway transcode test, and an integration test asserting new-only seeding.
This commit is contained in:
@@ -83,7 +83,10 @@ Telegram identity to an account from a browser. Both map a rejection to gRPC
|
||||
the main bot's direct link (`TELEGRAM_BOT_LINK`, the same link the UI uses) with
|
||||
`?startapp` — a `web_app` button would launch under the promo bot's identity (its token
|
||||
would sign the initData), which the main bot's validator rejects. It is fully
|
||||
self-contained: no bot-link, no gateway, no game.
|
||||
self-contained: no bot-link, no gateway, no game. Its `?startapp` payload
|
||||
(`TELEGRAM_PROMO_START_PARAM`, default `verudit_ru-scrabble_en`) is a variant-seed deep
|
||||
link the backend decodes to seed a brand-new user's variant preferences (English Scrabble
|
||||
alongside the default Erudit).
|
||||
- **Rate limiting.** Outbound sends are throttled (`TELEGRAM_SEND_RATE_PER_SECOND`,
|
||||
default 25) to respect the Bot API flood limits.
|
||||
|
||||
@@ -151,6 +154,7 @@ Bot (`cmd/bot`):
|
||||
| `TELEGRAM_PROMO_BOT_TOKEN` | — | the optional standalone promo bot's token; empty disables it |
|
||||
| `TELEGRAM_BOT_USERNAME` | — | the main bot's @username without the @ (promo message); required when the promo bot runs |
|
||||
| `TELEGRAM_BOT_LINK` | — | the main bot's Mini App link for the promo button (the UI's `VITE_TELEGRAM_LINK`); required when the promo bot runs |
|
||||
| `TELEGRAM_PROMO_START_PARAM` | `verudit_ru-scrabble_en` | the promo button's `startapp` payload — a variant-seed deep link the backend decodes to seed a brand-new user's variant preferences; empty forwards the user's own `/start` payload |
|
||||
| `TELEGRAM_OWNS_UPDATES` | `true` | run the exclusive `getUpdates` long-poll (one bot per token) |
|
||||
| `TELEGRAM_SEND_RATE_PER_SECOND` | `25` | outbound Bot API send cap (0 disables) |
|
||||
| `TELEGRAM_INSTANCE_ID` | hostname | bot identity reported to the gateway |
|
||||
|
||||
@@ -131,6 +131,7 @@ func run(ctx context.Context, cfg config.BotConfig, logger *zap.Logger) error {
|
||||
TestEnv: cfg.TestEnv,
|
||||
BotUsername: cfg.BotUsername,
|
||||
BotLinkURL: cfg.BotLinkURL,
|
||||
StartParam: cfg.PromoStartParam,
|
||||
SendRatePerSecond: cfg.SendRatePerSecond,
|
||||
}, logger)
|
||||
if err != nil {
|
||||
|
||||
@@ -65,6 +65,12 @@ type BotConfig struct {
|
||||
// button appends ?startapp=<payload> to it (TELEGRAM_BOT_LINK; required when the
|
||||
// promo bot runs). It is distinct from the BotLink mTLS dial config below.
|
||||
BotLinkURL string
|
||||
// PromoStartParam is the promo button's launch payload, appended as
|
||||
// ?startapp=<PromoStartParam> (TELEGRAM_PROMO_START_PARAM, default
|
||||
// "verudit_ru-scrabble_en"). It is a variant-seed deep link the backend decodes to
|
||||
// seed a brand-new user's variant preferences; its labels must match the backend's
|
||||
// known variants. Empty falls back to forwarding the user's own /start payload.
|
||||
PromoStartParam string
|
||||
// MiniAppURL is the HTTPS origin of the Mini App registered with BotFather; it is
|
||||
// the base of every launch button (TELEGRAM_MINIAPP_URL, required).
|
||||
MiniAppURL string
|
||||
@@ -113,6 +119,10 @@ const (
|
||||
defaultValidatorGRPCAddr = ":9091"
|
||||
defaultBotReconnectDelay = 2 * time.Second
|
||||
defaultSendRatePerSecond = 25
|
||||
// defaultPromoStartParam is the promo button's default launch payload: a variant-seed
|
||||
// deep link the backend decodes to add English Scrabble to a brand-new user's variant
|
||||
// preferences alongside the default Erudit. Override with TELEGRAM_PROMO_START_PARAM.
|
||||
defaultPromoStartParam = "verudit_ru-scrabble_en"
|
||||
)
|
||||
|
||||
// LoadValidator reads the validator configuration from the environment.
|
||||
@@ -145,6 +155,7 @@ func LoadBot() (BotConfig, error) {
|
||||
PromoBotToken: os.Getenv("TELEGRAM_PROMO_BOT_TOKEN"),
|
||||
BotUsername: strings.TrimPrefix(os.Getenv("TELEGRAM_BOT_USERNAME"), "@"),
|
||||
BotLinkURL: os.Getenv("TELEGRAM_BOT_LINK"),
|
||||
PromoStartParam: envOr("TELEGRAM_PROMO_START_PARAM", defaultPromoStartParam),
|
||||
SupportStateDir: envOr("TELEGRAM_SUPPORT_STATE_DIR", "/data"),
|
||||
LogLevel: envOr("TELEGRAM_LOG_LEVEL", "info"),
|
||||
BotLink: BotLinkClientConfig{
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package promobot
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -33,6 +34,11 @@ type Config struct {
|
||||
// BotLinkURL is the main bot's Mini App direct link; the button appends
|
||||
// ?startapp=<payload> to it.
|
||||
BotLinkURL string
|
||||
// StartParam is the campaign payload appended as ?startapp=<StartParam> to the launch
|
||||
// button — e.g. a variant-seed deep link ("verudit_ru-scrabble_en") the backend
|
||||
// decodes to seed a brand-new user's variant preferences. Empty falls back to
|
||||
// forwarding the user's own /start payload.
|
||||
StartParam string
|
||||
// SendRatePerSecond caps outbound sends to respect the Bot API flood limits; 0
|
||||
// disables the limiter. The burst equals the per-second rate.
|
||||
SendRatePerSecond int
|
||||
@@ -40,11 +46,12 @@ type Config struct {
|
||||
|
||||
// Bot is the promo bot wrapper around a Telegram Bot API client.
|
||||
type Bot struct {
|
||||
api *tgbot.Bot
|
||||
username string
|
||||
linkURL string
|
||||
log *zap.Logger
|
||||
limiter *rate.Limiter
|
||||
api *tgbot.Bot
|
||||
username string
|
||||
linkURL string
|
||||
startParam string
|
||||
log *zap.Logger
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
// New builds the promo bot, registering a /start (and default) handler that replies
|
||||
@@ -53,7 +60,7 @@ func New(cfg Config, log *zap.Logger) (*Bot, error) {
|
||||
if log == nil {
|
||||
log = zap.NewNop()
|
||||
}
|
||||
t := &Bot{username: cfg.BotUsername, linkURL: cfg.BotLinkURL, log: log}
|
||||
t := &Bot{username: cfg.BotUsername, linkURL: cfg.BotLinkURL, startParam: cfg.StartParam, log: log}
|
||||
if cfg.SendRatePerSecond > 0 {
|
||||
t.limiter = rate.NewLimiter(rate.Limit(cfg.SendRatePerSecond), cfg.SendRatePerSecond)
|
||||
}
|
||||
@@ -87,7 +94,8 @@ func (t *Bot) Run(ctx context.Context) {
|
||||
}
|
||||
|
||||
// handleStart replies to any message (typically /start) with the localized promo text
|
||||
// and a button that opens the main bot's Mini App, forwarding any /start payload.
|
||||
// and a button that opens the main bot's Mini App at the configured campaign payload
|
||||
// (falling back to forwarding any /start payload the user arrived with).
|
||||
func (t *Bot) handleStart(ctx context.Context, api *tgbot.Bot, update *models.Update) {
|
||||
if update.Message == nil {
|
||||
return
|
||||
@@ -106,9 +114,11 @@ func (t *Bot) handleStart(ctx context.Context, api *tgbot.Bot, update *models.Up
|
||||
}
|
||||
text, button := promoText(lang, t.username)
|
||||
if _, err := api.SendMessage(ctx, &tgbot.SendMessageParams{
|
||||
ChatID: update.Message.Chat.ID,
|
||||
Text: text,
|
||||
ReplyMarkup: t.launchMarkup(button, startPayload(update.Message.Text)),
|
||||
ChatID: update.Message.Chat.ID,
|
||||
Text: text,
|
||||
// The configured campaign payload (a variant-seed deep link) takes precedence;
|
||||
// absent one, fall back to forwarding any /start payload the user arrived with.
|
||||
ReplyMarkup: t.launchMarkup(button, cmp.Or(t.startParam, startPayload(update.Message.Text))),
|
||||
}); err != nil {
|
||||
t.log.Warn("promo: reply to start failed", zap.Error(err))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user