Skip to content

Commit c39b0f1

Browse files
ernadoclaude
andcommitted
feat(handler): wire dispatcher and add On* registration
installHandlers (called from New) binds the raw tg.UpdateDispatcher to the router: new/edited messages, channel posts (broadcast messages route to channel_post, supergroups to message), callback queries and inline queries are converted and routed. Conversion failures are logged, not fatal. Adds the convenience registrars OnMessage, OnEditedMessage, OnChannelPost, OnCallbackQuery and OnInlineQuery, each prepending a kind predicate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 085bea2 commit c39b0f1

3 files changed

Lines changed: 142 additions & 2 deletions

File tree

bot.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func New(token string, opt Options) (*Bot, error) {
8888
})
8989
*rawPlaceholder = *client.API()
9090

91-
return &Bot{
91+
b := &Bot{
9292
token: token,
9393
log: opt.Logger,
9494
client: client,
@@ -98,7 +98,9 @@ func New(token string, opt Options) (*Bot, error) {
9898
gaps: gaps,
9999
disp: disp,
100100
onStart: opt.OnStart,
101-
}, nil
101+
}
102+
b.installHandlers()
103+
return b, nil
102104
}
103105

104106
// Run connects, authorizes as a bot, and blocks serving updates until ctx is

on.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
6+
"github.com/gotd/td/tg"
7+
"go.uber.org/zap"
8+
)
9+
10+
// installHandlers wires the raw tg.UpdateDispatcher to the Bot API router. It is
11+
// called once from New. Update-conversion failures are logged and swallowed so
12+
// a single bad update never tears down the update stream.
13+
func (b *Bot) installHandlers() {
14+
b.disp.OnNewMessage(func(ctx context.Context, _ tg.Entities, u *tg.UpdateNewMessage) error {
15+
b.dispatchMessage(ctx, u.Message, false)
16+
return nil
17+
})
18+
b.disp.OnEditMessage(func(ctx context.Context, _ tg.Entities, u *tg.UpdateEditMessage) error {
19+
b.dispatchMessage(ctx, u.Message, true)
20+
return nil
21+
})
22+
b.disp.OnNewChannelMessage(func(ctx context.Context, _ tg.Entities, u *tg.UpdateNewChannelMessage) error {
23+
b.dispatchMessage(ctx, u.Message, false)
24+
return nil
25+
})
26+
b.disp.OnEditChannelMessage(func(ctx context.Context, _ tg.Entities, u *tg.UpdateEditChannelMessage) error {
27+
b.dispatchMessage(ctx, u.Message, true)
28+
return nil
29+
})
30+
b.disp.OnBotCallbackQuery(func(ctx context.Context, e tg.Entities, u *tg.UpdateBotCallbackQuery) error {
31+
b.route(ctx, &Update{CallbackQuery: callbackQueryFromTg(e, u)})
32+
return nil
33+
})
34+
b.disp.OnBotInlineQuery(func(ctx context.Context, e tg.Entities, u *tg.UpdateBotInlineQuery) error {
35+
b.route(ctx, &Update{InlineQuery: inlineQueryFromTg(e, u)})
36+
return nil
37+
})
38+
}
39+
40+
// dispatchMessage converts a message and routes it as the appropriate update
41+
// field. Channel-broadcast messages become channel posts; everything else is a
42+
// regular message. edited selects the edited_* fields.
43+
func (b *Bot) dispatchMessage(ctx context.Context, msg tg.MessageClass, edited bool) {
44+
m, err := b.messageFromTg(ctx, msg)
45+
if err != nil {
46+
b.log.Error("Convert message", zap.Error(err))
47+
return
48+
}
49+
if m == nil {
50+
return
51+
}
52+
53+
u := &Update{}
54+
switch {
55+
case m.Chat.Type == ChatTypeChannel && edited:
56+
u.EditedChannelPost = m
57+
case m.Chat.Type == ChatTypeChannel:
58+
u.ChannelPost = m
59+
case edited:
60+
u.EditedMessage = m
61+
default:
62+
u.Message = m
63+
}
64+
b.route(ctx, u)
65+
}
66+
67+
// OnMessage registers a handler for new messages matching the predicates.
68+
func (b *Bot) OnMessage(h Handler, predicates ...Predicate) {
69+
b.on(h, prepend(func(u *Update) bool { return u.Message != nil }, predicates)...)
70+
}
71+
72+
// OnEditedMessage registers a handler for edited messages.
73+
func (b *Bot) OnEditedMessage(h Handler, predicates ...Predicate) {
74+
b.on(h, prepend(func(u *Update) bool { return u.EditedMessage != nil }, predicates)...)
75+
}
76+
77+
// OnChannelPost registers a handler for new channel posts.
78+
func (b *Bot) OnChannelPost(h Handler, predicates ...Predicate) {
79+
b.on(h, prepend(func(u *Update) bool { return u.ChannelPost != nil }, predicates)...)
80+
}
81+
82+
// OnCallbackQuery registers a handler for callback queries from inline keyboards.
83+
func (b *Bot) OnCallbackQuery(h Handler, predicates ...Predicate) {
84+
b.on(h, prepend(func(u *Update) bool { return u.CallbackQuery != nil }, predicates)...)
85+
}
86+
87+
// OnInlineQuery registers a handler for inline queries.
88+
func (b *Bot) OnInlineQuery(h Handler, predicates ...Predicate) {
89+
b.on(h, prepend(func(u *Update) bool { return u.InlineQuery != nil }, predicates)...)
90+
}
91+
92+
// prepend returns p followed by rest, without mutating rest.
93+
func prepend(p Predicate, rest []Predicate) []Predicate {
94+
out := make([]Predicate, 0, len(rest)+1)
95+
out = append(out, p)
96+
out = append(out, rest...)
97+
return out
98+
}

on_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"testing"
6+
)
7+
8+
func TestOnRegistrationRoutesByKind(t *testing.T) {
9+
b := newTestBot(t)
10+
var got string
11+
b.OnMessage(func(c *Context) error { got = "message"; return nil })
12+
b.OnCallbackQuery(func(c *Context) error { got = "callback"; return nil })
13+
b.OnInlineQuery(func(c *Context) error { got = "inline"; return nil })
14+
15+
cases := []struct {
16+
update *Update
17+
want string
18+
}{
19+
{&Update{Message: &Message{}}, "message"},
20+
{&Update{CallbackQuery: &CallbackQuery{}}, "callback"},
21+
{&Update{InlineQuery: &InlineQuery{}}, "inline"},
22+
}
23+
for _, c := range cases {
24+
got = ""
25+
b.route(context.Background(), c.update)
26+
if got != c.want {
27+
t.Fatalf("update %+v routed to %q, want %q", c.update, got, c.want)
28+
}
29+
}
30+
}
31+
32+
func TestOnMessageDoesNotFireForCallback(t *testing.T) {
33+
b := newTestBot(t)
34+
fired := false
35+
b.OnMessage(func(c *Context) error { fired = true; return nil })
36+
b.route(context.Background(), &Update{CallbackQuery: &CallbackQuery{}})
37+
if fired {
38+
t.Fatal("OnMessage handler must not fire for a callback-only update")
39+
}
40+
}

0 commit comments

Comments
 (0)