Quick reference for common ExGram patterns and functions.

Configuration

# config/config.exs
config :ex_gram,
  token: "BOT_TOKEN",
  adapter: ExGram.Adapter.Req

Bot module example

defmodule MyBot.Bot do
  @bot :my_bot
  
  use ExGram.Bot,
    name: @bot,
    setup_commands: true
  
  # Declare commands
  command("start")
  command("help", description: "Show help")
  
  # Declare regex patterns
  regex(:email, ~r/\b[\w._%+-]+@[\w.-]+\.[A-Z|a-z]{2,}\b/)
  
  # Add middlewares
  middleware(ExGram.Middleware.IgnoreUsername)
  middleware(&my_middleware/2)
  
  # Init callback (optional)
  def init(opts) do
    # Setup before receiving updates
    :ok
  end
  
  # Handlers
  def handle({:command, "start", _}, context) do
    answer(context, "Hi!")
  end
end

Supervision Tree

children = [
  ExGram,  # Must come first
  {MyBot.Bot, [method: :polling, token: token]} 
]

Handler Patterns

# Commands
def handle({:command, "start", msg}, context)

# Text messages
def handle({:text, text, message}, context)

# Regex patterns (define with: regex(:name, ~r/pattern/))
def handle({:regex, :email, message}, context)

# Callback queries
def handle({:callback_query, callback}, context)

# Inline queries
def handle({:inline_query, query}, context)

# Location
def handle({:location, location}, context)

# Edited messages
def handle({:edited_message, edited_message}, context)

# Generic message
def handle({:message, message}, context)

# Default handler
def handle({:update, update}, context)

DSL Functions - Sending

# Text messages
answer(context, "Hello!")
answer(context, "Hello", parse_mode: "Markdown")

# Media
answer_photo(context, photo_id_or_file)
answer_document(context, doc_id_or_file)
answer_video(context, video_id_or_file)
answer_audio(context, audio_id_or_file)
answer_voice(context, voice_id_or_file)
answer_sticker(context, sticker_id)
answer_animation(context, animation_id)

# Callback queries
answer_callback(context, "Done!")
answer_callback(context, "Alert!", show_alert: true)

# Inline queries
answer_inline_query(context, results)
answer_inline_query(context, results, cache_time: 300)

DSL Functions - Editing & Deleting

# Edit message
edit(context, "New text")
edit(context, "New text", reply_markup: markup)

# Edit inline message
edit_inline(context, "New text")

# Edit only keyboard
edit_markup(context, new_markup)

# Delete message
delete(context)
delete(context, chat_id, message_id)

DSL Functions - Chaining

# Use result of previous action
context
|> answer("Sending photo...")
|> on_result(fn {:ok, message} ->
  # Do something with message
  :ok
end)

File Formats

# By Telegram file ID
"AgACAgIAAxkBAAI..."

# By local file path
{:file, "path/to/file.jpg"}

# By file content
{:file_content, binary_data, "filename.jpg"}

Keyboards

# Import the keyboard DSL
import ExGram.Dsl.Keyboard

# Simple inline keyboard
markup = keyboard :inline do
  row do
    button "Button 1", callback_data: "btn1"
    button "Button 2", callback_data: "btn2"
  end
  row do
    button "Button 3", callback_data: "btn3"
  end
end

# Use in different methods that accept reply_markup
answer(context, "Choose:", reply_markup: markup)

# With URL button
keyboard :inline do
  row do
    button "Visit", url: "https://example.com"
  end
end

# Reply keyboard (sticky keyboard)
keyboard :reply, [is_persistent: true] do
  row do
    reply_button "Help", style: "success"
  end
end

# Keyboards inspect as a visual layout
# #InlineKeyboardMarkup<
#   [ Button 1 (cb) ][ Button 2 (cb) ]
#   [ Button 3 (url) ]
# >

# Verbose mode shows action values
inspect(markup, custom_options: [verbose: true])
# #InlineKeyboardMarkup<
#   [ Button 1 (cb: "btn1") ][ Button 2 (cb: "btn2") ]
#   [ Button 3 (url: "https://example.com") ]
# >

Context Extractors

# Chat/User info
extract_id(context)           # Chat ID (User ID in private chats and Chat ID in groups)
extract_user(context)         # User struct
extract_chat(context)         # Chat struct

# Message info
extract_message_id(context)   # Message id from any message type
extract_message_type(context) # :text, :photo, :document, etc.

# Query info
extract_callback_id(context)  # Callback query ID
extract_inline_id_params(context)  # Inline message params

# Update info
extract_update_type(context)  # :message, :callback_query, etc.
extract_response_id(context)  # Response ID for editing

Update Types

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

Message Types

:text
:photo
:video
:audio
:document
:voice
:sticker
:animation
:location
:contact
:poll
:dice
:game
:venue

Low-Level API

# Basic call
ExGram.send_message(chat_id, "Hello")

# With options
ExGram.send_message(chat_id, "Hello", parse_mode: "Markdown")

# With token
ExGram.send_message(chat_id, "Hello", token: "BOT_TOKEN")

# With named bot
ExGram.send_message(chat_id, "Hello", bot: :my_bot)

# Bang version (raises on error)
ExGram.send_message!(chat_id, "Hello")

