diff --git a/chat/locales/en-US/xmpp.properties b/chat/locales/en-US/xmpp.properties index aab72f17ba..7f824e5342 100644 --- a/chat/locales/en-US/xmpp.properties +++ b/chat/locales/en-US/xmpp.properties @@ -74,6 +74,12 @@ conversation.error.banKickCommandNotAllowed=You don't have the required privileg conversation.error.banKickCommandConflict=Sorry, you can't remove yourself from the room. conversation.error.changeNickFailedConflict=Could not change your nick to %S as this nick is already in use. conversation.error.changeNickFailedNotAcceptable=Could not change your nick to %S as nicks are locked down in this room. +conversation.error.inviteFailedForbidden=You don't have the required privileges to invite users to this room. +# %S is the jid of user that is invited. +conversation.error.failedJIDNotFound=Could not reach %S. +# %S is the jid that is invalid. +conversation.error.invalidJID=%S is an invalid jid (Jabber identifiers must be of the form user@domain). +conversation.error.commandFailedNotInRoom=You have to rejoin the room to be able to use this command. conversation.error.unknownError=Unknown error # LOCALIZATION NOTE (tooltip.*): @@ -135,6 +141,12 @@ conversation.message.parted.you.reason=You have left the room: %S conversation.message.parted=%1$S has left the room. conversation.message.parted.reason=%1$S has left the room: %2$S +# LOCALIZATION NOTE (conversation.message.invitationDeclined*): +# %1$S is the invitee that declined the invitation. +# %2$S is the decline message supplied by the invitee. +conversation.message.invitationDeclined=%1$S has declined your invitation. +conversation.message.invitationDeclined.reason=%1$S has declined your invitation: %2$S + # LOCALIZATION NOTE (conversation.message.banned.*): # These are displayed as a system message when a participant is banned from # a room. @@ -229,5 +241,7 @@ command.part2=%S [<message>]: Leave the current room with an optional mess command.topic=%S [<new topic>]: Set this room's topic. command.ban=%S <nick>[<message>]: Ban someone from the room. You must be a room administrator to do this. command.kick=%S <nick>[<message>]: Remove someone from the room. You must be a room moderator to do this. +command.invite=%S <jid>[<message>]: Invite a user to join the current room with an optional message. +command.me=%S <action to perform>: Perform an action. command.nick=%S <new nickname>: Change your nickname. command.msg=%S <nick> <message>: Send a private message to a participant in the room. diff --git a/chat/protocols/xmpp/xmpp-commands.jsm b/chat/protocols/xmpp/xmpp-commands.jsm index c0deb77d35..2abada7330 100644 --- a/chat/protocols/xmpp/xmpp-commands.jsm +++ b/chat/protocols/xmpp/xmpp-commands.jsm @@ -22,6 +22,17 @@ function getAccount(aConv) { return getConv(aConv)._account; } +function getMUC(aConv) { + let conv = getConv(aConv); + if (conv.left) { + conv.writeMessage(conv.name, + _("conversation.error.commandFailedNotInRoom"), + {system: true}); + return null; + } + return conv; +} + // Trims the string and splits it in two parts on the first space // if there is one. Returns the non-empty parts in an array. function splitInput(aString) { @@ -33,7 +44,7 @@ function splitInput(aString) { let offset = params.indexOf(" "); if (offset != -1) { splitParams.push(params.slice(0, offset)); - splitParams.push(params.slice(offset + 1)); + splitParams.push(params.slice(offset + 1).trimLeft()); } else splitParams.push(params); @@ -90,9 +101,10 @@ var commands = [ get helpString() { return _("command.topic", "topic"); }, usageContext: Ci.imICommand.CMD_CONTEXT_CHAT, run: function(aMsg, aConv) { - let conv = getConv(aConv); - if (!conv.left) - conv.topic = aMsg; + let conv = getMUC(aConv); + if (!conv) + return true; + conv.topic = aMsg; return true; } }, @@ -105,9 +117,9 @@ var commands = [ if (!params.length) return false; - let conv = getConv(aConv); - if (!conv.left) - conv.ban(params[0], params[1]); + let conv = getMUC(aConv); + if (conv) + conv.ban(...params); return true; } }, @@ -120,9 +132,53 @@ var commands = [ if (!params.length) return false; + let conv = getMUC(aConv); + if (conv) + conv.kick(...params); + return true; + } + }, + { + name: "invite", + get helpString() { return _("command.invite", "invite"); }, + usageContext: Ci.imICommand.CMD_CONTEXT_CHAT, + run: function(aMsg, aConv) { + let params = splitInput(aMsg); + if (!params.length) + return false; + + let conv = getMUC(aConv); + if (!conv) + return true; + + // Check user's jid is valid. + let account = getAccount(aConv); + let jid = account._parseJID(params[0]); + if (!jid) { + conv.writeMessage(conv.name, + _("conversation.error.invalidJID", params[0]), + {system: true}); + return true; + } + conv.invite(...params); + return true; + } + }, + { + name: "me", + get helpString() { return _("command.me", "me"); }, + usageContext: Ci.imICommand.CMD_CONTEXT_CHAT, + run: function(aMsg, aConv) { + let params = aMsg.trim(); + if (!params) + return false; + + // XEP-0245: The /me Command. + // We need to append "/me " in the first four characters of the message + // body. let conv = getConv(aConv); - if (!conv.left) - conv.ban(params[0], params[1]); + conv.sendMsg("/me " + params); + return true; } }, @@ -135,8 +191,8 @@ var commands = [ if (!params[0]) return false; - let conv = getConv(aConv); - if (!conv.left) + let conv = getMUC(aConv); + if (conv) conv.setNick(params[0]); return true; } @@ -151,8 +207,8 @@ var commands = [ return false; let [nickName, msg] = params; - let conv = getConv(aConv); - if (conv.left) + let conv = getMUC(aConv); + if (!conv) return true; if (!conv._participants.has(nickName)) { diff --git a/chat/protocols/xmpp/xmpp.jsm b/chat/protocols/xmpp/xmpp.jsm index 865e7b3968..7d8c8babe4 100644 --- a/chat/protocols/xmpp/xmpp.jsm +++ b/chat/protocols/xmpp/xmpp.jsm @@ -423,6 +423,24 @@ const XMPPMUCConversationPrototype = { delete this.chatRoomFields; }, + // Invites a user to MUC conversation. + invite: function(aJID, aMsg = null) { + // XEP-0045 (7.8): Inviting Another User to a Room. + // XEP-0045 (7.8.2): Mediated Invitation. + let invite = Stanza.node("invite", null, {to: aJID}, + aMsg ? Stanza.node("reason", null, null, aMsg) : null); + let x = Stanza.node("x", Stanza.NS.muc_user, null, invite); + let s = Stanza.node("message", null, {to: this.name}, x); + this._account.sendStanza(s, this._account.handleErrors({ + forbidden: _("conversation.error.inviteFailedForbidden"), + // ejabberd uses error not-allowed to indicate that this account does not + // have the required privileges to invite users instead of forbidden error, + // and this is not mentioned in the spec (XEP-0045). + notAllowed: _("conversation.error.inviteFailedForbidden"), + itemNotFound: _("conversation.error.failedJIDNotFound", aJID) + }, this)); + }, + // Bans a participant from MUC conversation. ban: function(aNickName, aMsg = null) { // XEP-0045 (9.1): Banning a User. @@ -485,6 +503,29 @@ const XMPPMUCConversationPrototype = { }, this)); }, + // Called by the account when a message stanza is received for this muc and + // needs to be handled. + onMessageStanza: function(aStanza) { + let x = aStanza.getElement(["x"]); + let decline = x.getElement(["decline"]); + if (decline) { + // XEP-0045 (7.8): Inviting Another User to a Room. + // XEP-0045 (7.8.2): Mediated Invitation. + let invitee = decline.attributes["jid"]; + let reasonNode = decline.getElement(["reason"]); + let reason = reasonNode ? reasonNode.innerText : ""; + let msg; + if (reason) + msg = _("conversation.message.invitationDeclined.reason", invitee, reason); + else + msg = _("conversation.message.invitationDeclined", invitee); + + this.writeMessage(this.name, msg, {system: true}); + } + else + this.WARN("Unhandled message stanza."); + }, + /* Called when the user closed the conversation */ close: function() { if (!this.left) @@ -1647,6 +1688,7 @@ const XMPPAccountPrototype = { let norm = this.normalize(from); let type = aStanza.attributes["type"]; + let x = aStanza.getElement(["x"]); let body; let b = aStanza.getElement(["body"]); if (b) { @@ -1735,6 +1777,15 @@ const XMPPAccountPrototype = { if (conv) conv.incomingMessage(null, aStanza); } + else if (x && x.uri == Stanza.NS.muc_user) { + let muc = this._mucs.get(norm); + if (!muc) { + this.WARN("Received a groupchat message for unknown MUC " + norm); + return; + } + muc.onMessageStanza(aStanza); + return; + } // Don't create a conversation to only display the typing notifications. if (!this._conv.has(norm) && !this._conv.has(from))