Bug 1000398 - Support sending private message to a MUC participant. r=aleth
authorAbdelrhman Ahmed <a.ahmed1026@gmail.com>
Fri, 26 Jun 2015 14:48:00 +0200
changeset 22813 a88a7d21371a6ea937b76bedea68313b462900c4
parent 22812 23d2b24151c443a7a2a5e5fd28c47da06cf8069b
child 22814 59ae2306da5684907c1e72e1e1c5a14d2088702a
push id1443
push usermbanner@mozilla.com
push dateMon, 10 Aug 2015 18:31:17 +0000
treeherdercomm-beta@8fe07d686c22 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaleth
bugs1000398
Bug 1000398 - Support sending private message to a MUC participant. r=aleth
chat/components/public/prplIConversation.idl
chat/locales/en-US/xmpp.properties
chat/protocols/xmpp/xmpp.jsm
--- a/chat/components/public/prplIConversation.idl
+++ b/chat/components/public/prplIConversation.idl
@@ -104,19 +104,17 @@ interface prplIConvIM: prplIConversation
 interface prplIConvChat: prplIConversation {
 
   /* Get an nsISimpleEnumerator of prplIConvChatBuddy objects:
      The list of people participating in this chat */
   nsISimpleEnumerator getParticipants();
 
   /* The normalized chat buddy name will be suitable for calling
      createConversation to start a private conversation or calling
-     requestBuddyInfo. Generally this can just be aChatBuddyName, but some
-     protocols (e.g. XMPP) use this to strip out parts of the name (e.g. the
-     resource). */
+     requestBuddyInfo. */
   AUTF8String getNormalizedChatBuddyName(in AUTF8String aChatBuddyName);
 
   /* The topic of this chat room */
            attribute AUTF8String topic;
 
   /* The name/nick of the person who set the topic */
   readonly attribute AUTF8String topicSetter;
 
--- a/chat/locales/en-US/xmpp.properties
+++ b/chat/locales/en-US/xmpp.properties
@@ -55,16 +55,21 @@ conversation.error.creationFailedNotAllo
 #   %S is the name of MUC room.
 conversation.error.joinFailedRemoteServerNotFound=Could not join the room %S as the server the room is hosted on could not be reached.
 conversation.error.changeTopicFailedNotAuthorized=You are not authorized to set the topic of this room.
 #   This is displayed in a conversation as an error message when the user sends
 #   a message to a room that he is not in.
 #   %1$S is the name of MUC room.
 #   %2$S is the text of the message that wasn't delivered.
 conversation.error.sendFailedAsNotInRoom=Message could not be sent to %1$S as you are no longer in the room: %2$S
+#   This is displayed in a conversation as an error message when the user sends
+#   a message to a room that the recipient is not in.
+#   %1$S is the jid of the recipient.
+#   %2$S is the text of the message that wasn't delivered.
+conversation.error.sendFailedAsRecipientNotInRoom=Message could not be sent to %1$S as the recipient is no longer in the room: %2$S
 #   These are displayed in a conversation as a system error message.
 conversation.error.remoteServerNotFound=Could not reach the recipient's server
 conversation.error.unknownError=Unknown error
 
 # LOCALIZATION NOTE (tooltip.*):
 #   These are the titles of lines of information that will appear in
 #   the tooltip showing details about a contact or conversation.
 # LOCALIZATION NOTE (tooltip.status):
--- a/chat/protocols/xmpp/xmpp.jsm
+++ b/chat/protocols/xmpp/xmpp.jsm
@@ -289,17 +289,18 @@ const XMPPMUCConversationPrototype = {
       // Checks if a message exists in conversation to avoid duplication.
       if (this._messageIds.has(id))
         return;
       this._messageIds.add(id);
     }
     this.writeMessage(from, aMsg, flags);
   },
 
