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.

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)

Middleware System

Middleware allows you to wrap handlers with additional functionality. Middleware is applied in reverse order of addition (last added runs first):

router
|> router.use_middleware(router.with_logging)
|> router.use_middleware(auth_middleware)
|> router.use_middleware(rate_limit_middleware)

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.

Router Composition

Routers can be composed to build complex routing structures:

Merging Routers

merge combines two 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

compose creates a router that tries each sub-router in sequence. Each router maintains its own middleware and error handling:

let public_router =
  router.new("public")
  |> router.use_middleware(rate_limiting)
  |> router.on_command("start", handle_start)

let private_router =
  router.new("private")
  |> router.use_middleware(auth_required)
  |> router.on_command("admin", handle_admin)

let app = router.compose(private_router, public_router)

Scoped Routing

scope creates a sub-router that only processes updates matching a predicate:

let admin_router =
  router.new("admin")
  |> router.on_command("ban", handle_ban)
  |> router.scope(fn(update) {
    // Only process updates from admin users
    case update {
      update.CommandUpdate(from_id: id, ..) -> is_admin(id)
      _ -> False
    }
  })

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
)

// OR logic for multiple conditions
router
|> router.on_filtered(
  router.or([
    router.text_equals("help"),
    router.text_equals("?"),
    router.command_equals("help")
  ]),
  show_help
)

Available Filters

Message Type Filters:

Text Content Filters:

User/Chat Filters:

Callback Query Filters:

Filter Composition:

Advanced Features

Multiple Command Handlers

Register the same handler for multiple commands:

router
|> router.on_commands(["start", "help", "about"], show_info)

Media Handling

Handle different media types with dedicated handlers:

router
|> router.on_photo(handle_photo)
|> router.on_video(handle_video)
|> router.on_voice(handle_voice_message)
|> router.on_audio(handle_audio_file)
|> router.on_media_group(handle_media_album)

Handler Types

The router provides type-safe handlers for different update types:

Types

pub type AudioHandler(session, error, dependencies) =
  fn(bot.Context(session, error, dependencies), types.Audio) -> 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 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,
  )

Filter type for composable update filtering

pub opaque type Filter

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)

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)

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),
  )
  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),
  )
  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),
  )
  ChatJoinRequestRoute(
    handler: fn(
      bot.Context(session, error, dependencies),
      types.ChatJoinRequest,
    ) -> 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

Router with unified routes and middleware support

pub opaque type Router(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,
  )

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, or filtered 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.

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 callback_data_starts_with(prefix: String) -> Filter

Filter for callback data that starts with prefix

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, where each router maintains its own middleware and catch handlers. First router is tried first, if it doesn’t handle the update, second router is tried.

pub fn compose_many(
  routers: List(Router(session, error, dependencies)),
) -> Router(session, error, dependencies)

Compose multiple routers into one. Routers are tried in order. Each router maintains its own middleware and catch handlers.

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 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: bot.Context(session, error, dependencies),
  update: update.Update,
) -> Result(bot.Context(session, error, dependencies), error)

Process an update through the router

pub fn has_media() -> Filter

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

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

Filter for callback queries

pub fn is_command() -> Filter

Filter for commands

pub fn is_group_chat() -> Filter

Filter for group chats https://core.telegram.org/api/bots%2Fids#supergroup-channel-ids

pub fn is_media_group() -> Filter

Filter for media group messages

pub fn is_private_chat() -> Filter

Filter for private chats https://core.telegram.org/api/bots%2Fids#user-ids

pub fn is_text() -> Filter

Filter for text messages

pub fn merge(
  first: Router(session, error, dependencies),
  second: Router(session, error, dependencies),
) -> Router(session, error, dependencies)

Merge two routers into one. All routes are combined, with first router’s routes taking priority in case of conflicts. Middleware and catch handlers are shared.

pub fn new(name: String) -> Router(session, error, dependencies)

Create a new router

pub fn not(f: Filter) -> Filter

Negate a filter

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

Add handler for chat join requests

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)

Add handler for chat member updates

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)

Add handler for chosen inline results

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: fn(update.Update) -> Bool,
  handler: fn(
    bot.Context(session, error, dependencies),
    update.Update,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Router(session, error, dependencies)

Add a custom route with matcher function

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)

Add a filtered route

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)

Add handler for inline queries

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)

Add handler for media groups (albums of photos/videos)

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)

Add handler for paid reactions (stars)

Example

router
|> router.on_paid_reaction(handle_star_reaction)
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)

Add handler for poll updates

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)

Add handler for poll answer updates

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)

Add handler for pre-checkout queries (payments)

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)

Add handler for message reactions

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)

Add handler for added reactions only (filters out removed reactions)

Example

router
|> router.on_reaction_added(handle_new_reaction)
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)

Add handler for message reaction count updates (anonymous reactions in channels)

Example

router
|> router.on_reaction_count(handle_reaction_counts)
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)

Add handler for a specific emoji reaction

Example

router
|> router.on_reaction_emoji("👍", handle_like)
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)

Add handler for multiple emoji reactions

Example

router
|> router.on_reaction_emojis(["👍", "❤", "🔥"], handle_positive_reactions)
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)

Add handler for removed reactions only (filters out added reactions)

Example

router
|> router.on_reaction_removed(handle_removed_reaction)
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)

Add handler for shipping queries (payments)

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_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 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. Commands added with on_command (no description) are omitted. Flattens composed routers, so a fully composed router reports the union of its sub-routers’ described commands.

This is what telega.with_auto_commands feeds into setMyCommands.

pub fn scope(
  router: Router(session, error, dependencies),
  predicate: fn(update.Update) -> Bool,
) -> Router(session, error, dependencies)

Create a sub-router that processes updates within its own scope

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

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_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

Search Document