fix: match bot commands addressed with @botusername in groups
In group chats Telegram appends the bot addressee to the bot_command entity (/sync@hs_dyzur_bot), and the library's MatchTypeCommand compares the whole token verbatim, so commands never matched. Replace it with custom MatchFuncs that read the bot_command entity span and strip any @suffix before comparing the command name.
This commit is contained in:
parent
816aa45c86
commit
825331dce5
51
telegram.go
51
telegram.go
|
|
@ -14,9 +14,8 @@ import (
|
|||
|
||||
// Command names, the single source of truth shared by the handler registration
|
||||
// and the published command list. Underscore, not hyphen: Telegram command
|
||||
// names are [a-zA-Z0-9_] only, and MatchTypeCommand matches the parsed
|
||||
// bot_command entity, which would stop at a hyphen (/kto-sprzata -> entity
|
||||
// "/kto") and never match.
|
||||
// names are [a-zA-Z0-9_] only, and the bot_command entity we match against stops
|
||||
// at a hyphen (/kto-sprzata -> entity "/kto"), so a hyphen would never match.
|
||||
const (
|
||||
cmdKtoSprzata = "kto_sprzata"
|
||||
cmdSync = "sync"
|
||||
|
|
@ -35,12 +34,48 @@ var botCommands = []models.BotCommand{
|
|||
// than silently 401-ing inside the poll loop. Long polling starts on Start(ctx).
|
||||
func NewBot(token string, svc *Service) (*bot.Bot, error) {
|
||||
h := &botHandlers{svc: svc, now: time.Now}
|
||||
opts := []bot.Option{
|
||||
bot.WithDefaultHandler(h.unknown),
|
||||
bot.WithMessageTextHandler(cmdKtoSprzata, bot.MatchTypeCommand, h.ktoSprzata),
|
||||
bot.WithMessageTextHandler(cmdSync, bot.MatchTypeCommand, h.sync),
|
||||
b, err := bot.New(token, bot.WithDefaultHandler(h.unknown))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bot.New(token, opts...)
|
||||
// Custom match funcs instead of bot.MatchTypeCommand: in group chats Telegram
|
||||
// appends the addressee (/sync@hs_dyzur_bot) inside the bot_command entity,
|
||||
// and the library's MatchTypeCommand compares the whole token verbatim, so it
|
||||
// never matches. matchCommandEntity strips the @suffix first.
|
||||
b.RegisterHandlerMatchFunc(h.matchCommand(cmdKtoSprzata), h.ktoSprzata)
|
||||
b.RegisterHandlerMatchFunc(h.matchCommand(cmdSync), h.sync)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// matchCommand builds a MatchFunc that fires when the message leads with the
|
||||
// given bot command, tolerant of the @botusername suffix Telegram adds in groups.
|
||||
func (h *botHandlers) matchCommand(cmd string) bot.MatchFunc {
|
||||
return func(update *models.Update) bool {
|
||||
return update.Message != nil &&
|
||||
matchCommandEntity(update.Message.Text, update.Message.Entities, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// matchCommandEntity reports whether text carries a bot_command entity whose
|
||||
// name equals cmd. The name is taken from the entity span (dropping the leading
|
||||
// '/') with any @botusername suffix removed — Telegram includes that suffix in
|
||||
// the entity for group messages, but a bot still owns the bare command. Note: a
|
||||
// command explicitly addressed to another bot (/sync@otherbot) also matches;
|
||||
// acceptable for a single-bot group, the only place this bot runs.
|
||||
func matchCommandEntity(text string, entities []models.MessageEntity, cmd string) bool {
|
||||
for _, e := range entities {
|
||||
if e.Type != models.MessageEntityTypeBotCommand {
|
||||
continue
|
||||
}
|
||||
name := text[e.Offset+1 : e.Offset+e.Length]
|
||||
if at := strings.IndexByte(name, '@'); at >= 0 {
|
||||
name = name[:at]
|
||||
}
|
||||
if name == cmd {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BotSendFunc builds a SendFunc that posts to a fixed chat via the bot, so the
|
||||
|
|
|
|||
|
|
@ -3,8 +3,49 @@ package dyzurbot
|
|||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-telegram/bot/models"
|
||||
)
|
||||
|
||||
// cmdEntity builds the bot_command entity Telegram attaches to a leading
|
||||
// command, spanning the whole token (slash, name, and any @addressee).
|
||||
func cmdEntity(text string) []models.MessageEntity {
|
||||
return []models.MessageEntity{{
|
||||
Type: models.MessageEntityTypeBotCommand,
|
||||
Offset: 0,
|
||||
Length: len(text),
|
||||
}}
|
||||
}
|
||||
|
||||
func TestMatchCommandEntity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
cmd string
|
||||
want bool
|
||||
}{
|
||||
{"bare command (private chat)", "/sync", cmdSync, true},
|
||||
{"command with @botusername (group)", "/sync@hs_dyzur_bot", cmdSync, true},
|
||||
{"kto_sprzata with @botusername", "/kto_sprzata@hs_dyzur_bot", cmdKtoSprzata, true},
|
||||
{"underscore name survives @suffix", "/kto_sprzata@hs_dyzur_bot", cmdSync, false},
|
||||
{"wrong command", "/sync", cmdKtoSprzata, false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := matchCommandEntity(tc.text, cmdEntity(tc.text), tc.cmd); got != tc.want {
|
||||
t.Errorf("matchCommandEntity(%q, %q) = %v, want %v", tc.text, tc.cmd, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A plain message carrying no bot_command entity must not match any command.
|
||||
func TestMatchCommandEntityNoEntity(t *testing.T) {
|
||||
if matchCommandEntity("just chatting about /sync", nil, cmdSync) {
|
||||
t.Error("text without a bot_command entity should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// week is a small helper to build a DutyWeek from a YYYY-MM-DD start.
|
||||
func week(t *testing.T, start, p1, p2 string) DutyWeek {
|
||||
t.Helper()
|
||||
|
|
|
|||
Loading…
Reference in New Issue