Skip to content

Commit e55ed9b

Browse files
ernadoclaude
andcommitted
fix(handler): ignore commands targeted at another bot
In groups, Telegram clients append @BotUsername to a command ("/subscribe@other_bot"). The Command predicate ignored the suffix entirely, so a bot reacted to commands aimed at any bot. Match an untargeted command, or one whose @username is this bot's own (case-insensitive); the router now threads the bot's username onto the Update for the check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0761031 commit e55ed9b

12 files changed

Lines changed: 73 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ messages follow [Conventional Commits](https://www.conventionalcommits.org/).
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- **Targeted commands** — a command addressed to another bot (`/cmd@other_bot`,
13+
as clients send in groups) no longer matches. `Command`/`OnCommand` now match
14+
an untargeted command, or one whose `@username` is this bot's own.
15+
1016
### Changed
1117

1218
- **Logging port** — the library now logs through `github.com/gotd/log` instead

examples/advanced/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func main() {
4141
}
4242

4343
// Persist session, peers and update state so the bot resumes across restarts.
44-
store, err := storage.Open("advanced-session.bbolt")
44+
store, err := storage.Open("session.bbolt")
4545
if err != nil {
4646
log.Fatal("Open storage", zap.Error(err))
4747
}

examples/background/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func main() {
4848
}
4949

5050
// Persist session, peers and update state so the bot resumes across restarts.
51-
sess, err := storage.Open("background-session.bbolt")
51+
sess, err := storage.Open("session.bbolt")
5252
if err != nil {
5353
log.Fatal("Open storage", zap.Error(err))
5454
}

examples/buttons/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func main() {
4646
}
4747

4848
// Persist session, peers and update state so the bot resumes across restarts.
49-
store, err := storage.Open("buttons-session.bbolt")
49+
store, err := storage.Open("session.bbolt")
5050
if err != nil {
5151
log.Fatal("Open storage", zap.Error(err))
5252
}

examples/echo/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func main() {
3131
}
3232

3333
// Persist session, peers and update state so the bot resumes across restarts.
34-
store, err := storage.Open("echo-session.bbolt")
34+
store, err := storage.Open("session.bbolt")
3535
if err != nil {
3636
log.Fatal("Open storage", zap.Error(err))
3737
}

examples/inline/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func main() {
3232
}
3333

3434
// Persist session, peers and update state so the bot resumes across restarts.
35-
store, err := storage.Open("inline-session.bbolt")
35+
store, err := storage.Open("session.bbolt")
3636
if err != nil {
3737
log.Fatal("Open storage", zap.Error(err))
3838
}

examples/media/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func main() {
4040
}
4141

4242
// Persist session, peers and update state so the bot resumes across restarts.
43-
store, err := storage.Open("media-session.bbolt")
43+
store, err := storage.Open("session.bbolt")
4444
if err != nil {
4545
log.Fatal("Open storage", zap.Error(err))
4646
}

examples/rich/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func main() {
5353
}
5454

5555
// Persist session, peers and update state so the bot resumes across restarts.
56-
store, err := storage.Open("rich-session.bbolt")
56+
store, err := storage.Open("session.bbolt")
5757
if err != nil {
5858
log.Fatal("Open storage", zap.Error(err))
5959
}

handler.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ func (b *Bot) onWith(handler Handler, mws []Middleware, predicates []Predicate)
7575
// global middleware. Handler errors are logged, not propagated to the update
7676
// loop, so one failing handler does not tear down the bot.
7777
func (b *Bot) route(ctx context.Context, u *Update) {
78+
if b.self != nil {
79+
u.botUsername = b.self.Username
80+
}
81+
7882
b.router.mu.RLock()
7983
routes := b.router.routes
8084
mws := b.router.mws

predicates.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,34 +35,44 @@ func (u *Update) Text() string {
3535
return ""
3636
}
3737

38-
// commandName extracts the bot command name from message text: "/start@bot foo"
39-
// yields ("start", true). Pure.
40-
func commandName(text string) (string, bool) {
38+
// commandName extracts the bot command name and its optional @target from
39+
// message text: "/start@bot foo" yields ("start", "bot", true), "/start foo"
40+
// yields ("start", "", true). Pure.
41+
func commandName(text string) (name, target string, ok bool) {
4142
if !strings.HasPrefix(text, "/") {
42-
return "", false
43+
return "", "", false
4344
}
4445
field := text
4546
if i := strings.IndexAny(text, " \t\n"); i >= 0 {
4647
field = text[:i]
4748
}
4849
field = field[1:] // drop leading slash
4950
if at := strings.IndexByte(field, '@'); at >= 0 {
51+
target = field[at+1:]
5052
field = field[:at]
5153
}
52-
return field, field != ""
54+
return field, target, field != ""
5355
}
5456

5557
// Command matches a message whose first token is the given bot command (with or
56-
// without a leading slash, and ignoring a trailing @botusername).
58+
// without a leading slash).
59+
//
60+
// A command may be targeted at a specific bot with a trailing @username
61+
// ("/start@my_bot"), as Telegram clients do in groups. An untargeted command
62+
// always matches; a targeted one matches only when the @username is this bot's
63+
// own — so the bot ignores commands aimed at other bots.
5764
func Command(name string) Predicate {
5865
name = strings.TrimPrefix(name, "/")
5966
return func(u *Update) bool {
6067
m := u.EffectiveMessage()
6168
if m == nil {
6269
return false
6370
}
64-
got, ok := commandName(m.Text)
65-
return ok && got == name
71+
got, target, ok := commandName(m.Text)
72+
if !ok || got != name {
73+
return false
74+
}
75+
return target == "" || strings.EqualFold(target, u.botUsername)
6676
}
6777
}
6878

0 commit comments

Comments
 (0)