fix(telegram): allow group slash commands through mention filter

This commit is contained in:
mingmxren 2026-03-01 16:34:30 +08:00
parent 8d02e92e31
commit 15a1e7afdf
2 changed files with 215 additions and 28 deletions

View file

@ -41,14 +41,14 @@ var (
type TelegramChannel struct { type TelegramChannel struct {
*channels.BaseChannel *channels.BaseChannel
bot *telego.Bot bot *telego.Bot
bh *telegohandler.BotHandler bh *telegohandler.BotHandler
commands TelegramCommander commands TelegramCommander
dispatcher commands.Dispatching dispatcher commands.Dispatching
config *config.Config config *config.Config
chatIDs map[string]int64 chatIDs map[string]int64
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
registerFunc func(context.Context, []commands.Definition) error registerFunc func(context.Context, []commands.Definition) error
commandRegCancel context.CancelFunc commandRegCancel context.CancelFunc
@ -659,34 +659,34 @@ func escapeHTML(text string) string {
// isBotMentioned checks if the bot is mentioned in the message via entities. // isBotMentioned checks if the bot is mentioned in the message via entities.
func (c *TelegramChannel) isBotMentioned(message *telego.Message) bool { func (c *TelegramChannel) isBotMentioned(message *telego.Message) bool {
botUsername := c.bot.Username() text, entities := telegramEntityTextAndList(message)
if botUsername == "" { if text == "" || len(entities) == 0 {
return false return false
} }
entities := message.Entities botUsername := ""
if entities == nil { if c.bot != nil {
entities = message.CaptionEntities botUsername = c.bot.Username()
} }
runes := []rune(text)
for _, entity := range entities { for _, entity := range entities {
if entity.Type == "mention" { entityText, ok := telegramEntityText(runes, entity)
// Extract the mention text from the message if !ok {
text := message.Text continue
if text == "" {
text = message.Caption
}
runes := []rune(text)
end := entity.Offset + entity.Length
if end <= len(runes) {
mention := string(runes[entity.Offset:end])
if strings.EqualFold(mention, "@"+botUsername) {
return true
}
}
} }
if entity.Type == "text_mention" && entity.User != nil {
if entity.User.Username == botUsername { switch entity.Type {
case telego.EntityTypeMention:
if botUsername != "" && strings.EqualFold(entityText, "@"+botUsername) {
return true
}
case telego.EntityTypeTextMention:
if botUsername != "" && entity.User != nil && strings.EqualFold(entity.User.Username, botUsername) {
return true
}
case telego.EntityTypeBotCommand:
if isBotCommandEntityForThisBot(entityText, botUsername) {
return true return true
} }
} }
@ -694,6 +694,46 @@ func (c *TelegramChannel) isBotMentioned(message *telego.Message) bool {
return false return false
} }
func telegramEntityTextAndList(message *telego.Message) (string, []telego.MessageEntity) {
if message.Text != "" {
return message.Text, message.Entities
}
return message.Caption, message.CaptionEntities
}
func telegramEntityText(runes []rune, entity telego.MessageEntity) (string, bool) {
if entity.Offset < 0 || entity.Length <= 0 {
return "", false
}
end := entity.Offset + entity.Length
if entity.Offset >= len(runes) || end > len(runes) {
return "", false
}
return string(runes[entity.Offset:end]), true
}
func isBotCommandEntityForThisBot(entityText, botUsername string) bool {
if !strings.HasPrefix(entityText, "/") {
return false
}
command := strings.TrimPrefix(entityText, "/")
if command == "" {
return false
}
at := strings.IndexRune(command, '@')
if at == -1 {
// A bare /command delivered to this bot is intended for this bot.
return true
}
mentionUsername := command[at+1:]
if mentionUsername == "" || botUsername == "" {
return false
}
return strings.EqualFold(mentionUsername, botUsername)
}
// stripBotMention removes the @bot mention from the content. // stripBotMention removes the @bot mention from the content.
func (c *TelegramChannel) stripBotMention(content string) string { func (c *TelegramChannel) stripBotMention(content string) string {
botUsername := c.bot.Username() botUsername := c.bot.Username()

View file

@ -0,0 +1,147 @@
package telegram
import (
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/mymmrac/telego"
ta "github.com/mymmrac/telego/telegoapi"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/config"
)
type getMeCaller struct {
username string
}
func (c getMeCaller) Call(_ context.Context, url string, _ *ta.RequestData) (*ta.Response, error) {
if strings.HasSuffix(url, "/getMe") {
result := fmt.Sprintf(`{"id":1,"is_bot":true,"first_name":"bot","username":%q}`, c.username)
return &ta.Response{Ok: true, Result: []byte(result)}, nil
}
return &ta.Response{Ok: true, Result: []byte("true")}, nil
}
func newTestTelegramBot(t *testing.T, username string) *telego.Bot {
t.Helper()
token := "123456:" + strings.Repeat("a", 35)
bot, err := telego.NewBot(token,
telego.WithAPICaller(getMeCaller{username: username}),
telego.WithDiscardLogger(),
)
if err != nil {
t.Fatalf("NewBot error: %v", err)
}
return bot
}
func newGroupMentionOnlyChannel(t *testing.T, botUsername string) (*TelegramChannel, *bus.MessageBus) {
t.Helper()
messageBus := bus.NewMessageBus()
ch := &TelegramChannel{
BaseChannel: channels.NewBaseChannel("telegram", nil, messageBus, nil,
channels.WithGroupTrigger(config.GroupTriggerConfig{MentionOnly: true}),
),
bot: newTestTelegramBot(t, botUsername),
chatIDs: make(map[string]int64),
ctx: context.Background(),
}
return ch, messageBus
}
func TestHandleMessage_GroupMentionOnly_BotCommandEntity(t *testing.T) {
tests := []struct {
name string
text string
wantForwarded bool
wantContent string
}{
{
name: "command with bot username",
text: "/new@testbot",
wantForwarded: true,
wantContent: "/new",
},
{
name: "bare command",
text: "/new",
wantForwarded: true,
wantContent: "/new",
},
{
name: "command for another bot",
text: "/new@otherbot",
wantForwarded: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ch, messageBus := newGroupMentionOnlyChannel(t, "testbot")
msg := &telego.Message{
Text: tc.text,
Entities: []telego.MessageEntity{{
Type: telego.EntityTypeBotCommand,
Offset: 0,
Length: len([]rune(tc.text)),
}},
MessageID: 42,
Chat: telego.Chat{
ID: 123,
Type: "group",
},
From: &telego.User{
ID: 7,
FirstName: "Alice",
},
}
if err := ch.handleMessage(context.Background(), msg); err != nil {
t.Fatalf("handleMessage error: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
inbound, ok := messageBus.ConsumeInbound(ctx)
if tc.wantForwarded {
if !ok {
t.Fatal("expected inbound message to be forwarded")
}
if inbound.Content != tc.wantContent {
t.Fatalf("content=%q want=%q", inbound.Content, tc.wantContent)
}
return
}
if ok {
t.Fatalf("expected message to be filtered, got content=%q", inbound.Content)
}
})
}
}
func TestIsBotMentioned_MentionEntityUnaffected(t *testing.T) {
ch, _ := newGroupMentionOnlyChannel(t, "testbot")
msg := &telego.Message{
Text: "@testbot hello",
Entities: []telego.MessageEntity{{
Type: telego.EntityTypeMention,
Offset: 0,
Length: len("@testbot"),
}},
}
if !ch.isBotMentioned(msg) {
t.Fatal("expected mention entity to be treated as bot mention")
}
}