# 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