I love to create. Developer and designer. Looking forward to make cool things.
Telegram
Elixir

How to make a Telegram bot in Elixir

Introduction to Telegram Bots API

Exchanging messages with Telegram

Telegram supports two types of integration: webhooks and polling. Webhooks it's a type when Telegram sends request to your server whenever bot recevied a message from the user. There pros and cons of that type of integration. Webhooks are more sustainable in general. For webhooks it's necessary to have web-server with external IP adress. This server will receive new messages through get requests from Telegram. During development you can use ngrock.

Polling is a constant polling of the Telegram server for new messages. For polling, you do not need a server and an external address. Simple application that sends requests to the Telegram server without stopping is enough.

Obtaining a token from Telegram

The token for Telegram requests must be obtained from the BotFather bot.

Elixir

Elixir is a functional programming language. Based on the another programming language Erlang. The main advantage of Elixir is the ability to manage a huge number of processes. These processes are also made in a special way, so they take up significantly less memory and processor time than normal computer processes.

The application

I will gradually complicate the application. I'll start with a echo bot that sends a message back in response to a message. Next, I will add saving users to the database. And in the end, I'll try to make it a little useful - upon request from the user, the bot will send summary information about the stock market.

Creation of the skeleton of the application and installation of the necessary tools


mix new stocks_bot --sup
      

First you need to create a new application. The --sup option adds a supervisor to the application and starts it at startup. After creation, the structure of the application should look like this:


├── README.md 
├── lib 
│ ├── stocks_bot 
│ │ └── application.ex 
│ └──stocks_bot.ex 
├── mix.exs 
└── test 
├── stocks_bot_test.exs 
└── test_helper.exs
      

Additionally, you need to install HTTPoison to send requests and Jason to work with JSON in responses from the Telegram server.

stocks_bot/mix.exs

defp deps do
  [
    {:httpoison, "~> 1.8"},
    {:jason, "~> 1.2"}
  ]
end
      

Receiving user message

stocks_bot/lib/stocks_bot.ex

defmodule StocksBot do
  @basic_url "https://api.telegram.org/bot" <> "Token from BotFather"

  def get_updates(offset \\ nil) do
    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} =
           updates_url(offset) |> HTTPoison.get(),
         {:ok, data} = Jason.decode(body) do
      IO.inspect(data["result"])
    end
  end

  defp updates_url(_offset = nil) do
    @basic_url <> "/getUpdates"
  end
end
      

Now you can try how it works. Send your bot a message. Then open your terminal and enter these commands.

Terminal

iex -S mix 
StocksBot.get_updates()
      

In the terminal you will see incoming message:


[
  %{
    "message" => %{
      "chat" => %{
        "first_name" => "Bender",
        "id" => 300011235,
        "last_name" => "Rodriguez",
        "type" => "private",
        "username" => "bender"
      },
      "date" => 1636549063,
      "from" => %{
        "first_name" => "Bender",
        "id" => 300011235,,
        "is_bot" => false,
        "language_code" => "ru",
        "last_name" => "Rodriguez",
        "username" => "bender"
      },
      "message_id" => 1142,
      "text" => "Hello"
    },
    "update_id" => 475896056
  }
]
      

If you try to receive messages again, the answer will be the same. This happens because it is necessary to indicate to the telegram which messages have already been received. To do this, take the update_id of the last message, increase it to one and use it as a get parameter to receive new messages.

So far, the script receives one message and stops working, but it needs to continue listening to new messages. I'll fix it now.

stocks_bot/lib/stocks_bot.ex

defmodule StocksBot do
  @basic_url "https://api.telegram.org/bot" <> "Token from BotFather"

  def get_updates(offset \\ nil) do
    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} =
           updates_url(offset) |> HTTPoison.get(),
         {:ok, data} = Jason.decode(body) do

      parse_messages(data["result"])
      |> get_last_update_id()
      |> get_updates()
    end
  end

  defp parse_messages(messages) do
    Enum.each(messages, fn message ->
      IO.inspect(message)
    end)

    messages
  end

  defp get_last_update_id([]), do: nil

  defp get_last_update_id(messages) do
    List.last(messages) |> Map.fetch!("update_id")
  end

  defp updates_url(_offset = nil) do
    @basic_url <> "/getUpdates"
  end

  defp updates_url(offset) do
    @basic_url <> "/getUpdates?offset=#{offset + 1}"
  end
end
      

Polite answer

stocks_bot/lib/stocks_bot.ex

...

defp parse_messages(messages) do
  Enum.each(messages, fn message ->
    answer_to_message(message)
  end)

  messages
end

defp answer_to_message(message) do
  %{
    "message" => %{
      "chat" => %{"id" => chat_id},
      "text" => original_text
    }
  } = message

  answer = %{
    text: "Hello: #{original_text}",
    chat_id: chat_id
  }

  HTTPoison.post(
    @basic_url <> "/sendMessage",
    Jason.encode!(answer),
    [{"Content-Type", "application/json"}]
  )
end
        

The answer_to_message function uses pattern matching to pick up the sender's name and the text of the incoming message to send it back to the user as a post-request.

Using supervisor for the application

First step is converting app to a Genserver.

stocks_bot/lib/stocks_bot.ex

defmodule StocksBot do
  use GenServer
  @basic_url "https://api.telegram.org/bot" <> "Token from BotFather"

  def start_link(args) do
    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  @impl true
  def init(:ok) do
    get_updates()
    {:ok, %{}}
  end

...
        

Then this Genserver need to be added to Supervisor Tree.

stocks_bot/lib/stocks_bot/application.ex

defmodule StocksBot.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [ StocksBot ]

    opts = [strategy: :one_for_one, name: StocksBot.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
        

Demo time

Yes, the bot does not yet have superintelligence. In the next part, I will add user storage in the database and teach the bot to send stock price information.