# TelegramEx Elixir library for building Telegram bots. Provides a simple interface for handling messages, callbacks, and inline queries with automatic polling. ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/lsdrfrx/telegram_ex/ci.yml?style=for-the-badge) [![Hex Version](https://img.shields.io/hexpm/v/telegram_ex.svg?style=for-the-badge)](https://hex.pm/packages/telegram_ex) ![Last commit](https://img.shields.io/github/last-commit/lsdrfrx/telegram_ex?style=for-the-badge) ![Stars](https://img.shields.io/github/stars/lsdrfrx/telegram_ex?style=for-the-badge) ![License](https://img.shields.io/github/license/lsdrfrx/telegram_ex?style=for-the-badge)

Why This Library Exists

I decided to create this library because I couldn't find anything in the existing Elixir ecosystem that I liked. Maybe I just didn't search well enough, but still.

What makes this library different? I like the macro-based implementation, similar to how GenServer works. It feels like the right approach for this kind of library, and I think others might appreciate it too.

Usage

Add the bot to your application's supervision tree:

defmodule MyApp.Application do
  def start(_type, _args) do
    children = [MyBot]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

Create a bot module:

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(message, ctx) do
    # Handle incoming messages
  end

  def handle_callback(callback, ctx) do
    # Handle callback queries
    :ok
  end
end

Add your bot token to config/runtime.exs:

import Config

config :telegram_ex,
  my_bot: System.fetch_env!("MY_BOT_TELEGRAM_TOKEN")

Stateless Handlers

These handlers receive the incoming update and a context map (ctx). The context carries the bot token, FSM state, and is used as a pipeline accumulator for builders.

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(%{text: "/start", chat: chat}, ctx) do
    ctx
    |> Message.text("Welcome")
    |> Message.send(chat["id"])
  end

  def handle_callback(%{data: "ping", message: %{chat: chat}} = callback, ctx) do
    ctx
    |> Message.text("pong")
    |> Message.answer_callback_query(callback)
    |> Message.send(chat["id"])
  end
end

Use this style when the handler does not need to remember anything between updates.

Stateful Handlers

defstate/2 is used when you want handlers to be selected by the current FSM state, with the state injected into pattern matching automatically. The second argument of the handler is ctx, which contains :state, :data, and :token.

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(%{text: "/start", chat: chat}, ctx) do
    ctx
    |> Message.text("Welcome")
    |> Message.send(chat["id"])

    {:transition, :started, %{step: 1}}
  end

  defstate :started do
    def handle_message(%{text: text, chat: chat}, ctx) do
      ctx
      |> Message.text("You said: #{text}")
      |> Message.send(chat["id"])

      {:stay, Map.put(ctx.data, :last_message, text)}
    end
  end
end

Handlers can return:

FSM API

You can interact with the FSM directly to read or manipulate state programmatically:

alias TelegramEx.FSM

# Get current state and data for a user
{state, data} = FSM.get_state(:my_bot, chat_id)

# Set state only (keeps existing data)
FSM.set_state(:my_bot, chat_id, :waiting_input)

# Set state and replace data
FSM.set_state(:my_bot, chat_id, :waiting_input, %{retries: 0})

# Reset state (removes stored entry)
FSM.reset_state(:my_bot, chat_id)

Sending Messages

Messages are sent using the TelegramEx.Builder.Message module. All builders follow a pipeline pattern, accepting ctx as the first argument:

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(%{chat: chat}, ctx) do
    ctx
    |> Message.text("Hello!")
    |> Message.send(chat["id"])
  end
end

Message Builder Functions

Sending Photos

Use TelegramEx.Builder.Photo to send images:

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(%{chat: chat}, ctx) do
    ctx
    |> Photo.path("/path/to/image.jpg")
    |> Photo.caption("Here's a photo!")
    |> Photo.send(chat["id"])
  end
end

Photo Builder Functions

Sending Documents

Use TelegramEx.Builder.Document to send files:

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(%{chat: chat}, ctx) do
    ctx
    |> Document.path("/path/to/file.pdf")
    |> Document.caption("Here's the document")
    |> Document.send(chat["id"])
  end
end

Document Builder Functions

Sending Stickers

Use TelegramEx.Builder.Sticker to send stickers:

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(%{chat: chat}, ctx) do
    ctx
    |> Sticker.id("CAACAgIAAxkBA...")
    |> Sticker.send(chat["id"])
  end
end

Sticker Builder Functions

Sending Videos

