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.
The token for Telegram requests must be obtained from the BotFather bot.
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.
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.
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.
defp deps do
[
{:httpoison, "~> 1.8"},
{:jason, "~> 1.2"}
]
end
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.
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.
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
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.
First step is converting app to a Genserver.
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
end
Then this Genserver need to be added to Supervisor Tree.
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
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.