Tuesday, June 17, 2014

Real Time Slack Messaging in Atom Through a Chrome Extension

Over the past couple of months, I have been playing around with Github's Atom text editor. I've been really enjoying how extensible it is and have been expressing that in my creation of a package to allow instant messaging through Atom using Slack (The messaging system we, at Shortstack, use to communicate with one another).

My most recent challenge in building this package has been enabling messaging in real time. My first thought was to use Slack's web hook integration to capture incoming messages and relay them through slack. But, to my dismay, it would have required a user to configure a webhook for every user they wished to receive messages from. At this point, I realized that the webhook integration was meant for bots and not for replicating a chat client.

I had other wild ideas such as finding some sort of applescript to communicate with the native chat client. I also seriously considered scanning OS X notification center for notifications from Slack and then parsing them to decide who it was from. But alas, while I was digging through the inspector on the Slack page, I saw this:


Slack was kind enough to leave me cues in the markup, telling me that the channel was unread and giving me the channel id in the class name. So my first Chrome extension was born.


To take a step back and see the solution in whole. we'll start back at the Slack Chat package for Atom. In order for me to retrieve any data whatsoever from a browser tab, I was going to have to do something in Slack Chat. Fortunately, Atom is basically a node app and I have access to all sorts of node packages. So I installed express and started up a server.

  @app = express();
  @app.use(bodyParser())
  @app.all "/*", (req, res, next) ->
      res.header("Access-Control-Allow-Origin", "*");
      res.header("Access-Control-Allow-Headers", "X-Requested-With");
      next();

  server = @app.listen 51932, () ->;
      console.log("Listening on port %d", server.address().port);
        
  @app.post "/new", (req, res) =>
      @setNotifications(req.body.messages)
      res.send("success!")

I created an express server with a single endpoint for accepting notifications from the Chrome extension. The setNotifications function handles the new messages sent from the Chrome application and updates the UI accordingly.

Now that the Slack Chat package can accept messages through the server it runs, all that's left is collecting the new messages from Slacks markup and send them off.


var slackScraper;

slackScraper = (function() {
  function slackScraper() {
    this.messages = [];
  }

  slackScraper.prototype.getChannelMessages = function() {
    this.messages = [];

    // Search for all unread messages from channels
    $(".unread", "#channels").each((function(_this){ 
      return function(i, el){

        // Pull the channel id from the channel's list item
        var channel = $("a", el).data("channel-id")
        _this.messages.push({
          channel_id: channel,
          count: 1
        })
      }
    })(this));
  };
  
  slackScraper.prototype.getDirectMessages = function() {
    this.messages = [];

    // Search for all of the new direct messages 
    $(".unread", "#direct_messages").each((function(_this){
      return function(i, el){ 

        // Save the channel id of the direct message
        var channel = $("a", el).data("member-id");
        
        // Get a count for the number of unread messages
        var count = $(".unread_highlight_" + channel).text();
        _this.messages.push({
          channel_id: channel, 
          count: count
        })
      }
    })(this));
  };
  
  slackScraper.prototype.saveMessages = function() {
    if(this.messages.length > 0){
      // Send messages to the server set up in the Slack Chat package
      $.post("http://localhost:51932/new", {messages: this.messages})
       .done((function(_this){
         return function(data){
          
         }
      })(this));
    }
  }
  
  return slackScraper;

})();

// Set a banner to indicate the Slack Chat plugin is working
$("body").prepend("<style>.slackchat { height: 20px; width: 100%; background-color: #000; color: #ccc; z-index: 999; text-align: center; }</style>");
$("body").prepend("<div class="slackchat">Slack Chat is running</div>");

var s = new slackScraper();

// Set an event handler for the dom changing in either channels or direct 
// messages to check for new messages and send them off to Atom
$("#channels").bind("DOMSubtreeModified", function () {
  s.getChannelMessages();
  s.saveMessages();
});

$("#direct_messages").bind("DOMSubtreeModified", function () {
  s.getDirectMessages();
  s.saveMessages();
});


Github: https://github.com/callahanrts/slack-chat
Atom Package: https://atom.io/packages/slack-chat