Skip to content

Latest commit

 

History

History
277 lines (206 loc) · 8.46 KB

File metadata and controls

277 lines (206 loc) · 8.46 KB

Router

As your bot grows, the handle/2 function can become a long chain of pattern-match clauses — commands, callback queries, text handlers, inline queries, all interleaved in a single module. When you add conversation flows or role-based access, the clause count explodes and it becomes hard to see the overall structure at a glance.

ExGram.Router replaces those hand-written clauses with a declarative scope / filter / handle DSL. You describe what each handler should match, and the router takes care of dispatching. Everything compiles down to a standard handle/2 function, so the rest of ExGram (middlewares, DSL, testing) works exactly the same.

This guide covers the most common usage. For the full API reference and advanced options, see the ExGram.Router HexDocs.

Installation

Add ex_gram_router to your dependencies:

# mix.exs
def deps do
  [
    {:ex_gram, "~> 0.65"},
    {:ex_gram_router, "~> 0.1.0"},
    {:jason, ">= 1.0.0"},
    {:req, "~> 0.5"}
  ]
end

Then add :ex_gram_router to the formatter deps alongside :ex_gram:

# .formatter.exs
[
  import_deps: [:ex_gram, :ex_gram_router]
]

From handle/2 to Scopes

Here's a typical bot using plain handle/2:

defmodule MyBot do
  use ExGram.Bot, name: :my_bot, setup_commands: true

  command("start", description: "Start the bot")
  command("help",  description: "Show help")

  def handle({:command, :start, _}, ctx), do: answer(ctx, "Welcome!")
  def handle({:command, :help, _}, ctx),  do: answer(ctx, "Here is what I can do…")
  def handle({:text, _, _}, ctx),         do: answer(ctx, "You said something!")
  def handle(_, ctx),                     do: ctx
end

The same bot with ExGram.Router:

defmodule MyBot do
  use ExGram.Bot, name: :my_bot, setup_commands: true
  use ExGram.Router

  command("start", description: "Start the bot")
  command("help",  description: "Show help")

  scope do
    filter :command, :start
    handle &MyBot.Handlers.start/1
  end

  scope do
    filter :command, :help
    handle &MyBot.Handlers.help/1
  end

  scope do
    filter :text
    handle &MyBot.Handlers.echo/2
  end

  # Catch-all fallback
  scope do
    handle &MyBot.Handlers.fallback/1
  end
end
defmodule MyBot.Handlers do
  import ExGram.Dsl

  def start(ctx), do: answer(ctx, "Welcome!")
  def help(ctx),  do: answer(ctx, "Here is what I can do…")

  # 2-arity: receives (update_info, context) to extract the message text directly
  def echo({:text, text, _msg}, ctx), do: answer(ctx, text)

  def fallback(ctx), do: ctx
end

Notice that handlers live in their own module. This is optional — you can keep them inline — but separating handlers from routing keeps both sides clean as the bot grows.

Handler Arities

Handlers can be 1-arity or 2-arity:

# 1-arity: receives only context
def start(context) do
  answer(context, "Welcome!")
end

# 2-arity: receives (update_info, context)
def echo({:text, text, _msg}, context) do
  answer(context, text)
end

The router detects the arity at compile time. Use 2-arity when you need to extract data from the parsed update tuple directly.

How dispatch works

  1. Scopes are tried top-to-bottom in declaration order.
  2. All filters in a scope must pass (AND logic).
  3. The first matching leaf wins — its handler runs and dispatch stops.
  4. A scope with no filters matches everything, so a filter-less scope at the bottom acts as a fallback.

Built-in Filters

The router ships with filters for the most common update types. Use them by alias name:

# Commands
filter :command              # any command
filter :command, :start      # specific command

# Text messages
filter :text                 # any text
filter :text, "hello"        # exact match
filter :text, ~r/^\d+$/      # regex match
filter :text, prefix: "!"    # starts with
filter :text, contains: "hi" # contains substring

