Slack command with API Gateway and AWS Lambda
At work we operate the Agile methodology and work in two week sprints. At the end of each sprint we hold the retrospective, something that was introduced a few sprints back was to vote for the past sprints MVP (Most Valuable Player), or as I like to say; Most Valuable Programmer. At the end of the sprint the team leads ask’s for everyone to send their votes for MVP, and for a couple of days up to the sprint this is asked over and over, and also asked during the retro. So I made an assumption, of why people might not be voting.
People are not voting due to in not being anonymous
With this in mind, and having wanted to make a bot for Slack I thought it could not be too hard to create a slack command that users could use to cast their vote for MVP.
For it to be simple and not get too complicated the minimum requirements I set myself were:
- People votes by using a slack command and the users name eg: /mvp @matthewroach
- You can not vote for yourself
- You can only vote once
- Voting is per sprint, need a way to start and stop voting
- Only one active vote topic at a time
- Upon stopping the vote the bot would send an in channel message saying who the winner was
Maybe not a small list to accomplish. Over the course of a weekend I created a slack command that did all the above.
One requirement that Slack enforce for integrations is that they must be using https. With this is mind and not wanting to set up SSL and host things myself for something that’s likely to used very infrequent. I decided to use AWS services to handle this, most notable API Gateway and Lambda, for storing the data I went with MongoDB using mLab, mainly because I am familiar with Mongo. mLab offer a free 500mb sandbox database that would be ideal for this.
Slack slash commands
Slash commands are commands that allow users to interact with a third party service. The part of the / (slash) is the command name, then any text after the command is used by the service to do what it needs to do. A slash command can use either a GET or POST request. I decided to use the POST verb to pass along the data from the command.
Slash commands can either post back to use anonymously, or send back the result to the channel it was triggered in. By default it’s anonymous. The other options that you have like better formatting of messages, attachments you can see at their documentation.
AWS API Gateway
API Gateway act’s as a “front door” for applications to access data, business logic or functionality from your back-end services.
API gateway is not limited to the AWS infrastructure, for the slack command I hooked up a POST interface to a lambda function.
Once you deploy your API to an environment, Amazon allows you to deploy your API to multiple stages so you can have a test, staging and production set up. With each stage you get a different URL you can use to call your endpoints with.
The UI for setting up API’s via the AWS console is not the greatest and takes quite a few clicks to go through the different steps. Also, when you hook up an API to a lambda function you need to create a body mapping template that will take the incoming requests and convert to a format you wish to consume in your lambda function. In this case I added a mapping for the content type: application/x-www-form-urlencoded that look like this:
## convert HTTP POST data to JSON for insertion directly into a Lambda functionfirst we we set up our variable that holds the tokenised key value pairs
#set($httpPost = $input.path('$').split("&"))
next we set up our loop inside the output structure
{ #foreach( $kvPair in $httpPost )
now we tokenise each key value pair using "="
#set($kvTokenised = $kvPair.split("="))
finally we output the JSON for this pair and add a "," if this isn't the last pair
"$kvTokenised[0]" : "$kvTokenised[1]"#if( $foreach.hasNext ),#end #end }
Hopefully the comments in the code make it easy for you to understand what’s happening. Basically we are converting the form body slack pass us into a JSON object of key value pairs.
AWS Lambda
Is a serverless compute service that runs your code in response to events and automatically manages the underlying compute resources for you.
Lambda is where all the magic happens. I wrote some simple nodejs code that handles the different inputs from the slash command, does it’s logic, reads/stores data to MongoDB and then responds with a result for the user issuing the slack command.
I have pushed the code to a repository on my GitHub account if you wish to take a look at the node code.
As I mentioned earlier, I converted the incoming data from slack into a JSON object that is available to my node code under the event object. With this JSON object now available to me within my function I am able to look at the keys I need and do the required actions. The main thing we are after is the text key from the object this holds the text after the /mvp part of the slash command. I use this key to work out what action I should be taking from the caller.
There are only three commands available from to the user using /mvp; that is start, stop and voting. Voting is working about by looking for an @ as the first character of the text. If I don’t match either of these three, I tell the user you can not perform that action.
Some of the other keys I am using for the function is the team_domain, this is used to determine the mongoDB collection I need to look into. This keeps other teams data away for each other, and avoid having huge one huge collection of data. I also use the user_id to track if the user has voted already. The command does not track who voted for who, it will also not let you vote more than once, and you can only vote if we find an active mvp vote, which also means it’s only possible to have one mvp vote at a time.
I added some sample JSON files that I was using for testing the code locally. I used lambda-local to test my function locally, which makes for a much better experience than having to deal with the AWS interface all the time for writing code and testing.
Without going into great depths of lambda, you have up to three function arguments available to you within your main function, event, context, and callback. Data comes in on the event, context contains information about the lambda function that is executing and the callback is used to return information to the caller. The callback is an optional argument. You can read more about this in the lambda documentation.
Screenshots of working /mvp
Starting/ Opening the voting for a given item, I called this vote Sprint 99
Casting your vote for the MVP
Vote has been cast
Stopping/ Closing the voting for Sprint 99 and seeing who was the MVP!