mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-18 16:14:29 +00:00
fix(reply): forward message_thread_id so replies stay in the same forum topic
Telegram routes outgoing messages with no message_thread_id to a forum supergroup's General topic. Commands sent in a topic were being answered in General. chathelper.Reply / ReplyHTML now take *models.Message and forward both ChatID and MessageThreadID; help.go SendMessage and loldle trySendSticker do the same. Adds regression tests asserting the field is forwarded for forum topics and omitted for private/regular groups.
This commit is contained in:
1 parent
82825661e3
commit
16c9cce91d
11 files changed
+247
-122
No files matched your search
@@ -54,18 +54,35 @@ func ArgAfterCommand(text string) string {
|
||||
// NowMillis returns current UTC ms-since-epoch.
|
||||
func NowMillis() int64 { return time.Now().UTC().UnixMilli() }
|
||||
|
||||
// Reply sends a plain-text response to the given chat.
|
||||
func Reply(ctx context.Context, b *bot.Bot, chatID int64, text string) error {
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text})
|
||||
// Reply sends a plain-text response to the chat the inbound message came from.
|
||||
//
|
||||
// Forwards MessageThreadID so replies in a forum-supergroup topic stay in the
|
||||
// same topic. Telegram routes outgoing messages with an absent/zero
|
||||
// message_thread_id to the General topic — that mis-routing is the precise
|
||||
// reason this helper takes the whole message instead of just a chat ID.
|
||||
func Reply(ctx context.Context, b *bot.Bot, msg *models.Message, text string) error {
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: msg.Chat.ID,
|
||||
MessageThreadID: msg.MessageThreadID,
|
||||
Text: text,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// ReplyHTML sends a Telegram HTML-formatted response to the given chat.
|
||||
func ReplyHTML(ctx context.Context, b *bot.Bot, chatID int64, text string) error {
|
||||
// ReplyHTML sends a Telegram HTML-formatted response to the chat the inbound
|
||||
// message came from. Forwards MessageThreadID — see Reply for rationale.
|
||||
func ReplyHTML(ctx context.Context, b *bot.Bot, msg *models.Message, text string) error {
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: chatID,
|
||||
Text: text,
|
||||
ParseMode: models.ParseModeHTML,
|
||||
ChatID: msg.Chat.ID,
|
||||
MessageThreadID: msg.MessageThreadID,
|
||||
Text: text,
|
||||
ParseMode: models.ParseModeHTML,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package chathelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot/internal/testutil"
|
||||
)
|
||||
|
||||
func TestSubjectFor(t *testing.T) {
|
||||
@@ -107,6 +110,101 @@ func TestNowMillis(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestReply_ForwardsMessageThreadID locks in the forum-topic fix: when an
|
||||
// inbound command arrives in a forum-supergroup topic, the reply must carry
|
||||
// the same message_thread_id so Telegram posts it back to that topic. Without
|
||||
// this, Telegram routes the reply to the General topic — the bug this whole
|
||||
// signature change exists to prevent.
|
||||
func TestReply_ForwardsMessageThreadID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
msg *models.Message
|
||||
wantChat string
|
||||
wantThread string // "" means: field must be absent from form
|
||||
}{
|
||||
{
|
||||
name: "forum topic — thread id forwarded",
|
||||
msg: &models.Message{
|
||||
Chat: models.Chat{ID: -1001234, Type: models.ChatTypeSupergroup, IsForum: true},
|
||||
MessageThreadID: 42,
|
||||
Text: "/cmd",
|
||||
},
|
||||
wantChat: "-1001234",
|
||||
wantThread: "42",
|
||||
},
|
||||
{
|
||||
name: "private chat — no thread id sent",
|
||||
msg: &models.Message{
|
||||
Chat: models.Chat{ID: 999, Type: models.ChatTypePrivate},
|
||||
Text: "/cmd",
|
||||
},
|
||||
wantChat: "999",
|
||||
wantThread: "",
|
||||
},
|
||||
{
|
||||
name: "regular group (no topics) — no thread id sent",
|
||||
msg: &models.Message{
|
||||
Chat: models.Chat{ID: -100, Type: models.ChatTypeGroup},
|
||||
Text: "/cmd",
|
||||
},
|
||||
wantChat: "-100",
|
||||
wantThread: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rb := testutil.NewRecordingBot(t)
|
||||
if err := Reply(context.Background(), rb.Bot, tt.msg, "hi"); err != nil {
|
||||
t.Fatalf("Reply: %v", err)
|
||||
}
|
||||
got := rb.LastSent()
|
||||
if got.Method != "sendMessage" {
|
||||
t.Fatalf("method: got %q, want sendMessage", got.Method)
|
||||
}
|
||||
if got.ChatID() != tt.wantChat {
|
||||
t.Errorf("chat_id: got %q, want %q", got.ChatID(), tt.wantChat)
|
||||
}
|
||||
gotThread := got.Form["message_thread_id"]
|
||||
if gotThread != tt.wantThread {
|
||||
t.Errorf("message_thread_id: got %q, want %q", gotThread, tt.wantThread)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestReplyHTML_ForwardsMessageThreadID is the HTML-mode counterpart of the
|
||||
// plain Reply test; same invariant, same reason.
|
||||
func TestReplyHTML_ForwardsMessageThreadID(t *testing.T) {
|
||||
rb := testutil.NewRecordingBot(t)
|
||||
msg := &models.Message{
|
||||
Chat: models.Chat{ID: -1009999, Type: models.ChatTypeSupergroup, IsForum: true},
|
||||
MessageThreadID: 7,
|
||||
}
|
||||
if err := ReplyHTML(context.Background(), rb.Bot, msg, "<b>hi</b>"); err != nil {
|
||||
t.Fatalf("ReplyHTML: %v", err)
|
||||
}
|
||||
got := rb.LastSent()
|
||||
if got.Form["message_thread_id"] != "7" {
|
||||
t.Errorf("message_thread_id: got %q, want %q", got.Form["message_thread_id"], "7")
|
||||
}
|
||||
if got.Form["parse_mode"] != "HTML" {
|
||||
t.Errorf("parse_mode: got %q, want %q", got.Form["parse_mode"], "HTML")
|
||||
}
|
||||
}
|
||||
|
||||
// TestReply_NilMessage is a defensive check — handlers occasionally inherit
|
||||
// updates without a Message (channel posts routed through future code paths),
|
||||
// and Reply must no-op rather than panic.
|
||||
func TestReply_NilMessage(t *testing.T) {
|
||||
rb := testutil.NewRecordingBot(t)
|
||||
if err := Reply(context.Background(), rb.Bot, nil, "ignored"); err != nil {
|
||||
t.Fatalf("Reply(nil): %v", err)
|
||||
}
|
||||
if n := len(rb.Sent()); n != 0 {
|
||||
t.Errorf("Reply(nil) sent %d calls; want 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWinRate(t *testing.T) {
|
||||
tests := []struct {
|
||||
wins, played, want int
|
||||
|
||||
@@ -101,6 +101,7 @@ func helpCommand(reg *modules.Registry) modules.Command {
|
||||
text := RenderHelp(reg)
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: update.Message.Chat.ID,
|
||||
MessageThreadID: update.Message.MessageThreadID,
|
||||
Text: text,
|
||||
ParseMode: models.ParseModeHTML,
|
||||
LinkPreviewOptions: &models.LinkPreviewOptions{IsDisabled: bot.True()},
|
||||
|
||||
@@ -37,7 +37,7 @@ func infoCommand() modules.Command {
|
||||
senderID = fmt.Sprintf("%d", msg.From.ID)
|
||||
}
|
||||
text := fmt.Sprintf("chat id: %s\nthread id: %s\nsender id: %s", chatID, threadID, senderID)
|
||||
return chathelper.Reply(ctx, b, msg.Chat.ID, text)
|
||||
return chathelper.Reply(ctx, b, msg, text)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func stickerIDCommand() modules.Command {
|
||||
|
||||
sticker := stickerFrom(msg)
|
||||
if sticker == nil {
|
||||
return chathelper.Reply(ctx, b, msg.Chat.ID, stickerIDUsage)
|
||||
return chathelper.Reply(ctx, b, msg, stickerIDUsage)
|
||||
}
|
||||
|
||||
setName := sticker.SetName
|
||||
@@ -52,7 +52,7 @@ func stickerIDCommand() modules.Command {
|
||||
fmt.Fprintf(&sb, "set: %s · emoji: %s",
|
||||
html.EscapeString(setName), html.EscapeString(emoji))
|
||||
|
||||
return chathelper.ReplyHTML(ctx, b, msg.Chat.ID, sb.String())
|
||||
return chathelper.ReplyHTML(ctx, b, msg, sb.String())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user