timsch

IRC to Spark and Back Again

Blog Post created by timsch on Aug 22, 2016

With the recent release of bot functionality for Cisco Spark’s API, we’ve explored methods to integrate Spark rooms with older legacy technology, to bring them into the next generation. With this in mind, we built a Spark bot application that transmits messaging and state information between Cisco Spark and an IRC channel – effectively linking one of the original bot platforms to one of the most recent.

 

The first major step to building any multi-function application is to set a roadmap for the application’s functionality. The application that we are developing will:

 

Now that we have a basic idea of what our application needs, let’s locate some resources that will help us accomplish our end goal. NodeJS has a default webserver library, http, that fulfills the needs for our receiving portion perfectly, but we will need to install two additional libraries for NodeJS. One handles IRC, and the other handles sending RESTful requests. To install on OSX or Linux, use npm:

npm install irc

npm install request

 

Since we now have these two libraries installed, let’s generate a script file that includes the libraries we’ll be using:


// External libraries
var request = require('request');
var http = require('http');
var irc = require('irc');



At this point, we are ready to start building our bot application. Since our application is based around events that occur in both Cisco Spark and a specific IRC channel, we will need to make event listeners for both. Let’s start by generating the IRC connection, and the event listeners associated with it:


var botName = 'IRCToSparkBot'; // The name of your bot
var myChannel = '#mychan'; // The channel your bot is active on

var bot = new irc.Client('irc.freenode.net', botName, { //Create Bot
    channels: [myChannel]
});


bot.addListener('message' + myChannel, function (from, message) { // Add listener for channel

});


bot.addListener('pm', function (from, message) { // Add listener for PM

});


bot.addListener('join', function(channel, who) { // Add listener for user joins

});


bot.addListener('part', function(channel, who, reason) { // Add listener for user parts

});



Now that the framework for our IRC connection is set up, we can begin generating a way for it to communicate with our Spark Bot. Since Spark Bots communicate via REST calls, we will need to generate a function that handles sending these calls:


function sendRequest(myURL, myMethod, myHeaders, myData, callback) { // Sends RESTful requests

  var options = {
    url: myURL,
    method: myMethod,
    json: true,
    headers: myHeaders,
    body: myData
  };

  var res = '';

  request(options, function optionalCallback(error, response, body) {
    if (error) {
      res = "Request Failed: " + error;
    } else {
      res = body;
    }
    callback(res)
  });
}



You may notice that the function we created supports multiple methods, headers and URLs. We are building our request function this way so that it can be used universally throughout our application, if we ever decide to expand upon the systems it’s integrated with. Additionally, we also include a callback function; this will allow us to effectively receive and parse the information returned from our requests.

 

With the request function built, we can begin collecting commonly used global variables that will be reused throughout our application:

  • Spark Bot's Bearer Token
  • Spark Bot's Target Room ID
  • Spark's Required Headers
  • Spark's Message URL for Sending and Receiving Messages

 

We’ll define them globally in the application:


var myToken = ''; // user/bot bearer token
var myRoomID = ''; // Spark RoomId for bot
var sparkHeaders = {'content-type': 'application/json; charset=utf-8', 'Authorization':'Bearer ' + myToken}; // Basic Cisco Spark Header
var messagesURL = 'https://api.ciscospark.com/v1/messages/'; // Spark Messages API URL, do not modify



With a few more additions to the IRC listeners, we can effectively receive information with our bot in IRC and send it into Cisco Spark. In the next code section, we add calls to our sendRequest from each of the listeners POSTing messages to the Spark room:


// External libraries
var request = require('request');
var http = require('http');
var irc = require('irc');

var botName = {irc:'IRCToSparkBot', spark:'IRCToSparkBot', sparkEmail:'IRCToSparkBot@sparkbot.io'}; // The name of your bot
var myChannel = '#mychan'; // The channel your bot is active on
var myToken = ''; // user/bot bearer token
var myRoomID = ''; // Spark RoomId for bot
var sparkHeaders = {'content-type': 'application/json; charset=utf-8', 'Authorization':'Bearer ' + myToken}; // Basic Cisco Spark Header
var messagesURL = 'https://api.ciscospark.com/v1/messages/'; // Spark Messages API URL, do not modify

function sendRequest(myURL, myMethod, myHeaders, myData, callback) { // Sends RESTful requests

  var options = {
    url: myURL,
    method: myMethod,
    json: true,
    headers: myHeaders,
    body: myData
  };

  var res = '';

  request(options, function optionalCallback(error, response, body) {
    if (error) {
      res = "Request Failed: " + error;
    } else {
      res = body;
    }
    callback(res)
  });
}

var bot = new irc.Client('irc.freenode.net', botName, { //Connect to IRC
    channels: [myChannel]
});


bot.addListener('message' + myChannel, function (from, message) { // Add listener for channel

  sendRequest(messagesURL, "POST", sparkHeaders, { roomId: myRoomID, text: from + ': ' + message}, function(resp){});
});


