Building a Slack Bot with Node.js to Query Channel History
Shortly after we started using Slack on our team in 2014, it also became my task management tool. I hate having my notes spread over multiple systems, and Slack meets all my main task-management-system requirements:
- Easy to access (the Slack app is already open on my Desktop all day)
- Add tasks quickly from anywhere (messaging)
- See at a glance whether a task was completed (emoji reactions)
I use a private channel for logging my tasks, and give them a green checkmark as I complete them, or a skull to kill the task. I’m usually pretty good about tagging tasks as either dead or complete, but every once in a while a task slips through the cracks, and gets buried in my channel history. Slack doesn’t have a way to round up and list my “unresolved” (i.e. un-marked) “tasks” (i.e., messages), so I decided to build a bot to do it.
I don’t have a ton of experience writing node apps. I’ve written a few things here and there, but so far I haven’t written enough for it to become something I can just rattle off the top of my head and I’m sure I’m missing various things. But learning is cool! So I decided to go with node. This is the story of my adventure.
I started this off the way I start most of these kinds of projects: by going to Google to see if anyone has ever done anything like this before. I found two blog posts that became my jumping off points (and I even ended up borrowing some of each of their code in the final bot):
- Jeff Yates’ Somewhat Abstract blog: “Writing A Simple Slack Bot With Node slack-client”
- Nordic APIS blog: “Building an Intelligent Bot Using the Slack API” by Dennis Hotson
Both projects started with the slack-client API wrapper, so I went ahead and used that as my base as well. Using those examples, I build a rough skeleton for my app, so I could focus on the main functionality of my bot.
I’m going to start off by going over some of the set up involved in building my “TaskBot” (as she shall hereafter be known), and giving you a preview of the final result. I’m not going to walk you through how to install node or any of the other basic prerequisites, but there are a lot of resources online about that — believe in yourself.
In order to test and ultimately run TaskBot, you need to set up the integration for your Slack team. Don’t be intimidated: it’s very easy and there’s no real risk (you’re not using up precious resources or anything). Follow this link to your Slack settings, pick a username for your bot, and add the integration. Slack will return an API token when the bot is activated — copy this! You’ll need it in the final code.
Once the integration is set up, you’ll also need to invite your bot to some channels. She needs to be a member of any channels you want her to participate in (and, in this case, analyze). Invite her the same way you’d invite any user.
Additionally, we’ll be using two libraries that need to be installed via npm:
$ npm install xmlhttprequest
$ npm install slack-client
That ought to do it! Once you’ve got the code finalized (see the next couple sections of this post) and saved as a .js file (I called mine app.js), launch Terminal and the Slack app, and run the app via the command line like this:
$ node app.js
Here’s what you’ll see in the Slack app and in the terminal when you make a request to the bot with the final app:
This bot lives and runs locally via node, and needs to be started up when you want to use it. The next step (which I won’t cover) would be to put it on a real web server (like Heroku) so everyone can use it whenever they want.
The App Logic
Alright, we’ve seen what the bot will do and how to run it, so let’s take a step back and talk about how to actually build it. I start most of my new projects by writing out the logic of what my app will need to do. Of course things change as I start coding, but it’s a great way to start thinking through how you’ll need to structure things and where the dependencies are. Here’s a linear outline of what we’ll be doing in our bot code:
1. Watch a channel for messages.
2. Ignore the bot’s own messages.
Let’s not create an infinite loop.
3. Get the current channel, so we know where to post messages to later.
Your bot needs to be a member of any channels that you want her to be active in — this means both channels where you’re planning to talk to her, and channels that you want her to analyze (I don’t want TaskBot posting in my tasks channel and cluttering things up with redundant messages, so I’m planning on talking to her and asking her to post my task lists in a different channel). Invite her the same way you’d invite any other user.
4. Only respond to messages directed at this bot.
We don’t want our bot to respond to every single message posted in a channel — she only needs to pay attention when someone mentions her name.
5. Take the message that was sent to the bot and pull out just the name of the channel that is being queried.
For TaskBot, I chose to require a structured input message. When users want to call TaskBot, they must use the following format: “@taskbot: channel-name” (e.g., “@taskbot: nellie-notes”). This way, we already know how to split up the message so that we can feed TaskBot just the name of the channel she should analyze; she’ll use this to find the channel object, using functions that are part of the slack-client library.
(Though the reality is that TaskBot is a bit smarter than we’re giving her credit for: as long as her name is referenced in the message, and the very last item in the message is a channel name, she’ll know what to do. So you could also say, “Hey @taskbot you incredible genius, tell me the unresolved tasks that are in nellie-notes”.)
6. Retrieve the requested channel object using functions from the slack-client library.
I couldn’t find robust documentation of all the functions available in the slack-client library, but you can see what most of your options are by looking at the library code. For example, the function we’ll be using to get a channel object by it’s name can be found here. The function returns an object that contains all kinds of data about the requested channel, including its ID and name.
7. Check to verify if we’ve returned a valid channel object.
If the user asked to query a channel that doesn’t exist, we’ll skip all the next steps and just print out an error message.
8. Get the channel ID from the channel object.
channels.history and groups.history (the API methods we’ll be using to get all the messages in a channle — see the following note) require a channel ID. First you have to get the channel object, and then get the ID from that object.
9. Determine whether this is a public or private channel (i.e., group) by looking at the data in the channel object.
Public channels and private channels need different API calls. Although this bot is just for me right now in my private channels, eventually I’ll implement a similar collaborative channel for my team to track which issues and support requests we’re each working on, so the bot needs to be able to handle both. Fortunately the API calls are pretty similar: public channels need the prefix “channels”, and private channels need the prefix “groups”, but the specific function calls are the same for both (e.g., channels.history and groups.history do the same thing — get all the messages in a channel), so all we need to worry about is swapping out the prefix as appropriate.
10. Get the message history for the channel using a custom API call.
Our http request is going to run asynchronously, so remember that asynchronous functions just kind of run whenever it’s convenient! This means that once we get our result from the API call, we need to put all the code that is going to do something with the returned data inside our function.
FYI, the channels.history API call, in its default form, maxes out at 100 messages. I’m pretty sure there’s a way to get more pages of results after that, but I didn’t bother trying to add that functionality. If there are more than 100 tasks between me and an unresolved task, I have bigger problems.
11. Parse the returned messages to make them readable JSON objects.
12. Filter for messages that don’t have reactions.
“Reactions” are the emojis people add — in this case, my check marks and skulls. For my purposes, it doesn’t matter which reaction is attached to a message; all that matters is whether a reaction is present or not.
13. Combine our array of unresolved messages into one line so the bot can send just one message to Slack.
Instead of printing each unresolved task as it’s own message to Slack, I want the bot to just post a single list of my unresolved tasks. We can use new lines (\n) in Slack messages to create line breaks within a single message.
14. Print out our final, single message to Slack.
15. Print to the console that the query was successful.
16. Finally, if the message to the bot was formatted wrong, print out some help text instead of doing all the stuff we just talked about.
The App Code
And here’s ALL the final code, with comments inline to walk you through what’s happening. I’m not going to explain this step-by-step or dive too deeply into the principles of each step, or why I structured my code certain ways. If you’re like me, and you like to learn by looking at code and trying stuff out, then you should feel right at home. Below the code there are a few links to resources I reference, and then we’re done!