Hotline.Bot behaviour (Hotline v0.2.1)

Copy Markdown View Source

Behaviour and GenServer for building Telegram bots.

DSL Usage

The Bot DSL provides declarative macros for defining command handlers, update-type handlers, and access control:

defmodule MyBot do
  use Hotline.Bot

  # Restrict to specific user IDs (optional)
  allow [123_456_789]

  # Command handlers — binds: update, state, chat_id, args
  command "/start" do
    Hotline.send_message(%{chat_id: chat_id, text: "Welcome!"})
  end

  command "/echo" do
    Hotline.send_message(%{chat_id: chat_id, text: args})
  end

  # Update-type handlers — binds: update, state, chat_id, <type>
  on :message do
    Hotline.send_message(%{chat_id: chat_id, text: "Got: #{message.text}"})
  end

  on :callback_query do
    Hotline.answer_callback_query(%{callback_query_id: callback_query.id})
  end
end

Start the bot in your supervision tree:

children = [
  {MyBot, token: "your-bot-token"}
]

Dispatch Priority

When an update arrives, handlers are checked in this order:

  1. Commands — if the message text starts with /, declared command handlers are checked first (in declaration order)
  2. Type handlerson handlers matching the update type
  3. Fallbackhandle_update/2 if defined manually
  4. Default{:noreply, state}

Handler Bindings

Inside command blocks: update, state, chat_id, args Inside on blocks: update, state, chat_id, plus the type-specific variable (e.g., message, callback_query)

Handlers that return {:noreply, new_state} propagate the new state. Any other return value defaults to {:noreply, state}.

Access Control

Use allow at the module level to declare permitted user IDs:

allow [111, 222]                    # literal IDs
allow {:config, :my_allowed_ids}    # resolve from Application env at init

These merge with the runtime allowed_ids option.

Empty vs. omitted (important):

  • Omit allow and allowed_ids entirely → allow-all (every user accepted).
  • A merged allow-list that resolves to []deny-all (every update rejected).

These are deliberately different: an explicit empty list fails closed. The common footgun is an accidental empty list — e.g. allowed_ids: MyApp.admin_ids() where the query returns [] — which silently locks everyone out. The bot logs a Logger.warning at init whenever the merged allow-list is empty so this is visible in the logs.

channel_post note

For channel_post updates there is no from user, so extract_sender_id/1 falls back to the chat id. That chat id is then checked against the user allow-list. If you allow channel posts, add the relevant chat ids to the allow-list. Updates with no extractable sender fail closed (rejected).

Manual Usage

You can still use the raw callback approach for full control:

defmodule MyBot do
  use Hotline.Bot

  @impl Hotline.Bot
  def handle_update(update, state) do
    # full manual control
    {:noreply, state}
  end
end

Options

  • :token — bot token (can also be set via config or env var)
  • :allowed_ids — list of user IDs permitted to interact (merged with allow). An empty merged list means deny-all; omit entirely for allow-all.
  • :chat_id — initial chat ID (auto-detected from first message if omitted)
  • :name — process name (defaults to module name)

Callbacks

  • handle_update/2 — called for updates not matched by DSL handlers (optional with DSL)
  • init_bot/1 — called during init for custom state setup (optional)

Summary

Functions

Declare allowed user IDs for access control.

Declare a command handler.

Declare a handler for a specific update type.

Callbacks

handle_update(t, map)

(optional)
@callback handle_update(Hotline.Types.Update.t(), map()) :: {:noreply, map()}

init_bot(map)

(optional)
@callback init_bot(map()) :: {:ok, map()}

Functions

allow(ids)

(macro)

Declare allowed user IDs for access control.

Accepts a literal list of IDs or {:config, key} to resolve from Application.get_env(:hotline, key) at init time.

Multiple allow declarations are merged together, and also merge with the runtime allowed_ids option.

Example

allow [123_456_789, 987_654_321]
allow {:config, :admin_ids}

command(cmd_string, list)

(macro)

Declare a command handler.

The block receives bindings for update, state, chat_id, and args (the text after the command). Commands strip @botname suffixes for group chat support.

Example

command "/start" do
  Hotline.send_message(%{chat_id: chat_id, text: "Hello!"})
end

command "/echo" do
  Hotline.send_message(%{chat_id: chat_id, text: args})
end

on(update_type, list)

(macro)

Declare a handler for a specific update type.

The block receives bindings for update, state, chat_id, and the type-specific variable (e.g., message, callback_query).

Valid types

:message, :callback_query, :edited_message, :channel_post, :edited_channel_post, :inline_query, :chosen_inline_result, :shipping_query, :pre_checkout_query, :poll, :poll_answer, :my_chat_member, :chat_member, :chat_join_request

Example

on :message do
  Hotline.send_message(%{chat_id: chat_id, text: "Echo: #{message.text}"})
end

on :callback_query do
  Hotline.answer_callback_query(%{callback_query_id: callback_query.id})
end