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))
  reply.with_text(ctx, "Sorry, an error occurred")
})

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) =
  fn(bot.Context(session, error), types.Audio) -> Result(
    bot.Context(session, error),
    error,
  )
pub type CallbackHandler(session, error) =
  fn(bot.Context(session, error), String, String) -> Result(
    bot.Context(session, error),
    error,
  )
pub type CommandHandler(session, error) =
  fn(bot.Context(session, error), update.Command) -> Result(
    bot.Context(session, error),
    error,
  )

Filter type for composable update filtering

pub opaque type Filter

Generic handler type for all updates

pub type Handler(session, error) =
  fn(bot.Context(session, error), update.Update) -> Result(
    bot.Context(session, error),
    error,
  )
pub type MediaGroupHandler(session, error) =
  fn(bot.Context(session, error), String, List(types.Message)) -> Result(
    bot.Context(session, error),
    error,
  )
pub type MessageHandler(session, error) =
  fn(bot.Context(session, error), types.Message) -> Result(
    bot.Context(session, error),
    error,
  )

Middleware wraps a handler with additional functionality

pub type Middleware(session, error) =
  fn(
    fn(bot.Context(session, error), update.Update) -> Result(
      bot.Context(session, error),
      error,
    ),
  ) -> fn(bot.Context(session, error), update.Update) -> Result(
    bot.Context(session, error),
    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) =
  fn(bot.Context(session, error), List(types.PhotoSize)) -> Result(
    bot.Context(session, error),
    error,
  )

Unified route type that encompasses all route types

pub type Route(session, error) {
  TextPatternRoute(
    pattern: Pattern,
    handler: fn(bot.Context(session, error), String) -> Result(
      bot.Context(session, error),
      error,
    ),
  )
  PhotoRoute(
    handler: fn(
      bot.Context(session, error),
      List(types.PhotoSize),
    ) -> Result(bot.Context(session, error), error),
  )
  VideoRoute(
    handler: fn(bot.Context(session, error), types.Video) -> Result(
      bot.Context(session, error),
      error,
    ),
  )
  VoiceRoute(
    handler: fn(bot.Context(session, error), types.Voice) -> Result(
      bot.Context(session, error),
      error,
    ),
  )
  AudioRoute(
    handler: fn(bot.Context(session, error), types.Audio) -> Result(
      bot.Context(session, error),
      error,
    ),
  )
  MediaGroupRoute(
    handler: fn(
      bot.Context(session, error),
      String,
      List(types.Message),
    ) -> Result(bot.Context(session, error), error),
  )
  CustomRoute(
    matcher: fn(update.Update) -> Bool,
    handler: fn(bot.Context(session, error), update.Update) -> Result(
      bot.Context(session, error),
      error,
    ),
  )
  FilteredRoute(
    filter: Filter,
    handler: fn(bot.Context(session, error), update.Update) -> Result(
      bot.Context(session, error),
      error,
    ),
  )
}

Constructors

Router with unified routes and middleware support

pub opaque type Router(session, error)
pub type TextHandler(session, error) =
  fn(bot.Context(session, error), String) -> Result(
    bot.Context(session, error),
    error,
  )
pub type VideoHandler(session, error) =
  fn(bot.Context(session, error), types.Video) -> Result(
    bot.Context(session, error),
    error,
  )
pub type VoiceHandler(session, error) =
  fn(bot.Context(session, error), types.Voice) -> Result(
    bot.Context(session, error),
    error,
  )

Values

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),
  second: Router(session, error),
) -> Router(session, error)

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)),
) -> Router(session, error)

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

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

pub fn is_media_group() -> Filter

Filter for media group messages

pub fn is_private_chat() -> Filter

Filter for private chats

pub fn is_text() -> Filter

Filter for text messages

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

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)

Create a new router

pub fn not(f: Filter) -> Filter

Negate a filter

pub fn on_any_text(
  router: Router(session, error),
  handler: fn(bot.Context(session, error), String) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)

Add a handler for any text

pub fn on_audio(
  router: Router(session, error),
  handler: fn(bot.Context(session, error), types.Audio) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)
pub fn on_callback(
  router: Router(session, error),
  pattern: Pattern,
  handler: fn(bot.Context(session, error), String, String) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)

Add a callback query handler with pattern

pub fn on_command(
  router: Router(session, error),
  command: String,
  handler: fn(bot.Context(session, error), update.Command) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)

Add a command handler

pub fn on_commands(
  router: Router(session, error),
  commands: List(String),
  handler: fn(bot.Context(session, error), update.Command) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)

Add multiple commands with same handler

pub fn on_custom(
  router: Router(session, error),
  matcher: fn(update.Update) -> Bool,
  handler: fn(bot.Context(session, error), update.Update) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)

Add a custom route with matcher function

pub fn on_filtered(
  router: Router(session, error),
  filter: Filter,
  handler: fn(bot.Context(session, error), update.Update) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)

Add a filtered route

pub fn on_media_group(
  router: Router(session, error),
  handler: fn(
    bot.Context(session, error),
    String,
    List(types.Message),
  ) -> Result(bot.Context(session, error), error),
) -> Router(session, error)

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

pub fn on_photo(
  router: Router(session, error),
  handler: fn(bot.Context(session, error), List(types.PhotoSize)) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)

Add handlers for media types

pub fn on_text(
  router: Router(session, error),
  pattern: Pattern,
  handler: fn(bot.Context(session, error), String) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)

Add a text handler with pattern

pub fn on_video(
  router: Router(session, error),
  handler: fn(bot.Context(session, error), types.Video) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)
pub fn on_voice(
  router: Router(session, error),
  handler: fn(bot.Context(session, error), types.Voice) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)
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 scope(
  router: Router(session, error),
  predicate: fn(update.Update) -> Bool,
) -> Router(session, error)

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

Add middleware to the router

pub fn with_catch_handler(
  router: Router(session, error),
  catch_handler: fn(error) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> Router(session, error)

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

Filter middleware - only process updates that match predicate

pub fn with_logging(
  handler: fn(bot.Context(session, error), update.Update) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> fn(bot.Context(session, error), update.Update) -> Result(
  bot.Context(session, error),
  error,
)

Logging middleware - logs update processing

pub fn with_recovery(
  recover: fn(error) -> Result(bot.Context(session, error), error),
  handler: fn(bot.Context(session, error), update.Update) -> Result(
    bot.Context(session, error),
    error,
  ),
) -> fn(bot.Context(session, error), update.Update) -> Result(
  bot.Context(session, error),
  error,
)

Error recovery middleware

Search Document