dnlgrv

Building a Slack bot using Elixir

It’s almost impossible to use Slack without encountering a bot, especially as slackbot sends you a message whenever you first join an organisation (that’s the one that asks you for your profile information). Ever since I first joined a Slack organisation I’ve loved how they use slackbot to gather that information as it’s a great on boarding experience.

What can a bot do?

A bot is almost like another user in a Slack organisation, except you have a computer sending and receiving messages rather than a human. So a bot is only capable of doing anything that a human user can do (although hopefully a lot quicker!).

Our bot

We’re going to build a friendly bot called Winston (name taken from the scientist in Overwatch). Winston is politely going to say “Hello!” whenever someone in Slack messages him saying “hello”.

You’re going to need permission in your Slack organisation in order to add the bot integration. Before moving on I’m going to assume you have your Slack Bot API Token.

Connecting to Slack

First, we’re going to create our project using using mix new:

mix new winston --sup

I’m providing the --sup flag here to save some time setting up the Mix application and initial supervision tree.

To save us a whole amount of effort we’re going to use the Elixir-Slack library, which helps us open a websocket connection to Slack and handle incoming messages, so add it to your list of dependencies and applications (don’t forget to run mix deps.get):

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

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

The module that makes up the bulk of Winston is going to be Winston.Slack. So create the module and add it to the supervision tree in lib/winston.ex:

def start(_type, _args) do
  import Supervisor.Spec, warn: false

  children = [
    worker(Winston.Slack, [])
  ]

  opts = [strategy: :one_for_one, name: Winston.Supervisor]
  Supervisor.start_link(children, opts)
end

The Slack module defines start_link/2, which takes the Slack Bot API Token and the initial state. Rather than provide these arguments from the supervisor, we’ll define start_link/0 in our Winston.Slack module like so:

defmodule Winston.Slack do
  use Slack

  @token Application.get_env(:winston, __MODULE__)[:token]

  def start_link, do: start_link(@token, [])
end

I’m providing my Slack Bot API Token here using the module attribute @token, which is pulling from some configuration I defined in config/config.exs.

At this point, all you need to do is run your project (I like to use iex -S mix for debugging) and you should see your bot connect to Slack:

Becoming a bot

Winston managed to connect to Slack, but he isn’t fulfilling his function as a bot by just sitting there doing nothing. He’s nothing more than an idle user right now.

In our Winston.Slack module, if we define a function handle_message/3, it will be called every time a message is sent in Slack. Give it a try:

def handle_message(message, slack, state) do
  IO.inspect message
  {:ok, state}
end

You should start seeing messages coming through, even when someone starts typing in the channel.

We want to respond to any messages with type: "message". Also, we don’t want Winston to reply to every single message, only messages that are directed at him. If you take a look at an @ message, you can see it doesn’t actually include Winston’s name:

%{channel: "C0K7LHXB2",
  team: "T0K7L271V",
  text: "<@U0KPSRAPQ>: hello",
  ts: "1454086194.000008",
  type: "message",
  user: "U0K7L331V"}

U0KPSRAPQ is the internal Slack ID for Winston. All user’s have them, and that’s how you should format any @ messages. So how do we know which ID is Winston’s? Luckily for us, the second argument to handle_message/3 is a struct containing information about the Slack connection. If we look at slack.me.id we can retrieve Winston’s ID.

def handle_message(message, slack, state) do
  IO.inspect slack.me.id
  {:ok, state}
end

Now all we need to do is see whether the message matches our criteria (“@winston: hello”) and, using Slack.send_message/3, send the message “Hello!”:

def handle_message(message = %{type: "message", text: text}, slack, state) do
  if text == "<@#{slack.me.id}>: hello" do
    Slack.send_message("Hello!", message.channel, slack)
  end

  {:ok, state}
end
def handle_message(_message, _slack, state), do: {:ok, state}

Here we use pattern matching to only act on type: message messages, which is extremely useful. Winston is now a fully functional, polite, Slack bot!

From here on…

This is just the basics of building a Slack bot with Elixir. You should definitely take a look at other functions available in Elixir-Slack to see what you can do. At the end of the day, all you need to do is look at incoming messages and then respond accordingly; there are a lot of possibilities.

The source code for Winston is available on GitHub.