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

 

slackbots

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:
Bot Flow

  • 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!

Custom Software Development by Revelry