close

Day 11: Creating your own Franken-butler with Hubot

Say what?

Hubot is a highly customizable chat bot written entirely in Coffeescript (don’t worry – you don’t have to know Coffeescript) and Node.js.

This means that if you know a bit of JavaScript, you can create your very own robot that – with a bit of imagination – does whatever you want it to. Plug in the fact that it’s also built with Node.js and a whole new world of possibilities opens up to you thanks to the more than 52k modules available via npm.

At time of writing.

Getting set up

I could go into the nitty gritty here, but the official getting started guide will walk you through all you need to get started with a basic bot. With it comes a whole host of very useful stuff like finding images, translations, and finding youtube videos.

Basic Hubot Functionality

Adding third party scripts

Because Hubot is written in such a way as to be extensible, there is an entire repo dedicated to community scripts. There’s even a great site listing them all for a touch more convenience.

A few examples of what you can find:

  • Text to ascii.
  • All the Chuck Norris jokes you would ever need.
  • Check whether a site is down.
  • Get directions to somewhere.
  • Join an IRC/Skype chat (and more).

Installing these – if you could even call it that – is unbelievably simple. In your project directory, there’s a file called hubot-scripts.json which is just an array of strings. To add a community script, simply grab the name of the file that you want (without the .js or .coffee extension) and put it in that array. Next time you start up Hubot, that script will be active.

Note: Some scripts require a bit of configuration. This is usually set up in a .env file so make sure you’ve added the necessary config there and run source .env before starting up Hubot.

An example section of an .env file looks as follows:

#===== Hubot Environment Variables =====#
export HUBOT_AUTH_ADMIN="remybach"

Customise your Hubot

Or: The fun stuff!

As mentioned above, Hubot is written to be extensible and all you need is a bit of JS knowledge and you’re good to go!

The very first thing I recommend doing is giving your Hubot a better name… let’s go with “Alfred” (because Batman). All you need to do is pass an extra --name command line argument as follows: bin/hubot --name Alfred. From now on, we’ll refer to our Hubot by this name.

So let’s get customizing… the basic formula for a Hubot script is as follows:

At the top, by convention, is a comment that usually looks similar to the following:

/*
  Description:
    Valet at Wayne Manor.

  Configuration:
    HUBOT_FAVOURITE_MEAL - Something important Alfred needs to know.

  Commands:
    Man, I'm hungry! - Let Alfred know you're hungry and he'll let you know when dinner will be ready.
    Alfred make me dinner now! - Tell him to make dinner right now.

*/

Each script then needs to export a function as follows:

module.exports = function(alfred) {
  /* Boss Alfred around in here. */
};

Note: The official documentation (and therefore most scripts) use robot where I’ve chosen to name the argument passed to the function alfred as it makes the code easier to read.

Now, to make Alfred useful there are two commands that you’re going to be using: hear and respond. Both of these take a string (or more usefully if you prefer, a regular expression) and a callback function. The difference between them is that:

  • hear: Will listen to all messages sent and check whether it matches before firing the callback.
  • respond: Will only analyze the message if it is addressed directly to Alfred (i.e. it starts with your hubot’s name: Alfred image me the joker)

The callback function will receive a msg object which lets you tell Alfred to do one of three things:

  • send: Sends a message to the chat.
  • reply: Replies to a user directly (i.e.: Mr. Wayne: Right away sir!)
  • emote: Those familiar with IRC will know this to be equivalent to /me. Kind of like an inline status message, and will only work where such a message is supported.
alfred.hear("Man, I'm hungry!", function(msg) {
    msg.send("Dinner will be served in an hour.");
});

// Matches the following: "Alfred make me dinner now!
alfred.respond("make me dinner now!", function(msg) {
    msg.reply("I'll tell the chef to prepare your "+process.env.HUBOT_FAVOURITE_MEAL+" right away sir!");
});

And that’s it! Here it is in action:

Basic Alfred Functionality

P.S.: Alfred’s response to the last query begins with Shell: because I’m running this from the terminal. Normally, this would be the username of the person being responded to.

#protip On Natural Language Comprehension

Or: Make conversing with Alfred less robotic.

Currently, the only way to directly tell Alfred to do something is by using the respond function. This means that you need to begin your sentence with his name which tends to sound rather unnatural after a while: “Alfred make me dinner now!”, “Alfred prepare the Batmobile.”, etc.

One quick fix I use for this is to create a variable that can be used to identify him using the hear function as follows (note that I use a string which we’ll use in a regex to account for both his name and any alias we might decide to give him using the --alias command line switch):

