Building a Serverless Slack bot

At orangejellyfish one of our longest running problems has been the question of which pub to go to for a couple of drinks after work. Recently, we decided it was time to solve that problem and put some of our open source software to work in the process! Since we use Slack for most of our day-to-day communication it would be great if we could stay within that environment when considering possible solutions. Fortunately, Slack has wonderful support for third-party integrations in a variety of forms including full-featured "bot" users that are capable of facilitating stateful conversations with "real" human users within your organisation. We don't need quite that level of power to achieve our goal so we'll focus on a simpler form of integration known as "Slash Commands".

Understanding Slack Slash Commands

The concept of Slash Commands in Slack is largely similar to the much older concept of commands in IRC. A bunch of commands are available by default, such as /remind or /archive but anyone in a Slack workspace can create custom commands.

A command is made up of only two parts - the name of the command (such as /remind) and the payload portion which follows. An example "remind" command might look like /remind me tomorrow "plan team night out". The payload portion is a single string and will be sent as-is to the service configured to process the command in the form of an HTTPS POST request. We can't set up the Slack side of things without first having a web service running somewhere it can talk to. So we'll look at that first.

Building a Slash Command service

When a user issues a custom Slash Command in a Slack channel an HTTPS POST request is made to a URL specified when you configure the command (something we will look at later). Because it's unlikely that our command will be invoked at massive scale it's a great use case for a "serverless" application, for which our go-to stack revolves around the Serverless Framework, AWS Lambda and Node.js.

Setting up a Serverless application

At orangejellyfish we're big fans of the Serverless Framework and having used it across a wide range of projects we have developed a starter kit based around our experiences and best practices. You can use the Serverless CLI to quickly scaffold a new Serverless application from that kit:

npx serverless create --template-url https://github.com/orangejellyfish/serverless-starter --path your/local/path

A Slack Slash Command service consists of a single HTTPS endpoint, which in Serverless Framework terms maps to a single function. The starter kit we used is already set up with one, called hello. We can repurpose that for our needs, and rename it to be a bit more semantic. We've gone with slackHook:

export default async function slackHook() {
  // Our handler logic will go here.
}

And we can make a small change to the function configuration YAML file which is necessary because Slack will make a POST request rather than a GET. We've also removed the "schedule" event because our Slack bot is not a mission-critical service so the Lambda cold-start problem is less of a concern:

slackHook:
  handler: src/functions/slack-hook/index.default
  events:
    - http:
        path: slack-hook
        method: post

Handling the POST request from Slack

With our Lambda function set up to respond to HTTP POST requests we can take a look at the logic. The Slack documentation details exactly what the request will look like. All of the items of data that we might need to build a useful command are contained within the request body. The request has a Content-Type of application/x-www-form-urlencoded which means its body looks pretty much like a querystring might look. In fact, we can use the built-in Node.js querystring module to decode it.

Once we've done whatever it is we need to do with the request, Slack expects a response. At a minimum, it needs to receive an HTTP 200 status code. That response can also contain text, or more complex content. Let's flesh out our function so that it echos back whatever the user has sent to it:

import qs from 'querystring';

export default async function slackHook(event) {
  const data = qs.parse(event.body);

  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      response_type: 'ephemeral',
      text: data.text,
    }),
  };
}

This response is "ephemeral" which means only the user who used the Slash Command will be able to see it. You can make messages visible to the whole channel by setting the response_type property to "in_channel" instead.

Deploying the service

At this point we have a working service. For Slack to be able to contact it to complete the integration we need to deploy it somewhere accessible. The Serverless starter kit that we used expects to be deployed to AWS. If you have followed the instructions for configuring AWS you can deploy the service to your account with a single command:

npm run deploy

The output of this command should include a URL that when requested via HTTP POST invokes the function to respond to the Slash Command:

Deploying a Serverless application The output from the Serverless Framework CLI on deployment

We can then trigger the deployed Lambda function by making a request to the API Gateway endpoint, just like Slack itself will do when we configure it. The easiest way to do this is with curl:

Using curl to trigger the Lambda function Using curl to trigger the Lambda function

Configuring the Slash Command in Slack

Now that we have a service that is accessible to Slack we can set up the Slash Command itself. The first step is to create a new Slack App which you can do on this page. We'll call this example app "Echo Slash Command". You will need to select a workspace to which the app will belong:

Creating a Slack App Creating a Slack App

We then get a number of options to configure the new Slack App. We can select "Slash Commands" in the "Add features and functionality" section and then configure the Slash Command itself. The most important fields here are the "Command" itself and the "Request URL". The "Command" is the name of the Slash Command that users will type in their workspace. The "Request URL" is the endpoint to which Slack will make requests. We can use the public API Gateway URL given in the Serverless deployment output previously:

Configuring a Slash Command Configuring a Slack Slash Command

The final step, back on the App setup page, is to "Install your app to your workspace". Once that's done it will be available to use. You can try it out in any channel, including your private "yourself" direct message channel which is an ideal place for testing.

Using a Slash Command The new Slash Command in action

Next steps

So far our Slash Command is not very useful. Our original goal was to build a command that would recommend a pub for us to visit. Stay tuned for part two where we look at how we achieved that with the Google Maps API. In the meantime, here's a few things you could continue to explore:

  • Play around with "ephemeral" and "in channel" responses to Slash Commands.
  • Investigate the other types of Slack App that can be developed.
  • Have a look at the existing Slash Command ecosystem to get an idea of the kind of things that can be built.
Twitter