How to create Slack bot in Elixir

Alex Beznos - 15.08.2017
elixir, slack, hedwig


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.


comments powered by Disqus