Coding fun – let me take that back

Effectively our bot is done.  It’s doing all the things we set out, receiving messages, searching for images, and replying when it finds one.

However, people are fickle things, and sometimes might get offended or otherwise want something the bot has published removed.

Admins of a chat group can remove anything anytime they want, but it’d be nice to tell the bot to remove it’s own messages.  Sounds easy, right?

It is and it isn’t.  We have two methods to accomplish this in Spark.  First, we could use the API to query for the last few messages, deleting them in reverse order.  Or we could use a data structure to remember the last few messages (10 in my case) and delete them as the user requests.

The power of programming, choice.  To that end, the act of deleting a message is easy, here’s the routine for it (within our spark module).

var deleteMessage = (msgId, callback) => {
 var sparkAPI = 'https://api.ciscospark.com/v1/messages/';

 var options = {
  url: sparkAPI + msgId,
  headers: {
   'Authorization': 'Bearer '
  }
 }

 request.del(options, (err, res, body) => {
  if(!err) {
   callback(res.statusCode);
  }
 });
};

At this point most of the functionality should be familiar to you.  We first define a call named deleteMessage, passing in a msgId(the actual message you want to delete) and a callback to deal with any output.

We define options, including the URL.  This time however we actually append the msgId to the URL.  Why? Because that’s what the API tells us to do.

The only new functionality is the API call:

request.del(options, (err, res, body) => {

We’ve only used .post and .get up to now, why .del?  Reading the documentation for the API it requires the delete method, and thankfully the request module has one.  It’s effectively a get by another name, just so the Spark back-end code can be clear that this is a deletion.

The only reply we expect back is an HTTP status code of 204, anything else and it didn’t work.

This is the easy part, the harder part is altering our main server code to record messages and then delete them in reverse order(newest first).  I’ve chosen to implement this with a double ended queue.  Other programmers will probably immediately have gravitated toward a stack or queue.  Here’s the notes about those

  • Stack
    • Push messages on “top”, remove them from the “top”
    • Upside, readily available in any language, generally easy to implement
    • Downside, you can typically only remove from the top.
      • I want to limit my message history to the newest 10 items.  To do this on a stack you’d have to rewrite it every time you hit 10, a performance nightmare.
      • There’s generally no way to limit how big the stack can get, and if you do it’ll just stop adding things, not functionality iwant
  • Queue
    • First in First out (FIFO) is the most common implementation
    • The First thing you add is the first thing you remove
      • Good for things that need to be done in order
      • Bad for us, we want to give the user the option to delete the Most Recent message, not the oldest

A double-ended-queue allows the best of both worlds.  It’s default implementation in this case is a stack, push all new messages on the top, and “pop” them off with the newest first.  But, it gives us a method to remove something at the bottom of the stack(the oldest message) effectively allowing us to keep the most 10 recent messages.  Programmers will also mention that these are also implemented with linked lists, but why do that complexity when you can simply use a module already defined, in this case double-ended-queue is an NPM module.

When I approach these types of problems, I write out pseudocode, this is always language independant, but helps visualize what I’m trying to do.  I’ve chosen to implement this with a

postMessage {
  success: 
   check queue size, if > 10:
    remove oldest item
   if < 10:
    add to queue
}
deleteMessage {
  check if queue is empty, if not:
   delete newest item
}

Logic done, let’s look at code.  Please note, all of this is done in the server module that we’re developing.  I’m ONLY going to review the code for the new queue and deletion.  I’ll post my final full server method at the end.

const dequeue = require('double-ended-queue');

var msgQueue = new dequeue();

 if(msgText.match(/^delete/i)) {
  if(!msgQueue.isEmpty()) {
   var deleteMsgId = msgQueue.pop();
   spark.deleteMessage(deleteMsgId.id, (statusCode) => {
  });
 }
 } else {
  spark.postMessage(link, messageDetail, (msgId) => {
   if(msgQueue.length >= 10) {
    msgQueue.shift();
   }

   msgQueue.push(msgId);
  });

This all happens after we’ve checked for a group message, and stripped out the Bot’s name (look at the last article for details on that). I’m only implementing this history function for group messages, if you get offended after private messaging the bot, just close the chat window.

I’m using regex to ensure that the first word is delete, in reality I should validate the second word too, but in this case if you say delete it just assumes to delete the last one.  An interesting excercise would be something like a ‘delete last X’ or ‘delete all’, but at this point if you’re following what’s happening the implementation of that should be fairly simple.

If it is a delete, we then check the messageQue, ensuring it’s not empty.  If it is nothing happens (maybe the script restarted, if it does all history is gone).

If the queue has entries, we pop off the first entry and pass it to deleteMessage.  At this point nothing is done with the statuscode.  If it succeeds you’ll see the message disappear in spark, if it fails nothing will happen.

If the first word isn’t delete, we assume a message needs to be posted.  I’ve stripped out the imgur search, but that occurs first and the return is send to postMessage.  The return callback from postMessage includes the JSON data of that exact message that it just sent. We are going to push this message on to the queue, but before we do that I check to ensure that it’s not full, if it is, I use the shift method to remove the oldest entry, and then push the newest into the queue.

And that completes this bot.  As I said, I’ll probably extend some of this functionality in a future post, and try to harden it a bit.

For reference, here is the complete server side code:

const express = require('express');
const bodyparser = require('body-parser');
const dequeue = require('double-ended-queue');

const spark = require('./spark.js');
const imgur = require('./imgur.js');

var botName = 'ImgurBot';
var msgQueue = new dequeue();

var app = express();
app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: true }));

app.all('/spark', (req, res) => {
 res.send();
 spark.getMessage(req.body.data.id, (messageDetail) => {
  if (messageDetail.personEmail !== botName + '@sparkbot.io') {
   if (messageDetail.roomType === 'group') {
   // Check if the bot is mentioned, if not ignore this
   if (messageDetail.text.split(' ')[0] === botName) {
    // Strip off the @ reference to the Bot
    msgText = messageDetail.text.split(' ').slice(1).join(' ');
     if(msgText.match(/^delete/i)) {
      if(!msgQueue.isEmpty()) {
       var deleteMsgId = msgQueue.pop();
       spark.deleteMessage(deleteMsgId.id, (statusCode) => {
       });
      }
     } else {
      imgur.search(msgText, (link) => {
       spark.postMessage(link, messageDetail, (msgId) => {
        if(msgQueue.length >= 10) {
         msgQueue.shift();
        }

        msgQueue.push(msgId);
       });
      });
     } // end of delete check
    } // end of botName check
   } else {
    imgur.search(messageDetail.text, (link) => {
     spark.postMessage(link, messageDetail, (msgId) => {
     });
    });
   } // end of group msg check
  } // end of bot own msg check
 });
});

app.listen(90);

Here is a link to a github repository with all the code

ImgurBot

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s