Writing a Telegram-bot in Elixir

Since Telegram presented a platform for creating bots, it has been gaining popularity. Wrappers for Telegram API are already available for many programming languages, including Elixir.

This article is a step-by-step guide for creating a simple Telegram bot and deploying it to a remote server.

We have a website – ManualsBrain.com, written in Ruby, which helps to search for instructions and manuals for home appliances.
We decided to write a bot at manualsbrain to enable users to find manuals without leaving Telegram. We’ve developed it as a separate Elixir app because it’s a versatile language with growing community. It has a familiar Ruby-like syntax on top of the robust Erlang virtual machine.

How it works

Bots are third-party applications that run inside Telegram. Users can interact with bots by sending them messages, commands, and inline requests. There are a lot of use cases for the bots. They could be used in place of RSS readers, provide tech support and have many other applications.

There are two ways the bot interacts with Telegram API: long polling or webhooks.
 
With long polling, your application requests updates from Telegram API at regular intervals. The benefit of this method is that Telegram stores and manages the updates on its own, therefore you’ll be able to run the bot from any machine connected to the Internet (including your development environment).

In contrast, if you’re using webhooks, you’ll need to setup a remote server with a valid SSL certificate (you can self-signed one) to receive POST requests from Telegram.

After receiving the update, you construct an answer (or multiple answers) and send it to Telegram API.

For further details follow the official Telegram Bot API documentation (https://core.telegram.org/bots).

Creating a bot
To create a new bot add BotFather bot (https://telegram.me/botfather) and check for instructions on how to create and configure our bot. At first, we need a token. You’ll be asked for a name of your bot (displayed in contact details) and its username
(used in mentions and telegram.me links). When all is set, we are ready to go.

There is an Elixir wrapper for the Telegram Bot API called Nadia (https://github.com/zhyu/nadia) and several bot boilerplates on GitHub. We’re going to use one of them (https://github.com/lubien/elixir-telegram-bot-boilerplate).

After you have cloned the repository, add given token to config / config.ex:

config :app,   bot_name: "bot_user_name"  
config :nadia, token: "abcdefg_12345678910_the_game"

Let’s take a look at app.ex next.

defmodule App do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
worker(App.Poller, []),
worker(App.Matcher, [])
]
opts = [strategy: :one_for_one, name: App.Supervisor]
Supervisor.start_link children, opts
end
end

In order to give our App module the behavior of the OTP application, we use the Application module (https://hexdocs.pm/elixir/Application.html) and declare the start/2 function.

Now when we run our application, App will start App.Poller and App.Matcher processes and will create a supervision tree for them. Supervisor is a process that monitors child processes and automatically restarts them in case of failures. To read more about OTP: https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html.

App.Poller receives responses from Telegram and sends them to App.Matcher:

poller.ex
def handle_cast(:update, offset) do
new_offset = Nadia.get_updates([offset: offset, timeout: 60])
|> process_messages
{:noreply, new_offset + 1, 100}
end
...
defp process_messages({:ok, results}) do
results
|> Enum.map(fn %{update_id: id} = message ->
message
|> process_message
id
end)
|> List.last
end
...
defp process_message(message) do
try do
TelegramBot.Matcher.match message
rescue
err in MatchError ->
Logger.log :warn, "Errored with #{err} at #{Poison.encode! message}"
end
end

The App.Matcher determines the type of update and allows us to write the processing of any type we need in the commands.ex.

When a user adds the bot, your application receives the /start command automatically.

Let’s send a welcome message to the user.

command "start" do
Logger.log :info, "Command /start received"
send_message "Hello, " <> update.message.from.username
end

Now run “mix deps.get && mix” (or iex -S mix for the debug mode) in the project folder, add the bot to your telegram and see it greeting you.

We would like to talk to the user in his language. Fortunately,
Telegram provides ReplyKeyboardMarkup object that can be used to create a custom keyboard for arbitrary actions. That’s exactly what we need.

command "start" do
send_message "Hello, " <> update.message.from.username
send_message "Please, choose your language:",
reply_markup: %Model.InlineKeyboardMarkup{
inline_keyboard: [
[
%{
callback_data: "/set_language en",
text: "English",
},
%{
callback_data: "/set_language ru",
text: "Russian",
},
]
]
}
end

We’re using Redis to store the chosen languages because it is a fast and easy-to-use key-value storage. And Gettext for localization because it’s reliable and popular solution.

Add them both to application’s dependencies in mix.ex:

defp deps do
[{:nadia, "~> 0.4.2"},
{:redix, ">= 0.0.0"},
{:gettext, "~> 0.13"}
end
commands.ex:
callback_query_command "set_language" do
locale = case update.callback_query.data do
"/set_language " <> loc -> loc
end
user_id = update.callback_query.from.id
RedisStore.set_user_locale(user_id, locale)
Gettext.put_locale(App.Gettext, locale)
case send_message gettext "instruction" do
{:ok, _} -> nil
{:error, %Model.Error{reason: reason}} ->
Logger.log :error, "Reason: #{reason}"
end
end
defmodule RedisStore do
def set_user_locale(user_id, locale) do
Redix.command(:redix, ~w(SET users:#{user_id}:locales #{locale}))
end
end

Error processing

Nadia provides a wrapper for various errors from the Telegram API. Use it while replying to the user. For example, if he blocks the bot, there is no point in continuing the dialogue and trying to do some more actions.

t :: %Nadia.Model.Error{__exception__: term, reason: any}

Back to the task

We needed to search for user manuals on request, and send the link to our site where the user could download them.

We’ve implemented searching module as a separate umbrella application. It allowed us to keep the related modules in a single project (and repository) without compromising the separation of concerns.
Underneath we’re using ElasticSearch to perform the full-text search via Elastix.

Release and deploy

After making sure that everything works, we’d like to install the application on a remote server. First, we will build a release, using Distillery (https://github.com/bitwalker/distillery).

Add Distillery to the list of dependencies. If you use Umbrella, then to mix.ex, common for all applications. If you have a regular application, then to your mix.ex file.

Then run mix deps.get and mix release.init. The last command will create config.exs file in the rel/ directory. Let’s see if we need to change something in this config.

For Umbrella projects, you can specify several different releases, including different applications:

release :apps_a_and_b do 
set version: current_version(:app_a)
set applications: [:app_a, :app_b] end

Next is the settings for various environments.

environment :prod do
set include_erts: true
set include_src: false
set cookie: :"some_long_string"
end

Set include_erts is used to specify whether to include Erlang Runtime System into the release, with which the application was assembled. If used, there is no need to install Erlang on a remote server. But if you still decide to use system ERTS servers, make sure that the version of Erlang and the server architecture match the version and architecture of the building environment (either your development machine, docker container or remote server).

Next, add Edeliver (https://github.com/edeliver/edeliver) to the dependencies of the project, and create .deliver/config file in the root of the project. Edeliver assumes that we have a separate building and staging servers. But it’s too complicated for our case, so we’re going to use development machine for both purposes. We’re going to use Docker to achieve that. See: (https://github.com/appdojolabs/myapp) to setup it.

After editing the Distillery and Edeliver config files, be sure to commit the changes to git. Without this, the deploy will not work! We had to spend a lot of time to discover this simple fact.

There’s no need to execute “mix release” command separately because Edeliver will do it for us:

$ mix edeliver build release
$ mix edeliver deploy release to production
$ mix edeliver start production

In general, that’s all. The bot is up and running, you may try it here: http://t.me/manualsbrain_bot