bot.addListener('pm', function (from, message) { // Add listener for PM

  sendRequest(messagesURL, "POST", sparkHeaders, { roomId: myRoomID, text: 'PM from ' + from + ': ' + message}, function(resp){});
});


bot.addListener('join', function(channel, who) { // Add listener for user joins

  sendRequest(messagesURL, "POST", sparkHeaders, { roomId: myRoomID, text: who + ' has joined ' + channel + ' - '}, function(resp){});
});


bot.addListener('part', function(channel, who, reason) { // Add listener for user parts

  sendRequest(messagesURL, "POST", sparkHeaders, { roomId: myRoomID, text: who + ' has left ' + channel + ' - '}, function(resp){});
});



The first half of our application is complete, so it’s time to start on the rest - sending messages received in Cisco Spark to the IRC channel where our bot resides. During this portion, we need to set up a listener (which will require us to declare a port), and parse the REST body received by our application. To do this, we will use the http library and create the listener, as well as a catch to handle all unsupported information:


var portNumber = 8080; // Set listen port number


http.createServer(function (req, res) { // Set up web listener to receive Webhook POST / Relaying. AKA the magic.

  if (req.method == 'POST') {

    req.on('data', function(chunk) {
      var resObj = JSON.parse(chunk.toString());
   
    });

    req.on('end', function() {
      res.writeHead(200, "OK", {'Content-Type': 'text/html'});
      res.end();
    });

  } else {
    console.log("[405] " + req.method + " to " + req.url);
    res.writeHead(405, "Method not supported", {'Content-Type': 'text/html'});
    res.end('405 - Method not supported');
  }
}).listen(portNumber); // listen on tcp portNumber value (all interfaces)



Notice that we pass in the port number via a global variable when creating our http server. This will allow us to easily set up different instances of the same application as new processes at a later time. Be sure to define the port number for the http server to access globally.

 

Our application is ready to receive requests. It’s time to generate the webhook that will translate Spark messages into requests sent to the IRC room. First, we must create a bot. To create a bot, go to this page and select the button “Create a Bot”.

 

On the page to follow, you are given the choice to name your bot, address it, and add an icon to its spark profile. Now that the bot has been created, we can collect the bot’s bearer token, which will be used throughout our application as we develop it. Proceed to the bots page by going to the “My Apps” page, selecting the bot and then clicking “Regenerate Access Token”.

 

With the bot’s bearer token recorded, it’s time to create a webhook. To do this, we are going to send a POST request to https://api.ciscospark.com/v1/webhooks with the following headers and body:


{
      'content-type': 'application/json; charset=utf-8',
      'Authorization':'Bearer MYTOKEN'
}

{
      "name": "IRCToSparkBot",
      "targetUrl": "http://www.myboturl.com/",
      "resource": "messages",
      "event": "created",
      "filter": "roomId=MYROOMID"
}



With the webhook created using the bot’s token, Spark will send a POST request to our defined target URL whenever the bot is explicitly mentioned in the designated room.

 

We can now safely assign values to the myRoomID and myToken global variables in our application - they had placeholders earlier:


var myToken = ''; // user/bot bearer token
var myRoomID = ''; // Spark RoomId for bot



These values should reflect the roomId and bearer token of the bot.

 

Now that our webhook is configured, we can begin interpreting the received JSON body. For security purposes, the JSON body sent by the webhook does not include the message text, it contains an encrypted ID. We need to set up logic within our http listener to collect the encrypted message ID from the received JSON and send an authenticated GET request to grab the message text from Spark using the bot Bearer Token:



http.createServer(function (req, res) { // Set up web listener to receive Webhook POST / Relaying. AKA the magic.

  if (req.method == 'POST') {

    req.on('data', function(chunk) {
      var resObj = JSON.parse(chunk.toString());
      sendRequest(messagesURL + resObj.data.id, "GET", sparkHeaders, '', function(resp){});
   
    });

    req.on('end', function() {
      res.writeHead(200, "OK", {'Content-Type': 'text/html'});
      res.end();
    });

  } else {
    console.log("[405] " + req.method + " to " + req.url);
    res.writeHead(405, "Method not supported", {'Content-Type': 'text/html'});
    res.end('405 - Method not supported');
  }
}).listen(portNumber); // listen on tcp portNumber value (all interfaces)



It’s time to do something with our returned message text. We are going to make our bot in IRC send a message to the channel it resides in based off the text received from the JSON GET response body:



http.createServer(function (req, res) { // Set up web listener to receive Webhook POST / Relaying. AKA the magic.

  if (req.method == 'POST') {

    req.on('data', function(chunk) {
      var resObj = JSON.parse(chunk.toString());
      sendRequest(messagesURL + resObj.data.id, "GET", sparkHeaders, '', function(resp){
     
        var myMessageObj = {};
        if (resp.personEmail != botName.sparkEmail) {bot.say(myChannel, resp.text);}
     
      });
   
    });

    req.on('end', function() {
      res.writeHead(200, "OK", {'Content-Type': 'text/html'});
      res.end();
    });

  } else {
    console.log("[405] " + req.method + " to " + req.url);
    res.writeHead(405, "Method not supported", {'Content-Type': 'text/html'});
    res.end('405 - Method not supported');
  }
}).listen(portNumber); // listen on tcp portNumber value (all interfaces)



