fix(telegram): reply to /start only in private chats
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) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
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) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
The main bot is now an admin in the moderated discussion group and receives its messages (allowed_updates includes message). Its default handler replied to every message with a Mini App launch button — an inline web_app button, which Telegram permits only in private chats — so replying in the group failed with BUTTON_TYPE_INVALID (silently: the send fails, no user-facing error). Reply only in a private chat; in the group the bot only manages permissions. The promo bot gets the same guard.
This commit is contained in:
@@ -191,6 +191,13 @@ func (t *Bot) handleStart(ctx context.Context, api *tgbot.Bot, update *models.Up
|
|||||||
if update.Message == nil {
|
if update.Message == nil {
|
||||||
return
|
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)
|
startParam := startPayload(update.Message.Text)
|
||||||
if _, err := api.SendMessage(ctx, &tgbot.SendMessageParams{
|
if _, err := api.SendMessage(ctx, &tgbot.SendMessageParams{
|
||||||
ChatID: update.Message.Chat.ID,
|
ChatID: update.Message.Chat.ID,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
"go.uber.org/zap"
|
"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) {
|
func TestStartPayload(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
"/start g123": "g123",
|
"/start g123": "g123",
|
||||||
|
|||||||
@@ -92,6 +92,11 @@ func (t *Bot) handleStart(ctx context.Context, api *tgbot.Bot, update *models.Up
|
|||||||
if update.Message == nil {
|
if update.Message == nil {
|
||||||
return
|
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 {
|
if err := t.throttle(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func TestHandleStartReplies(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.handleStart(context.Background(), b.api, &models.Update{Message: &models.Message{
|
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"},
|
From: &models.User{LanguageCode: "ru"},
|
||||||
Text: "/start f99",
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user