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

















Tuesday, May 20, 2014

Is Atom extensible enough to compete with emacs?



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

I recently set out to create my first package for Atom. I started by looking through their documentation on how to create packages and was surprised how easy Atom was to work with. I also took a look at the api (https://atom.io/docs/api/v0.96.0/api/) to see what other models existed and what could be done with them.

We've been using Slack as our in house messaging client for a while now and I've wanted to be able to send code snippets and messages to my colleagues from Atom. Since there was no slack package for Atom, it seemed like a prime opportunity to create one. I started by mimicking the tree-view package created by the atom team (https://github.com/atom/tree-view) where you can see the coffeescript source of the package.

It was relatively easy to follow the code and create my own Slack tree-view where the channels and members of my slack account are the items in the list. Selecting a user or channel opens the chat history as well as an input for sending messages. Creating commands for users to call from their keymaps and the command palette was also very easy. There are currently default keymaps for toggling the side view, navigating up and down through the members and channels, and opening and closing conversations.  It's still fairly undeveloped, but the main functionality of sending and viewing messages is working.

The thought of complex add-ons reminds me of the spirit of my emacs-using friends. They will do anything to use terminal, return emails, listen to music, and manage a shuttle on a voyage to mars from their text editors. So it begs the question, is Atom extensible enough to compete with emacs? And, will Atom be the emacs of the future?