This guide explains how to handle different types of updates from Telegram in your ExGram bot.

The handle/2 Function

Every bot must implement the ExGram.Handler.handle/2 function. It receives:

  1. Update tuple - Different tuple patterns for different update types
  2. Context - A ExGram.Cnt.t/0 struct with update information
def handle(update_tuple, context) do
  # Process the update and return context
  context
end

The context contains:

  • update - The full Update object
  • name - Your bot's name (important for multiple bots)
  • bot_info - You bot's information, extracted with ExGram.get_me/1 at startup
  • extra - Custom data from middlewares
  • Internal fields used by ExGram

Update Patterns

ExGram parses updates into convenient tuples for pattern matching.

Commands

Matches messages starting with /command or /command@your_bot.

def handle({:command, "start", msg}, context) do
  answer(context, "Welcome! You sent: #{msg}")
end

def handle({:command, "help", _msg}, context) do
  answer(context, """
  Available commands:
  /start - Start the bot
  /help - Show this help
  /settings - Configure settings
  """)
end

The msg parameter contains any text after the command:

  • /startmsg = ""
  • /start hello worldmsg = "hello world"

You can also declare commands at the module level:

command("start")
command("help", description: "Show help message")
command("settings", description: "Configure your settings")

With setup_commands: true, these are automatically registered with Telegram.

And, once you declare them, you will receive the commands as atoms:

def handle({:command, :start, msg}, context) do
  # ...
end

def handle({:command, :help, _msg}, context)
def handle({:command, :settings, _msg}, context)

# You can still handle not defined commands
def handle({:command, "othercommand", _msg}, context)

This is really important if you want to provide command translations or commands for different roles.

Check the commands guide if you want to know more.

Plain Text

Matches regular text messages (respects privacy mode).

def handle({:text, text, message}, context) do
  cond do
    String.contains?(text, "hello") ->
      answer(context, "Hello to you too!")
    
    String.length(text) > 100 ->
      answer(context, "That's a long message!")
    
    true ->
      answer(context, "You said: #{text}")
  end
end

Regex Patterns

Define regex patterns at module level and match against them:

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

  # Define regex patterns
  regex(:email, ~r/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/)
  regex(:phone, ~r/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/)

  def handle({:regex, :email, message}, context) do
    answer(context, "I detected an email address in your message!")
  end

  def handle({:regex, :phone, message}, context) do
    answer(context, "That looks like a phone number!")
  end
end

Callback Queries

Handles button presses from inline keyboards.

def handle({:callback_query, %{data: "button_" <> id} = callback}, context) do
  context
  |> answer_callback("Processing button #{id}")
  |> answer("You clicked button #{id}")
end

def handle({:callback_query, %{data: "delete"}}, context) do
  context
  |> answer_callback("Deleting message...")
  |> delete()
end

See Sending Messages for creating inline keyboards.

Inline Queries

Handles inline queries (e.g., @yourbot search term).

def handle({:inline_query, query}, context) do
  results = [
    %{
      type: "article",
      id: "1",
      title: "Result 1",
      input_message_content: %{message_text: "You selected result 1"}
    },
    %{
      type: "article",
      id: "2",
      title: "Result 2",
      input_message_content: %{message_text: "You selected result 2"}
    }
  ]

  answer_inline_query(context, results)
end

Location Messages

Handles location sharing.

def handle({:location, %ExGram.Model.Location{latitude: lat, longitude: lon}}, context) do
  answer(context, "You're at #{lat}, #{lon}. Thanks for sharing!")
end

Edited Messages

Handles message edits.

def handle({:edited_message, edited_msg}, context) do
  # You can choose to process edited messages differently
  # or ignore them entirely
  Logger.info("Message #{edited_msg.message_id} was edited")
  context
end

Generic Message Handler

Catches any message that doesn't match other patterns.

def handle({:message, message}, context) do
  cond do
    message.photo ->
      answer(context, "Nice photo!")
    
    message.document ->
      answer(context, "Thanks for the document!")
    
    message.sticker ->
      answer(context, "Cool sticker!")
    
    message.voice ->
      answer(context, "I received your voice message!")
    
    true ->
      answer(context, "I received your message, but I'm not sure what to do with it.")
  end
end

Default Handler

Catches all other updates.

ExGram will slowly add more specific handlers to make it easier to differentiate all the possible update types.

def handle({:update, update}, context) do
  Logger.debug("Received unhandled update: #{inspect(update)}")
  context
end

The Context (ExGram.Cnt.t/0)

The context struct contains:

%ExGram.Cnt{
  update: %ExGram.Model.Update{},  # Full Telegram update, useful to get more information about the update in specific handlers
  name: :my_bot,                    # Your bot's name (the one from "use ExGram.Bot, name: :my_bot")
  bot_info: %ExGram.Model.User{} | nil, # The bot's information, extracted with ExGram.get_me at bot's startup
  extra: %{}                        # Custom data from middlewares
  # More fields used internally
}

Adding Extra Data

Middlewares can add custom data to context.extra:

# In a middleware
use ExGram.Middleware

def call(context, _opts) do
  user_id = extract_id(context)
  extra_data = %{user_role: fetch_user_role(user_id)}
  
  add_extra(context, extra_data)
end

# In your handler
def handle({:command, "admin", _msg}, context) do
  case context.extra[:user_role] do
    :admin -> answer(context, "Admin panel: ...")
    _ -> answer(context, "Access denied")
  end
end

Read more about middlewares in this guide

The ExGram.Handler.init/1 Callback

The optional ExGram.Handler.init/1 callback runs once before processing updates. Use it to initialize your bot:

def init(opts) do
  # opts contains [:bot, :token]
  ExGram.set_my_description!(
    description: "This bot helps you manage tasks",
    bot: opts[:bot]
  )
  
  ExGram.set_my_name!(
    name: "TaskBot",
    token: opts[:token]
  )
  
  # Do some logic you need before starting your bots
  # MyBot.notify_admins_restart(opts[:bot])
  
  :ok
end

Note: If you use setup_commands: true, commands are automatically registered. Use init/1 for additional setup.

Pattern Matching Tips

Multiple Clauses

Use multiple function clauses for clean code:

def handle({:command, :start, _}, context), do: answer(context, "Welcome!")
def handle({:command, :help, _}, context), do: show_help(context)
def handle({:command, :about, _}, context), do: show_about(context)

def handle({:callback_query, %{data: "yes"}}, context) do
  answer_callback(context, "You chose yes!")
end

def handle({:callback_query, %{data: "no"}}, context) do
  answer_callback(context, "You chose no!")
end

def handle({:text, text, _msg}, context) when is_binary(text) do
  answer(context, "Echo: #{text}")
end

def handle(_update, context), do: context

Guards

Use guards for additional filtering:

def handle({:text, text, _msg}, context) when byte_size(text) > 500 do
  answer(context, "Please send shorter messages (max 500 characters)")
end

def handle({:text, text, _msg}, context) when text in ["hi", "hello", "hey"] do
  answer(context, "Hello there!")
end

Extracting Data

Pattern match to extract specific fields:

def handle({:message, %{from: %{id: user_id, username: username}}}, context) do
  answer(context, "Hello @#{username} (ID: #{user_id})")
end

def handle({:callback_query, %{from: user, data: data}}, context) do
  Logger.info("User #{user.id} clicked: #{data}")
  answer_callback(context, "Got it!")
end

Next Steps