Packages
ex_gram
0.65.0
0.67.0
0.66.0
0.65.0
0.64.0
0.63.0
0.62.0
0.61.0
0.60.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.1
0.55.0
0.54.0
0.53.0
0.52.2
0.52.1
0.52.0
0.51.1
0.51.0
0.50.2
0.50.1
0.50.0
0.41.0
0.40.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.0
0.5.0-rc6
0.5.0-rc5
0.5.0-rc4
0.5.0-rc3
0.5.0-rc2
Telegram Bot API low level and framework
Current section
Files
Jump to
Current section
Files
guides/middlewares.md
# Middlewares
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.Middleware` behaviour
- 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.
```elixir
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
end
```
**Why 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:
```elixir
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
end
```
### Module-based Middleware
For more complex logic, implement the `ExGram.Middleware` behaviour:
```elixir
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
end
```
Use it in your bot:
```elixir
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
end
```
## Halting the Middleware Chain
Set `halted: true` to stop processing:
```elixir
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
end
```
When `halted: true`, no further middlewares or handlers execute.
### `middleware_halted` vs `halted`
- `middleware_halted: true` - Stop middleware chain, but run handler
- `halted: true` - Stop everything (middlewares + handler)
## Enriching Context with Extra Data
Add custom data to `context.extra` for use in handlers:
```elixir
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
end
```
Use in handlers:
```elixir
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
end
```
## Command and Regex Macros
The `command/2` and `regex/2` macros are actually middleware builders:
```elixir
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!")
end
```
With `setup_commands: true`, commands are automatically registered with Telegram's BotFather menu.
## Multiple Middlewares
Middlewares execute in the order they're defined:
```elixir
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
end
```
If middleware 2 halts, middleware 3 and the handler don't run.
## Common Middleware Patterns
### Rate Limiting
```elixir
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
end
```
### Language Detection
```elixir
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
end
```
### Command Analytics
```elixir
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: nil
```
## Testing Middlewares
Test middlewares by creating a context and calling them:
```elixir
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
end
```
## Next Steps
- [Handling Updates](handling-updates.md) - Understanding handlers
- [Sending Messages](sending-messages.md) - DSL for building responses
- [Low-Level API](low-level-api.md) - Direct API calls for complex scenarios
- [Multiple Bots](multiple-bots.md) - Running multiple bots