fix(telegram): reply to /start only in private chats #100

Merged
developer merged 1 commits from feature/telegram-private-reply-guard into development 2026-06-21 15:32:11 +00:00
4 changed files with 53 additions and 1 deletions
+7
View File
@@ -191,6 +191,13 @@ func (t *Bot) handleStart(ctx context.Context, api *tgbot.Bot, update *models.Up
if update.Message == nil {
return
}
// Reply only in a private chat: the Mini App launch button is an inline web_app
// button, which Telegram permits only in private chats — replying to a group message
// (the bot is an admin in the moderated chat and now receives its messages) fails with
// BUTTON_TYPE_INVALID. In the group the bot only manages permissions, it never chats.
if update.Message.Chat.Type != models.ChatTypePrivate {
return
}
startParam := startPayload(update.Message.Text)
if _, err := api.SendMessage(ctx, &tgbot.SendMessageParams{
ChatID: update.Message.Chat.ID,
@@ -8,6 +8,7 @@ import (
"strings"
"testing"
"github.com/go-telegram/bot/models"
"go.uber.org/zap"
)
@@ -103,6 +104,29 @@ func TestTestEnvironmentRoutesGetMe(t *testing.T) {
}
}
func TestHandleStartRepliesPrivateOnly(t *testing.T) {
t.Run("private replies", func(t *testing.T) {
api := &fakeBotAPI{}
b := newTestBot(t, api)
b.handleStart(context.Background(), b.api, &models.Update{Message: &models.Message{
Chat: models.Chat{ID: 42, Type: models.ChatTypePrivate}, Text: "/start g7",
}})
if api.chatID != "42" || !strings.Contains(api.replyMarkup, "web_app") {
t.Errorf("private /start: chat=%q markup=%q, want a web_app reply", api.chatID, api.replyMarkup)
}
})
t.Run("group ignored", func(t *testing.T) {
api := &fakeBotAPI{}
b := newTestBot(t, api)
b.handleStart(context.Background(), b.api, &models.Update{Message: &models.Message{
Chat: models.Chat{ID: -100, Type: models.ChatTypeSupergroup}, Text: "/start",
}})
if api.chatID != "" {
t.Errorf("group /start got a reply (chat=%q); an inline web_app button is invalid in groups", api.chatID)
}
})
}
func TestStartPayload(t *testing.T) {
cases := map[string]string{
"/start g123": "g123",
@@ -92,6 +92,11 @@ func (t *Bot) handleStart(ctx context.Context, api *tgbot.Bot, update *models.Up
if update.Message == nil {
return
}
// Only respond to a private /start: the promo bot is a one-on-one onboarding entry
// point and should never reply to group messages.
if update.Message.Chat.Type != models.ChatTypePrivate {
return
}
if err := t.throttle(ctx); err != nil {
return
}
@@ -85,7 +85,7 @@ func TestHandleStartReplies(t *testing.T) {
}
b.handleStart(context.Background(), b.api, &models.Update{Message: &models.Message{
Chat: models.Chat{ID: 42},
Chat: models.Chat{ID: 42, Type: models.ChatTypePrivate},
From: &models.User{LanguageCode: "ru"},
Text: "/start f99",
}})
@@ -103,3 +103,19 @@ func TestHandleStartReplies(t *testing.T) {
t.Errorf("reply_markup = %q, want startapp=f99 (the /start payload forwarded)", api.replyMarkup)
}
}
func TestHandleStartIgnoresGroup(t *testing.T) {
api := &fakeAPI{}
srv := httptest.NewServer(api)
t.Cleanup(srv.Close)
b, err := New(Config{Token: "1:2", APIBaseURL: srv.URL, BotUsername: "B", BotLinkURL: "https://t.me/b/a"}, zap.NewNop())
if err != nil {
t.Fatalf("new: %v", err)
}
b.handleStart(context.Background(), b.api, &models.Update{Message: &models.Message{
Chat: models.Chat{ID: -100, Type: models.ChatTypeSupergroup}, Text: "/start",
}})
if api.chatID != "" {
t.Errorf("replied to a group message (chat=%q); want none", api.chatID)
}
}