Bug 1302447 - Add tag handling infrastructure r=clokep
authorMartin Giger <martin@humanoids.be>
Wed, 16 Nov 2016 13:22:00 -0500
changeset 20793 7111a1bd9fa8fd043791eb4f4fb9d43ba802239e
parent 20792 75f891b74ae80e5e0c7627d0659e810830d2963d
child 20794 08af9c37ea6aea0d51253e3fda5837d994f8d7d8
push id12599
push userclokep@gmail.com
push dateThu, 01 Dec 2016 23:49:02 +0000
treeherdercomm-central@08af9c37ea6a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersclokep
bugs1302447
Bug 1302447 - Add tag handling infrastructure r=clokep
chat/protocols/irc/irc.js
chat/protocols/irc/ircBase.jsm
chat/protocols/irc/ircCTCP.jsm
chat/protocols/irc/ircHandlers.jsm
chat/protocols/irc/ircNonStandard.jsm
--- a/chat/protocols/irc/irc.js
+++ b/chat/protocols/irc/irc.js
@@ -148,16 +148,22 @@ function _setMode(aAddNewMode, aNewModes
     if (hasMode && !aAddNewMode)
       this._modes.delete(newMode);
     // If the mode is not in the list of modes and we want to add it.
     else if (!hasMode && aAddNewMode)
       this._modes.add(newMode);
   }
 }
 