module.exports = function(alfred) {
  var _name = '('+alfred.name+'|'+alfred.alias+')',
      isBatmobileReady;

  alfred.hear(new RegExp("Is the Batmobile ready yet "+_name+"\?"), isBatmobileReady);

  isBatmobileReady = function(msg) {
    /* Make Alfred respond here. */
  };
};

It’s perhaps not the most comprehensive example, but it should give you a good idea of how to start making conversation with Alfred a little more natural.

Friendly reminder…

Did you notice the isBatmobileReady function by the way? Don’t forget that this is all just good old JavaScript.

IRC where this is going…

As groan-worthy as that heading is, putting Alfred in an IRC room is extremely useful if you have one open all day anyway. I tend to have Adium open all the time so for me this was a no brainer.

To do this, we need to update the hubot-scripts.json with the IRC plugin by adding "hubot-irc" to the array in that file.
Next, you need to update your .env file with the following required config (filling in the necessary details):

# IRC
export HUBOT_IRC_SERVER=""
export HUBOT_IRC_ROOMS="" # The room/s you want hubot to join.
export HUBOT_IRC_NICK="" # The nickname you want hubot to have in the chatroom.

There are a few more configuration vars you can set such as HUBOT_IRC_UNFLOOD which helps prevent “flooding” the channel with messages by pacing the sending slightly. Don’t forget to run source .env after adding any configuration like this before running Hubot again.

One last thing you need to do is make sure to pass an extra parameter when starting up your Hubot as follows: bin/hubot -a irc --name Alfred

Check out the official getting started documentation if you get stuck.

This is what it currently looks like for me:

Alfred within Adium

Notice that the last response now has my correct username.

“Git” Alfred down to business

Remember earlier where I mentioned Node.js modules? We’re going to make use of one of them called gift to allow Alfred to manage some Git repositories for us. Make sure to follow the installation instructions to get it set up in your project.

Since this is all just regular old JS running in a Node.js environment, to include it within our hubot script, we simply add the following to the top of the file:

var git = require('gift');

Now let’s make use of some of what this module lets us do by having Alfred check the status of a repository. Let’s assume that the repo is going to be in /var/www/. Here’s what our log function might look like (I know it may look scary, but we’ll break it down below):

var log = function(msg, name, numCommits) {
  var repo = git("/var/www/" + name);

  // Fall back to showing just 1 commit message.
  numCommits = Number(numCommits) || 1;

  repo.commits(null, Number(numCommits), function(err, commits) {
    var commitsStr = "";
    if (err) {
      msg.send("There was an error synchronizing. Here are the details:");
      msg.send(err);
    } else {
      commits.forEach(function(commit, i) {
        commitsStr += commit.id.substring(0, 7) + " -  " + commit.message + "\n";
      });

      if (commits.length === 1) {
        msg.send("Here is the last commit for you:");
      } else {
        msg.send("Here are the last " + commits.length + " commits for you:");
      }

      msg.send(commitsStr);
    }
  });
};

To use this function, we’ll make use of a simple respond:

alfred.respond(/.*last ([0-9]+)? ?commits? (in|on|for) ([^ ?]+)\?/, function(msg) {
  log(msg, msg.match[3], msg.match[1]);
});

If you want to know what that regex is doing, this should give you a good idea. Suffice it to say that the 1st thing it captures is the number of commits, and the 3rd thing it catches is the repo to query.

Breaking it down

As promised, let’s step through that log function and see what’s going on.

  1. Get an instance of the git repo:
var repo = git("/var/www/" + name);
  1. Query the repo for however many commits it is we’re after:
repo.commits(null, numCommits, function(err, commits) {
  1. If there is an error, let the user know:
if (err) {
    msg.send("There was an error synchronizing. Here are the details:");
    msg.send(err);
}
  1. Otherwise put together a string containing the details that we’ll send to the user:
commits.forEach(function(commit, i) {
    commitsStr += commit.id.substring(0, 7) + " -  " + commit.message + "\n";
});
  1. Let the user know what we have for them:
if (commits.length === 1) {
    msg.send("Here is the last commit for you:");
} else {
    msg.send("Here are the last " + commits.length + " commits for you:");
}
  1. Send them the commit message/s:
msg.send(commitsStr);

In action

Finally, this is what the above script looks like in action:

Querying Alfred for commits.

In closing

As Alfred himself once said:

Sir, as loath as you might be to hear this, I do not intend to spend the rest of my life playing nurse.
Alfred Pennyworth (New Earth)

Indeed, and now you hopefully have the know-how to teach him to do so much more!