Create a Google Talk bot with Node.js Part Two

Flattr the Create a Google Talk bot with Node.js Part Two post

In part one of the tutorial you built a bot with Node.js that could connect to the Google Talk network and announce its presence to other users with a status message. The bot was also configured to listen for subscription requests from other users and automatically accept them.

Now you are going to further enhance the bot with additional functionality and commands as you proceed through part two of the tutorial. Once complete your bot will be able to furnish users with help information, bounce messages back and search twitter for user supplied keywords.

So lets get back to the hacking!

Note

This article was originally published in the April 2012 issue of .net Magazine. You can also download a PDF of the original article.

Part one of this article is also published on my blog.

There is a demo bot for this article documented at http://njsbot.simonholywell.com and the complete code is on github.

Making the bot do something

Whilst it was fun and exciting to get your bot to this point, it is, unfortunately a little boring. Wouldn't it be neat if the bot could receive commands and action them?

To begin with a simple callback registry is added to allow for commands to be easily added to the bot. Near the top of bot.js add a new object variable definition and two new functions:

var commands = {};
The first function allows you to easily add new functionality to the bot by simply setting up a callback function for a command name.
function add_command(command, callback) {
    commands[command] = callback;
}

Now to allow the execution of a request we need to check if the supplied command has been added to the bot's registry with the add_command() function.

function execute_command(request) {
    if(typeof commands[request.command] === "function") {
        return commands[request.command](request);
    }
    return false;
}

These two functions mean that you can very easily add new commands to the bot. The following example changes the bot's status message.

add_command('s', function(request) {
    set_status_message(request.argument);
    return true;
});

As you can see though a request object is passed as the parameter to the function, but where does this come from?

Dispatching requests

To handle the routing of incoming requests I like to use a simple dispatching mechanism that takes the incoming stanza and interprets it.

function message_dispatcher(stanza) {
    if('error' === stanza.attrs.type) {
        util.log('[error] ' + stanza.toString());
    } else if(stanza.is('message')) {
         util.log('[message] RECV: ' + stanza.toString());
        var request = split_request(stanza);
        if(request) {
            if(!execute_command(request)) {
                send_unknown_command_message(request);
            }
        }
    }
}

The first step in the code addresses a common oversight; an XMPP client must not respond to a stanza with a type of error. In this instance we should log it to the console using util for debugging purposes.

If the stanza is a message then this function will log it to the console and attempt to split it up into a custom request object created by the split_request() function (defined in the next code block). Given that a valid request has been made it will then attempt to execute the supplied command.

Should the command not exist it will send the user an error message suggesting that they try again or consult the help text.

function split_request(stanza) {
    var message_body = stanza.getChildText('body');
    if(null !== message_body) {
        message_body = message_body.split(config.command_argument_separator);
        var command = message_body[0].trim().toLowerCase();
        if('help' === command || '?' == command) {
            send_help_information(stanza.attrs.from);
        } else if(typeof message_body[1] !== "undefined") {
            return { "command" : command,
                     "argument": message_body[1].trim(),
                     "stanza"  : stanza };
        }
    }
    return false;
}

This simply extracts the message text from the supplied stanza object and then splits the message using the regular expression defined earlier in the configuration file. By default this will split on a semicolon and allow for any amount of white space surrounding the separator (/\s*;\s*/).

The command is assumed to be the first part of the message before the separator and the argument is after it. The following is an example of what a command message sent by a user might look like:

s;This is the bot's new status message!!

If the user has simply typed help or ? then we should send them a message explaining the available commands otherwise the function will construct the request object.

The request object consists of the command string, an argument string and the full initial stanza object that was sent to the bot for processing.

Now let's attach the dispatcher to an event so that it is triggered when each new stanza comes in over the wire.

conn.addListener('stanza', message_dispatcher);

Handling errors and help

In the previous section the send_help_information() and send_unknown_command_message() functions were mentioned so here they are!

The help function outputs a simple message that describes how the user can make use of the bot's commands.

function send_help_information(to_jid) {
    var message_body = "Currently bounce (b), twitter (t) and status (s) are supported:\n";
    message_body += "b;example text\n";
    message_body += "t;some search string\n";
    message_body += "s;A new status message\n";
    send_message(to_jid, message_body);
}

Should the user attempt to access a command that the bot does not recognise then they will be sent an error message.

function send_unknown_command_message(request) {
    send_message(request.stanza.attrs.from, 'Unknown command: "' + request.command + '". Send "help" for more information.');
}

As you may have noticed both of these functions depend upon the send_message() helper function that we have yet to define.

function send_message(to_jid, message_body) {
    var elem = new xmpp.Element('message', { to: to_jid, type: 'chat' })
                 .c('body').t(message_body);
    conn.send(elem);
    util.log('[message] SENT: ' + elem.up().toString());
}

This function accepts two strings as parameters; the JID of the receiving user and the message text. All messages that are sent are also logged to the console using the util package.

Finally we also need to add a handler for system error events originating from node-xmpp:

conn.on('error', function(stanza) {
    util.log('[error] ' + stanza.toString());
});

Increasing one's vocabulary

This is another good point to test your bot as there is already a command you can use: "s;My new status message" and error messages to provoke. This should be working flawlessly before you continue to add new functionality to the bot.

As the help function text alluded to earlier there will be two further commands added to the bot in this tutorial.

The first of which is the bounce functionality; when sent a message by the user the bot will bounce it straight back to them. This is a useful sanity check for when you are testing the bot as you add extra commands.

add_command('b', function(request) {
    send_message(request.stanza.attrs.from, request.stanza.getChildText('body'));
    return true;
});

Going global with Twitter

Next up the bot will talk to the outside world by searching Twitter for the user supplied request argument against the public JSON API.

add_command('t', function(request) {
    var to_jid = request.stanza.attrs.from;
    send_message(to_jid, 'Searching twitter, please be patient...');
    var url = 'http://search.twitter.com/search.json?rpp=5&show_user=true&lang=en&q='
             + encodeURIComponent(request.argument);
    request_helper(url, function(error, response, body){
        if (!error && response.statusCode == 200) {
            var body = JSON.parse(body);
            if(body.results.length) {
                for(var i in body.results) {
                    send_message(to_jid, body.results[i].text);
                }
            } else {
                send_message(to_jid, 'There are no results for your query. Please try again.');
            }
        } else {
            send_message(to_jid, 'Twitter was unable to provide a satisfactory response. Please try again.');
        }
    });
    return true;
});

Whilst the function is the longest in the bot so far it is also possibly the easiest to follow so I will skip to the more interesting bits.

The request_helper object was created at the top of the bot.js file right at the beginning of the tutorial and now it is finally coming to some use. It is basically a nice simple wrapper around the httpClient functionality provided in Node.js by default.

If the request receives a satisfactory response then the JSON will be parsed and each tweet is sent to the user as a new message.

C ya l8r

So you now possess a complete Google Talk bot (don't get too giddy!), which you can easily add new functionality to with add_command(). It is pretty neat, but what use is it?

Perhaps you could program it to message you whenever a server drops offline or when your continuous integration builds break.

If your bot needs to handle exceptionally large numbers of users then you may wish to consider refactoring it as an XMPP Component rather than a XMPP Client (node-xmpp makes this very easy). The Twitter bot reached its limit with 40,000 subscribers though so there is plenty of head room with a client based bot.

Stay in touch

If you liked this article then please follow me on Twitter and let me know.

Note

Part one of this article is also published on my blog.

This article was originally published in the April 2012 issue of .net Magazine.

In tandem with the Google bot tutorial I wrote four smaller articles: