telega/router
Telega Router
The router module provides a flexible and composable routing system for Telegram bot updates. It allows you to define handlers for different types of messages and organize them into logical groups with middleware support, error handling, and composition capabilities.
One type
A Router holds routes, middleware, a catch handler, an
optional scope — and, through append and branch,
other routers it consults in order when none of its own routes claims an
update. telega.router takes one; it is reduced to a
Routable internally.
Basic Usage
import telega/router
import telega/update
import telega/reply
let router =
router.new("my_bot")
|> router.on_command("start", handle_start)
|> router.on_command("help", handle_help)
|> router.on_any_text(handle_text)
|> router.on_photo(handle_photo)
|> router.fallback(handle_unknown)
Routing Priority
Routes are matched in the following priority order:
- Commands - Exact command matches (e.g., “/start”, “/help”)
- Callback Queries - Callback data patterns
- Custom Routes - User-defined matchers
- Media Routes - Photo, video, voice, audio handlers
- Text Routes - Text pattern matching
- Fallback - Catch-all handler for unmatched updates
Within each category, routes are tried in the order they were added, with the first matching route handling the update.
Pattern Matching
Text and callback queries support flexible pattern matching:
router
|> router.on_text(Exact("hello"), handle_hello)
|> router.on_text(Prefix("search:"), handle_search)
|> router.on_text(Contains("help"), handle_help_mention)
|> router.on_text(Suffix("?"), handle_question)
router
|> router.on_callback(Prefix("page:"), handle_pagination)
|> router.on_callback(Exact("cancel"), handle_cancel)
Typed callback routes
A keyboard.KeyboardCallbackData
factory already knows how to serialize and parse its own payloads.
on_callback_data registers a route for exactly that factory’s payloads and
hands the handler the decoded value — no unpack_callback in the handler,
and a payload that fails to decode never reaches it:
let page = keyboard.int_callback_data("page")
router
|> router.on_callback_data(page, fn(ctx, query, page_number) {
// page_number: Int
reply.with_text(ctx, "Page " <> int.to_string(page_number))
})
Middleware System
Middleware allows you to wrap handlers with additional functionality. The first middleware added is the outermost one, so it runs first and sees the handler’s result last:
router
|> router.use_middleware(router.with_logging) // outermost, runs first
|> router.use_middleware(auth_middleware)
|> router.use_middleware(rate_limit_middleware) // innermost, closest to the handler
Middleware wraps whatever handled the update — one of the router’s own routes, a branch, or the fallback — so middleware added to a composed router sees every update it dispatches, including to branches added later.
Built-in middleware includes:
with_logging- Logs all update processingwith_filter- Conditionally processes updateswith_recovery- Recovers from handler errors
Error Handling
Routers support catch handlers to gracefully handle errors from routes:
router
|> router.with_catch_handler(fn(error) {
log.error("Route error: " <> string.inspect(error))
Error(error)
})
The catch handler receives only the error (no context) and must return
Result(Context, error) — log and re-raise with Error(error), or recover
with a context already in scope.
Note: The router’s catch handler only handles errors from route handlers.
System-level errors (like session persistence failures) are handled by
the bot’s main catch handler configured via telega.with_catch_handler.
Composition
Merging leaves
merge combines two leaf routers into one, with all routes unified.
Routes from the first router take priority in case of conflicts:
let admin_router =
router.new("admin")
|> router.on_command("ban", handle_ban)
|> router.on_command("stats", handle_stats)
let user_router =
router.new("user")
|> router.on_command("start", handle_start)
|> router.on_command("help", handle_help)
let main_router = router.merge(admin_router, user_router)
Composing routers
append adds a router that is tried in order once this one’s own routes
have declined; branch adds one that is only consulted when a filter
matches. Each keeps its own middleware and catch handler:
let app =
router.new("app")
|> router.branch(router.is_private_chat(), private_router)
|> router.branch(router.is_group_chat(), group_router)
|> router.append(shared_router)
|> router.fallback(handle_unknown)
telega.new(api_client)
|> telega.router(app)
compose(a, b) and compose_many([a, b, c]) are shorthand for a router
of unconditional branches.
Scoped leaves
scope restricts a whole leaf to updates matching a predicate. A scoped
leaf declines out-of-scope updates outright, so the next branch of the tree
gets its turn:
let admin_router =
router.new("admin")
|> router.on_command("ban", handle_ban)
|> router.scope(fn(update) { is_admin(update.from_id) })
Custom Routes
For complex routing logic, use custom matchers:
router
|> router.on_custom(
matcher: fn(update) {
case update {
update.TextUpdate(text: t, ..) ->
string.starts_with(t, "http://") || string.starts_with(t, "https://")
_ -> False
}
},
handler: handle_link
)
Magic Filters
The router includes a powerful filter system for creating complex routing conditions:
// Simple filters
router
|> router.on_filtered(router.is_private_chat(), handle_private)
|> router.on_filtered(router.from_user(admin_id), handle_admin)
// Combining filters with AND logic
router
|> router.on_filtered(
router.and2(
router.is_group_chat(),
router.text_starts_with("!")
),
handle_group_command
)
// Combining multiple filters
router
|> router.on_filtered(
router.and([
router.is_text(),
router.from_users([admin1, admin2, admin3]),
router.not(router.text_starts_with("/"))
]),
handle_admin_text
)
Filter reference
Every filter is a predicate over the whole update, so one table covers them all. “Reads” says where the answer comes from — an update that has no such field never matches.
| Filter | Reads | True when |
|---|---|---|
is_text() | update kind | the update is a plain text message |
text_equals(t) | text | the text is exactly t |
text_starts_with(p) | text | the text starts with p |
text_contains(s) | text | the text contains s |
is_command() | update kind | the update is a command |
command_equals(c) | command | the command is c (leading / optional) |
from_user(id) | update.from_id | the sender is id |
from_users(ids) | update.from_id | the sender is one of ids |
in_chat(id) | update.chat_id | the update happened in chat id |
from_chats(ids) | update.chat_id | the chat is one of ids |
is_private_chat() | update.chat().type_ | the chat is "private" |
is_group_chat() | update.chat().type_ | the chat is "group" or "supergroup" |
chat_type(t) | update.chat().type_ | the chat type is exactly t |
has_photo() | update kind | the message carries photos |
has_video() | update kind | the message carries a video |
is_media_group() | update kind | the update is a buffered album |
has_media() | update kind | photo, video, voice, audio or album |
is_callback_query() | update kind | the update is a button press |
callback_data_starts_with(p) | query.data | the callback payload starts with p |
is_forwarded() | message.forward_origin | the message was forwarded |
is_reply() | message.reply_to_message | the message replies to another |
in_topic(id) | message.message_thread_id | the message is in forum topic id |
has_entity(kind) | message.entities + caption_entities | an entity of that type is present (e.g. "url", "mention") |
via_bot() | message.via_bot | the message was sent through an inline bot |
via_bot_id(id) | message.via_bot.id | it was sent through bot id |
is_automatic_forward() | message.is_automatic_forward | a channel post auto-forwarded to its discussion group |
has_media_spoiler() | message.has_media_spoiler | the media is spoiler-covered |
The message-reading filters use update.message, which answers None for
updates that are not about a message (callback queries, inline queries,
polls, member changes) — those match none of them.
Handler Types
Different route types receive different handler signatures:
CommandHandler- Receives the parsed commandTextHandler- Receives the message textCallbackHandler- Receives callback query id and dataCallbackDataHandler- Receives the callback query and a decoded payloadPhotoHandler- Receives list of photo sizesVideoHandler- Receives video infoVoiceHandler- Receives voice message infoAudioHandler- Receives audio fileMediaGroupHandler- Receives media group ID and list of messagesMessageHandler- Receives a whole message (edits, channel posts, business messages)Handler- Generic handler for any update type
Types
pub type AudioHandler(session, error, dependencies) =
fn(context.Context(session, error, dependencies), types.Audio) -> Result(
context.Context(session, error, dependencies),
error,
)
Handler for a typed callback route registered with on_callback_data:
the callback query itself plus the payload already decoded by the
keyboard.KeyboardCallbackData factory the route was registered with.
pub type CallbackDataHandler(
session,
error,
dependencies,
data,
) =
fn(
context.Context(session, error, dependencies),
types.CallbackQuery,
data,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type CallbackHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
String,
String,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type ChatBoostHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.ChatBoostUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type ChatJoinRequestHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.ChatJoinRequest,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type ChatMemberUpdatedHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type ChosenInlineResultHandler(
session,
error,
dependencies,
) =
fn(
context.Context(session, error, dependencies),
types.ChosenInlineResult,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type CommandHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
update.Command,
) -> Result(
context.Context(session, error, dependencies),
error,
)
Generic handler type for all updates
pub type Handler(session, error, dependencies) =
fn(context.Context(session, error, dependencies), update.Update) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type InlineQueryHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.InlineQuery,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type MediaGroupHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
String,
List(types.Message),
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type MessageHandler(session, error, dependencies) =
fn(context.Context(session, error, dependencies), types.Message) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type MessageReactionCountHandler(
session,
error,
dependencies,
) =
fn(
context.Context(session, error, dependencies),
types.MessageReactionCountUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type MessageReactionHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
)
Middleware wraps a handler with additional functionality
pub type Middleware(session, error, dependencies) =
fn(
fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type PaidMediaPurchaseHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.PaidMediaPurchased,
) -> Result(
context.Context(session, error, dependencies),
error,
)
Pattern matching for text and callbacks
pub type Pattern {
Exact(String)
Prefix(String)
Contains(String)
Suffix(String)
}
Constructors
-
Exact(String) -
Prefix(String) -
Contains(String) -
Suffix(String)
pub type PhotoHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
List(types.PhotoSize),
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type PollAnswerHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.PollAnswer,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type PollHandler(session, error, dependencies) =
fn(context.Context(session, error, dependencies), types.Poll) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type PreCheckoutQueryHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.PreCheckoutQuery,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type RemovedChatBoostHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.ChatBoostRemoved,
) -> Result(
context.Context(session, error, dependencies),
error,
)
What telega actually stores: a router reduced to the four things the bot
needs from it. Build one with routable.
pub type Routable(session, error, dependencies) {
Routable(
name: String,
handle: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
allowed_updates: List(String),
registered_commands: List(#(String, String)),
)
}
Constructors
-
Routable( name: String, handle: fn( context.Context(session, error, dependencies), update.Update, ) -> Result( context.Context(session, error, dependencies), error, ), allowed_updates: List(String), registered_commands: List(#(String, String)), )Arguments
- allowed_updates
-
The derived
allowed_updatesset;[]means “do not restrict”. - registered_commands
-
#(command, description)pairs forsetMyCommands.
Unified route type that encompasses all route types
pub type Route(session, error, dependencies) {
TextPatternRoute(
pattern: Pattern,
handler: fn(
context.Context(session, error, dependencies),
String,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
PhotoRoute(
handler: fn(
context.Context(session, error, dependencies),
List(types.PhotoSize),
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
VideoRoute(
handler: fn(
context.Context(session, error, dependencies),
types.Video,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
VoiceRoute(
handler: fn(
context.Context(session, error, dependencies),
types.Voice,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
AudioRoute(
handler: fn(
context.Context(session, error, dependencies),
types.Audio,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
MediaGroupRoute(
handler: fn(
context.Context(session, error, dependencies),
String,
List(types.Message),
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
WebAppDataRoute(
handler: fn(
context.Context(session, error, dependencies),
types.WebAppData,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
EditedMessageRoute(
handler: fn(
context.Context(session, error, dependencies),
types.Message,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
ChannelPostRoute(
handler: fn(
context.Context(session, error, dependencies),
types.Message,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
EditedChannelPostRoute(
handler: fn(
context.Context(session, error, dependencies),
types.Message,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
BusinessMessageRoute(
handler: fn(
context.Context(session, error, dependencies),
types.Message,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
InlineQueryRoute(
handler: fn(
context.Context(session, error, dependencies),
types.InlineQuery,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
ChosenInlineResultRoute(
handler: fn(
context.Context(session, error, dependencies),
types.ChosenInlineResult,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
ShippingQueryRoute(
handler: fn(
context.Context(session, error, dependencies),
types.ShippingQuery,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
PreCheckoutQueryRoute(
handler: fn(
context.Context(session, error, dependencies),
types.PreCheckoutQuery,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
PaidMediaPurchaseRoute(
handler: fn(
context.Context(session, error, dependencies),
types.PaidMediaPurchased,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
PollRoute(
handler: fn(
context.Context(session, error, dependencies),
types.Poll,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
PollAnswerRoute(
handler: fn(
context.Context(session, error, dependencies),
types.PollAnswer,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
MessageReactionRoute(
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
MessageReactionEmojiRoute(
emojis: List(String),
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
MessageReactionPaidRoute(
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
MessageReactionAddedRoute(
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
MessageReactionRemovedRoute(
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
MessageReactionCountRoute(
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionCountUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
ChatMemberUpdatedRoute(
handler: fn(
context.Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
MyChatMemberUpdatedRoute(
handler: fn(
context.Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
ChatJoinRequestRoute(
handler: fn(
context.Context(session, error, dependencies),
types.ChatJoinRequest,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
ChatBoostRoute(
handler: fn(
context.Context(session, error, dependencies),
types.ChatBoostUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
RemovedChatBoostRoute(
handler: fn(
context.Context(session, error, dependencies),
types.ChatBoostRemoved,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
UnknownUpdateRoute(
handler: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
CustomRoute(
matcher: fn(update.Update) -> Bool,
handler: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
FilteredRoute(
filter: Filter,
handler: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
)
}
Constructors
-
TextPatternRoute( pattern: Pattern, handler: fn( context.Context(session, error, dependencies), String, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
PhotoRoute( handler: fn( context.Context(session, error, dependencies), List(types.PhotoSize), ) -> Result( context.Context(session, error, dependencies), error, ), ) -
VideoRoute( handler: fn( context.Context(session, error, dependencies), types.Video, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
VoiceRoute( handler: fn( context.Context(session, error, dependencies), types.Voice, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
AudioRoute( handler: fn( context.Context(session, error, dependencies), types.Audio, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
MediaGroupRoute( handler: fn( context.Context(session, error, dependencies), String, List(types.Message), ) -> Result( context.Context(session, error, dependencies), error, ), ) -
WebAppDataRoute( handler: fn( context.Context(session, error, dependencies), types.WebAppData, ) -> Result( context.Context(session, error, dependencies), error, ), )Data sent by a Mini App through
Telegram.WebApp.sendData. -
EditedMessageRoute( handler: fn( context.Context(session, error, dependencies), types.Message, ) -> Result( context.Context(session, error, dependencies), error, ), )A message the user edited.
-
ChannelPostRoute( handler: fn( context.Context(session, error, dependencies), types.Message, ) -> Result( context.Context(session, error, dependencies), error, ), )A post in a channel the bot administers.
-
EditedChannelPostRoute( handler: fn( context.Context(session, error, dependencies), types.Message, ) -> Result( context.Context(session, error, dependencies), error, ), )An edited channel post.
-
BusinessMessageRoute( handler: fn( context.Context(session, error, dependencies), types.Message, ) -> Result( context.Context(session, error, dependencies), error, ), )A message from a connected business account’s chat.
-
InlineQueryRoute( handler: fn( context.Context(session, error, dependencies), types.InlineQuery, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
ChosenInlineResultRoute( handler: fn( context.Context(session, error, dependencies), types.ChosenInlineResult, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
ShippingQueryRoute( handler: fn( context.Context(session, error, dependencies), types.ShippingQuery, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
PreCheckoutQueryRoute( handler: fn( context.Context(session, error, dependencies), types.PreCheckoutQuery, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
PaidMediaPurchaseRoute( handler: fn( context.Context(session, error, dependencies), types.PaidMediaPurchased, ) -> Result( context.Context(session, error, dependencies), error, ), )A paid media purchase, for
sendPaidMediawith a payload. -
PollRoute( handler: fn( context.Context(session, error, dependencies), types.Poll, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
PollAnswerRoute( handler: fn( context.Context(session, error, dependencies), types.PollAnswer, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
MessageReactionRoute( handler: fn( context.Context(session, error, dependencies), types.MessageReactionUpdated, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
MessageReactionEmojiRoute( emojis: List(String), handler: fn( context.Context(session, error, dependencies), types.MessageReactionUpdated, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
MessageReactionPaidRoute( handler: fn( context.Context(session, error, dependencies), types.MessageReactionUpdated, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
MessageReactionAddedRoute( handler: fn( context.Context(session, error, dependencies), types.MessageReactionUpdated, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
MessageReactionRemovedRoute( handler: fn( context.Context(session, error, dependencies), types.MessageReactionUpdated, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
MessageReactionCountRoute( handler: fn( context.Context(session, error, dependencies), types.MessageReactionCountUpdated, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
ChatMemberUpdatedRoute( handler: fn( context.Context(session, error, dependencies), types.ChatMemberUpdated, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
MyChatMemberUpdatedRoute( handler: fn( context.Context(session, error, dependencies), types.ChatMemberUpdated, ) -> Result( context.Context(session, error, dependencies), error, ), )The bot’s own membership changed — it was blocked, unblocked, added to a group, or promoted. A different Telegram update kind from
chat_member, andallowed_updateslists them separately. -
ChatJoinRequestRoute( handler: fn( context.Context(session, error, dependencies), types.ChatJoinRequest, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
ChatBoostRoute( handler: fn( context.Context(session, error, dependencies), types.ChatBoostUpdated, ) -> Result( context.Context(session, error, dependencies), error, ), )A chat boost was added or changed.
-
RemovedChatBoostRoute( handler: fn( context.Context(session, error, dependencies), types.ChatBoostRemoved, ) -> Result( context.Context(session, error, dependencies), error, ), )A chat boost was removed.
-
UnknownUpdateRoute( handler: fn( context.Context(session, error, dependencies), update.Update, ) -> Result( context.Context(session, error, dependencies), error, ), )An update this version of the library cannot interpret. Registering one makes
allowed_updatesderivation give up on narrowing — an update kind the library does not know is, by definition, not in the derived set. -
CustomRoute( matcher: fn(update.Update) -> Bool, handler: fn( context.Context(session, error, dependencies), update.Update, ) -> Result( context.Context(session, error, dependencies), error, ), ) -
FilteredRoute( filter: Filter, handler: fn( context.Context(session, error, dependencies), update.Update, ) -> Result( context.Context(session, error, dependencies), error, ), )
pub type ShippingQueryHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.ShippingQuery,
) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type TextHandler(session, error, dependencies) =
fn(context.Context(session, error, dependencies), String) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type VideoHandler(session, error, dependencies) =
fn(context.Context(session, error, dependencies), types.Video) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type VoiceHandler(session, error, dependencies) =
fn(context.Context(session, error, dependencies), types.Voice) -> Result(
context.Context(session, error, dependencies),
error,
)
pub type WebAppDataHandler(session, error, dependencies) =
fn(
context.Context(session, error, dependencies),
types.WebAppData,
) -> Result(
context.Context(session, error, dependencies),
error,
)
Values
pub fn allowed_updates(
router: Router(session, error, dependencies),
) -> List(String)
Derive the set of Telegram update types this router actually handles, as the
strings expected by allowed_updates (e.g. "message", "callback_query"),
its branches included. The result is deduplicated and sorted for stable
output.
If the router — or any branch — has a fallback, custom, filtered, or
on_unknown_update route, the handled set cannot be determined statically
(those routes can match anything), so an empty list is returned to signal
“do not restrict” — Telegram then sends its default update set. Use a manual
override when you need narrowing alongside catch-all routes.
A non-empty result always contains "callback_query", even for a router
with no callback route. Static derivation only sees the router: a handler
that parks on bot.wait_callback is invisible to it, and narrowing the set
without callback_query would leave that conversation waiting forever.
Allowing it costs nothing when unused — Telegram only sends callback queries
for keyboards the bot itself put on screen.
pub fn append(
router: Router(session, error, dependencies),
leaf: Router(session, error, dependencies),
) -> Router(session, error, dependencies)
Add a router that is consulted for every update this router’s own routes decline, after the branches already added.
pub fn branch(
router: Router(session, error, dependencies),
when filter: Filter,
router leaf: Router(session, error, dependencies),
) -> Router(session, error, dependencies)
Add a router that is only consulted when when matches the update.
router.new("app")
|> router.branch(router.is_private_chat(), private_router)
|> router.branch(router.is_group_chat(), group_router)
pub fn callback_data_starts_with(prefix: String) -> Filter
Filter for callback data that starts with prefix
pub fn chat_type(type_: String) -> Filter
Filter on the chat’s type_ verbatim: “private”, “group”, “supergroup” or
“channel”. Updates that happen in no chat never match.
pub fn compose(
first: Router(session, error, dependencies),
second: Router(session, error, dependencies),
) -> Router(session, error, dependencies)
Compose two routers: both are tried in order. The result is named
first+second.
pub fn compose_many(
routers: List(Router(session, error, dependencies)),
) -> Router(session, error, dependencies)
Compose many routers, tried in order.
pub fn fallback(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Set fallback handler for unmatched updates
pub fn filter(
name: String,
check: fn(update.Update) -> Bool,
) -> Filter
Create a filter from a custom function
pub fn from_chats(chat_ids: List(Int)) -> Filter
Filter by multiple chat IDs. Matches when the update’s chat is one of
chat_ids — a whitelist of chats. Combine with not for a blacklist:
// Only react in the support chats
router.on_filtered(router.from_chats([-100_1, -100_2]), handler)
// React everywhere except the banned chats
router.on_filtered(router.not(router.from_chats([-100_666])), handler)
pub fn handle(
router: Router(session, error, dependencies),
ctx: context.Context(session, error, dependencies),
update: update.Update,
) -> Result(context.Context(session, error, dependencies), error)
Process an update: this router’s own route if one matches, else the first branch whose filter passes and which has a route for it, else the fallback. Middleware and the catch handler wrap whichever of those ran.
pub fn has_entity(kind: String) -> Filter
The message carries an entity of the given type — "url", "mention",
"hashtag", "bot_command", "spoiler", … Both the text entities and the
caption entities are searched, so a captioned photo with a link matches
has_entity("url") the same way a text message does.
pub fn has_media_spoiler() -> Filter
The message’s media is covered by a spoiler animation.
pub fn in_topic(thread_id: Int) -> Filter
The message belongs to the given forum topic / message thread.
pub fn is_automatic_forward() -> Filter
A channel post automatically forwarded to the linked discussion group.
pub fn is_group_chat() -> Filter
Filter for group and supergroup chats. Channels are neither — use
chat_type for those.
pub fn is_private_chat() -> Filter
Filter for private chats.
Reads the chat’s own type_, not the sign of chat_id: an update that
happens in no chat at all (an inline query, a poll answer) is not a private
chat, however its stand-in chat_id is keyed.
pub fn matched_route(
ctx: context.Context(session, error, dependencies),
) -> option.Option(String)
The label of the route that handled the current update, as recorded by
handle — "command:/start", "callback:exact:menu",
"text:exact:hi", "photo", "fallback", or "unmatched" when no route
(and no fallback) claimed it.
This is what the route metadata of the telega.update.stop telemetry
event carries; read it from a middleware or a catch handler to log or
count by route. None outside of routing (a wait_* continuation, a
background context).
pub fn matched_router(
ctx: context.Context(session, error, dependencies),
) -> option.Option(String)
The name of the router whose route claimed the current update — the branch, for a composed router.
pub fn matches(filter: Filter, update: update.Update) -> Bool
Evaluate a composable Filter against an update.
This is the bridge that lets the filter combinators (and/or/not,
is_text, has_photo, …) be reused outside the router — most notably to
drive telega.wait_filtered / telega.wait_for in conversations:
use ctx, upd <- telega.wait_for(
ctx,
filter: router.matches(router.or2(router.is_text(), router.has_photo()), _),
or: None,
timeout: None,
)
pub fn merge(
first: Router(session, error, dependencies),
second: Router(session, error, dependencies),
) -> Router(session, error, dependencies)
Merge two routers into one flat router. All routes are combined, with the first router’s routes taking priority in case of conflicts. Middleware and catch handlers are shared; branches are kept in order, first’s before second’s.
pub fn name(
router: Router(session, error, dependencies),
) -> String
The router’s name, as given to new (with _scoped/+ suffixes from
scope and merge).
pub fn on_any_text(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
String,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Add a handler for any text
pub fn on_audio(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.Audio,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
pub fn on_business_message(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.Message,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
A message in a chat connected to the bot’s business account.
pub fn on_callback(
router: Router(session, error, dependencies),
pattern: Pattern,
handler: fn(
context.Context(session, error, dependencies),
String,
String,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Add a callback query handler with pattern
pub fn on_callback_data(
router: Router(session, error, dependencies),
factory: keyboard.KeyboardCallbackData(data),
handler: fn(
context.Context(session, error, dependencies),
types.CallbackQuery,
data,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Add a callback route for one keyboard.KeyboardCallbackData factory.
The route matches exactly the payloads that factory builds
(Prefix(id <> delimiter)) and the handler receives the value already
decoded — no unpack_callback boilerplate, and a payload that belongs to
another factory or fails to deserialize never reaches the handler.
let page = keyboard.int_callback_data("page")
router.new("bot")
|> router.on_callback_data(page, fn(ctx, _query, page_number) {
reply.with_text(ctx, "Page " <> int.to_string(page_number))
})
A payload the factory rejects leaves the context untouched, so a sibling
route or the fallback never sees it — register the factory routes you
expect and a Prefix route for anything else.
pub fn on_channel_post(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.Message,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
A post in a channel the bot administers.
pub fn on_chat_boost(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.ChatBoostUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
A chat boost was added or changed. The bot must be an administrator.
pub fn on_chat_join_request(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.ChatJoinRequest,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
pub fn on_chat_member_updated(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Another member’s status in a chat changed.
pub fn on_chosen_inline_result(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.ChosenInlineResult,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
pub fn on_command(
router: Router(session, error, dependencies),
command: String,
handler: fn(
context.Context(session, error, dependencies),
update.Command,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Add a command handler
pub fn on_command_with_description(
router: Router(session, error, dependencies),
command: String,
description: String,
handler: fn(
context.Context(session, error, dependencies),
update.Command,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Add a command handler together with a human-readable description.
The description is what shows up in the Telegram command menu. When the bot
is started with telega.with_auto_commands, all commands registered this way
are published via setMyCommands automatically, and telega_i18n can supply
per-language variants. The description is ignored for routing — it only feeds
command auto-synchronization.
router
|> router.on_command_with_description("start", "Start the bot", handle_start)
|> router.on_command_with_description("help", "Show help", handle_help)
pub fn on_commands(
router: Router(session, error, dependencies),
commands: List(String),
handler: fn(
context.Context(session, error, dependencies),
update.Command,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Add multiple commands with same handler
pub fn on_custom(
router: Router(session, error, dependencies),
matcher matcher: fn(update.Update) -> Bool,
handler handler: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Route on a hand-written predicate over the whole update.
pub fn on_edited_channel_post(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.Message,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
An edited channel post.
pub fn on_edited_message(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.Message,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
A message the user edited. Telegram does not send these by default —
allowed_updates derivation adds "edited_message" for you.
pub fn on_filtered(
router: Router(session, error, dependencies),
filter: Filter,
handler: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Route on a composable Filter.
pub fn on_inline_query(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.InlineQuery,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
pub fn on_media_group(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
String,
List(types.Message),
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Handle a whole album as one update.
Only fires when incoming albums are buffered — see
telega.with_media_group_timeout. Without it every photo of an album
arrives on its own on_photo/on_video/on_audio route.
pub fn on_my_chat_member_updated(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
The bot’s own status in a chat changed (blocked, added, promoted).
pub fn on_paid_media_purchase(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.PaidMediaPurchased,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
A user bought paid media the bot sent with a payload.
pub fn on_paid_reaction(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Handle paid (star) reactions.
pub fn on_photo(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
List(types.PhotoSize),
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Add handlers for media types
pub fn on_poll(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.Poll,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
pub fn on_poll_answer(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.PollAnswer,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
pub fn on_pre_checkout_query(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.PreCheckoutQuery,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
pub fn on_reaction(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Handle any reaction change on a message.
pub fn on_reaction_added(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Handle only reaction additions.
pub fn on_reaction_count(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionCountUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Handle anonymous reaction counters in large chats.
pub fn on_reaction_emoji(
router: Router(session, error, dependencies),
emoji: String,
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Handle reactions with one specific emoji.
pub fn on_reaction_emojis(
router: Router(session, error, dependencies),
emojis: List(String),
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Handle reactions with any of the given emojis.
pub fn on_reaction_removed(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Handle only reaction removals.
pub fn on_removed_chat_boost(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.ChatBoostRemoved,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
A chat boost was removed.
pub fn on_shipping_query(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.ShippingQuery,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
pub fn on_text(
router: Router(session, error, dependencies),
pattern: Pattern,
handler: fn(
context.Context(session, error, dependencies),
String,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Add a text handler with pattern
pub fn on_unknown_update(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
An update this version of the library cannot interpret: a Bot API kind it
does not know yet, or one whose payload failed to decode. The handler gets
the UnknownUpdate and can read update.raw.
Registering this route turns off allowed_updates narrowing — an update
kind the library does not know can never be in a derived set.
pub fn on_video(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.Video,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
pub fn on_voice(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.Voice,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
pub fn on_web_app_data(
router: Router(session, error, dependencies),
handler: fn(
context.Context(session, error, dependencies),
types.WebAppData,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Data a Mini App sent with Telegram.WebApp.sendData.
pub fn registered_commands(
router: Router(session, error, dependencies),
) -> List(#(String, String))
List every command registered with a description, as #(command, description)
pairs sorted by command name — this router’s own and its branches’, an
earlier registration winning a duplicate. Commands added with on_command
(no description) are omitted.
This is what telega.with_auto_commands feeds into setMyCommands.
pub fn routable(
router: Router(session, error, dependencies),
) -> Routable(session, error, dependencies)
Reduce a router to what the bot needs from it.
pub fn scope(
router: Router(session, error, dependencies),
predicate: fn(update.Update) -> Bool,
) -> Router(session, error, dependencies)
Restrict a whole router to updates matching a predicate.
The predicate is also what handle and the tree consult before dispatching,
so an out-of-scope update is declined rather than swallowed: the next
branch of a tree gets its turn instead of the scoped router eating it.
pub fn text_contains(substring: String) -> Filter
Filter for text that contains a substring
pub fn text_equals(text: String) -> Filter
Filter for text that equals a specific value
pub fn text_starts_with(prefix: String) -> Filter
Filter for text that starts with a prefix
pub fn use_middleware(
router: Router(session, error, dependencies),
middleware: fn(
fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Add middleware to the router. The first middleware added is the outermost one: it runs first and sees the handler’s result last.
pub fn via_bot_id(bot_id: Int) -> Filter
The message was sent through the inline bot with this id.
pub fn with_catch_handler(
router: Router(session, error, dependencies),
catch_handler: fn(error) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> Router(session, error, dependencies)
Add a catch handler to the router that handles errors from all routes
pub fn with_filter(
predicate: fn(update.Update) -> Bool,
handler: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(context.Context(session, error, dependencies), error)
Filter middleware - only process updates that match predicate
pub fn with_logging(
handler: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(context.Context(session, error, dependencies), error)
Logging middleware - logs update processing
pub fn with_rate_limit(
limit limit: Int,
window_ms window_ms: Int,
on_limit on_limit: fn(
context.Context(session, error, dependencies),
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> fn(
fn(context.Context(session, error, dependencies), update.Update) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(context.Context(session, error, dependencies), error)
Per-user flood control middleware: allows at most limit updates per
window_ms window for each {chat_id}:{from_id} pair. Counters live in
ETS, so the limit is shared across all routes of the bot.
on_limit is called instead of the handler when the limit is exceeded —
pass fn(ctx) { Ok(ctx) } to drop the update silently, or reply from it
to inform the user. Every rejected update emits a
telega.rate_limit.hit telemetry event.
Updates without user context (e.g. poll updates, from_id is -1) are
not limited.
router.new("bot")
|> router.use_middleware(router.with_rate_limit(
limit: 5,
window_ms: 3000,
on_limit: fn(ctx) { Ok(ctx) },
))
Call with_rate_limit once at bot setup: the limiter’s ETS table is owned
by the calling process and is deleted when that process exits.
pub fn with_recovery(
recover: fn(error) -> Result(
context.Context(session, error, dependencies),
error,
),
handler: fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(context.Context(session, error, dependencies), error)
Error recovery middleware
pub fn with_timeout(
ms ms: Int,
on_timeout on_timeout: fn(
context.Context(session, error, dependencies),
) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> fn(
fn(context.Context(session, error, dependencies), update.Update) -> Result(
context.Context(session, error, dependencies),
error,
),
) -> fn(
context.Context(session, error, dependencies),
update.Update,
) -> Result(context.Context(session, error, dependencies), error)
Bound how long a handler may run: ms milliseconds, after which it is
killed and on_timeout answers the update instead.
A handler that hangs — an HTTP call without a timeout of its own, a lock
never released — holds its chat instance, and with it every later update
of that chat and one in_flight slot of the bot, for good. With a
deadline the chat goes on.
The handler runs in a process of its own, linked to the chat instance so
a crash is still a crash. The update’s scope travels with it both ways:
a pre-handler’s annotations are readable inside, and a toast the handler
showed is still known to reply.answer_callback_once afterwards. A
wait_* returns at once and parks its continuation, which later runs
outside this deadline. Every timeout logs, and emits
telega.handler.timeout.
router.new("bot")
|> router.use_middleware(router.with_timeout(
ms: 30_000,
on_timeout: fn(ctx) { reply.text(ctx, "That took too long — try again.") },
))