Compare commits

..

15 Commits

Author SHA1 Message Date
developer 8fe1bdba6b Merge pull request 'release: v1.6.0 — promo deep-link seeds EN variant (+ UI nits)' (#135) from development into master 2026-06-23 21:02:19 +00:00
developer c1d1c1624b Merge pull request 'feat(telegram): promo deep-link seeds EN variant (+ UI: share label, header padding)' (#134) from feature/promo-deeplink-and-ui-nits into development
CI / unit (push) Successful in 10s
CI / ui (push) Successful in 56s
CI / gate (push) Successful in 0s
CI / changes (push) Successful in 2s
CI / integration (push) Successful in 16s
CI / deploy (push) Successful in 1m18s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
2026-06-23 20:50:14 +00:00
Ilia Denisov 9207664fbd docs(telegram): note the promo body @username is a deep link
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
2026-06-23 22:45:54 +02:00
Ilia Denisov a4581663f4 feat(telegram): link the promo body @username to the Mini App deep link
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 1m22s
The promo message body now renders "@<bot>" as an HTML text_link to the same ?startapp deep link the button uses (ParseMode HTML), so tapping the mention opens the seeded Mini App instead of the bot profile. Same payload (campaign start-param, else the forwarded /start payload) backs both the button and the mention.
2026-06-23 22:40:52 +02:00
developer 7923b3cc09 Merge pull request 'release v1.5.1: support-relay card + topic-reopen fixes' (#133) from development into master 2026-06-23 16:54:01 +00:00
developer 4891216749 Merge pull request 'release v1.5.0: Telegram bot support relay' (#131) from development into master 2026-06-23 16:16:04 +00:00
developer f1b8769c89 Merge pull request 'release: v1.4.1 — Telegram nav (windowed, own back button, debug panel)' (#129) from development into master 2026-06-23 13:27:31 +00:00
developer b6f28a2423 Merge pull request 'release: v1.4.0 — Telegram launch diagnostic + dynamic SDK load' (#127) from development into master 2026-06-23 08:40:09 +00:00
developer e32ee9ce68 Merge pull request 'Release: development → master' (#125) from development into master 2026-06-22 22:36:42 +00:00
developer dc946a1faf Merge pull request 'release v1.2.2: edge HTTP/3 stall fix + db-size dashboard threshold' (#121) from development into master 2026-06-22 19:50:58 +00:00
developer 384bd143d0 Merge pull request 'Promote development → master: banner tip set + banner/push language fix' (#114) from development into master 2026-06-22 18:28:00 +00:00
developer c5d22fceca Merge pull request 'Promote development → master: Erudit blank star + dictionary v1.3.0 pin' (#111) from development into master 2026-06-22 13:12:01 +00:00
developer deaa7a29c5 Merge pull request 'Promote development → master (docs finalize + UI tweaks + Telegram name fallback)' (#108) from development into master 2026-06-22 07:27:40 +00:00
developer 24017bcb7f Merge pull request 'Promote development → master (deploy v2: versioning + visible jobs + rollback)' (#106) from development into master 2026-06-22 06:01:03 +00:00
developer 2c4f4b10dc Merge pull request 'Promote development → master (initial production release: pre-release line + Stage 18)' (#104) from development into master 2026-06-22 05:05:48 +00:00
4 changed files with 36 additions and 30 deletions
-9
View File
@@ -46,15 +46,6 @@ func (s *Server) handleTelegramAuth(c *gin.Context) {
s.abortErr(c, err) s.abortErr(c, err)
return return
} }
// TEMP DEBUG (revert before merge): trace the language_code Telegram actually sent and
// what we seeded, to settle the "ru interface" question on the test contour.
s.log.Info("telegram auth debug",
zap.String("external_id", req.ExternalID),
zap.String("language_code", req.LanguageCode),
zap.String("username", req.Username),
zap.String("start_param", req.StartParam),
zap.Bool("created", created),
zap.Strings("variant_preferences", acc.VariantPreferences))
if created { if created {
// First registration: re-evaluate moderated-chat write access, so a user who // First registration: re-evaluate moderated-chat write access, so a user who
// joined the chat before registering is granted on the spot (no chat_member // joined the chat before registering is granted on the spot (no chat_member
+3 -1
View File
@@ -86,7 +86,9 @@ Telegram identity to an account from a browser. Both map a rejection to gRPC
self-contained: no bot-link, no gateway, no game. Its `?startapp` payload 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 (`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 link the backend decodes to seed a brand-new user's variant preferences (English Scrabble
alongside the default Erudit). alongside the default Erudit). The message body also renders the bot's `@username` as that
same deep link (an HTML `text_link`), so tapping the mention — not just the button — opens
the seeded Mini App rather than the bot profile.
- **Rate limiting.** Outbound sends are throttled (`TELEGRAM_SEND_RATE_PER_SECOND`, - **Rate limiting.** Outbound sends are throttled (`TELEGRAM_SEND_RATE_PER_SECOND`,
default 25) to respect the Bot API flood limits. default 25) to respect the Bot API flood limits.
+19 -11
View File
@@ -11,6 +11,7 @@ package promobot
import ( import (
"cmp" "cmp"
"context" "context"
"html"
"net/url" "net/url"
"strings" "strings"
@@ -112,13 +113,16 @@ func (t *Bot) handleStart(ctx context.Context, api *tgbot.Bot, update *models.Up
if update.Message.From != nil { if update.Message.From != nil {
lang = update.Message.From.LanguageCode lang = update.Message.From.LanguageCode
} }
text, button := promoText(lang, t.username) // The configured campaign payload (a variant-seed deep link) takes precedence; absent
// one, fall back to forwarding any /start payload the user arrived with. The same
// payload backs both the inline button and the @username link in the body.
param := cmp.Or(t.startParam, startPayload(update.Message.Text))
text, button := promoText(lang, t.username, t.launchURL(param))
if _, err := api.SendMessage(ctx, &tgbot.SendMessageParams{ if _, err := api.SendMessage(ctx, &tgbot.SendMessageParams{
ChatID: update.Message.Chat.ID, ChatID: update.Message.Chat.ID,
Text: text, Text: text,
// The configured campaign payload (a variant-seed deep link) takes precedence; ParseMode: models.ParseModeHTML,
// absent one, fall back to forwarding any /start payload the user arrived with. ReplyMarkup: t.launchMarkup(button, param),
ReplyMarkup: t.launchMarkup(button, cmp.Or(t.startParam, startPayload(update.Message.Text))),
}); err != nil { }); err != nil {
t.log.Warn("promo: reply to start failed", zap.Error(err)) t.log.Warn("promo: reply to start failed", zap.Error(err))
} }
@@ -169,11 +173,15 @@ func startPayload(text string) string {
return strings.TrimSpace(strings.TrimPrefix(text, cmd)) return strings.TrimSpace(strings.TrimPrefix(text, cmd))
} }
// promoText returns the localized message body and button label, naming the main bot // promoText returns the localized message body and button label. The body names the main
// (Russian for a "ru" language code, English otherwise). // bot as a clickable @username whose link is the Mini App deep link (launchURL, the same
func promoText(lang, username string) (text, button string) { // target as the button), so tapping the mention opens the seeded Mini App rather than the
// bot profile. The body is sent with ParseMode HTML; only the link carries markup, so the
// static sentences need no escaping. Russian for a "ru" language code, English otherwise.
func promoText(lang, username, launchURL string) (text, button string) {
mention := `<a href="` + html.EscapeString(launchURL) + `">@` + html.EscapeString(username) + `</a>`
if strings.HasPrefix(strings.ToLower(lang), "ru") { if strings.HasPrefix(strings.ToLower(lang), "ru") {
return "Откройте @" + username + " и выберите в настройках профиля нужный вариант игры.", "🤩 Хочу играть!" return "Откройте " + mention + " и выберите в настройках профиля нужный вариант игры.", "🤩 Хочу играть!"
} }
return "Open @" + username + " and choose your game variant in the profile settings.", "🤩 I want to play!" return "Open " + mention + " and choose your game variant in the profile settings.", "🤩 I want to play!"
} }
@@ -13,25 +13,30 @@ import (
) )
func TestPromoTextLocalization(t *testing.T) { func TestPromoTextLocalization(t *testing.T) {
en, enBtn := promoText("en", "ScrabbleBot") const url = "https://t.me/bot/app?startapp=verudit_ru-scrabble_en"
if !strings.Contains(en, "@ScrabbleBot") || !strings.Contains(en, "profile settings") { // The @username is rendered as a clickable deep link (the same target as the button),
// not a plain mention, so tapping it opens the seeded Mini App.
wantLink := `<a href="` + url + `">@ScrabbleBot</a>`
en, enBtn := promoText("en", "ScrabbleBot", url)
if !strings.Contains(en, wantLink) || !strings.Contains(en, "profile settings") {
t.Errorf("en text = %q", en) t.Errorf("en text = %q", en)
} }
if enBtn != "🤩 I want to play!" { if enBtn != "🤩 I want to play!" {
t.Errorf("en button = %q", enBtn) t.Errorf("en button = %q", enBtn)
} }
ru, ruBtn := promoText("ru-RU", "ScrabbleBot") ru, ruBtn := promoText("ru-RU", "ScrabbleBot", url)
if !strings.Contains(ru, "@ScrabbleBot") || !strings.Contains(ru, "Откройте") { if !strings.Contains(ru, wantLink) || !strings.Contains(ru, "Откройте") {
t.Errorf("ru text = %q", ru) t.Errorf("ru text = %q", ru)
} }
if ruBtn != "🤩 Хочу играть!" { if ruBtn != "🤩 Хочу играть!" {
t.Errorf("ru button = %q", ruBtn) t.Errorf("ru button = %q", ruBtn)
} }
// An unknown language falls back to English. // An unknown language falls back to English, still with the linked mention.
if got, _ := promoText("de", "B"); !strings.Contains(got, "Open @B") { if got, _ := promoText("de", "B", url); !strings.Contains(got, "Open ") || !strings.Contains(got, `">@B</a>`) {
t.Errorf("fallback text = %q, want English", got) t.Errorf("fallback text = %q, want English with a linked mention", got)
} }
} }
@@ -93,8 +98,8 @@ func TestHandleStartReplies(t *testing.T) {
if api.chatID != "42" { if api.chatID != "42" {
t.Errorf("chat_id = %q, want 42", api.chatID) t.Errorf("chat_id = %q, want 42", api.chatID)
} }
if !strings.Contains(api.text, "@ScrabbleBot") { if !strings.Contains(api.text, `<a href="https://t.me/bot/app?startapp=f99">@ScrabbleBot</a>`) {
t.Errorf("text = %q, want the @mention", api.text) t.Errorf("text = %q, want the @mention linked to the startapp deep link", api.text)
} }
if strings.Contains(api.replyMarkup, "web_app") { if strings.Contains(api.replyMarkup, "web_app") {
t.Errorf("reply_markup = %q has a web_app button; want a url button", api.replyMarkup) t.Errorf("reply_markup = %q has a web_app button; want a url button", api.replyMarkup)