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:
- Commands - Exact command matches (e.g., “/start”, “/help”)
- Callback Queries - Callback data patterns
- Custom Routes - User-defined matchers
- Media Routes - Photo, video, voice, audio handlers
- Text Routes - Text pattern matching
- Fallback - Catch-all handler for unmatched updates
Within each category, routes are tried in the order they were added, with the first matching route handling the update.
Pattern Matching
Text and callback queries support flexible pattern matching:
router
|> router.on_text(Exact("hello"), handle_hello)
|> router.on_text(Prefix("search:"), handle_search)
|> router.on_text(Contains("help"), handle_help_mention)
|> router.on_text(Suffix("?"), handle_question)
router
|> router.on_callback(Prefix("page:"), handle_pagination)
|> router.on_callback(Exact("cancel"), handle_cancel)
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:
with_logging
- Logs all update processingwith_filter
- Conditionally processes updateswith_recovery
- Recovers from handler errors
Error Handling
Routers support catch handlers to gracefully handle errors from routes:
router
|> router.with_catch_handler(fn(error) {
log.error("Route error: " <> string.inspect(error))
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:
is_text()
- Text messagesis_command()
- Command messageshas_photo()
- Photo messageshas_video()
- Video messageshas_media()
- Any media (photo, video, audio, voice)is_media_group()
- Media group/album messagesis_callback_query()
- Callback button presses
Text Content Filters:
text_equals(text)
- Exact text matchtext_starts_with(prefix)
- Text starts with prefixtext_contains(substring)
- Text contains substringcommand_equals(cmd)
- Specific command
User/Chat Filters:
from_user(user_id)
- From specific userfrom_users(user_ids)
- From any of the usersin_chat(chat_id)
- In specific chatis_private_chat()
- Private messages onlyis_group_chat()
- Group/supergroup messages only
Callback Query Filters:
callback_data_starts_with(prefix)
- Callback data prefix
Filter Composition:
and(filters)
/and2(f1, f2)
- All filters must matchor(filters)
/or2(f1, f2)
- Any filter must matchnot(filter)
- Negate a filterfilter(name, check_fn)
- Custom filter function
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:
CommandHandler
- Receives parsed command with argumentsTextHandler
- Receives text stringCallbackHandler
- Receives callback query ID and dataPhotoHandler
- Receives list of photo sizesVideoHandler
- Receives video objectVoiceHandler
- Receives voice messageAudioHandler
- Receives audio fileMediaGroupHandler
- Receives media group ID and list of messagesHandler
- Generic handler for any update type
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,
)
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
-
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, ), )
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 callback_data_starts_with(prefix: String) -> Filter
Filter for callback data that starts with prefix
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 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 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 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 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