Skip to content

Commit 6c70ff1

Browse files
ernadoclaude
andcommitted
feat(handler): add handler Groups with scoped middleware
Routes now carry their own middleware, applied inside the global middleware. Group bundles shared predicates and middleware: handlers registered via a group inherit its predicates as additional guards and its middleware scope. Adds Group.Use/Handle/OnMessage/OnCallbackQuery/OnCommand. Shared kind predicates are factored out and reused by Bot.On* and Group.On*. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0344a80 commit 6c70ff1

4 files changed

Lines changed: 122 additions & 6 deletions

File tree

group.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package botapi
2+
3+
// Group is a set of handlers that share predicates and middleware. Every handler
4+
// registered through the group inherits the group's predicates (as additional
5+
// guards) and its middleware (applied inside the global middleware).
6+
//
7+
// Create one with Bot.Group:
8+
//
9+
// admin := bot.Group(ChatTypeIs(ChatTypeSupergroup))
10+
// admin.Use(Recover())
11+
// admin.OnCommand("ban", banHandler)
12+
type Group struct {
13+
bot *Bot
14+
preds []Predicate
15+
mws []Middleware
16+
}
17+
18+
// Group returns a new handler group guarded by the given predicates.
19+
func (b *Bot) Group(predicates ...Predicate) *Group {
20+
return &Group{bot: b, preds: predicates}
21+
}
22+
23+
// Use adds middleware applied to every handler registered through this group.
24+
// Returns the group for chaining.
25+
func (g *Group) Use(mws ...Middleware) *Group {
26+
g.mws = append(g.mws, mws...)
27+
return g
28+
}
29+
30+
// Handle registers a handler in the group with optional extra predicates.
31+
func (g *Group) Handle(h Handler, predicates ...Predicate) {
32+
preds := make([]Predicate, 0, len(g.preds)+len(predicates))
33+
preds = append(preds, g.preds...)
34+
preds = append(preds, predicates...)
35+
g.bot.onWith(h, g.mws, preds)
36+
}
37+
38+
// OnMessage registers a message handler in the group.
39+
func (g *Group) OnMessage(h Handler, predicates ...Predicate) {
40+
g.Handle(h, prepend(hasMessage, predicates)...)
41+
}
42+
43+
// OnCallbackQuery registers a callback-query handler in the group.
44+
func (g *Group) OnCallbackQuery(h Handler, predicates ...Predicate) {
45+
g.Handle(h, prepend(hasCallbackQuery, predicates)...)
46+
}
47+
48+
// OnCommand registers a command handler in the group.
49+
func (g *Group) OnCommand(name string, h Handler, predicates ...Predicate) {
50+
g.OnMessage(h, prepend(Command(name), predicates)...)
51+
}

group_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"testing"
6+
)
7+
8+
func TestGroupPredicatesGuardHandlers(t *testing.T) {
9+
b := newTestBot(t)
10+
var fired bool
11+
g := b.Group(ChatTypeIs(ChatTypeSupergroup))
12+
g.OnMessage(func(c *Context) error { fired = true; return nil })
13+
14+
// Wrong chat type: group predicate fails, handler must not fire.
15+
b.route(context.Background(), &Update{Message: &Message{Chat: Chat{Type: ChatTypePrivate}}})
16+
if fired {
17+
t.Fatal("group predicate should have blocked the handler")
18+
}
19+
20+
// Right chat type: handler fires.
21+
b.route(context.Background(), &Update{Message: &Message{Chat: Chat{Type: ChatTypeSupergroup}}})
22+
if !fired {
23+
t.Fatal("handler should fire when group predicate matches")
24+
}
25+
}
26+
27+
func TestGroupMiddlewareScopedAndOrdered(t *testing.T) {
28+
b := newTestBot(t)
29+
var order []string
30+
b.Use(func(next Handler) Handler {
31+
return func(c *Context) error { order = append(order, "global"); return next(c) }
32+
})
33+
g := b.Group()
34+
g.Use(func(next Handler) Handler {
35+
return func(c *Context) error { order = append(order, "group"); return next(c) }
36+
})
37+
g.OnMessage(func(c *Context) error { order = append(order, "handler"); return nil })
38+
39+
b.route(context.Background(), &Update{Message: &Message{}})
40+
41+
want := []string{"global", "group", "handler"}
42+
for i := range want {
43+
if i >= len(order) || order[i] != want[i] {
44+
t.Fatalf("order = %v, want %v (global outermost, group inside)", order, want)
45+
}
46+
}
47+
}

