All examples were made with Elixir 1.3, however any Elixir >= 1.0 should work.
Slack has a few different ways for you to write awesome bots for your slack teams. One of the easier ways is through a “slash command.”
A slash command works by sending a post request to an HTTPS (yes, SSL is required) endpoint whenever someone issues the command in chat. The post data consists of what command was sent, any text that came after the command, and some other data that we’ll get into in a bit. Then your service can respond with plain-text or some JSON and that response will be displayed in chat.
So, lets dig into this.
Let’s do the “hello, world” of bots, an echo bot. First make sure you have elixir installed and then lets create a new mix project:
[~] $ mix new slashbot[~] $ cd slashbot
Now open up mix.exs and lets specify our dependencies:
defp deps do[{:cowboy, "~> 1.0.0"},{:plug, "~> 1.0"}]end
and don’t forget do add them to the applications list:
def application do[applications: [:cowboy, :plug]]end
If you’ve heard about Elixir, you’ve probably heard of the Phoenix Framework for doing web applications. However phoenix is a little over kill for what we need to do, so we are using Plug (which phoenix is in part built on top of). Plug allows us to easily build a small web app. By default it comes with an adapter for Cowboy, which is an HTTP server written in Erlang.
Anyway, lets get to writing down some code. Create a lib/slashbot directory in the project and a router.ex file within it.
[~/slashbot] $ mkdir lib/slashbot[~/slashbot] $ touch lib/slashbot/router.ex
Now lets open that up and write down some elixir:
defmodule Slashbot.Router douse Plug.Router
plug Plug.Parsers, parsers: [:urlencoded]plug :matchplug :dispatch
get "/" dosend_resp(conn, 200, "")end
post "/" do%{"text" => text} = conn.paramssend_resp(conn, 200, text)end
match _ dosend_resp(conn, 404, "not found")endend
Do we’ve defined out router module, and we’re using Plug.Router so we can get all its delicious macros for Plug’s router DSL. Next we define our pipline of plugs. A plug pipline defines the functions a connection will get passed down through and processed. So first we have the connection get process with Plug.Parsers, which will parse out any urlencoded parameters and stick them into the ‘params’ map that is attached to the plug connection. The :match and :dispatch plugs come next and are required, :match being the plug is responsible for finding the matching route, and :dispatch being the one that, well, handles the dispatching of the connection.
After all that, we get to the real meat of what’s happening. We define a few routes.
Our first route handles a simple GET request for ‘/’ and sends a blank 200 response. This is because slack will periodically send get requests to make sure the bot is still around
The second route handles a POST request for ‘/’ and this is what will be handling our bot logic. We pull out the “text” field from the posted params, which contains the text a user would put after the slash comment. Like for: “/slashbot hello there” we would get “hello there” in the “text” parameter. So we get that text using pattern-matching on the conn.params map, and then send it back with send_resp, echoing the text back to the slack channel.
The last one is our catch-all route for anything that doesn’t match the above routes. It just sends back a 404 saying the page they tried to get doesn’t exist.
So now that we have a basic implementation going, lets fire it up and test it out with curl. Open up two terminal windows. In the first, we’re gonna make sure we got our dependencies, compile, and fire up iex:
[~/slashbot] $ mix do deps.get, compile[~/slashbot] $ iex -S mixErlang/OTP 19 [erts-8.0.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help)iex(1)> {:ok, _} = Plug.Adapters.Cowboy.http(Slashbot.Router, []){:ok, #PID<0.163.0>}iex(2)>
Now leave that running and head to your other terminal window:
[~/slashbot] $ curl -XPOST "http://localhost:4000?text=hello"hello[~/slashbot] $
Yay! It works, with curl at least. Now, opening up iex and typing those commands is fine for testing, but would be annoying for any kind of actual deployment, so lets fix that.
Alright, we’re going to open up lib/slashbot.ex and make it a prod-ready, supervisor-using, application:
defmodule Slashbot douse Application
def start(_type, _args) dochildren = [Plug.Adapters.Cowboy.child_spec(:http, Slashbot.Router, [],[port: 4000])]
opts = \[strategy: :one\_for\_one, name: Slashbot.Supervisor\]
Supervisor.start\_link(children, opts)
endend
We’re just creating a simple start function for our app that will spin out a supervisor that will start up our cowboy webserver and plug router. If you don’t know what a supervisor does, it’s a process (as in an elixir process, not a system process) within your application that starts and monitors other processes in your application and restarts them if they die. This way, some transient error could crash our plug router, but the application as a whole would stay up and our router would be instantly restarted.
Next we need to tell mix to start this module with start function when the application… starts. So key this into the applications second in your mix.exs:
def application do[applications: [:cowboy, :plug],mod: {Slashbot, []}]end
Now we can test this out with
mix run --no-halt
and run our curl:
[~/slashbot] $ curl -XPOST "http://localhost:4000?text=hello"hello[~/slashbot] $
Still not good enough for prod though, we want to be able to just start this and forget about it. So lets create a release with distillery.
Add distillery to your deps:
defp deps do[{:cowboy, "~> 1.0.0"},{:plug, "~> 1.0"},{:distillery, "~> 0.9"}]end
And now we can create a release:
[~/slashbot] $ mix do deps.get[~/slashbot] $ mix release.init[~/slashbot] $ MIX_ENV=prod mix release --env=prod
This will generate a slashbot.tar.gz in rel/slashbot/releases/0.1.0/ that you can copy up to your server.
Now at this point, we haven’t set up any sort of SSL. Plug with Cowboy can handle SSL, but I prefer to have nginx or some other process do it, that way I can let my application focus on being the application and some other webserver can handle the SSL. If you don’t have a server set up with SSL certs, you can hit-up Lets Encrypt for free, trusted certificates.
If you go the nginx route, you’ll need to set up a proxypass to your app:
location /slashbot {proxy_pass http://127.0.0.1:4000/;}
Once you’ve setup your SSL, lets continue on with getting the slashbot started:
[~] $ mkdir slashbot[~] $ cp slashbot.tar.gz slashbot[~] $ cd slashbot[~/slashbot] $ bin/slashbot start
That will start slashbot in the background for you. Now we can test it with our curl again:
[~/slashbot] $ curl -XPOST "http://localhost:4000/?text=hello"hello[~/slashbot] $
Wee! That’s working. Now you’ll want to set up a slash command on your slack team, putting in the URL of your server + the path that you have the command running on. With any luck, you’ll be able to do this:
There’s a couple of things improve upon here.
First, you’ll notice that the messages only echo back to you! That’s boring. With a few tweaks, we can send back some JSON that will allow us to set an option to show the message to everyone in the channel.
Second, we do no authentication what-so-ever, anyone could set this bot up as a slash command in their slack and get a response back. With our simple echo bot, that’s not too terrible, but with anything slightly more complicated, that’s a huge problem. This can be fixed by checking the token that gets posted to the app, against the token that’s generated when you create the slash command integration.
These improvements will be left as an exercise to the reader, or maybe if there’s enough interest I’ll create a part 2 that gets into that stuff.