
How to Build SlackBots As Smart As Puppies
While much has been written about SlackBots and their roles on teams, there’s not much material on building one that’s more complicated than a simple slash command. Let’s take a high-level overview of some strategies to begin building a stable, extendable bot for Slack.
This bot won’t have the text processing a prediction capabilities of a Watson or M, but will be one step above a slash command, living somewhere in the uncanny valley of bot design, as smart as a puppy.
How to Build DJ Chrono, the Timekeeping SlackBot
We’re going to look at DJ Chrono, a SlackBot that queries the Toggl API to tell you how many hours have been logged on projects. From this foundation, the bot can be expanded to query hours logged by users and connect to other APIs.
Even if querying hours is not your thing, hopefully the lessons we share here will be helpful in your own projects.
DJ Chrono, a node.js bot, uses following modules:
– slackbotapi: A module for connecting to Slack’s RTM API with an excellent example bot script.
– dustJS: A templating engine with plain-text support.
– toggl-api: A Toggl API for node.js
– momentjs: For formatting dates and such.
– dustfs: A module for loading dust templates from the filesystem.
– string_score: A module for fuzzy matching strings.
The bot has commands: list projects
and view {project_name},
and it only responds to direct messages. Even though the commands are limited at first, we will build them in such a way that it will be easy to add new ones.
The full annotated source code is available here, so you can follow along with this article.
Big Picture
The flow for DJ Chrono looks like this:
- Authentication Grab the user’s Toggl API key.
- Onboarding Once the user is authenticated, we ask them to select a workspace and populate the commands available.
- General Query Here is the Main Loop, where the user’s input will be matched with the available commands and parsed into queries to the Toggl API.
To gather the available commands, we build a commandVocabulary
, which is an array of JavaScript objects. The objects contain the string that must be matched to trigger the command and meta-data about the command. Using this commandVocabulary
approach, we can create a bot that can respond to a wide variety of different commands with very little code.
Scaffolding
Once we’ve initialized our connection to the Slack API, it’s a good idea to store the bot’s username and UID somewhere. This will allow checks to prevent the bot from responding to itself. The auth.test
method of the Slack API is a good way to grab this information.
// Some vars to keep track of the bot's information
var my_username, my_uid;
// Call auth test in order to get the bot's UID
slack.reqAPI('auth.test', {}, function(data) {
my_username = new RegExp('<@' + data.user_id + '>', 'gi');
my_uid = data.user_id;
});
Once we have it, we can make some preliminary checks when a message is received to make sure the bot is behaving properly.
slack.on('message', function(data) {
// If no text, return
if(typeof data.text == 'undefined') return;
// If it's not a direct message, return
if(data.channel[0] != 'D') return;
// If it's a message from the bot itself, return
if(data.user == my_uid) return;
//snip...
});
To keep track of the users the bot is communicating with, we can store them in-memory in an array.
var users = [];
slack.on('message', function(data) {
// snip....
if(users[user.name] == undefined) {
var record = {
state: 'brand_new',
onboarded: false
};
} else {
var record = users[user.name];
};
// snip...
});
This users
array filled with record
objects is key to walking users through the onboarding process and storing bits of per-user information that need to persist. In fact, we hang our objects for querying Toggl and parsing text off the record
object.
The record.state
parameter is used to check the state of the user in the onboarding process.
Onboarding Users
We need two pieces of information before we can make a call to the Toggl API: an API key and a workspace ID. Why not get it from the users themselves?
While it may seem strange to ask a user questions one by one when we’re used to the world of HTML forms, inspiration can be drawn from the onboarding processes of games like Animal Crossing for how to do this process in a fun, natural way.
Using our record.state
parameter, we can walk them through the onboarding process.
var res = new response(slack, data);
if(record.state == 'brand_new'){
// We update the state and return our greeting message
// asking for authentication
record.state = 'needs_authentication';
users[user.name] = record;
return res.greeting();
} else if(record.state == 'needs_authentication'){
// snip...
} else if(record.state == 'workspace_selection'){
// snip..
In the first if
statement, we are returning our template. Bot responses can be complicated, and if you do not abstract out the “view” layer of this application, you may soon find yourself in a mess of string concatenation.
Our return text is returned by the response
class, which is simply takes parameters and puts them into dust templates and sends them through slack.
//Init
var response = function(slack, data) {
this.slack = slack;
this.data = data;
// Load up the dust templates
dustfs.dirs('templates');
};
response.prototype.slackWrapper = function(err, out) {
this.slack.sendMsg(this.data.channel, out);
};
response.prototype.greeting = function(){
username = '@' + this.slack.getUser(this.data.user).name;
dustfs.render('greeting.dust', {username: username}, this.slackWrapper.bind(this));
};
Using our simple record.state
checks, we can walk the user through the process of getting their API key and having them select a workspace.
Parsing Input
What enables this bot to be as smart as a puppy is how it handles input. The inputParser
class has an array of available commands, the commandVocabulary
. An entry in the commandVocabulary
looks like this:
var projectCommands = projects.map(function(project){
// Reformat each object in the workspace
var reformatted = {};
reformatted = {
name: project.name,
id: project.id,
type: 'project'
};
return reformatted;
});
record.inputParser.addCommands(projectCommands);
This data structure allows us to narrow down commands by type (e.g. determine if text from a user matches a project or a workspace). We use the matchType
function to match these command types and input strings.
inputParser.prototype.matchType = function(commandType, inputString){
var availableCommands = this.commandVocabulary.filter(function(command){
// Only return the commands that match
if(command.type == commandType){
// Do a fuzzy match provided by string_score
if(command.name.score(inputString) > 0.5) return command;
}
});
return availableCommands;
};
The Main Loop
Onboarding is but a small part of bot-building, and it’s a good idea to think of the application as one big loop that constantly checks to see if user input has changed.
DJ Chrono’s main loop relies on examining our parsed query and determining what to do next.
if(record.onboarded){
var parsedQuery = record.inputParser.query(data.text);
if(parsedQuery.type == 'list'){
// snip...
} else if(parsedQuery.type == 'view') {
// snip...
Again, the inputParser
class simply breaks up the input from the user (data.text
) and returns an object with the query type and it’s respective arguments.
inputParser.prototype.query = function(inputString) {
// Split the input string
inputSplit = inputString.split(" ");
var queryType = inputSplit.shift();
if(queryType != 'view' && queryType != 'list'){
throw 'Invalid Query Type';
};
return {
type: queryType,
args: inputSplit
}
};
What SlackBots Will You Build?
We hope the insight we’ve shared with you into building onboarding flows and fuzzy-matching command logic for SlackBots will be helpful. Don’t forget to check out the code and play with it yourself!
Let’s build amazing things together.
Check out our thoughts on Development, including Google Maps with React.js, gzip, or Flow Typechecking!