-  getNormalizedChatBuddyName: function(aNick) this.name + "/" + aNick,
+  getNormalizedChatBuddyName: function(aNick)
+    this._account.normalizeFullJid(this.name + "/" + aNick),
 
   // Removes a participant from MUC conversation.
   removeParticipant: function(aNick) {
     if (!this._participants.has(aNick))
       return;
 
     this._participants.delete(aNick);
     let nickString = Cc["@mozilla.org/supports-string;1"]
@@ -351,29 +352,47 @@ XMPPMUCConversation.prototype = XMPPMUCC
 /* Helper class for buddy conversations */
 const XMPPConversationPrototype = {
   __proto__: GenericConvIMPrototype,
 
   _typingTimer: null,
   supportChatStateNotifications: true,
   _typingState: "active",
 
+  // Indicates that current conversation is with a MUC participant and the
+  // recipient jid (stored in the userName) is of the form room@domain/nick.
+  _isMucParticipant: false,
+
   get buddy() this._account._buddies.get(this.name),
   get title() this.contactDisplayName,
   get contactDisplayName() this.buddy ? this.buddy.contactDisplayName : this.name,
   get userName() this.buddy ? this.buddy.userName : this.name,
 
+  // Returns jid (room@domain/nick) if it is with a MUC participant, and the
+  // name of conversation otherwise.
+  get normalizedName() {
+    if (this._isMucParticipant)
+      return this._account.normalizeFullJid(this.name);
+    return this._account.normalize(this.name);
+  },
+
   // Used to avoid showing full jids in typing notifications.
   get shortName() {
     if (this.buddy)
       return this.buddy.contactDisplayName;
 
     let jid = this._account._parseJID(this.name);
-    if (!jid || !jid.node)
+    if (!jid)
       return this.name;
+
+    // Returns nick of the recipient if conversation is with a participant of
+    // a MUC we are in as jid of the recipient is of the form room@domain/nick.
+    if (this._isMucParticipant)
+      return jid.resource;
+
     return jid.node;
   },
 
   get shouldSendTypingNotifications()
     this.supportChatStateNotifications &&
     Services.prefs.getBoolPref("purple.conversations.im.send_typing"),
 
   /* Called when the user is typing a message
@@ -398,34 +417,41 @@ const XMPPConversationPrototype = {
 
     this._setTypingState("paused");
   },
 
   _setTypingState: function(aNewState) {
     if (this._typingState == aNewState)
       return;
 
-    /* to, msg, state, attrib, data */
     let s = Stanza.message(this.to, null, aNewState);
-    this._account.sendStanza(s);
+
+    // We don't care about errors in response to typing notifications
+    // (e.g. because the user has left the room when talking to a MUC
+    // participant).
+    this._account.sendStanza(s, () => true);
+
     this._typingState = aNewState;
   },
   _cancelTypingTimer: function() {
     if (this._typingTimer) {
       clearTimeout(this._typingTimer);
       delete this._typingTimer;
     }
   },
 
+  // Holds the resource of user that you are currenty talking to, but if the
+  // user is a participant of a MUC we are in, holds the nick of user you are
+  // talking to.
   _targetResource: "",
+
   get to() {
-    let to = this.userName;
-    if (this._targetResource)
-      to += "/" + this._targetResource;
-    return to;
+    if (!this._targetResource || this._isMucParticipant)
+      return this.userName;
+    return this.userName + "/" + this._targetResource;
   },
 
   /* Called when the user enters a chat message */
   sendMsg: function(aMsg) {
     this._cancelTypingTimer();
     let cs = this.shouldSendTypingNotifications ? "active" : null;
     let s = Stanza.message(this.to, aMsg, cs);
     this._account.sendStanza(s);
@@ -449,24 +475,43 @@ const XMPPConversationPrototype = {
 
   /* Called by the account when a messsage is received from the buddy */
   incomingMessage: function(aMsg, aStanza, aDate) {
     let from = aStanza.attributes["from"];
     this._targetResource = this._account._parseJID(from).resource;
     let flags = {};
     let error = this._account.parseError(aStanza);
     if (error) {
+      let norm = this._account.normalize(from);
+      let muc = this._account._mucs.get(norm);
+
       if (!aMsg) {
         // Failed outgoing message unknown.
         if (error.condition == "remote-server-not-found")
           aMsg = _("conversation.error.remoteServerNotFound");
         else
           aMsg = _("conversation.error.unknownError");
       }
-      aMsg = _("conversation.error.notDelivered", aMsg);
+      else if (this._isMucParticipant && muc && !muc.left &&
+               error.condition == "item-not-found") {
+        // XEP-0045 (7.5): MUC private messages.
+        // If we try to send to participant not in a room we are in.
+        aMsg = _("conversation.error.sendFailedAsRecipientNotInRoom",
+                 this._targetResource, aMsg);
+      }
+      else if (this._isMucParticipant &&
+               (error.condition == "item-not-found" ||
+                error.condition == "not-acceptable")) {
+        // If we left a room and try to send to a participant in it or the
+        // room is removed.
+        aMsg = _("conversation.error.sendFailedAsNotInRoom",
+                 this._account.normalize(from), aMsg);
+      }
+      else
+        aMsg = _("conversation.error.notDelivered", aMsg);
       flags.system = true;
       flags.error = true;
     }
     else
       flags = {incoming: true, _alias: this.contactDisplayName};
     if (aDate) {
       flags.time = aDate / 1000;
       flags.delayed = true;
@@ -479,19 +524,23 @@ const XMPPConversationPrototype = {
     // TODO send the stanza indicating we have left the conversation?
     GenericConvIMPrototype.close.call(this);
   },
   unInit: function() {
     this._account.removeConversation(this.normalizedName);
     GenericConvIMPrototype.unInit.call(this);
   }
 };
-function XMPPConversation(aAccount, aName)
+
+// Creates XMPP conversation.
+function XMPPConversation(aAccount, aNormalizedName, aMucParticipant)
 {
-  this._init(aAccount, aName);
+  this._init(aAccount, aNormalizedName);
+  if (aMucParticipant)
+    this._isMucParticipant = true;
 }
 XMPPConversation.prototype = XMPPConversationPrototype;
 
 /* Helper class for buddies */
 const XMPPAccountBuddyPrototype = {
   __proto__: GenericAccountBuddyPrototype,
 
   subscription: "none",
@@ -819,18 +868,22 @@ const XMPPAccountPrototype = {
   /* Generate unique id for a stanza. Using id and unique sid is defined in
    * RFC 6120 (Section 8.2.3, 4.7.3).
    */
   generateId: function() UuidGenerator.generateUUID().toString().slice(1, -1),
 
   _init: function(aProtoInstance, aImAccount) {
     GenericAccountPrototype._init.call(this, aProtoInstance, aImAccount);
 
-    /* Ongoing conversations */
-    this._conv = new NormalizedMap(this.normalize.bind(this));
+    // Ongoing conversations.
+    // The keys of this._conv are assumed to be normalized like account@domain
+    // for normal conversations and like room@domain/nick for MUC participant
+    // convs.
+    this._conv = new NormalizedMap(this.normalizeFullJid.bind(this));
+
     this._buddies = new NormalizedMap(this.normalize.bind(this));
     this._mucs = new NormalizedMap(this.normalize.bind(this));
   },
 
   get canJoinChat() true,
   chatRoomFields: {
     room: {get label() _("chatRoomField.room"), required: true},
     server: {get label() _("chatRoomField.server"), required: true},
@@ -1325,17 +1378,17 @@ const XMPPAccountPrototype = {
       let delay = aStanza.getElement(["delay"]);
       if (delay && delay.uri == Stanza.NS.delay) {
         if (delay.attributes["stamp"])
           date = new Date(delay.attributes["stamp"]);
       }
       if (date && isNaN(date))
         date = undefined;
       if (type == "groupchat" ||
-          (type == "error" && this._mucs.has(norm))) {
+          (type == "error" && this._mucs.has(norm) && !this._conv.has(from))) {
         if (!this._mucs.has(norm)) {
           this.WARN("Received a groupchat message for unknown MUC " + norm);
           return;
         }
         let muc = this._mucs.get(norm);
         muc.incomingMessage(body, aStanza, date);
         return;
       }
@@ -1361,29 +1414,29 @@ const XMPPAccountPrototype = {
         }
         // Otherwise, just notify the user.
         let conv = this.createConversation(invitation.from);
         if (conv)
           conv.writeMessage(invitation.from, body, {system: true});
         return;
       }
 
-      let conv = this.createConversation(norm);
+      let conv = this.createConversation(from);
       if (!conv)
         return;
       conv.incomingMessage(body, aStanza, date);
     }
     else if (type == "error") {
-      let conv = this.createConversation(norm);
+      let conv = this.createConversation(from);
       if (conv)
         conv.incomingMessage(null, aStanza);
     }
 
     // Don't create a conversation to only display the typing notifications.
-    if (!this._conv.has(norm))
+    if (!this._conv.has(norm) && !this._conv.has(from))
       return;
 
     // Ignore errors while delivering typing notifications.
     if (type == "error")
       return;
 
     let typingState = Ci.prplIConvIM.NOT_TYPING;
     let state;
@@ -1392,17 +1445,22 @@ const XMPPAccountPrototype = {
       state = s[0].localName;
     if (state) {
       this.DEBUG(state);
       if (state == "composing")
         typingState = Ci.prplIConvIM.TYPING;
       else if (state == "paused")
         typingState = Ci.prplIConvIM.TYPED;
     }
-    let conv = this._conv.get(norm);
+    let convName = norm;
+    if (this._mucs.has(norm))
+      convName = from;
+    let conv = this._conv.get(convName);
+    if (!conv)
+      return;
     conv.updateTyping(typingState, conv.shortName);
     conv.supportChatStateNotifications = !!state;
   },
 
   /* Called when there is an error in the xmpp session */
   onError: function(aError, aException) {
     if (aError === null || aError === undefined)
       aError = Ci.prplIAccount.ERROR_OTHER_ERROR;
@@ -1433,16 +1491,21 @@ const XMPPAccountPrototype = {
       if (c.localName == "PHOTO")
         buddy._saveIcon(c);
     }
     if (!foundFormattedName && buddy._vCardFormattedName)
       buddy.vCardFormattedName = "";
     buddy._vCardReceived = true;
   },
 
+  // XEP-0029 (Section 2) and RFC 6122 (Section 2): The node and domain are
+  // lowercase, while resources are case sensitive and can contain spaces.
+  normalizeFullJid: function(aJID) this._parseJID(aJID.trim()).jid,
+
+  // Standard normalization for XMPP removes the resource part of jids.
   normalize: function(aJID) {
     return aJID.trim()
                .split("/", 1)[0] // up to first slash
                .toLowerCase();
   },
 
   _parseJID: function(aJid) {
     let match =
@@ -1594,23 +1657,39 @@ const XMPPAccountPrototype = {
   },
 
   // Variations of the XMPP protocol can change these default constructors:
   _conversationConstructor: XMPPConversation,
   _MUCConversationConstructor: XMPPMUCConversation,
   _accountBuddyConstructor: XMPPAccountBuddy,
 
   /* Create a new conversation */
-  createConversation: function(aNormalizedName) {
-    if (!this._conv.has(aNormalizedName)) {
-      this._conv.set(aNormalizedName,
-        new this._conversationConstructor(this, aNormalizedName));
+  createConversation: function(aName) {
+    let convName = this.normalize(aName);
+
+    // Checks if conversation is with a participant of a MUC we are in. We do
+    // not want to strip the resource as it is of the form room@domain/nick.
+    let isMucParticipant = this._mucs.has(convName);
+    if (isMucParticipant)
+      convName = this.normalizeFullJid(aName);
+
+    // Checking that the aName can be parsed and is not broken.
+    let jid = this._parseJID(convName);
+    if (!jid || !jid.node || (isMucParticipant && !jid.resource)) {
+      this.ERROR("Could not create conversation as jid is broken: " + convName);
+      throw "Invalid JID";
     }
 
-    return this._conv.get(aNormalizedName);
+    if (!this._conv.has(convName)) {
+      this._conv.set(convName,
+                     new this._conversationConstructor(this, convName,
+                                                       isMucParticipant));
+    }
+
+    return this._conv.get(convName);
   },
 
   /* Remove an existing conversation */
   removeConversation: function(aNormalizedName) {
     if (this._conv.has(aNormalizedName))
       this._conv.delete(aNormalizedName);
     else if (this._mucs.has(aNormalizedName))
       this._mucs.delete(aNormalizedName);