# ExGram Cheatsheet
Quick reference for common ExGram patterns and functions.
## Configuration
```elixir
# config/config.exs
config :ex_gram,
token: "BOT_TOKEN",
adapter: ExGram.Adapter.Req
```
## Bot module example
```elixir
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
```elixir
children = [
ExGram, # Must come first
{MyBot.Bot, [method: :polling, token: token]}
]
```
## Handler Patterns
```elixir
# 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
```elixir
# 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
```elixir
# 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
```elixir
# Use result of previous action
context
|> answer("Sending photo...")
|> on_result(fn {:ok, message} ->
# Do something with message
:ok
end)
```
## File Formats
```elixir
# By Telegram file ID
"AgACAgIAAxkBAAI..."
# By local file path
{:file, "path/to/file.jpg"}
# By file content
{:file_content, binary_data, "filename.jpg"}
```
## Keyboards
```elixir
# 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
```elixir
# 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
```elixir
: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
```elixir
:text
:photo
:video
:audio
:document
:voice
:sticker
:animation
:location
:contact
:poll
:dice
:game
:venue
```
## Low-Level API
```elixir
# 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
```elixir
# 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
```elixir
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:
```elixir
# 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
```elixir
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
```elixir
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
```elixir
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
```elixir
# 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
```elixir
# 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
- [ExGram Hex Docs](https://hexdocs.pm/ex_gram)
- [Telegram Bot API](https://core.telegram.org/bots/api)