Dispatcher & handlers
Telegram pushes updates over the same MTProto channel that carries your RPC calls. The dispatcher decodes them, routes them through your registered handlers, and runs the callbacks on a worker pool.
The update loop
When you call Connect, gogram starts a goroutine that reads service messages and updates from the wire. Service messages (acks, salts, pings) are handled internally. Updates go to the dispatcher, which matches them against registered handlers and runs the matching callbacks.
client.Idle() blocks the main goroutine until Stop is called. Put it at the end of main so the process stays alive while the dispatcher works.
Two registration styles
Every event kind is reachable two ways: the generic On registrar with a pattern string, or a typed Add*Handler helper. They register the same handler under the hood; pick by taste.
The On registrar
On takes flexible arguments. The most common shape is a pattern string, a typed handler, and any number of filters after that.
client.On("message:*", func(m *telegram.NewMessage) error {
_, err := m.Reply(m.Text())
return err
})client.On("message:*", func(m *telegram.NewMessage) error {
_, err := m.Reply(m.Text())
return err
})Commands have a shortcut: a pattern starting with / or ! is recognised as a command match:
client.On("/start", func(m *telegram.NewMessage) error {
_, err := m.Reply("welcome!")
return err
})
client.On("/help", func(m *telegram.NewMessage) error {
_, err := m.Reply("commands: /start /help /about")
return err
})client.On("/start", func(m *telegram.NewMessage) error {
_, err := m.Reply("welcome!")
return err
})
client.On("/help", func(m *telegram.NewMessage) error {
_, err := m.Reply("commands: /start /help /about")
return err
})You can skip the pattern entirely — the handler's parameter type tells the dispatcher what to route:
// Handler type alone identifies the event; no pattern needed
client.On(func(m *telegram.NewMessage) error {
// every incoming message
return nil
})
client.On(func(q *telegram.CallbackQuery) error {
return q.Answer("")
})
client.On(func(q *telegram.InlineQuery) error {
_, err := q.Builder().Answer()
return err
})// Handler type alone identifies the event; no pattern needed
client.On(func(m *telegram.NewMessage) error {
// every incoming message
return nil
})
client.On(func(q *telegram.CallbackQuery) error {
return q.Answer("")
})
client.On(func(q *telegram.InlineQuery) error {
_, err := q.Builder().Answer()
return err
})Filters are trailing arguments. They must all pass for the handler to fire:
client.On("message:*", onlyPrivateAdmins,
telegram.FilterPrivate,
telegram.Func(func(m *telegram.NewMessage) bool {
return isAdmin(m.SenderID())
}),
)client.On("message:*", onlyPrivateAdmins,
telegram.FilterPrivate,
telegram.Func(func(m *telegram.NewMessage) bool {
return isAdmin(m.SenderID())
}),
)Direct Add* helpers
If you prefer types over strings, every event kind has a direct helper. They take the typed handler and any filters, return a Handle you can use to remove the handler later.
client.AddMessageHandler("*", onMessage)
client.AddCommandHandler("start", onStart)
client.AddCallbackHandler("vote:*", onVote)
client.AddInlineHandler("*", onInline)
client.AddEditHandler("*", onEdit)
client.AddDeleteHandler("*", onDelete)
client.AddAlbumHandler(onAlbum)
client.AddActionHandler(onAction)
client.AddJoinRequestHandler(onJoin)
client.AddParticipantHandler(onParticipant)
client.AddInlineSendHandler(onChosen)
client.AddInlineCallbackHandler("*", onInlineCallback)
client.AddGuestChatHandler(onGuestChat)
client.AddE2EHandler(onSecret)
client.AddRawHandler(nil, onRaw)client.AddMessageHandler("*", onMessage)
client.AddCommandHandler("start", onStart)
client.AddCallbackHandler("vote:*", onVote)
client.AddInlineHandler("*", onInline)
client.AddEditHandler("*", onEdit)
client.AddDeleteHandler("*", onDelete)
client.AddAlbumHandler(onAlbum)
client.AddActionHandler(onAction)
client.AddJoinRequestHandler(onJoin)
client.AddParticipantHandler(onParticipant)
client.AddInlineSendHandler(onChosen)
client.AddInlineCallbackHandler("*", onInlineCallback)
client.AddGuestChatHandler(onGuestChat)
client.AddE2EHandler(onSecret)
client.AddRawHandler(nil, onRaw)Event types
- message / msg / newmessage
- Incoming text or media message. Handler:
func(*NewMessage) error. - command / cmd
- Bot command starting with
/or!. Same handler shape as message. - edit / editmessage
- An existing message was edited. Same handler shape.
- delete
- Server notification that one or more messages were deleted. Handler:
func(*DeleteMessage) error. - album
- Multiple related media arriving as a group. Handler:
func(*Album) error. The dispatcher waitsAlbumWaitTimemilliseconds for stragglers before firing. - action
- Service messages: joins, leaves, Web App data, group creation. Handler:
func(*NewMessage) error— the message'sActionfield tells you which service action. - callback
- Inline-keyboard button press. Handler:
func(*CallbackQuery) error. - inlinecallback
- Callback from a keyboard on a message posted through inline mode. Handler:
func(*InlineCallbackQuery) error. - inline
- Inline query (
@yourbot ...). Handler:func(*InlineQuery) error. - choseninline
- Feedback that a user picked an inline result. Requires /setinlinefeedback in BotFather. Handler:
func(*InlineSend) error. - guestchat
- Guest chat query. Handler:
func(*GuestChatQuery) error. - joinrequest
- User requested to join a chat that has join-request approval on. Handler:
func(*JoinRequestUpdate) error. - participant
- Chat membership changed (join, leave, promoted, banned). Handler:
func(*ParticipantUpdate) error. - raw
- Catch-all for anything above the typed layer. Handler:
func(Update, *Client) error.
Pattern syntax
The string after event: is the body filter:
*matches anything.- A literal string matches by substring on the message body (or exact prefix on callback data).
r:orregex:switches on regex matching — e.g.message:r:^/order\s+(\d+)$.
Removing handlers
Every Add*Handler and On call returns a Handletoken. Pass it back through client.RemoveHandle to unregister:
h := client.AddMessageHandler("*", onMessage)
// ...later:
client.RemoveHandle(h)h := client.AddMessageHandler("*", onMessage)
// ...later:
client.RemoveHandle(h)Middleware with Use
For cross-cutting behaviour (timing, panic recovery, structured logging, per-request context), wrap the dispatcher with middleware. client.Use takes any number of middleware functions; they compose in registration order.
client.Use(func(handler telegram.Handler) telegram.Handler {
return func(ctx telegram.HandlerCtx) error {
start := time.Now()
err := handler(ctx)
log.Printf("%T finished in %s (err=%v)",
ctx.Update, time.Since(start), err)
return err
}
})client.Use(func(handler telegram.Handler) telegram.Handler {
return func(ctx telegram.HandlerCtx) error {
start := time.Now()
err := handler(ctx)
log.Printf("%T finished in %s (err=%v)",
ctx.Update, time.Since(start), err)
return err
}
})Middleware runs on every update before any handler. Filters narrow individual handlers. Use middleware for cross-cutting, filters for routing.
Errors from handlers
client.On("message:*", func(m *telegram.NewMessage) error {
if err := process(m); err != nil {
return fmt.Errorf("processing %d: %w", m.ID, err)
}
return nil
})client.On("message:*", func(m *telegram.NewMessage) error {
if err := process(m); err != nil {
return fmt.Errorf("processing %d: %w", m.ID, err)
}
return nil
})Returning an error logs it through the configured logger but does not crash anything. The dispatcher keeps running and the next update is processed normally. Wrap with fmt.Errorf so the log line carries enough context to diagnose.
Concurrency
The dispatcher uses a worker pool. Two updates that arrive close together may run in two different goroutines. Your handler must be safe for concurrent execution — either stateless, or synchronised at whatever shared state it touches.
If a single handler is slow, the queue grows. For heavy work (LLM calls, video processing, downstream HTTP that could take seconds) hand off to your own worker pool inside the handler and return immediately so the dispatcher can move on.
