Getting started writing a Slack bot with Elixir

Robots are cool, as is playing with new languages. At carwow we decided to combine both by creating a Slack bot in Elixir as a playground for us to have some fun with a new language. This post describes the basics of getting such a bot up and running. Our goal is to have a bot which responds to messages directed at it containing “ping” with a message “pong”.

Installing Elixir

First of all we’re going to need to have Erlang and Elixir installed. For this follow the guide to installing over at the Elixir website.

Next you need to come up with a name for your bot. This is probably the hardest part of the process since naming things is hard. For carwow we chose KITT after the car from the TV show Knight Rider

Creating the app

Now we can create our Elixir app. This can be created with the following command:

mix new kitt --sup

The ‘sup’ option makes sure our app also includes a supervisor tree.

Adding dependencies

We are going to use the Elixir-Slack project in order to make use of the Slack Realtime Messaging API. As the Elixir-Slack README describes, we add this to our apps dependencies by updating the deps private function in the mix.exs file of our app. The updated code should look something like this:

defp deps do
[{:slack, "~> 0.6.0"},
{:websocket_client,
git: https://github.com/jeremyong/websocket_client"}]
end

Next we ask mix to install our new dependencies:

mix deps.get

And then again in the mix.exs file, we add slack to our list of applications. We do this by updating the application function to look like this:

def application do
[applications: [:logger, :slack],
mod: {Kitt, []}]
end

Writing code to receive messages

To make our simple Slack bot work, we need to run a process which will receive messages from Slack. To do this we create a new module and bring in the Elixir-Slack functionality. So in lib/kitt/slack.ex we add:

defmodule Kitt.Slack do
use Slack
end

If you check the Elixir-Slack documentation you can see that our module can implement a handle_message callback. This will be called whenever we receive an event from Slack. You can see all the available event types in the Slack RTM documentation. For now we only care about messages and so we can use pattern matching to just get these. So we update our handler to look like this:

defmodule Kitt.Slack do
use Slack
  def handle_message(message = %{type: “message”}, slack, state) do
if Regex.run ~r/<@#{slack.me.id}>:?\sping/, message.text do
send_message(“<@#{message.user}> pong”,
message.channel, slack)
end
    {:ok, state}
end
end

One initially confusing thing is that a message in the form:

@kitt ping

will actually be received as:

<@USER_ID> ping

Usefully, the slack parameter we receive includes a “me” property which among other things includes an “id” property.

So in the code above we simply check the message against a regular expression, interpolating “slack.me.id” into the check. If this matches we then send the user a message back, to the same channel and using the “message.user” property to make sure we @ them.

Since we’ve implemented the handle_message function, the runtime will also attempt to call us with other event types. To stop our process from crashing we therefore also need to add a catch all variant of the handle_message function. This means our resulting code looks like this:

defmodule Kitt.Slack do
use Slack
  def handle_message(message = %{type: "message"}, slack, state) do
if Regex.run ~r/<@#{slack.me.id}>:?\sping/, message.text do
send_message("<@#{message.user}> pong",
message.channel, slack)
end
    {:ok, state}
end
  # Catch all message handler so we don't crash
def handle_message(_message, _slack, state) do
{:ok, state}
end
end

Adding our Bot to Slack

Now we’re almost ready to go. We just need to add a Bot Integration to our Slack team and then configure our app to use it.

Adding a Bot Integration is done from within your Slack teams settings (which should be at a URL like https://your-team.slack.com/apps/manage/custom-integrations). Once you add your bot grab the API token.

Keeping our configuration neat

So that we don’t need to commit our token with our code, we will use environment variables to get the token into our app. In development, the simplest way to do this is to create a file, which I usually name .env and put the value in there:

export SLACK_TOKEN=my-slack-token

You can then add .env to your .gitignore file and source it into your shell while developing:

source .env # loads the SLACK_TOKEN into the current shell

In our app, we use the file config/config.exs to handle config. Update it to load the value from the environment variable:

use Mix.Config
config :kitt, Kitt.Slack,
token: System.get_env("SLACK_TOKEN")

And finally we need to start our Slack module and put it into our supervisor tree. We do this in lib/kitt.ex

defmodule Kitt do
use Application
  def start(_type, _args) do
import Supervisor.Spec, warn: false
    slack_token = Application.get_env(:kitt, Kitt.Slack)[:token]
    # Define workers and child supervisors to be supervised
children = [worker(Kitt.Slack, [slack_token, :whatever])]
    opts = [strategy: :one_for_one, name: Kitt.Supervisor]
Supervisor.start_link(children, opts)
end
end

The important parts here our how we pull out the token from our config:

slack_token = Application.get_env(:kitt, Kitt.Slack)[:token]

and then we add our module as a worker in the children of the supervisor tree:

children = [worker(Kitt.Slack, [slack_token, :whatever])]

The moment of truth

Now that’s all done we can run our app inside iex:

iex -S mix

If things worked well, you should see your Bot come online.

Hopefully it should also reply to your messages.

KITT in action

Interested in making an Impact? Join the carwow-team!
Feeling social? Connect with us on Twitter and LinkedIn :-)