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
endStart the bot in your supervision tree:
children = [
{MyBot, token: "your-bot-token"}
]Dispatch Priority
When an update arrives, handlers are checked in this order:
- Commands — if the message text starts with
/, declaredcommandhandlers are checked first (in declaration order) - Type handlers —
onhandlers matching the update type - Fallback —
handle_update/2if defined manually - 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 initThese merge with the runtime allowed_ids option.
Empty vs. omitted (important):
- Omit
allowandallowed_idsentirely → 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
endOptions
:token— bot token (can also be set via config or env var):allowed_ids— list of user IDs permitted to interact (merged withallow). 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
@callback handle_update(Hotline.Types.Update.t(), map()) :: {:noreply, map()}
Functions
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}
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
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