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.
Two types, two jobs
Routeris a leaf: routes, middleware, a catch handler, an optional scope. Everything namedon_*registers on a leaf.RouterTreeis a composition: an ordered list of leaves, each optionally guarded by aFilter. It has no routes of its own, soon_commandon a tree does not compile — the registration that a composed router used to swallow is now a type error.
telega.router takes a Router; telega.router_tree takes a
RouterTree. Both are converted 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
On a RouterTree, use_middleware_on_tree pushes the middleware into every
branch (each branch keeps its own copy, applied around its own handlers).
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)
Building a tree
append adds a leaf that is tried in order; branch adds one that is only
consulted when a filter matches. Each leaf keeps its own middleware and
catch handler:
let tree =
router.tree()
|> router.branch(router.is_private_chat(), private_router)
|> router.branch(router.is_group_chat(), group_router)
|> router.append(shared_router)
|> router.tree_fallback(handle_unknown)
telega.new(api_client)
|> telega.router_tree(tree)
compose(a, b) and compose_many([a, b, c]) are shorthand for a tree 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(bot.Context(session, error, dependencies), types.Audio) -> Result(
bot.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(
bot.Context(session, error, dependencies),
types.CallbackQuery,
data,
) -> Result(bot.Context(session, error, dependencies), error)
pub type CallbackHandler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), String, String) -> Result(
bot.Context(session, error, dependencies),
error,
)
pub type ChatBoostHandler(session, error, dependencies) =
fn(
bot.Context(session, error, dependencies),
types.ChatBoostUpdated,
) -> Result(bot.Context(session, error, dependencies), error)
pub type ChatJoinRequestHandler(session, error, dependencies) =
fn(
bot.Context(session, error, dependencies),
types.ChatJoinRequest,
) -> Result(bot.Context(session, error, dependencies), error)
pub type ChatMemberUpdatedHandler(session, error, dependencies) =
fn(
bot.Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(bot.Context(session, error, dependencies), error)
pub type ChosenInlineResultHandler(
session,
error,
dependencies,
) =
fn(
bot.Context(session, error, dependencies),
types.ChosenInlineResult,
) -> Result(bot.Context(session, error, dependencies), error)
pub type CommandHandler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), update.Command) -> Result(
bot.Context(session, error, dependencies),
error,
)
Generic handler type for all updates
pub type Handler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), update.Update) -> Result(
bot.Context(session, error, dependencies),
error,
)
pub type InlineQueryHandler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), types.InlineQuery) -> Result(
bot.Context(session, error, dependencies),
error,
)
pub type MediaGroupHandler(session, error, dependencies) =
fn(
bot.Context(session, error, dependencies),
String,
List(types.Message),
) -> Result(bot.Context(session, error, dependencies), error)
pub type MessageHandler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), types.Message) -> Result(
bot.Context(session, error, dependencies),
error,
)
pub type MessageReactionCountHandler(
session,
error,
dependencies,
) =
fn(
bot.Context(session, error, dependencies),
types.MessageReactionCountUpdated,
) -> Result(bot.Context(session, error, dependencies), error)
pub type MessageReactionHandler(session, error, dependencies) =
fn(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.Context(session, error, dependencies), error)
Middleware wraps a handler with additional functionality
pub type Middleware(session, error, dependencies) =
fn(
fn(bot.Context(session, error, dependencies), update.Update) -> Result(
bot.Context(session, error, dependencies),
error,
),
) -> fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.Context(session, error, dependencies), error)
pub type PaidMediaPurchaseHandler(session, error, dependencies) =
fn(
bot.Context(session, error, dependencies),
types.PaidMediaPurchased,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
List(types.PhotoSize),
) -> Result(bot.Context(session, error, dependencies), error)
pub type PollAnswerHandler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), types.PollAnswer) -> Result(
bot.Context(session, error, dependencies),
error,
)
pub type PollHandler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), types.Poll) -> Result(
bot.Context(session, error, dependencies),
error,
)
pub type PreCheckoutQueryHandler(session, error, dependencies) =
fn(
bot.Context(session, error, dependencies),
types.PreCheckoutQuery,
) -> Result(bot.Context(session, error, dependencies), error)
pub type RemovedChatBoostHandler(session, error, dependencies) =
fn(
bot.Context(session, error, dependencies),
types.ChatBoostRemoved,
) -> Result(bot.Context(session, error, dependencies), error)
What telega actually stores: a leaf and a tree reduced to the four things
the bot needs from a router. Build one with routable or
tree_routable.
pub type Routable(session, error, dependencies) {
Routable(
name: String,
handle: fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.Context(session, error, dependencies), error),
allowed_updates: List(String),
registered_commands: List(#(String, String)),
)
}
Constructors
-
Routable( name: String, handle: fn( bot.Context(session, error, dependencies), update.Update, ) -> Result(bot.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(bot.Context(session, error, dependencies), String) -> Result(
bot.Context(session, error, dependencies),
error,
),
)
PhotoRoute(
handler: fn(
bot.Context(session, error, dependencies),
List(types.PhotoSize),
) -> Result(bot.Context(session, error, dependencies), error),
)
VideoRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.Video,
) -> Result(bot.Context(session, error, dependencies), error),
)
VoiceRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.Voice,
) -> Result(bot.Context(session, error, dependencies), error),
)
AudioRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.Audio,
) -> Result(bot.Context(session, error, dependencies), error),
)
MediaGroupRoute(
handler: fn(
bot.Context(session, error, dependencies),
String,
List(types.Message),
) -> Result(bot.Context(session, error, dependencies), error),
)
WebAppDataRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.WebAppData,
) -> Result(bot.Context(session, error, dependencies), error),
)
EditedMessageRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.Message,
) -> Result(bot.Context(session, error, dependencies), error),
)
ChannelPostRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.Message,
) -> Result(bot.Context(session, error, dependencies), error),
)
EditedChannelPostRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.Message,
) -> Result(bot.Context(session, error, dependencies), error),
)
BusinessMessageRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.Message,
) -> Result(bot.Context(session, error, dependencies), error),
)
InlineQueryRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.InlineQuery,
) -> Result(bot.Context(session, error, dependencies), error),
)
ChosenInlineResultRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.ChosenInlineResult,
) -> Result(bot.Context(session, error, dependencies), error),
)
ShippingQueryRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.ShippingQuery,
) -> Result(bot.Context(session, error, dependencies), error),
)
PreCheckoutQueryRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.PreCheckoutQuery,
) -> Result(bot.Context(session, error, dependencies), error),
)
PaidMediaPurchaseRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.PaidMediaPurchased,
) -> Result(bot.Context(session, error, dependencies), error),
)
PollRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.Poll,
) -> Result(bot.Context(session, error, dependencies), error),
)
PollAnswerRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.PollAnswer,
) -> Result(bot.Context(session, error, dependencies), error),
)
MessageReactionRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.Context(session, error, dependencies), error),
)
MessageReactionEmojiRoute(
emojis: List(String),
handler: fn(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.Context(session, error, dependencies), error),
)
MessageReactionPaidRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.Context(session, error, dependencies), error),
)
MessageReactionAddedRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.Context(session, error, dependencies), error),
)
MessageReactionRemovedRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.Context(session, error, dependencies), error),
)
MessageReactionCountRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.MessageReactionCountUpdated,
) -> Result(bot.Context(session, error, dependencies), error),
)
ChatMemberUpdatedRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(bot.Context(session, error, dependencies), error),
)
MyChatMemberUpdatedRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(bot.Context(session, error, dependencies), error),
)
ChatJoinRequestRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.ChatJoinRequest,
) -> Result(bot.Context(session, error, dependencies), error),
)
ChatBoostRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.ChatBoostUpdated,
) -> Result(bot.Context(session, error, dependencies), error),
)
RemovedChatBoostRoute(
handler: fn(
bot.Context(session, error, dependencies),
types.ChatBoostRemoved,
) -> Result(bot.Context(session, error, dependencies), error),
)
UnknownUpdateRoute(
handler: fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.Context(session, error, dependencies), error),
)
CustomRoute(
matcher: fn(update.Update) -> Bool,
handler: fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.Context(session, error, dependencies), error),
)
FilteredRoute(
filter: Filter,
handler: fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.Context(session, error, dependencies), error),
)
}
Constructors
-
TextPatternRoute( pattern: Pattern, handler: fn(bot.Context(session, error, dependencies), String) -> Result( bot.Context(session, error, dependencies), error, ), ) -
PhotoRoute( handler: fn( bot.Context(session, error, dependencies), List(types.PhotoSize), ) -> Result(bot.Context(session, error, dependencies), error), ) -
VideoRoute( handler: fn( bot.Context(session, error, dependencies), types.Video, ) -> Result(bot.Context(session, error, dependencies), error), ) -
VoiceRoute( handler: fn( bot.Context(session, error, dependencies), types.Voice, ) -> Result(bot.Context(session, error, dependencies), error), ) -
AudioRoute( handler: fn( bot.Context(session, error, dependencies), types.Audio, ) -> Result(bot.Context(session, error, dependencies), error), ) -
MediaGroupRoute( handler: fn( bot.Context(session, error, dependencies), String, List(types.Message), ) -> Result(bot.Context(session, error, dependencies), error), ) -
WebAppDataRoute( handler: fn( bot.Context(session, error, dependencies), types.WebAppData, ) -> Result(bot.Context(session, error, dependencies), error), )Data sent by a Mini App through
Telegram.WebApp.sendData. -
EditedMessageRoute( handler: fn( bot.Context(session, error, dependencies), types.Message, ) -> Result(bot.Context(session, error, dependencies), error), )A message the user edited.
-
ChannelPostRoute( handler: fn( bot.Context(session, error, dependencies), types.Message, ) -> Result(bot.Context(session, error, dependencies), error), )A post in a channel the bot administers.
-
EditedChannelPostRoute( handler: fn( bot.Context(session, error, dependencies), types.Message, ) -> Result(bot.Context(session, error, dependencies), error), )An edited channel post.
-
BusinessMessageRoute( handler: fn( bot.Context(session, error, dependencies), types.Message, ) -> Result(bot.Context(session, error, dependencies), error), )A message from a connected business account’s chat.
-
InlineQueryRoute( handler: fn( bot.Context(session, error, dependencies), types.InlineQuery, ) -> Result(bot.Context(session, error, dependencies), error), ) -
ChosenInlineResultRoute( handler: fn( bot.Context(session, error, dependencies), types.ChosenInlineResult, ) -> Result(bot.Context(session, error, dependencies), error), ) -
ShippingQueryRoute( handler: fn( bot.Context(session, error, dependencies), types.ShippingQuery, ) -> Result(bot.Context(session, error, dependencies), error), ) -
PreCheckoutQueryRoute( handler: fn( bot.Context(session, error, dependencies), types.PreCheckoutQuery, ) -> Result(bot.Context(session, error, dependencies), error), ) -
PaidMediaPurchaseRoute( handler: fn( bot.Context(session, error, dependencies), types.PaidMediaPurchased, ) -> Result(bot.Context(session, error, dependencies), error), )A paid media purchase, for
sendPaidMediawith a payload. -
PollRoute( handler: fn( bot.Context(session, error, dependencies), types.Poll, ) -> Result(bot.Context(session, error, dependencies), error), ) -
PollAnswerRoute( handler: fn( bot.Context(session, error, dependencies), types.PollAnswer, ) -> Result(bot.Context(session, error, dependencies), error), ) -
MessageReactionRoute( handler: fn( bot.Context(session, error, dependencies), types.MessageReactionUpdated, ) -> Result(bot.Context(session, error, dependencies), error), ) -
MessageReactionEmojiRoute( emojis: List(String), handler: fn( bot.Context(session, error, dependencies), types.MessageReactionUpdated, ) -> Result(bot.Context(session, error, dependencies), error), ) -
MessageReactionPaidRoute( handler: fn( bot.Context(session, error, dependencies), types.MessageReactionUpdated, ) -> Result(bot.Context(session, error, dependencies), error), ) -
MessageReactionAddedRoute( handler: fn( bot.Context(session, error, dependencies), types.MessageReactionUpdated, ) -> Result(bot.Context(session, error, dependencies), error), ) -
MessageReactionRemovedRoute( handler: fn( bot.Context(session, error, dependencies), types.MessageReactionUpdated, ) -> Result(bot.Context(session, error, dependencies), error), ) -
MessageReactionCountRoute( handler: fn( bot.Context(session, error, dependencies), types.MessageReactionCountUpdated, ) -> Result(bot.Context(session, error, dependencies), error), ) -
ChatMemberUpdatedRoute( handler: fn( bot.Context(session, error, dependencies), types.ChatMemberUpdated, ) -> Result(bot.Context(session, error, dependencies), error), ) -
MyChatMemberUpdatedRoute( handler: fn( bot.Context(session, error, dependencies), types.ChatMemberUpdated, ) -> Result(bot.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( bot.Context(session, error, dependencies), types.ChatJoinRequest, ) -> Result(bot.Context(session, error, dependencies), error), ) -
ChatBoostRoute( handler: fn( bot.Context(session, error, dependencies), types.ChatBoostUpdated, ) -> Result(bot.Context(session, error, dependencies), error), )A chat boost was added or changed.
-
RemovedChatBoostRoute( handler: fn( bot.Context(session, error, dependencies), types.ChatBoostRemoved, ) -> Result(bot.Context(session, error, dependencies), error), )A chat boost was removed.
-
UnknownUpdateRoute( handler: fn( bot.Context(session, error, dependencies), update.Update, ) -> Result(bot.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( bot.Context(session, error, dependencies), update.Update, ) -> Result(bot.Context(session, error, dependencies), error), ) -
FilteredRoute( filter: Filter, handler: fn( bot.Context(session, error, dependencies), update.Update, ) -> Result(bot.Context(session, error, dependencies), error), )
A leaf router: routes, middleware, a catch handler and an optional scope.
Every on_* function registers on one of these. Compositions live in
RouterTree — a separate type, so a registration on a
composition is a compile error rather than a route that quietly goes
nowhere.
pub opaque type Router(session, error, dependencies)
An ordered composition of leaf routers.
A tree holds no routes of its own: on_command and friends are not defined
for it. Build one with tree and add leaves with
append (always tried) or branch (tried only when a
filter matches). Branches are consulted in the order they were added, and
the first one that both passes its filter and has a route for the update
handles it.
pub opaque type RouterTree(session, error, dependencies)
pub type ShippingQueryHandler(session, error, dependencies) =
fn(
bot.Context(session, error, dependencies),
types.ShippingQuery,
) -> Result(bot.Context(session, error, dependencies), error)
pub type TextHandler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), String) -> Result(
bot.Context(session, error, dependencies),
error,
)
pub type VideoHandler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), types.Video) -> Result(
bot.Context(session, error, dependencies),
error,
)
pub type VoiceHandler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), types.Voice) -> Result(
bot.Context(session, error, dependencies),
error,
)
pub type WebAppDataHandler(session, error, dependencies) =
fn(bot.Context(session, error, dependencies), types.WebAppData) -> Result(
bot.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").
The result is deduplicated and sorted for stable output.
If the router 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(
tree: RouterTree(session, error, dependencies),
router: Router(session, error, dependencies),
) -> RouterTree(session, error, dependencies)
Add a leaf that is tried for every update, after the branches already added.
pub fn branch(
tree: RouterTree(session, error, dependencies),
when filter: Filter,
router router: Router(session, error, dependencies),
) -> RouterTree(session, error, dependencies)
Add a leaf that is only consulted when when matches the update.
router.tree()
|> 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),
) -> RouterTree(session, error, dependencies)
Compose two leaf routers into a tree. Both are tried in order.
pub fn compose_many(
routers: List(Router(session, error, dependencies)),
) -> RouterTree(session, error, dependencies)
Compose many leaf routers into a tree, tried in order.
pub fn fallback(
router: Router(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.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: bot.Context(session, error, dependencies),
update: update.Update,
) -> Result(bot.Context(session, error, dependencies), error)
Process an update through a leaf router.
pub fn handle_tree(
tree: RouterTree(session, error, dependencies),
ctx: bot.Context(session, error, dependencies),
update: update.Update,
) -> Result(bot.Context(session, error, dependencies), error)
Process an update through a tree: the first branch whose filter matches and which has a route for the update handles it, otherwise the tree fallback.
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: bot.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: bot.Context(session, error, dependencies),
) -> option.Option(String)
The name of the leaf router whose route claimed the current update — the
tree branch, for a bot built with router_tree.
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 leaf routers into one. All routes are combined, with the first router’s routes taking priority in case of conflicts. Middleware and catch handlers are shared.
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(bot.Context(session, error, dependencies), String) -> Result(
bot.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(
bot.Context(session, error, dependencies),
types.Audio,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
pub fn on_business_message(
router: Router(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
types.Message,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
String,
String,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.CallbackQuery,
data,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.Message,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.ChatBoostUpdated,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.ChatJoinRequest,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
pub fn on_chat_member_updated(
router: Router(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.ChosenInlineResult,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
pub fn on_command(
router: Router(session, error, dependencies),
command: String,
handler: fn(
bot.Context(session, error, dependencies),
update.Command,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
update.Command,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
update.Command,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.Message,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.Message,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.InlineQuery,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
pub fn on_media_group(
router: Router(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
String,
List(types.Message),
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.PaidMediaPurchased,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
Handle paid (star) reactions.
pub fn on_photo(
router: Router(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
List(types.PhotoSize),
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.Poll,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
pub fn on_poll_answer(
router: Router(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
types.PollAnswer,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
pub fn on_pre_checkout_query(
router: Router(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
types.PreCheckoutQuery,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
pub fn on_reaction(
router: Router(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.MessageReactionCountUpdated,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.MessageReactionUpdated,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.ChatBoostRemoved,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.ShippingQuery,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
pub fn on_text(
router: Router(session, error, dependencies),
pattern: Pattern,
handler: fn(bot.Context(session, error, dependencies), String) -> Result(
bot.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(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.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(
bot.Context(session, error, dependencies),
types.Video,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
pub fn on_voice(
router: Router(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
types.Voice,
) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)
pub fn on_web_app_data(
router: Router(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
types.WebAppData,
) -> Result(bot.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. 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 leaf 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 tree() -> RouterTree(session, error, dependencies)
An empty tree. Add leaves with append and branch.
pub fn tree_allowed_updates(
tree: RouterTree(session, error, dependencies),
) -> List(String)
The union of every branch’s derived set. One branch that gives up on narrowing gives up for the whole tree — as does a tree fallback.
pub fn tree_fallback(
tree: RouterTree(session, error, dependencies),
handler: fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.Context(session, error, dependencies), error),
) -> RouterTree(session, error, dependencies)
A handler for updates no branch claimed.
pub fn tree_name(
tree: RouterTree(session, error, dependencies),
) -> String
The tree’s name: its branch names joined with +.
pub fn tree_registered_commands(
tree: RouterTree(session, error, dependencies),
) -> List(#(String, String))
The union of every branch’s described commands, sorted by command name. Earlier branches win a duplicate.
pub fn tree_routable(
tree: RouterTree(session, error, dependencies),
) -> Routable(session, error, dependencies)
Reduce a tree to what the bot needs from it.
pub fn use_middleware(
router: Router(session, error, dependencies),
middleware: fn(
fn(bot.Context(session, error, dependencies), update.Update) -> Result(
bot.Context(session, error, dependencies),
error,
),
) -> fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.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 use_middleware_on_tree(
tree: RouterTree(session, error, dependencies),
middleware: fn(
fn(bot.Context(session, error, dependencies), update.Update) -> Result(
bot.Context(session, error, dependencies),
error,
),
) -> fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.Context(session, error, dependencies), error),
) -> RouterTree(session, error, dependencies)
Push middleware into every branch. Each branch keeps its own copy, applied around its own handlers, so a branch’s catch handler still sees its errors.
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(
bot.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_catch_handler_on_tree(
tree: RouterTree(session, error, dependencies),
catch_handler: fn(error) -> Result(
bot.Context(session, error, dependencies),
error,
),
) -> RouterTree(session, error, dependencies)
Give every branch the same catch handler. A branch that already has one keeps it.
pub fn with_filter(
predicate: fn(update.Update) -> Bool,
handler: fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.Context(session, error, dependencies), error),
) -> fn(bot.Context(session, error, dependencies), update.Update) -> Result(
bot.Context(session, error, dependencies),
error,
)
Filter middleware - only process updates that match predicate
pub fn with_logging(
handler: fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.Context(session, error, dependencies), error),
) -> fn(bot.Context(session, error, dependencies), update.Update) -> Result(
bot.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(bot.Context(session, error, dependencies)) -> Result(
bot.Context(session, error, dependencies),
error,
),
) -> fn(
fn(bot.Context(session, error, dependencies), update.Update) -> Result(
bot.Context(session, error, dependencies),
error,
),
) -> fn(bot.Context(session, error, dependencies), update.Update) -> Result(
bot.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(
bot.Context(session, error, dependencies),
error,
),
handler: fn(
bot.Context(session, error, dependencies),
update.Update,
) -> Result(bot.Context(session, error, dependencies), error),
) -> fn(bot.Context(session, error, dependencies), update.Update) -> Result(
bot.Context(session, error, dependencies),
error,
)
Error recovery middleware