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:

  1. Commands - Exact command matches (e.g., “/start”, “/help”)
  2. Callback Queries - Callback data patterns
  3. Custom Routes - User-defined matchers
  4. Media Routes - Photo, video, voice, audio handlers
  5. Text Routes - Text pattern matching
  6. 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:

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.

FilterReadsTrue when
is_text()update kindthe update is a plain text message
text_equals(t)textthe text is exactly t
text_starts_with(p)textthe text starts with p
text_contains(s)textthe text contains s
is_command()update kindthe update is a command
command_equals(c)commandthe command is c (leading / optional)
from_user(id)update.from_idthe sender is id
from_users(ids)update.from_idthe sender is one of ids
in_chat(id)update.chat_idthe update happened in chat id
from_chats(ids)update.chat_idthe 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 kindthe message carries photos
has_video()update kindthe message carries a video
is_media_group()update kindthe update is a buffered album
has_media()update kindphoto, video, voice, audio or album
is_callback_query()update kindthe update is a button press
callback_data_starts_with(p)query.datathe callback payload starts with p
is_forwarded()message.forward_originthe message was forwarded
is_reply()message.reply_to_messagethe message replies to another
in_topic(id)message.message_thread_idthe message is in forum topic id
has_entity(kind)message.entities + caption_entitiesan entity of that type is present (e.g. "url", "mention")
via_bot()message.via_botthe message was sent through an inline bot
via_bot_id(id)message.via_bot.idit was sent through bot id
is_automatic_forward()message.is_automatic_forwarda channel post auto-forwarded to its discussion group
has_media_spoiler()message.has_media_spoilerthe 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:

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,
  )

Filter type for composable update filtering

pub opaque type Filter

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_updates set; [] means “do not restrict”.

    registered_commands

    #(command, description) pairs for setMyCommands.

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

A router: routes, middleware, a catch handler, an optional scope, and the routers it consults in order (append / branch) when none of its own routes claims an update.

pub opaque type Router(session, error, dependencies)
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 and(filters: List(Filter)) -> Filter

Combine filters with AND logic

pub fn and2(left: Filter, right: Filter) -> Filter

Combine two filters with AND logic

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 command_equals(cmd: String) -> Filter

Filter for specific command

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 from_user(user_id: Int) -> Filter

Filter by user ID

pub fn from_users(user_ids: List(Int)) -> Filter

Filter by multiple user IDs

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() -> Filter

Filter for media (photo, video, audio, voice)

pub fn has_media_spoiler() -> Filter

The message’s media is covered by a spoiler animation.

pub fn has_photo() -> Filter

Filter for photo messages

pub fn has_video() -> Filter

Filter for video messages

pub fn in_chat(chat_id: Int) -> Filter

Filter by chat ID

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_callback_query() -> Filter

Filter for callback queries

pub fn is_command() -> Filter

Filter for commands

pub fn is_forwarded() -> Filter

The message was forwarded from somewhere else.

pub fn is_group_chat() -> Filter

Filter for group and supergroup chats. Channels are neither — use chat_type for those.

pub fn is_media_group() -> Filter

Filter for media group messages

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 is_reply() -> Filter

The message is a reply to another message.

pub fn is_text() -> Filter

Filter for text messages

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 new(name: String) -> Router(session, error, dependencies)

Create a new leaf router

pub fn not(f: Filter) -> Filter

Negate a filter

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 or(filters: List(Filter)) -> Filter

Combine filters with OR logic

pub fn or2(left: Filter, right: Filter) -> Filter

Combine two filters with OR logic

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() -> Filter

The message was sent through an inline bot.

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.") },
))
✨ Search Document