Updates

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 waits AlbumWaitTime milliseconds for stragglers before firing.
action
Service messages: joins, leaves, Web App data, group creation. Handler: func(*NewMessage) error — the message's Action field 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: or regex: 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.