If you’re testing the code as we’re walking through it, you might notice the bot’s name is included in the message, due to the mention requirement discussed earlier. This won’t look right to IRC users, as they won’t know anything about the Spark bot, so we’ll create a String filter to remove the mention (note this may change in the future where the mention will be omitted, which may require a code change). While we are making that filter, let’s also look at other functionality our bot will need to be fully operational:

  • Sending and receiving private messages
  • Determining whether to send a private message, or a channel message
  • Displaying a help message for new chat room users

 

Our solution to overcoming these obstacles is a function - this function will take the initial string received and search for a command, such as a short character combo like “m” or “pm” and then parse the individual words to follow. A message command may be as simple as “m MESSAGE” while a private message may be as complex as “pm USER MESSAGE”. This means each of the commands will require a different number of arguments so we should create an object that defines how many arguments each command has:


var commands = {
  m:{args:0},
  pm:{args:1},
  help:{args:0}
};



Phrasing can be pretty flexible since the bot needs to be mentioned before it will act, so accidental typing of letters like just “m” won’t trigger any action unless preceded by the botname.

 

Now that the object contains each command and their argument count, let’s generate the function that will decipher the message text and output an object that contains all the necessary values to send the correct text to the correct destination in IRC:


function messageInterpreter(myMessage) {

  var myReturnObj = {};
  var preProcessedString = myMessage;
  var index = 0;

  if (myMessage === undefined) {
    return '';
  }

  //Determines Command
  preProcessedString = myMessage.slice(myMessage.search(botName.spark) + botName.spark.length + 1);
  if (preProcessedString.includes(' ')) {
    index = preProcessedString.search(' ');
    myReturnObj.command = preProcessedString.slice(0, index);
    preProcessedString = preProcessedString.slice(index + 1);
  } else {
    myReturnObj.command = preProcessedString.slice(0);
    return myReturnObj;
  }

  if (commands.hasOwnProperty(myReturnObj.command)) {
    myReturnObj.argument = {};
    for (i = 0; i < commands[myReturnObj.command].args; i++) {
      index = preProcessedString.search(' ');
      myReturnObj.argument[i] = preProcessedString.slice(0, index);
      preProcessedString = preProcessedString.slice(index + 1);
    }
    myReturnObj.value = preProcessedString;
  }
  return myReturnObj;
}



The final step is to create the switch that controls what to do in response to each of the commands. This will be done from within the http server’s logic:



http.createServer(function (req, res) { // Set up web listener to receive Webhook POST / Relaying. AKA the magic.

  if (req.method == 'POST') {

    req.on('data', function(chunk) {
      var resObj = JSON.parse(chunk.toString());
      sendRequest(messagesURL + resObj.data.id, "GET", sparkHeaders, '', function(resp){
        var myMessageObj = {};
        if (resp.personEmail != botName.sparkEmail) {myMessageObj = messageInterpreter(resp.text);}
     
        switch (myMessageObj.command) {
          case 'pm': 
            if (bot.chans[myChannel].users.hasOwnProperty(myMessageObj.argument[0]) && myMessageObj.value !== '') {
              bot.say(myMessageObj.argument[0], myMessageObj.value);
            } else if (myMessageObj.value === '') {
              sendRequest(messagesURL, "POST", sparkHeaders, { roomId: myRoomID, text: 'PM FAILED TO ' + myMessageObj.argument[0] + ' FAILED: NO VALUE TO SEND'}, function(resp){});
            } else {
              sendRequest(messagesURL, "POST", sparkHeaders, { roomId: myRoomID, text: 'PM FAILED: USER ' + myMessageObj.argument[0] + ' DOESNT EXIST '}, function(resp){});
            }
            break;
         
          case 'm':
            bot.say(myChannel, myMessageObj.value);
            break;
         
          case 'help':
            sendRequest(messagesURL, "POST", sparkHeaders, { roomId: myRoomID, text: helpMessage}, function(resp){});
            break;
         
        }
      });
    });

    req.on('end', function() {
      res.writeHead(200, "OK", {'Content-Type': 'text/html'});
      res.end();
    });

  } else {
    console.log("[405] " + req.method + " to " + req.url);
    res.writeHead(405, "Method not supported", {'Content-Type': 'text/html'});
    res.end('405 - Method not supported');
  }
}).listen(portNumber); // listen on tcp portNumber value (all interfaces)



And with that, you have the complete IRC to Cisco Spark and back demo bot. The complete code can be located on our Github, and if you have any questions or need any help, please feel free to reach out via devsupport@ciscospark.com!

 

Tim Scheuering, Customer Support Engineer

Outcomes