+function TagMessage(aMessage, aTagName) {
+    this.message = aMessage;
+    this.tagName = aTagName;
+    this.tagValue = aMessage.tags.get(aTagName);
+}
+
 // Properties / methods shared by both ircChannel and ircConversation.
 var GenericIRCConversation = {
   _observedNicks: [],
   // This is set to true after a message is sent to notify the 401
   // ERR_NOSUCHNICK handler to write an error message to the conversation.
   _pendingMessage: false,
   _waitingForNick: false,
 
@@ -169,16 +175,48 @@ var GenericIRCConversation = {
   getMaxMessageLength: function() {
     // Build the shortest possible message that could be sent to other users.
     let baseMessage = ":" + this._account._nickname + this._account.prefix +
                       " " + this._account.buildMessage("PRIVMSG", this.name) +
                       " :\r\n";
     return this._account.maxMessageLength -
            this._account.countBytes(baseMessage);
   },
+  /**
+   * @param {string} aWho - Message author's username.
+   * @param {string} aMessage - Message text.
+   * @param {Object} aObject - Other properties to set on the imMessage.
+   */
+  writeMessage: function(aWho, aMessage, aObject) {
+    let messageProps = aObject;
+    if ("tags" in aObject && ircHandlers.hasTagHandlers) {
+      // Merge extra info for the handler into the props.
+      messageProps = Object.assign({
+        who: aWho,
+        message: aMessage,
+        get originalMessage() {
+          return aMessage;
+        }
+      }, messageProps);
+      for (let tag of aObject.tags.keys()) {
+        // Unhandled tags may be common, since a tag does not have to be handled
+        // with a tag handler, it may also be handled by a message command handler.
+        ircHandlers.handleTag(this._account, new TagMessage(messageProps, tag));
+      }
+
+      // Remove helper prop for tag handlers. We don't want to remove the other
+      // ones, since they might have been changed and will override aWho and
+      // aMessage in the imMessage constructor.
+      delete messageProps.originalMessage;
+    }
+    // Remove the IRC tags, as those were passed in just for this step.
+    delete aObject.tags;
+
+    GenericConversationPrototype.writeMessage.call(this, aWho, aMessage, messageProps);
+  },
   // Apply CTCP formatting before displaying.
   prepareForDisplaying: function(aMsg) {
     aMsg.displayMessage = ctcpFormatToHTML(aMsg.displayMessage);
     GenericConversationPrototype.prepareForDisplaying.apply(this, arguments);
   },
   prepareForSending: function(aOutgoingMessage, aCount) {
     // Split the message by line breaks and send each one individually.
     let messages = aOutgoingMessage.message.split(/[\r\n]+/);
--- a/chat/protocols/irc/ircBase.jsm
+++ b/chat/protocols/irc/ircBase.jsm
@@ -23,17 +23,17 @@ var {interfaces: Ci, utils: Cu} = Compon
 
 Cu.import("resource:///modules/imXPCOMUtils.jsm");
 Cu.import("resource:///modules/imServices.jsm");
 Cu.import("resource:///modules/ircHandlers.jsm");
 Cu.import("resource:///modules/ircUtils.jsm");
 Cu.import("resource:///modules/jsProtoHelper.jsm");
 
 function privmsg(aAccount, aMessage, aIsNotification) {
-  let params = {incoming: true};
+  let params = {incoming: true, tags: aMessage.tags};
   if (aIsNotification)
     params.notification = true;
   aAccount.getConversation(aAccount.isMUCName(aMessage.params[0]) ?
                              aMessage.params[0] : aMessage.origin)
           .writeMessage(aMessage.origin, aMessage.params[1], params);
   return true;
 }
 
@@ -74,16 +74,17 @@ function leftRoom(aAccount, aNicks, aCha
     }
   }
   return true;
 }
 
 function writeMessage(aAccount, aMessage, aString, aType) {
   let type = {};
   type[aType] = true;
+  type.tags = aMessage.tags;
   aAccount.getConversation(aMessage.origin)
           .writeMessage(aMessage.origin, aString, type);
   return true;
 }
 
 // If aNoLastParam is true, the last parameter is not printed out.
 function serverMessage(aAccount, aMsg, aNoLastParam) {
   // If we don't want to show messages from the server, just mark it as handled.
--- a/chat/protocols/irc/ircCTCP.jsm
+++ b/chat/protocols/irc/ircCTCP.jsm
@@ -118,17 +118,17 @@ var ctcpBase = {
   // These represent CTCP commands.
   commands: {
     "ACTION": function(aMessage) {
       // ACTION <text>
       // Display message in conversation
       this.getConversation(this.isMUCName(aMessage.params[0]) ?
                              aMessage.params[0] : aMessage.origin)
           .writeMessage(aMessage.origin, "/me " + aMessage.ctcp.param,
-                        {incoming: true});
+                        {incoming: true, tags: aMessage.tags});
       return true;
     },
 
     // Used when an error needs to be replied with.
     "ERRMSG": function(aMessage) {
       this.WARN(aMessage.origin + " failed to handle CTCP message: " +
                 aMessage.ctcp.param);
       return true;
@@ -203,17 +203,17 @@ var ctcpBase = {
       else {
         // TIME :<human-readable-time-string>
         // Received a TIME reply, display it.
         // Remove the : prefix, if it exists and display the result.
         let time = aMessage.ctcp.param.slice(aMessage.ctcp.param[0] == ":");
         this.getConversation(aMessage.origin)
             .writeMessage(aMessage.origin,
                           _("ctcp.time", aMessage.origin, time),
-                          {system: true});
+                          {system: true, tags: aMessage.tags});
       }
       return true;
     },
 
     // This is commented out since CLIENTINFO automatically returns the
     // supported CTCP parameters and this is not supported.
 
     // A string set by the user (never the client coder)
@@ -230,14 +230,15 @@ var ctcpBase = {
         this.sendCTCPMessage(aMessage.origin, true, "VERSION", version);
       }
       else if (aMessage.command == "NOTICE" && aMessage.ctcp.param.length) {
         // VERSION #:#:#
         // Received VERSION response, display to the user.
         let response = _("ctcp.version", aMessage.origin,
                          aMessage.ctcp.param);
         this.getConversation(aMessage.origin)
-            .writeMessage(aMessage.origin, response, {system: true});
+            .writeMessage(aMessage.origin, response,
+                          {system: true, tags: aMessage.tags});
       }
       return true;
     }
   }
 };
