Hubot + ES6 + Promises

Lately we've been playing around with ChatOps at wehkamp. We added a Hubot to our Slack channels to automate some operational jobs. It makes work more fun and way easier. As it is hosted in our own infrastructure, it can interact with our micro-services.

Consuming an API-endpoint is easy with Hubot. In this article I'll explore how by responding to the word "Norris" with a quote from the Internet Chuck Norris Database.

I will demonstrate how to:

  • use JavaScript instead of CoffeeScript
  • consume a simple JSON webservice endpoint
  • leverage the ES6-style Promise to build a nice chain.

Building stuff for Hubot is really easy!!

GitHub created Hubot back in 2011 to
automate many of their tasks.

I like to drink my coffee, instead of scripting it

I'm sure CoffeeScript has its uses, but it's not for me. I like to do my programming in standard ES6 JavaScript (or TypeScript nowadays). Fortunately Hubot understands .js scripts just as well as .coffee scripts.

Hubot provides its own HTTP client to talk web-services. Let's wrap a GET request with an ES6 Promise using function arrow expressions and convert the result to a JSON object:

const norrisUrl = 'https://api.icndb.com/jokes/random';

// wrap with promise
new Promise((resolve, reject) => 
    robot.http(norrisUrl).get()((err, response, body) =>
        err ? reject(err) : resolve(body)
    )
)

// parse to js object
.then(body => JSON.parse(body))

Promises can make asynchronous programming easier. When chained together they create a neat pipeline. Another advantage is error handling. We can add a .catch(err => { }) at the end of the chain to catch any errors that might occur during the processing of the endpoint data.

Let Hubot listen in

Hubot uses a regular expression to "hear" commands. We want to respond to phrases that contain the word Norris. The following regular expression will do so: /Norris/i. This expression will respond to "norris", "Norris" and even "noRRis" (due to the /i).

The response (res) object can echo a reply to the channel. Here is the code:

const
    norrisUrl = 'https://api.icndb.com/jokes/random',
    decode = require('decode-html');

//the export is used to init the bot
module.exports = (robot) => {

    //listen to phrases that contain "Norris"
    robot.hear(/Norris/i, (res) => {

        //wrap the HTTP get call as a Promise
        new Promise((resolve, reject) => 
            robot.http(norrisUrl).get()((err, response, body) =>
                err ? reject(err) : resolve(body)
            )
        )
        
        //parse to js object
        .then(body => JSON.parse(body))

        //get joke - jokes may have stuff like "
        .then(json => decode(json.value.joke))

        //reply joke
        .then(joke => res.reply(joke))

        //problems? Annoy the user with the problem
        .catch(err => res.reply('Not even Chuck Norris can deal with this one: ' + err));
    });
};

The promise allows us to build a nice chain of pluggable operations. It feels a bit like overkill for this example as the JSON parsing, decoding and replying can be done in 1 then. Yet it has a nice "ring" of separation of concern to it.

And action!

When I run the bot, it responds to my chats:

Hubot reacts to mentions of Chuck Norris.

Super fun. Super simple.

PS. Be careful with hearing too much

When you have 2 bots using hear on the same topic and respond with a hear keyword, you might get into trouble. I had to kick these 2 bots discussing Chuck Norris facts out of the channel:

When 2 bots discuss Chuck Norris.
2 versions of Hubot reaction to each other... this can go on for while...

We've mitigated this by checking if the sender of a certain message is a bot or not.

Do you want to know more about our Hubot? Read Jump-starting Slack bot projects: bot-zero.

Changelog

2022-08-18 😳 Many typos.

  1. Nathan Pearce says:

    Great article! I was just going to look into this. Even better that you used the Chuck Norris example. Fantastic effort!

expand_less