guides/bot-init-hooks.md

# 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