# Callback queries
filter :callback_query                    # any callback
filter :callback_query, "confirm"         # exact data
filter :callback_query, ~r/^page_\d+$/   # regex match
filter :callback_query, prefix: "settings:" # prefix match

# Inline queries
filter :inline_query
filter :inline_query, prefix: "@"

# Media & other types
filter :photo
filter :document
filter :location
filter :sticker
filter :voice
filter :video
filter :animation
filter :audio
filter :contact
filter :poll
filter :video_note
filter :message    # any message-type update
filter :regex      # any named regex match

Each filter alias maps to a module under ExGram.Router.Filters.*. You never need to reference them directly, but you can if you prefer.

Nested Scopes

Scopes can be nested. A child scope only runs if its parent's filters already passed, so parent filters act as guards for all children:

scope do
  filter :callback_query, prefix: "settings:"

  scope do
    filter :callback_query, "settings:language"
    handle &MyBot.Handlers.settings_language/1
  end

  scope do
    filter :callback_query, "settings:timezone"
    handle &MyBot.Handlers.settings_timezone/1
  end
end

This is especially useful for callback queries with hierarchical data patterns. Instead of repeating the prefix in every leaf, you filter it once at the parent level.

Prefix Propagation

For deeply nested callback data, you can use propagate: true on a prefix filter. The matched prefix is stripped and child scopes match against the remainder:

scope do
  filter :callback_query, prefix: "proj:", propagate: true

  scope do
    filter :callback_query, "change"   # matches "proj:change"
    handle &MyBot.Handlers.change_project/1
  end

  scope do
    filter :callback_query, prefix: "settings:", propagate: true

    scope do
      filter :callback_query, "volume"   # matches "proj:settings:volume"
      handle &MyBot.Handlers.volume/1
    end
  end
end

Propagation stacks across nesting levels, so you can model arbitrary callback data hierarchies cleanly.

Custom Filters

When the built-in filters aren't enough, you can implement the ExGram.Router.Filter behaviour to encode any runtime predicate - user roles, conversation state, feature flags, and more. See the Custom Filters section in the ExGram.Router documentation for the full guide including examples and alias registration.

Inspecting Routes

The router ships with two mix tasks for visualizing your bot's routing configuration:

mix ex_gram.router.tree

Prints the full scope tree with indentation:

$ mix ex_gram.router.tree MyBot

MyBot routing tree:
├── scope
│   ├── filters: [Command(:start)]
│   └── handle: &MyBot.Handlers.start/1
├── scope
│   ├── filters: [Command(:help)]
│   └── handle: &MyBot.Handlers.help/1
└── scope
    ├── filters: [CallbackQuery([prefix: "proj:"]) [propagate]]
    ├── scope
    │   ├── filters: [CallbackQuery("change")]
    │   └── handle: &MyBot.Handlers.change_project/1
    └── scope
        ├── filters: [CallbackQuery("delete")]
        └── handle: &MyBot.Handlers.delete_project/1

mix ex_gram.router.flat

Prints one line per handler with the full accumulated filter chain, similar to mix phx.routes in Phoenix:

$ mix ex_gram.router.flat MyBot

MyBot handlers:
MyBot.Handlers  start/1          filters: [Command(:start)]
MyBot.Handlers  help/1           filters: [Command(:help)]
MyBot.Handlers  change_project/1 filters: [CallbackQuery([prefix: "proj:"]) [propagate], CallbackQuery("change")]
MyBot.Handlers  delete_project/1 filters: [CallbackQuery([prefix: "proj:"]) [propagate], CallbackQuery("delete")]
MyBot.Handlers  fallback/1       filters: []

These are invaluable for debugging routing issues or onboarding new team members.

Testing

The router generates a standard handle/2 function, so testing works exactly like any other ExGram bot. Use ExGram.Adapter.Test, push updates, and assert on outgoing API calls. No special setup needed.

See the Testing guide for details.

Next Steps

  • FSM - Add finite state machine conversation flows (works great with the router)
  • Middlewares - Enrich context with data your filters need
  • Handling Updates - Understand the update tuples that filters match against
  • Cheatsheet - Quick reference for common patterns