When we're not building Fugue, frequenting our favorite place for burgers and wings (Rex's Downtown Grill FTW!), or finalizing our plans to colonize Mars, we write Slack bots.
We have a few Slack bots sitting on EC2 instances managed by Fugue but nothing running in Lambda... yet. Our goal for this project:
- User types a keyword into Slack
- Slack POSTs to a URL hosted on AWS API Gateway, backed by Lambda
- Votebot posts voting ballot to Slack, ready for votes via reaction emoji
- User closes the ballot and bot tallies then posts the results.
In short, this:
(More options were available, but not shown in this screenshot.) Then, once we've battled over which pizza is best, we close voting and votebot
does the rest!
Note: Because votebot
seeded each option with a pizza emoji, the totals are (votes - 1) to reflect votes cast by humans.
Meet Our New Bot Overlord
We love pizza. Near Fugue HQ, there are a few good pizza places. It's easy to come to a consensus about whether we're in the mood for NY style or the quirkier toppings of the brick oven pizza place. The hard part is figuring out how many people want meats vs. veggies vs. white pizza and then extrapolating that into a number of pies to order.
Our original plan was just dumping our requests into Slack and letting some unlucky soul figure it out. When Slack released reaction emoji, it was a bit easier to put the pizza options to a vote. But someone still had to enter the menu options line by line manually… like a caveman. (And if they’re paleo, they’re not even eating pizza.)
Thankfully, the Slack API allows bots to post reaction emoji too. So we should be able to craft a bot that allows us to post the pizza list with a seed vote already attached to each option. Here's our shopping list:
- A Slack Outgoing Webhook
- A Lambda function written in Python
- A way to communicate with the Slack API
- A way to communicate with the AWS control plane
The Design
Using the project goals above, we can design votebot
. Long running functions are one possibility but not necessary. Plus, we don't want a function to spin and run up our bill! We will post a ballot when triggered and on closing, retrieve the ballot. No thumb twiddling necessary. Rather than design this as a pizza-exclusive bot, we'll create a general use bot. Then we can call votebot
for voting on pizza, other take-out restaurants, party snacks, board games, or anything we want to vote on. (Spoiler: We're going to use this for planning poker.)
We also don't want to update the code every time we want to add a new pizza place or thing we want to vote on. So we'll use DynamoDB to store pizza places and options.
To get data out of Slack, we'll use an Outgoing Webhook. Set this up first and enter a dummy URL. Be sure to grab the token because we'll use this in our code. You can use any trigger word you want, but I've set our company Slack to votebot
. The description, custom name, and icon are all up to you. Naturally I used votebot
for the bot's name.
The other piece of Slack to set up is a bot integration. This will get you access to the Slack Real Time Messaging API for bots. Once created, grab your API token; you'll need this for authentication.
DynamoDB Table
We're going to set up a simple DynamoDB table for this bot called vote-options
. The hash key will simply be called selection
. The key we'll use for the options (such as which pizzas to allow everyone to vote on) will be options
. The read and write throughput is set to 1
. This can be tuned later if necessary.
Code
We'll get data out of Slack by using the Outgoing Webhooks, so now let's look at how to get data back into Slack.
You can do this directly using the Python requests
module and the Slack API directly. I'm a fan of slacker, which is available on PyPi.
The last thing we need is a way to talk to AWS. For that, we'll use boto3.
Got that? Awesome. Now we can construct our bot!
Unfortunately, non-humans may be throttled. For now, we will work around this with sleep()
. In a future version, we will use the information returned from Slack to perform adequate retries to avoid throttling.
The last step is packaging. Our project votebot
has two dependencies: boto3
and slacker
. Each has dependencies of its own. Lambda includes the latest version of boto3
and its dependencies. Note: If you need to use a previous version, you must include it with your deployment package. slacker
has one dependency, requests
. There are a couple of options for packaging, but for this we'll simply pip install
the module to the directory where the code lives.
[:~]
cd votebot[:~/votebot]
lslambda_module.py requirements.txt SLACK_BOT_API_TOKEN SLACK_CHANNEL_TOKEN [:~/votebot]
pip install -r requirements.txt -t ....[:~/votebot]
lslambda_module.py requests-2.8.0.dist-info slacker-0.7.3-py2.7.egg-info requests slacker requirements.txt SLACK_BOT_API_TOKEN SLACK_CHANNEL_TOKEN
From here, we put all this stuff into a zip file and we have our deployment package! Remember:
[:~/votebot]
zip -r lambda.zip *...[:~/votebot]
lslambda.zip requests slacker lambda_module.py requests-2.8.0.dist-info slacker-0.7.3-py2.7.egg-info requirements.txt SLACK_BOT_API_TOKEN SLACK_CHANNEL_TOKEN
Deploying
Before deploying, you will need to create the IAM role for your code. There are two things you need - the policy and trust relationship. The policy will need to include DynamoDB permissions for the tables we need. Those are vote-options
and vote-open
. At some point we may want to create/delete the tables in code, so we'll just restrict the permissions to these tables but allow all operations. We will also allow CloudWatch logs since I have my function logging there now. You can use this or not. It's up to you.
The trust relationship needs to use sts:AssumeRole
for the lambda.amazonaws.com
service.
Now we can create the Lambda function. Skip the blueprint. Set your function up like the following screenshot, then upload the .zip file you previously created.
Frontend'ing
When configuring Slack for an integration, the request body is sent as the Content-Type application/x-www-form-urlencoded
. This is a problem because the API Gateway currently expects a JSON document. Both are pretty strict with these settings at the time of this writing. Fortunately, there is a workaround. We can use the magic $input
variable at the API Gateway to turn our request into a string that we can parse in code.
After setting up your API Gateway entry, open the POST method for the resource (this one is called vote). Click Integration Request
and under Mapping Templates
enter application/x-www-form-urlencoded
as the Content-Type
and edit the mapping template to include:
{ "formparams" :
$input.json("$") }
Now when your Lambda function executes, event
will contain the key formparams
.
Next Steps
Now that we've made a neat and tidy way to vote for our favorite pizzas and other stuff, here are some features we'd like to add to this bot:
- Pizza math or some kind of fuzziness: Using the number of votes and the servings per pie for a particular restaurant, Pizzabot suggests how many of each of the top scoring pies to order.
- Edit table entries: A way (through Slack) to add/remove/edit entries from the DynamoDB table that hold our voting options. (Example:
/votebot flippin add Bacon Explosion Pizza
)
- Voting window: When someone opens voting, create a Lambda timer to fire 15 minutes after voting opens. This will close voting and write the voting results to the channel. As of now there isn't a documented API call to do this.
- Menu URLs: For things with an online menu (like pizza!) we'd like to display the URL when voting opens.
Check It Out!
You can get the code for Votebot on Github. Let us know what you think!
Want to learn more about Lambda? AWS is offering a webinar entitled AWS Lambda Best Practices: Python, Scheduled Jobs, and More on October 29, 2015 at noon EST.