# Bot Init Hooks Bot Init hooks allow you to run custom code during the bot's startup sequence, before it begins processing updates. They are the right place for initialization that depends on the bot's token or identity - fetching external configuration, pre-warming caches, validating credentials, or anything else that must succeed before the bot can function. ## Overview A Bot Init hook is a module that implements the `ExGram.BotInit` behaviour. The single callback `on_bot_init/1` is called once during startup. Each hook can pass data forward to subsequent hooks and to every handler call via `context.extra`. Hooks run in this sequence during startup: 1. `ExGram.BotInit.GetMe` (built-in, enabled by default) 2. `ExGram.BotInit.SetupCommands` (built-in, enabled when `setup_commands: true`) 3. Your custom hooks (in declaration order) 4. `init/1` callback on the bot module 5. Bot starts receiving updates If any hook returns `{:error, reason}`, the bot supervisor shuts down cleanly with reason `{:on_bot_init_failed, module, reason}`. ## Declaring Hooks Use the `on_bot_init/1-2` macro inside your bot module: ```elixir defmodule MyApp.Bot do use ExGram.Bot, name: :my_bot on_bot_init(MyApp.ConfigHook) on_bot_init(MyApp.CacheWarmHook, ttl: 300) command("start") def handle({:command, :start, _}, context) do answer(context, "Hello! Config: #{context.extra[:app_config]}") end end ``` ## Implementing a Hook Implement the `ExGram.BotInit` behaviour with a single callback: ```elixir defmodule MyApp.ConfigHook do @behaviour ExGram.BotInit @impl ExGram.BotInit def on_bot_init(opts) do token = opts[:token] bot = opts[:bot] current_extra = opts[:extra_info] case MyApp.Config.fetch(token) do {:ok, config} -> # Return a map to merge into context.extra for all subsequent hooks and handlers {:ok, %{app_config: config}} {:error, reason} -> # Returning {:error, reason} stops startup and shuts down the bot {:error, {:config_fetch_failed, reason}} end end end ``` ### Callback Return Values | Return | Effect | |---|---| | `:ok` | Hook succeeded; `extra_info` is unchanged | | `{:ok, map}` | Hook succeeded; `map` is merged into `extra_info` for subsequent hooks and handlers | | `{:error, reason}` | Hook failed; bot shuts down with `{:on_bot_init_failed, module, reason}` | ### Hook Options `on_bot_init/1` receives a keyword list with: | Key | Type | Description | |---|---|---| | `:bot` | `atom()` | The bot's registered name | | `:token` | `String.t()` | The bot's token | | `:extra_info` | `map()` | Accumulated extra data from previous hooks | | custom keys | `any()` | Any options passed to `on_bot_init/2` | ## Passing Options to Hooks Use `on_bot_init/2` to pass custom options to a hook at declaration time: ```elixir on_bot_init(MyApp.CacheWarmHook, ttl: 300, namespace: "my_bot") ``` The options are merged into the keyword list received by `on_bot_init/1`: ```elixir defmodule MyApp.CacheWarmHook do @behaviour ExGram.BotInit @impl ExGram.BotInit def on_bot_init(opts) do ttl = Keyword.get(opts, :ttl, 60) namespace = Keyword.get(opts, :namespace, "default") MyApp.Cache.warm(namespace, ttl: ttl) :ok end end ``` ## Sharing Data Between Hooks Each hook receives `extra_info` containing all data produced by hooks that ran before it. Return `{:ok, map}` to merge new data in: ```elixir defmodule MyApp.AuthHook do @behaviour ExGram.BotInit @impl ExGram.BotInit def on_bot_init(opts) do bot = opts[:bot] case MyApp.Auth.validate_bot(bot) do {:ok, permissions} -> {:ok, %{bot_permissions: permissions}} {:error, :invalid} -> {:error, :invalid_token} end end end defmodule MyApp.SetupHook do @behaviour ExGram.BotInit @impl ExGram.BotInit def on_bot_init(opts) do # Permissions were set by AuthHook, accessible via extra_info permissions = opts[:extra_info][:bot_permissions] if :admin in permissions do {:ok, %{admin_chat_id: MyApp.Config.admin_chat_id()}} else :ok end end end ``` ## Accessing Hook Data in Handlers Data added by hooks is available in every handler call via `context.extra`: ```elixir defmodule MyApp.Bot do use ExGram.Bot, name: :my_bot on_bot_init(MyApp.AuthHook) on_bot_init(MyApp.SetupHook) command("status") def handle({:command, :status, _}, context) do permissions = context.extra[:bot_permissions] answer(context, "Permissions: #{inspect(permissions)}") end def handle(_, context), do: context end ``` ## Built-in Hooks ExGram provides two built-in hooks that are automatically injected by the dispatcher at startup. ### `ExGram.BotInit.GetMe` Calls `ExGram.get_me/1` to fetch the bot's identity from Telegram. Enabled by default (`get_me: true`). The result, the bot's information in an `ExGram.Model.User` struct, is stored as `state.bot_info` in the dispatcher and becomes available as `context.bot_info` in every handler call. Disable it when you don't need the bot's identity or want to avoid the startup API call: ```elixir {MyApp.Bot, [method: :polling, token: token, get_me: false]} ``` ### `ExGram.BotInit.SetupCommands` Registers the bot's declared commands with Telegram via `setMyCommands`. Only runs when `setup_commands: true`: ```elixir use ExGram.Bot, name: :my_bot, setup_commands: true # or at startup: {MyApp.Bot, [method: :polling, token: token, setup_commands: true]} ``` ### Execution Order Built-in hooks always run before custom hooks: ``` GetMe -> SetupCommands (if enabled) -> your custom hooks -> init/1 ``` ## Error Handling When a hook returns `{:error, reason}`, the dispatcher: 1. Logs an error: `ExGram: on_bot_init hook MyHook failed for bot :my_bot: reason` 2. Stops the dispatcher with reason `{:shutdown, {:on_bot_init_failed, MyHook, reason}}` The bot's supervisor propagates this shutdown, which means the whole bot process tree stops. This is intentional - if a required initialization step fails, there is no safe state to operate in. ```elixir defmodule MyApp.RequiredHook do @behaviour ExGram.BotInit @impl ExGram.BotInit def on_bot_init(opts) do case fetch_required_config(opts[:token]) do {:ok, config} -> {:ok, %{config: config}} {:error, reason} -> # Bot will not start - logged and supervisor shuts down {:error, {:required_config_missing, reason}} end end end ``` ## Testing Hooks Hooks participate in the normal test adapter lifecycle. Use `ExGram.Test.stub/2` or `ExGram.Test.expect/2` to control any API calls your hook makes. By default, `ExGram.Test.start_bot/3` sets `get_me: false` and `setup_commands: false` to avoid unnecessary API calls. Your own hooks declared with `on_bot_init/1-2` always run. If you want to optionally enable/disable your init hooks, you can stop running them if a specific field exists in the extra_info map, and start your bots with that value. ```elixir defmodule MyHook do @behaviour ExGram.BotInit @impl ExGram.BotInit def on_bot_init(opts) do if opts[:extra_info][:my_hook_disable] do :ok else do_init(opts) end end end defmodule MyApp.BotTest do use ExUnit.Case, async: true use ExGram.Test import ExGram.TestHelpers setup context do {bot_name, _} = ExGram.Test.start_bot(context, MyApp.Bot, extra_info: %{__my_hook_disable: true}) {:ok, bot_name: bot_name} end end ``` To test `get_me: true` or `setup_commands: true` startup hooks, pass them explicitly: ```elixir ExGram.Test.stub(:get_me, %{id: 1, is_bot: true, username: "my_bot"}) {bot_name, _} = ExGram.Test.start_bot(context, MyApp.Bot, get_me: true) ``` ## Next Steps - [Middlewares](middlewares.md) - Add preprocessing logic that runs on every update - [Testing](testing.md) - Test your bot and its initialization hooks - [Handling Updates](handling-updates.md) - Learn about the handler patterns