handler.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Middleware func(next Handler) Handler
3030
type route struct {
3131
handler Handler
3232
predicates []Predicate
33+
mws []Middleware
3334
}
3435

3536
func (r route) matches(u *Update) bool {
@@ -59,9 +60,15 @@ func (b *Bot) Use(mws ...Middleware) {
5960

6061
// on registers a handler guarded by the given predicates.
6162
func (b *Bot) on(handler Handler, predicates ...Predicate) {
63+
b.onWith(handler, nil, predicates)
64+
}
65+
66+
// onWith registers a handler with route-scoped middleware (applied inside the
67+
// global middleware) and predicates.
68+
func (b *Bot) onWith(handler Handler, mws []Middleware, predicates []Predicate) {
6269
b.router.mu.Lock()
6370
defer b.router.mu.Unlock()
64-
b.router.routes = append(b.router.routes, route{handler: handler, predicates: predicates})
71+
b.router.routes = append(b.router.routes, route{handler: handler, predicates: predicates, mws: mws})
6572
}
6673

6774
// route dispatches an update to the first matching handler, wrapped in the
@@ -78,6 +85,9 @@ func (b *Bot) route(ctx context.Context, u *Update) {
7885
continue
7986
}
8087
h := r.handler
88+
for i := len(r.mws) - 1; i >= 0; i-- {
89+
h = r.mws[i](h)
90+
}
8191
for i := len(mws) - 1; i >= 0; i-- {
8292
h = mws[i](h)
8393
}

on.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,29 +64,37 @@ func (b *Bot) dispatchMessage(ctx context.Context, msg tg.MessageClass, edited b
6464
b.route(ctx, u)
6565
}
6666

67+
// Kind predicates select an update by which field it carries. They are shared
68+
// by Bot.On* and Group.On*.
69+
func hasMessage(u *Update) bool { return u.Message != nil }
70+
func hasEditedMessage(u *Update) bool { return u.EditedMessage != nil }
71+
func hasChannelPost(u *Update) bool { return u.ChannelPost != nil }
72+
func hasCallbackQuery(u *Update) bool { return u.CallbackQuery != nil }
73+
func hasInlineQuery(u *Update) bool { return u.InlineQuery != nil }
74+
6775
// OnMessage registers a handler for new messages matching the predicates.
6876
func (b *Bot) OnMessage(h Handler, predicates ...Predicate) {
69-
b.on(h, prepend(func(u *Update) bool { return u.Message != nil }, predicates)...)
77+
b.on(h, prepend(hasMessage, predicates)...)
7078
}
7179

7280
// OnEditedMessage registers a handler for edited messages.
7381
func (b *Bot) OnEditedMessage(h Handler, predicates ...Predicate) {
74-
b.on(h, prepend(func(u *Update) bool { return u.EditedMessage != nil }, predicates)...)
82+
b.on(h, prepend(hasEditedMessage, predicates)...)
7583
}
7684

7785
// OnChannelPost registers a handler for new channel posts.
7886
func (b *Bot) OnChannelPost(h Handler, predicates ...Predicate) {
79-
b.on(h, prepend(func(u *Update) bool { return u.ChannelPost != nil }, predicates)...)
87+
b.on(h, prepend(hasChannelPost, predicates)...)
8088
}
8189

8290
// OnCallbackQuery registers a handler for callback queries from inline keyboards.
8391
func (b *Bot) OnCallbackQuery(h Handler, predicates ...Predicate) {
84-
b.on(h, prepend(func(u *Update) bool { return u.CallbackQuery != nil }, predicates)...)
92+
b.on(h, prepend(hasCallbackQuery, predicates)...)
8593
}
8694

8795
// OnInlineQuery registers a handler for inline queries.
8896
func (b *Bot) OnInlineQuery(h Handler, predicates ...Predicate) {
89-
b.on(h, prepend(func(u *Update) bool { return u.InlineQuery != nil }, predicates)...)
97+
b.on(h, prepend(hasInlineQuery, predicates)...)
9098
}
9199

92100
// prepend returns p followed by rest, without mutating rest.

0 commit comments

Comments
 (0)