Middlewares are functions that run before your handler processes an update. They're perfect for authentication, logging, rate limiting, or enriching the context with additional data.
What Are Middlewares?
A middleware is either:
- A module implementing the
ExGram.Middlewarebehaviour - A function with signature:
(Cnt.t(), opts :: any()) -> Cnt.t()
Middlewares receive the context, modify it, and return it. The modified context is passed to the next middleware or handler.
Using Built-in Middlewares
ExGram.Middleware.IgnoreUsername
This middleware strips the bot's username from commands, allowing both /start and /start@yourbot to work identically.
defmodule MyBot.Bot do
use ExGram.Bot, name: :my_bot
# Add middleware at module level
middleware(ExGram.Middleware.IgnoreUsername)
def handle({:command, "start", _}, context) do
# Handles both /start and /start@my_bot
answer(context, "Welcome!")
end
endWhy use this?
In group chats, users often mention the bot explicitly (/command@botname). This middleware normalizes commands.
Creating Custom Middlewares
Function-based Middleware
The simplest approach is a function:
defmodule MyBot.Bot do
use ExGram.Bot, name: :my_bot
# Define middleware function
middleware(&log_updates/2)
# Middleware function
def log_updates(context, _opts) do
user = extract_user(context)
update_type = extract_update_type(context)
Logger.info("Update from #{user.id}: #{update_type}")
context # Return context
end
def handle({:command, "start", _}, context) do
answer(context, "Hello!")
end
endModule-based Middleware
For more complex logic, implement the ExGram.Middleware behaviour:
defmodule MyBot.AuthMiddleware do
@behaviour ExGram.Middleware
def call(context, opts) do
user = ExGram.Dsl.extract_user(context)
if authorized?(user.id, opts) do
# User is authorized, continue
context
else
# User is not authorized, halt processing
ExGram.Dsl.answer(context, "⛔ Access denied")
|> Map.put(:halted, true)
end
end
defp authorized?(user_id, opts) do
allowed_users = Keyword.get(opts, :allowed_users, [])
user_id in allowed_users
end
endUse it in your bot:
defmodule MyBot.Bot do
use ExGram.Bot, name: :my_bot
# Pass options to middleware
middleware({MyBot.AuthMiddleware, [allowed_users: [123456, 789012]]})
def handle({:command, "admin", _}, context) do
# Only authorized users reach here
answer(context, "Admin panel: ...")
end
endHalting the Middleware Chain
Set halted: true to stop processing:
def call(context, _opts) do
if rate_limited?(context) do
context
|> ExGram.Dsl.answer("⏱️ Please wait before sending another command")
|> Map.put(:halted, true)
else
context
end
endWhen halted: true, no further middlewares or handlers execute.
middleware_halted vs halted
middleware_halted: true- Stop middleware chain, but run handlerhalted: true- Stop everything (middlewares + handler)
Enriching Context with Extra Data
Add custom data to context.extra for use in handlers:
defmodule MyBot.UserDataMiddleware do
@behaviour ExGram.Middleware
def call(context, _opts) do
user = ExGram.Dsl.extract_user(context)
user_data = fetch_user_from_database(user.id)
# Add to context.extra
ExGram.Cnt.add_extra(context, %{
user_role: user_data.role,
user_premium: user_data.premium?,
user_lang: user_data.language
})
end
defp fetch_user_from_database(user_id) do
# Database lookup
%{role: :user, premium?: false, language: "en"}
end
endUse in handlers:
def handle({:command, "premium_feature", _}, context) do
if context.extra[:user_premium] do
answer(context, "✨ Premium feature unlocked!")
else
answer(context, "⭐ This feature requires premium")
end
endCommand and Regex Macros
The command/2 and regex/2 macros are actually middleware builders:
defmodule MyBot.Bot do
use ExGram.Bot, name: :my_bot
# These register commands and patterns
command("start", description: "Start the bot")
command("help", description: "Get help")
regex(:email, ~r/\b[A-Za-z0-9._%+-]+@/)
regex(:url, ~r|https?://[^\s]+|)
# Handlers match against registered commands/patterns
def handle({:command, "start", _}, context), do: answer(context, "Hi!")
def handle({:regex, :email, _}, context), do: answer(context, "Found an email!")
endWith setup_commands: true, commands are automatically registered with Telegram's BotFather menu.
Multiple Middlewares
Middlewares execute in the order they're defined:
defmodule MyBot.Bot do
use ExGram.Bot, name: :my_bot
# Execution order: 1 → 2 → 3 → handler
middleware(&log_middleware/2) # 1
middleware(MyBot.AuthMiddleware) # 2
middleware(ExGram.Middleware.IgnoreUsername) # 3
def handle({:command, "start", _}, context) do
answer(context, "Hello!")
end
endIf middleware 2 halts, middleware 3 and the handler don't run.
Common Middleware Patterns
Rate Limiting
defmodule MyBot.RateLimitMiddleware do
@behaviour ExGram.Middleware
def call(context, opts) do
user_id = ExGram.Dsl.extract_user(context).id
limit = Keyword.get(opts, :per_minute, 10)
case check_rate_limit(user_id, limit) do
:ok ->
context
{:error, retry_after} ->
context
|> ExGram.Dsl.answer("⏱️ Rate limited. Try again in #{retry_after}s")
|> Map.put(:halted, true)
end
end
defp check_rate_limit(user_id, limit) do
# Check Redis/ETS for request count
:ok
end
endLanguage Detection
defmodule MyBot.LanguageMiddleware do
@behaviour ExGram.Middleware
def call(context, _opts) do
user = ExGram.Dsl.extract_user(context)
# Detect from user's Telegram language or database
lang = user.language_code || "en"
ExGram.Cnt.add_extra(context, %{language: lang})
end
endCommand Analytics
def analytics_middleware(context, _opts) do
case ExGram.Dsl.extract_update_type(context) do
:message ->
if command = extract_command(context) do
track_command(command)
end
_ -> :ok
end
context
end
defp extract_command(%{update: %{message: %{text: "/" <> cmd}}}), do: cmd
defp extract_command(_), do: nilTesting Middlewares
Test middlewares by creating a context and calling them:
defmodule MyBot.AuthMiddlewareTest do
use ExUnit.Case
test "allows authorized users" do
context = build_context(user_id: 123456)
opts = [allowed_users: [123456]]
result = MyBot.AuthMiddleware.call(context, opts)
refute result.halted
end
test "blocks unauthorized users" do
context = build_context(user_id: 999999)
opts = [allowed_users: [123456]]
result = MyBot.AuthMiddleware.call(context, opts)
assert result.halted
end
endNext Steps
- Handling Updates - Understanding handlers
- Sending Messages - DSL for building responses
- Low-Level API - Direct API calls for complex scenarios
- Multiple Bots - Running multiple bots