lambda-slack-router
is a pattern for building Slack slash commands using the Amazon AWS Lambda service and Node.js. It functions as a single endpoint that receives a JSON payload from Slack and returns an appropriate response. For instance, if you were to enter
/testbot ping
into a correctly configured Slack channel, it would call the appropriate ping
command and return the generated response to the user. A help
command is also generated that is based on the provided commands and their descriptions, so that you can also call
/testbot help
and a usage message will be returned.
This package is hosted on npm. From the root of your project run:
$ npm install --save lambda-slack-router
Commands are added to the slackbot through the addCommand
function. Sample configuration for the above ping command would look like
var SlackBot = require('lambda-slack-router');
var slackbot = new SlackBot({ token: "<token>" });
slackbot.addCommand('ping', 'Ping the lambda', function (event, callback) {
callback(null, this.inChannelResponse('Hello World'));
});
In the above code, a slackbot is created with the given token (used for verifying the authenticity of each request). The ping command is then added to the routing, and when called responds with an in-channel response of 'Hello World'.
The first argument to the addCommand
function is the name of the command. The second argument is the description of the function. This is used in the generated help
command, and is useful to your users when they can't remember the syntax of your bot.
The two arguments passed to the command callback are event
and callback
. The event
object contains all of the properties that came from lambda, as well as the args
property (the arguments passed to the function, as an object). The callback function is the same as the context.done
function that's built into lambda. The function expects that an error will be passed in as the left argument if there is one, and otherwise a successful execution's response will be passed in as the right argument.
The responses for Slack can either be ephemeral (returning to the user that invoked the function) or in-channel (returning to everyone in the channel in which the function was invoked). SlackBot has a built-in helper for each of these types of responses which are ephemeralResponse
and inChannelResponse
respectively. If you pass a string to either one of these functions they return a correctly-formatted object. If you want more fine-grained control, you can pass an object to them and they will set the response_type
attribute. You can also ignore these functions entirely if you want to return a custom payload.
By default, the SlackBot will respond with the exact structure that slack expects. This will function properly using our scaffold because of the request template that we constructed:
"requestTemplates": {
"application/x-www-form-urlencoded": "{\"body\" : $input.json(\"$\")}"
}
You can, however, set up your own function to build the structure of your responses by setting the structureResponse
property of the configuration object that you pass to the SlackBot constructor. This example below will be sufficient to get API gateway working out of the box:
var slackbot = new SlackBot({
structureResponse: function (response) {
return { body: JSON.stringify(response) };
}
});
See the slack documentation for more information on buttons.
The action configured must match the name
value of the presented button.
There's no need to use inChannelResponse or ephemeralResponse since your provided message will replace the existing message when using the callback.
slackbot.addAction('buttonPress', function (event, callback) {
callback(null, { text: "You pressed a button" });
});
Slackbots can optionally be locked down with the token provided by slack. If provided, the token will be checked against each request. It is recommended to provide a token whenever possible, as the endpoint is otherwise vulnerable to exploitation. It is usually easiest to maintain the token in an environment variable, like so:
var slackbot = new SlackBot({ token: process.env.SLACK_VERIFICATION_TOKEN });
After uploading a new version of your lambda function or extended periods of inactivity, AWS spins down your lambda in order to conserve resources. This results in the lambda being in a "cold" state, and the next time it is accessed it must spin up again, resulting in a "cold start penalty." This can be especially problematic for slackbots, as the extra time needed to spin up the lambda can take longer than slack is willing to wait for a response. To mediate this, you can set pingEnabled
to true in the slackbot config and set up a CloudWatch event that will ping your lambda every 5 minutes to keep it running hot. Below is an example of the necessary configuration:
var slackbot = new SlackBot({ pingEnabled: true });
If you're using Serverless to configure your lambda project, you can add the following to your s-function.json
file to configure the event source on function deploy:
{
"events": [
{
"name": "ping-slackbot",
"type": "schedule",
"config": {
"schedule": "rate(5 minutes)",
"enabled": true
}
}
]
}
Commands can optionally have arguments. When commands have arguments, the router will only invoke that command if the number of arguments matches. Arguments are specified as an array as the second argument to addCommand
. As an example:
slackbot.addCommand('testing', ['one', 'two'], 'Testing', function (event, callback) {
callback(null, this.ephemeralResponse('One: ' + event.args.one + ', Two: ' + event.args.two));
});
There are three types of arguments: required, optional, and splat. The two arguments in the above command are both required. To specify an optional command use an object with one key where the key is the name of the argument and the value is the optional value. To specify a splat argument (one that will be an array of indeterminant length) append an ellipsis to the end of the name. An example that uses all three is below:
slackbot.addCommand('echo', ['title', { lastName: 'User' }, 'words...'], 'Respond to the user', function (event, callback) {
var response = 'Hello ' + event.args.title + ' ' + event.args.lastName;
if (option.args.words.length) {
response += ', ' + event.args.words.join(' ');
}
callback(null, this.ephemeralResponse(response));
});
The routing for the commands is achieved by the Slackbot's router acting as the handler function for the lambda. After a Slackbot has been fully configured (adding in configuration, building the command callbacks, etc.), the handler should be set to the return value of the buildRouter function.
exports.handler = slackbot.buildRouter();
It's helpful in testing your function to also export the slackbot itself. If it's part of the module's exports, each function can be tested explicitly as opposed to having to go through the router (which would be testing library code instead of your own). A sample test using mocha
and chai
for the aforementioned ping
function would look like
var expect = require('chai').expect;
var slackbot = require('../slackbot/handler').slackbot;
describe('slackbot', function () {
it('responds to ping', function () {
var received = false, receivedArgs = [], callback = function (error, success) {
received = true;
receivedArgs = [error, success];
};
slackbot.ping(null, callback);
expect(received).to.be.true;
expect(receivedArgs).to.deep.eq([null, slackbot.inChannelResponse('Hello World')]);
});
});
assuming your handler is named index.js and you had exports.slackbot = slackbot
in your handler.