--- a/chat/protocols/irc/ircHandlers.jsm
+++ b/chat/protocols/irc/ircHandlers.jsm
@@ -31,16 +31,19 @@ var ircHandlers = {
   _capHandlers: [],
   // Object to hold the CTCP handlers, expects the same fields as _ircHandlers.
   _ctcpHandlers: [],
   // Object to hold the DCC handlers, expects the same fields as _ircHandlers.
   _dccHandlers: [],
   // Object to hold the Services handlers, expects the same fields as
   // _ircHandlers.
   _servicesHandlers: [],
+  // Object to hold irc message tag handlers, expects the same fields as
+  // _ircHandlers.
+  _tagHandlers: [],
 
   _registerHandler: function(aArray, aHandler) {
     // Protect ourselves from adding broken handlers.
     if (!("commands" in aHandler)) {
       Cu.reportError(new Error("IRC handlers must have a \"commands\" " +
                                "property: " + aHandler.name));
       return false;
     }
@@ -98,26 +101,32 @@ var ircHandlers = {
   registerServicesHandler: function(aHandler) {
     return this._registerHandler(this._servicesHandlers, aHandler);
   },
   unregisterServicesHandler: function(aHandler) {
     this._servicesHandlers = this._unregisterHandler(this._servicesHandlers,
                                                      aHandler);
   },
 
+  registerTagHandler: function(aHandler) {
+    return this._registerHandler(this._tagHandlers, aHandler);
+  },
+  unregisterTagHandler: function(aHandler) {
+    this._tagHandlers = this._unregisterHandler(this._tagHandlers, aHandler);
+  },
+
   // Handle a message based on a set of handlers.
   _handleMessage: function(aHandlers, aAccount, aMessage, aCommand) {
     // Loop over each handler and run the command until one handles the message.
     for (let handler of aHandlers) {
       try {
         // Attempt to execute the command, by checking if the handler has the
         // command.
         // Parse the command with the JavaScript account object as "this".
-        if (handler.isEnabled.call(aAccount) &&
-            Object.prototype.hasOwnProperty.call(handler.commands, aCommand) &&
+        if (handler.isEnabled.call(aAccount) && aCommand in handler.commands &&
             handler.commands[aCommand].call(aAccount, aMessage))
           return true;
       } catch (e) {
         // We want to catch an error here because one of our handlers are
         // broken, if we don't catch the error, the whole IRC plug-in will die.
         aAccount.ERROR("Error running command " + aCommand + " with handler " +
                        handler.name + ":\n" + JSON.stringify(aMessage), e);
       }
@@ -154,21 +163,28 @@ var ircHandlers = {
   },
 
   // aMessage is a Services Message.
   handleServicesMessage: function(aAccount, aMessage) {
     return this._handleMessage(this._servicesHandlers, aAccount, aMessage,
                                aMessage.serviceName);
   },
 
+  // aMessage is a Tag Message.
+  handleTag: function(aAccount, aMessage) {
+    return this._handleMessage(this._tagHandlers, aAccount, aMessage,
+                               aMessage.tagName);
+  },
+
   // Checking if handlers exist.
   get hasHandlers() { return this._ircHandlers.length > 0; },
   get hasISUPPORTHandlers() { return this._isupportHandlers.length > 0; },
   get hasCAPHandlers() { return this._capHandlers.length > 0; },
   get hasCTCPHandlers() { return this._ctcpHandlers.length > 0; },
   get hasDCCHandlers() { return this._dccHandlers.length > 0; },
   get hasServicesHandlers() { return this._servicesHandlers.length > 0; },
+  get hasTagHandlers() { return this._tagHandlers.length > 0 },
 
   // Some constant priorities.
   get LOW_PRIORITY() { return -100; },
   get DEFAULT_PRIORITY() { return 0; },
   get HIGH_PRIORITY() { return 100; }
 };
--- a/chat/protocols/irc/ircNonStandard.jsm
+++ b/chat/protocols/irc/ircNonStandard.jsm
@@ -75,17 +75,17 @@ var ircNonStandard = {
       // message, which falls through to normal NOTICE processing.
       // Note that if the user's nick is auth this COULD be a notice directed at
       // them. For reference: moznet sends Auth (previously sent AUTH), freenode
       // sends *.
       let isAuth = target == "auth" && this._nickname.toLowerCase() != "auth";
       if (!aMessage.params[1].startsWith("***") && !isAuth) {
         this.getConversation(aMessage.origin)
             .writeMessage(aMessage.origin, aMessage.params[1],
-                          {incoming: true});
+                          {incoming: true, tags: aMessage.tags});
         return true;
       }
 
       return false;
     },
 
     "042": function(aMessage) { // RPL_YOURID (IRCnet)
       // <nick> <id> :your unique ID