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.