Use TelegramEx.Builder.Video to send videos:

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(%{chat: chat}, ctx) do
    ctx
    |> Video.path("/path/to/video.mp4")
    |> Video.duration(120)
    |> Video.cover_path("/path/to/cover.jpg")
    |> Video.send(chat["id"])
  end
end

Video Builder Functions

Sending Locations

Use TelegramEx.Builder.Location to send geo coordinates:

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(%{chat: chat}, ctx) do
    ctx
    |> Location.coordinates(55.7558, 37.6173)
    |> Location.send(chat["id"])
  end
end

Location Builder Functions

Sending Contacts

Use TelegramEx.Builder.Contact to send contacts:

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(%{chat: chat}, ctx) do
    ctx
    |> Contact.contact("John", "+1234567890")
    |> Contact.send(chat["id"])
  end
end

Contact Builder Functions

Keyboard Examples

Inline Keyboard:

def handle_message(%{chat: chat}, ctx) do
  keyboard = [[
    %{text: "Button 1", callback_data: "btn_1"},
    %{text: "Button 2", callback_data: "btn_2"}
  ]]

  ctx
  |> Message.text("Choose an option:", "Markdown")
  |> Message.inline_keyboard(keyboard)
  |> Message.send(chat["id"])
end

Reply Keyboard:

def handle_message(%{chat: chat}, ctx) do
  keyboard = [["/help", "/settings"], ["Contact"]]

  ctx
  |> Message.text("Use the buttons below:")
  |> Message.reply_keyboard(keyboard, resize_keyboard: true, one_time_keyboard: true)
  |> Message.send(chat["id"])
end

Reply Keyboard Options:

Handling Callback Queries

When a user presses an inline keyboard button, handle_callback/2 is called with a %TelegramEx.Types.CallbackQuery{} struct and ctx:

def handle_callback(%{data: "btn_1"} = callback, ctx) do
  # Handle button 1 press
end

def handle_callback(%{data: "btn_2"} = callback, ctx) do
  # Handle button 2 press
end

Answering Callback Queries

To show an alert or update the user after a callback:

def handle_callback(%{data: data, message: %{chat: chat}} = callback, ctx) do
  ctx
  |> Message.text("Processed: #{data}")
  |> Message.answer_callback_query(callback)
  |> Message.send(chat["id"])
end

Callback Query Structure (%TelegramEx.Types.CallbackQuery{}):

Message Structure

The handle_message/2 callback receives a %TelegramEx.Types.Message{} struct and a ctx map with the following fields:

Examples

Echo Bot

defmodule EchoBot do
  use TelegramEx, name: :echo_bot

  def handle_message(%{text: text, chat: chat}, ctx) do
    ctx
    |> Message.text("Echo: #{text}")
    |> Message.send(chat["id"])
  end
end

Command Handling

defmodule MyBot do
  use TelegramEx, name: :my_bot

  def handle_message(%{text: "/start", chat: chat}, ctx) do
    ctx
    |> Message.text("Welcome! Send me any message.")
    |> Message.send(chat["id"])
  end

  def handle_message(%{text: text, chat: chat}, ctx) do
    ctx
    |> Message.text("Echo: #{text}")
    |> Message.send(chat["id"])
  end
end

Routers

Use TelegramEx.Router to group handlers by logic into separate modules. This keeps the main bot module clean and lets you organize handlers by domain.

defmodule MyApp.Routers.Admin do
  use TelegramEx.Router

  defstate :admin do
    def handle_message(%{text: "/exit", chat: chat}, ctx) do
      ctx
      |> Message.text("Exiting admin mode")
      |> Message.send(chat["id"])

      FSM.reset_state(:my_bot, chat["id"])
    end

    def handle_message(%{text: text, chat: chat}, ctx) do
      ctx
      |> Message.text("Admin command: #{text}")
      |> Message.send(chat["id"])
    end
  end
end

Register routers in the main bot module:

defmodule MyBot do
  use TelegramEx, name: :my_bot, routers: [MyApp.Routers.Admin]

  def handle_message(%{text: "/admin", chat: chat}, ctx) do
    ctx
    |> Message.text("Entering admin mode")
    |> Message.send(chat["id"])

    {:transition, :admin}
  end
end

Routers are checked in order before the main bot module. If a router's handler returns :pass, the next router (or the bot module) is tried.

Forum Topics

When replying to messages from forum chats (topics/threads), message_thread_id is handled automatically. The library injects it from the incoming message into the outgoing payload, so replies are sent to the correct thread without any extra code.

Roadmap

Sending Messages

Keyboards

Message Management

Group Actions

Chat Effects

Integrations & Infrastructure