# Common methods
ExGram.get_me()
ExGram.get_updates()
ExGram.send_photo(chat_id, photo)
ExGram.edit_message_text(chat_id, message_id, "New text")
ExGram.delete_message(chat_id, message_id)
ExGram.pin_chat_message(chat_id, message_id)
ExGram.get_chat(chat_id)
ExGram.get_chat_member(chat_id, user_id)

Middleware

# Function middleware
middleware(&my_middleware/2)

def my_middleware(context, opts) do
  # Process context
  context
end

# Module middleware
middleware(MyMiddleware)
middleware({MyMiddleware, opts})

# Built-in
middleware(ExGram.Middleware.IgnoreUsername)

# Add information to the extra field in `t:ExGram.Cnt.t/0`
context |> ExGram.Middleware.add_extra(:key, value)
context |> ExGram.Middleware.add_extra(%{key1: value1, key2: value2})

# Halt processing
context |> ExGram.Middleware.halt()

Bot Init Hooks

defmodule MyApp.Bot do
  use ExGram.Bot, name: :my_bot

  # Declare hooks - run once on startup before handling updates
  on_bot_init(MyApp.SetupHook)
  on_bot_init(MyApp.CacheHook, ttl: 300)
end

defmodule MyApp.SetupHook do
  @behaviour ExGram.BotInit

  @impl ExGram.BotInit
  def on_bot_init(opts) do
    token = opts[:token]           # bot token
    bot = opts[:bot]               # bot registered name
    extra = opts[:extra_info]      # data from previous hooks

    case MyApp.Config.fetch(token) do
      {:ok, config} -> {:ok, %{app_config: config}}  # merged into context.extra
      {:error, :not_found} -> :ok                    # non-fatal, no extra data
      {:error, reason} -> {:error, reason}            # stops startup
    end
  end
end

# Access in handlers via context.extra
def handle({:command, :status, _}, context) do
  answer(context, "Config: #{inspect(context.extra[:app_config])}")
end

Built-in hooks controlled via startup options:

# get_me: true (default) - fetches bot identity via getMe, available as context.bot_info
# get_me: false - skips the API call (default in tests)
{MyBot.Bot, [method: :polling, token: token, get_me: false]}

# setup_commands: true - registers declared commands with Telegram at startup
{MyBot.Bot, [method: :polling, token: token, setup_commands: true]}

Common Patterns

Multi-step Conversation

def handle({:command, "order", _}, context) do
  # Store state somewhere (ETS, Agent, Database)
  set_user_state(extract_id(context), :awaiting_item)
  answer(context, "What would you like to order?")
end

def handle({:text, text, _}, context) do
  case get_user_state(extract_id(context)) do
    :awaiting_item ->
      set_user_state(extract_id(context), {:awaiting_quantity, text})
      answer(context, "How many?")
    
    {:awaiting_quantity, item} ->
      clear_user_state(extract_id(context))
      answer(context, "Ordered #{text}x #{item}!")
    
    _ ->
      answer(context, "I don't understand")
  end
end

Admin Check

defmodule AdminMiddleware do
  use ExGram.Middleware
  
  def call(context, opts) do
    user_id = ExGram.Dsl.extract_id(context)
    admin_ids = Keyword.get(opts, :admins, [])
    
    if user_id in admin_ids do
      add_extra(context, :user_id, user_id)
    else
      context
      |> ExGram.Dsl.answer("⛔ Admin only")
      |> halt()
    end
  end
end

Pagination

import ExGram.Dsl.Keyboard

def handle({:command, "list", _}, context) do
  show_page(context, 1)
end

def handle({:callback_query, %{data: "page:" <> page}}, context) do
  page_num = String.to_integer(page)
  
  context
  |> answer_callback()
  |> show_page(page_num)
end

defp show_page(context, page) do
  items = get_items(page)
  
  markup = keyboard :inline do
    row do
      button "⬅️", callback_data: "page:#{page - 1}"
      button "#{page}", callback_data: "current"
      button "➡️", callback_data: "page:#{page + 1}"
    end
  end
  
  edit(context, format_items(items), reply_markup: markup)
end

Debugging

# Enable debug logging
config :logger, level: :debug

# Debug single request
ExGram.send_message(chat_id, "Test", debug: true)

# Log in handler
require Logger
def handle(update, context) do
  Logger.debug("Received: #{inspect(update)}")
  context
end

# Keyboards display as visual layouts in logs automatically
Logger.debug("Keyboard: #{inspect(markup)}")
# => Keyboard: #InlineKeyboardMarkup<
#      [ OK (cb) ][ Cancel (cb) ]
#    >

# Use verbose for action values
Logger.debug("Keyboard: #{inspect(markup, custom_options: [verbose: true])}")
# => Keyboard: #InlineKeyboardMarkup<
#      [ OK (cb: "ok_pressed") ][ Cancel (cb: "cancel_action") ]
#    >

Testing

# Start bot in noup mode
{MyBot.Bot, [method: :noup, token: "test"]}

# Build test context
%ExGram.Cnt{
  update: update,
  name: :my_bot,
  halted: false,
  responses: []
}

# Test handler
result = MyBot.Bot.handle({:command, "start", ""}, context)
assert result.responses != []

Resources