In our company Elixirator, we have an investment time every Friday. Four hours of free time when anyone can build tools that can improve or facilitate our everyday workflow. As my project, I decided to build the Slack bot, that will help to store every day notes and remind about them before the weekly meeting. In this article I will show how to build a small slack bot via Elixir lang and I will point your attention to the little things that were not described in the official documentation.
Introduction
First of all, after the decision to create a bot, I had to decide which language to choose. At Elixirator I work as a ruby developer, so I flatly refused to use ruby lang for it. After that, my attention was caught by Hubot and javascript, but to write a code in Coffeescript in 2k17, come on 😂. So I chose Elixir with Hedwig chat-bot framework. As my main stack I will use such libs:
- Hedwig - as a chat bot framework
- Ecto - as a database toolkit.
- Postgrex - PostgreSQL adapter for Ecto
- Cronex - simple scheduler inspired by well-known whenever Ruby gem.
Expectation
Our bot should be able to accept messages from the user during the week, aggregate it and remind about them in 15 minutes till the weekly meeting. Also it should be able to explicitly give back messages added during the week and user should be able to remove any of them. Expected chating should be like that:
user: bot note add more specs to the current task
bot: Get it
user: bot note send info mail to the customer
bot: Get it
user: bot notes list
bot:
1 - add more specs to the current task
2 - send info mail to the customer
Check it out.
user: bot destroy note 2
bot: I threw it away
Setup
To create an application in Elixir for Hedwig chat-bot you need to run:
$ mix new bot_name --sup
The sup
option is given to generate an OTP application with supervision tree. We need an supervisor to deal with our Hedwig bot, Ecto and scheduler processes.
After the creation of the application we need to add required libraries to the dependencies:
# mix.exs file
def application do
[extra_applications: [:hedwig, :ecto, :postgrex, :logger],
mod: {BotName.Application, []}]
end
defp deps do
[
{:hedwig, "~> 1.0"},
{:postgrex, ">= 0.0.0"},
{:ecto, "~> 2.1"},
{:cronex, "~> 0.4.0"}
]
end
To fetch them run:
$ mix deps.get
After the fetching, you need to generate the bot module by the command mix hedwig.gen.robot
and add it to your supervision tree.
# lib/bot_name/application.ex
defmodule BotName.Application do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
worker(BotName.Robot, [])
]
opts = [strategy: :one_for_one, name: BotName.Supervisor]
Supervisor.start_link(children, opts)
end
end
All modules which should be supervised will be added to the children
list. Further, we will add here Ecto repository and a scheduler. But for now, we are ready to add our first responder.
Responders
For the current application I need three responders:
- Notes creation
- Notes destroy
- Notes list
All of them were decided to leave in one file because the logic of processing will be leaved in services.
Hedwig provides two macroses: hear/2
and respond/2
. The difference between them is that hear/2
matches messages containing regular expression and respond/2
matches only when regular expression is prefixed by the bot name or bot name alias. My responder will looks like that:
# lib/bot_name/responders/notes.ex
defmodule BotName.Responders.Notes do
use Hedwig.Responder
alias BotName.Services.Notes.Create
alias BotName.Services.Notes.List
alias BotName.Services.Notes.Destroy
alias Hedwig.Message
hear ~r/note (.*)/, %Message{matches: %{1 => content}, user: user, room: room} = msg do
emote msg, Create.run(%{username: user, room: room, content: content})
end
hear ~r/notes list/, %Message{user: user} = msg do
emote msg, List.run(%{username: user})
end
hear ~r/destroy (?<all>all) (?=notes)|destroy notes (?<ids>.*)/, msg do
emote msg, Destroy.run(msg)
end
end
Sensitive data
To start working on the processing logic, first of all, we need to setup Ecto with Postgrex. It is nicely documented here so I will not go into details. But this documentation is not covering the topic: how to deal with sensitive data. Because no one wants to store passwords in the repository on Github or wherever. During the research, I haven't found any tool that solves this problem, so had to make it in such way. My configurations file looks like that:
# config/config.exs
use Mix.Config
config :slave, ecto_repos: [BotName.Repo]
config :slave, BotName.Repo,
adapter: Ecto.Adapters.Postgres,
database: System.get_env("DB_NAME"),
username: System.get_env("DB_USERNAME"),
password: System.get_env("DB_PASS"),
hostname: System.get_env("DB_HOSTNAME")
import_config "#{Mix.env}.exs"
Depending on the environment, it imports different configs which merged with the current one. Also, all sensitive data are taken from environment variables. To expose them to the development environment, config/.env
file was made.
# config/.env
export DB_NAME="bot_name_dev"
export DB_USERNAME="bot_name_app"
export DB_PASS="2eYdW8D4"
export DB_HOSTNAME="localhost"
Before the application run, we should export it with:
$ source config/.env
After that you are ready to go.
Gotchas
Depending on adapter, message has a bit different structure. In console adapter, user
will be a string, but in case of Slack adapter, it gonna be a map with user name and user id.
# Messages inspected in create `hear` responder:
# console adapter
%Hedwig.Message{
matches: %{0 => "note list", 1 => "list"},
private: %{},
ref: #Reference<0.0.4.1460>,
robot: #PID<0.321.0>,
room: nil,
text: "note list",
type: "chat",
user: "beznosa"
}
# slack adapter
%Hedwig.Message{
matches: %{0 => "note list", 1 => "list"},
private: %{},
ref: #Reference<0.0.1.2804>,
robot: #PID<0.214.0>,
room: "D5ZNBVD2P",
text: "note list",
type: "message",
user: %Hedwig.User{
id: "U0JBHFDK9",
name: "beznosa"
}
}
This fact is bothering me a bit, because it's not clear how to work in the development and production environment when message structure is different from adapter to adapter (because when I started, I thought to use console adapter in development and the Slack adapter in the production environment).
Wrapping up
During the whole development I felt the lack of documentation, tutorials, guides, but it's probably due to the youth of the Elixir lang and youth of libs. To be honest, the source code was the best documentation for me. I hope, it's gonna be changed soon with our efforts. Results of my work that is done so far can be checked here. In further articles I will describe how to deploy it.
Also recommend
We’re always attentive to the opinion of our customers and take into account all the shortcomings