Skip to content

Instantly share code, notes, and snippets.

@arlolra
Created July 3, 2014 16:02
Show Gist options
  • Save arlolra/c39f8c318970a437a4d6 to your computer and use it in GitHub Desktop.
Save arlolra/c39f8c318970a437a4d6 to your computer and use it in GitHub Desktop.
diff --git a/chat/components/public/imIConversationsService.idl b/chat/components/public/imIConversationsService.idl
--- a/chat/components/public/imIConversationsService.idl
+++ b/chat/components/public/imIConversationsService.idl
@@ -1,17 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
#include "prplIConversation.idl"
+#include "prplIMessage.idl"
#include "imIContactsService.idl"
-interface prplIMessage;
+interface imIMessage;
[scriptable, uuid(81b8d9a9-4715-4109-b522-84b9d31493a3)]
interface imIConversation: prplIConversation {
// Will be null for MUCs and IMs from people not in the contacts list.
readonly attribute imIContact contact;
// Write a system message into the conversation.
// Note: this will not be logged.
@@ -38,17 +39,17 @@ interface imIConversation: prplIConversa
// conversation. If the conversation is a left MUC or an IM
// conversation without unread message, the implementation will call
// close().
// The returned value indicates if the conversation was closed.
boolean checkClose();
// Get an array of all messages of the conversation.
void getMessages([optional] out unsigned long messageCount,
- [retval, array, size_is(messageCount)] out prplIMessage messages);
+ [retval, array, size_is(messageCount)] out imIMessage messages);
};
[scriptable, uuid(984e182c-d395-4fba-ba6e-cc80c71f57bf)]
interface imIConversationsService: nsISupports {
void initConversations();
void unInitConversations();
// Register a conversation. This will create a unique id for the
@@ -62,8 +63,32 @@ interface imIConversationsService: nsISu
imIConversation getUIConversationByContactId(in long aId);
nsISimpleEnumerator getConversations();
prplIConversation getConversationById(in unsigned long aId);
prplIConversation getConversationByNameAndAccount(in AUTF8String aName,
in imIAccount aAccount,
in boolean aIsChat);
};
+
+[scriptable, uuid(4391ba5c-9566-41a9-bb9b-fd0a0a490c2c)]
+interface imIOutgoingMessage: nsISupports {
+ attribute AUTF8String message;
+ attribute boolean cancelled;
+ readonly attribute imIConversation conversation;
+};
+
+[scriptable, uuid(0684ed1e-97b7-4eba-bf58-dd9dd81c668c)]
+interface imISendableMessage: nsISupports {
+ void getSendableMessages([optional] out unsigned long messageCount,
+ [retval, array, size_is(messageCount)] out string messages);
+ void setSendableMessages(in unsigned long messageCount,
+ [array, size_is(messageCount)] in string messages);
+ readonly attribute AUTF8String originalMessage;
+ attribute boolean cancelled;
+ readonly attribute prplIConversation target;
+};
+
+[scriptable, uuid(bd2f77d4-1fad-432d-a914-6bb5ed5c13d0)]
+interface imIMessage: prplIMessage {
+ attribute boolean cancelled;
+ attribute AUTF8String decodedMessage;
+};
diff --git a/chat/components/public/imILogger.idl b/chat/components/public/imILogger.idl
--- a/chat/components/public/imILogger.idl
+++ b/chat/components/public/imILogger.idl
@@ -5,18 +5,18 @@
#include "nsISupports.idl"
#include "nsISimpleEnumerator.idl"
#include "nsIFile.idl"
interface imIAccount;
interface imIAccountBuddy;
interface imIBuddy;
interface imIContact;
+interface imIMessage;
interface prplIConversation;
-interface prplIMessage;
[scriptable, uuid(5bc06f3b-33a1-412b-a4d8-4fc7ba4c962b)]
interface imILogConversation: nsISupports {
readonly attribute AUTF8String title;
readonly attribute AUTF8String name;
readonly attribute PRTime startDate;
// Simplified account implementation:
@@ -26,17 +26,17 @@ interface imILogConversation: nsISupport
// - protocol will only contain a "name" attribute, with the prpl's normalized name.
// Other methods/attributes aren't implemented.
readonly attribute imIAccount account;
readonly attribute boolean isChat; // always false (compatibility with prplIConversation).
readonly attribute imIAccountBuddy buddy; // always null (compatibility with prplIConvIM).
void getMessages([optional] out unsigned long messageCount,
- [retval, array, size_is(messageCount)] out prplIMessage messages);
+ [retval, array, size_is(messageCount)] out imIMessage messages);
// Callers that process the messages asynchronously should use the enumerator
// instead of the array version of the getMessages* methods to avoid paying
// up front the cost of xpconnect wrapping all message objects.
nsISimpleEnumerator getMessagesEnumerator([optional] out unsigned long messageCount);
};
[scriptable, uuid(164ff6c3-ca64-4880-b8f3-67eb1817955f)]
diff --git a/chat/components/public/prplIConversation.idl b/chat/components/public/prplIConversation.idl
--- a/chat/components/public/prplIConversation.idl
+++ b/chat/components/public/prplIConversation.idl
@@ -4,16 +4,17 @@
#include "nsISupports.idl"
#include "nsISimpleEnumerator.idl"
#include "nsIObserver.idl"
interface imIAccountBuddy;
interface imIAccount;
+interface imIOutgoingMessage;
interface nsIURI;
interface nsIDOMDocument;
/*
* This is the XPCOM purple conversation component, a proxy for PurpleConversation.
*/
[scriptable, uuid(e40dc3e5-c9ff-457b-a6cc-655cce81042c)]
@@ -39,16 +40,20 @@ interface prplIConversation: nsISupports
readonly attribute PRTime startDate;
/* Unique identifier of the conversation */
/* Setable only once by purpleCoreService while calling addConversation. */
attribute unsigned long id;
/* Send a message in the conversation */
void sendMsg(in AUTF8String aMsg);
+ void prepareForSending(in imIOutgoingMessage message,
+ [optional] out unsigned long messageCount,
+ [retval, array, size_is(messageCount)] out string messages);
+
/* Send information about the current typing state to the server.
aString should contain the content currently in the text field. The
protocol should return the number of characters that can still be typed. */
long sendTyping(in AUTF8String aString);
const long NO_TYPING_LIMIT = 2147483647; // max int = 2 ^ 31 - 1
/* Un-initialize the conversation. Will be called by
purpleCoreService::RemoveConversation when the conversation is
diff --git a/chat/components/src/imConversations.js b/chat/components/src/imConversations.js
--- a/chat/components/src/imConversations.js
+++ b/chat/components/src/imConversations.js
@@ -10,16 +10,85 @@ Cu.import("resource:///modules/jsProtoHe
var gLastUIConvId = 0;
var gLastPrplConvId = 0;
XPCOMUtils.defineLazyGetter(this, "bundle", function()
Services.strings.createBundle("chrome://chat/locale/conversations.properties")
);
+function OutgoingMessage(message, conversation) {
+ this.message = message;
+ this.conversation = conversation;
+}
+OutgoingMessage.prototype = {
+ __proto__: ClassInfo("imIOutgoingMessage", "Outgoing Message"),
+ cancelled: false
+};
+
+function SendableMessage(message, target) {
+ this._originalMessage = message;
+ this.target = target;
+}
+SendableMessage.prototype = {
+ __proto__: ClassInfo("imISendableMessage", "Sendable Message"),
+ cancelled: false,
+ get originalMessage() this._originalMessage,
+ _sendableMessages: null,
+ getSendableMessages: function(aMessageCount) {
+ let sms = this._sendableMessages || [this.originalMessage];
+ if (aMessageCount)
+ aMessageCount.value = sms.length;
+ return sms;
+ },
+ setSendableMessages: function(aMessageCount, aMessages) {
+ this._sendableMessages = aMessages;
+ }
+};
+
+function imMessage(prplMessage) {
+ this.prplMessage = prplMessage;
+}
+imMessage.prototype = {
+ __proto__: ClassInfo(["imIMessage", "prplIMessage"], "IM Message"),
+ cancelled: false,
+ _decodedMessage: null,
+
+ get decodedMessage() {
+ return this._decodedMessage || this.prplMessage.originalMessage
+ },
+ set decodedMessage(msg) { this._decodedMessage = msg; },
+
+ get message() this.prplMessage.message,
+ set message(msg) { this.prplMessage.message = msg; },
+
+ // from prplIMessage
+ get who() this.prplMessage.who,
+ get time() this.prplMessage.time,
+ get id() this.prplMessage.id,
+ get alias() this.prplMessage.alias,
+ get iconURL() this.prplMessage.iconURL,
+ get conversation() this.prplMessage.conversation,
+ set conversation(aConv) { this.prplMessage.conversation = aConv; },
+ get color() this.prplMessage.color,
+ get outgoing() this.prplMessage.outgoing,
+ get incoming() this.prplMessage.incoming,
+ get system() this.prplMessage.system,
+ get autoResponse() this.prplMessage.autoResponse,
+ get containsNick() this.prplMessage.containsNick,
+ get noLog() this.prplMessage.noLog,
+ get error() this.prplMessage.error,
+ get delayed() this.prplMessage.delayed,
+ get noFormat() this.prplMessage.noFormat,
+ get containsImages() this.prplMessage.containsImages,
+ get notification() this.prplMessage.notification,
+ get noLinkification() this.prplMessage.noLinkification,
+ getActions: function(aCount) this.prplMessage.getActions(aCount)
+};
+
function UIConversation(aPrplConversation)
{
this._prplConv = {};
this.id = ++gLastUIConvId;
this._observers = [];
this._messages = [];
this.changeTargetTo(aPrplConversation);
let iface = Ci["prplIConv" + (aPrplConversation.isChat ? "Chat" : "IM")];
@@ -261,16 +330,22 @@ UIConversation.prototype = {
},
observeConv: function(aTargetId, aSubject, aTopic, aData) {
if (aTargetId != this._currentTargetId &&
(aTopic == "new-text" ||
(aTopic == "update-typing" &&
this._prplConv[aTargetId].typingState == Ci.prplIConvIM.TYPING)))
this.target = this._prplConv[aTargetId];
+
+ if (aTopic == "new-text") {
+ aSubject = new imMessage(aSubject);
+ this.notifyObservers(aSubject, "received-message");
+ }
+
this.notifyObservers(aSubject, aTopic, aData);
if (aTopic == "new-text") {
Services.obs.notifyObservers(aSubject, aTopic, aData);
if (aSubject.incoming && !aSubject.system &&
(!this.isChat || aSubject.containsNick)) {
this.notifyObservers(aSubject, "new-directed-incoming-message", aData);
Services.obs.notifyObservers(aSubject, "new-directed-incoming-message", aData);
}
@@ -284,17 +359,42 @@ UIConversation.prototype = {
// prplIConversation
get isChat() this.target.isChat,
get account() this.target.account,
get name() this.target.name,
get normalizedName() this.target.normalizedName,
get title() this.target.title,
get startDate() this.target.startDate,
- sendMsg: function (aMsg) { this.target.sendMsg(aMsg); },
+ sendMsg: function(aMsg) {
+ // add-ons (eg. pastebin) have an opportunity to cancel the message at this
+ // point, or change the text content of the message.
+ // If an add-on wants to split a message, it should truncate the first
+ // message, and insert new messages using the conversation's sendMsg method.
+ let om = new OutgoingMessage(aMsg, this);
+ this.notifyObservers(om, "preparing-message", null);
+ if (om.cancelled)
+ return;
+
+ // prpls have an opportunity here to preprocess messages before they are
+ // sent, eg. split long messages. If a message is split here, the split
+ // will be visible in the UI.
+ // prpls can return null if they don't need to make any change
+ let messages = this.target.prepareForSending(om) || [om.message];
+
+ for (let msg of messages) {
+ // add-ons (eg. OTR) have an opportunity to tweak or cancel the message
+ // point.
+ let sm = new SendableMessage(msg, this.target);
+ this.notifyObservers(sm, "sending-message", null);
+ if (sm.cancelled)
+ continue;
+ sm.getSendableMessages().forEach((m) => this.target.sendMsg(m));
+ }
+ },
unInit: function() {
for each (let conv in this._prplConv)
gConversationsService.forgetConversation(conv);
if (this._observedContact) {
this._observedContact.removeObserver(this);
delete this._observedContact;
}
this._prplConv = {}; // Prevent .close from failing.
diff --git a/chat/components/src/logger.js b/chat/components/src/logger.js
--- a/chat/components/src/logger.js
+++ b/chat/components/src/logger.js
@@ -162,32 +162,32 @@ ConversationLog.prototype = {
logMessage: function cl_logMessage(aMessage) {
if (!this._log)
this._init();
if (this.format == "json") {
let msg = {
date: new Date(aMessage.time * 1000),
who: aMessage.who,
- text: aMessage.originalMessage,
+ text: aMessage.decodedMessage,
flags: ["outgoing", "incoming", "system", "autoResponse",
"containsNick", "error", "delayed",
"noFormat", "containsImages", "notification",
"noLinkification"].filter(function(f) aMessage[f])
};
let alias = aMessage.alias;
if (alias && alias != msg.who)
msg.alias = alias;
this._log.writeString(JSON.stringify(msg) + "\n");
return;
}
let date = new Date(aMessage.time * 1000);
let line = "(" + date.toLocaleTimeString() + ") ";
- let msg = this._serialize(aMessage.originalMessage);
+ let msg = this._serialize(aMessage.decodedMessage);
if (aMessage.system)
line += msg;
else {
let sender = aMessage.alias || aMessage.who;
if (aMessage.autoResponse)
line += sender + " <AUTO-REPLY>: " + msg;
else {
if (msg.startsWith("/me "))
@@ -317,17 +317,21 @@ function LogMessage(aData, aConversation
this._init(aData.who, aData.text);
this._conversation = aConversation;
this.time = Math.round(new Date(aData.date) / 1000);
if ("alias" in aData)
this._alias = aData.alias;
for each (let flag in aData.flags)
this[flag] = true;
}
-LogMessage.prototype = GenericMessagePrototype;
+LogMessage.prototype = {
+ __proto__: GenericMessagePrototype,
+ _interfaces: [Ci.imIMessage, Ci.prplIMessage],
+ get decodedMessage() this.originalMessage
+};
function LogConversation(aLineInputStreams)
{
// If aLineInputStreams isn't an Array, we'll assume that it's a lone
// InputStream, and wrap it in an Array.
if (!Array.isArray(aLineInputStreams))
aLineInputStreams = [aLineInputStreams];
diff --git a/chat/content/convbrowser.xml b/chat/content/convbrowser.xml
--- a/chat/content/convbrowser.xml
+++ b/chat/content/convbrowser.xml
@@ -464,17 +464,18 @@
let csFlags = cs.kStructPhrase;
// Automatically find and link freetext URLs
if (!aMsg.noLinkification)
csFlags |= cs.kURLs;
if (aFirstUnread)
this.setUnreadRuler();
- let msg = aMsg.originalMessage;
+ // Components.utils.reportError(aMsg.originalMessage)
+ let msg = aMsg.decodedMessage;
// The slash of a leading '/me' should not be used to
// format as italic, so we remove the '/me' text before
// scanning the HTML, and we add it back later.
let meRegExp = /^((<[^>]+>)*)\/me /;
let me = false;
if (meRegExp.test(msg)) {
me = true;
diff --git a/chat/modules/imThemes.jsm b/chat/modules/imThemes.jsm
--- a/chat/modules/imThemes.jsm
+++ b/chat/modules/imThemes.jsm
@@ -365,17 +365,17 @@ const statusMessageReplacements = {
else {
msgClass.push("message");
if (aMsg.incoming)
msgClass.push("incoming");
else if (aMsg.outgoing)
msgClass.push("outgoing");
- if (/^(<[^>]+>)*\/me /.test(aMsg.originalMessage))
+ if (/^(<[^>]+>)*\/me /.test(aMsg.decodedMessage))
msgClass.push("action");
if (aMsg.autoResponse)
msgClass.push("autoreply");
}
if (aMsg.containsNick)
msgClass.push("nick");
@@ -490,21 +490,21 @@ function getHTMLForMessage(aMsg, aTheme,
else {
html = aMsg.incoming ? "incoming" : "outgoing";
if (aIsNext)
html += "Next";
html += aIsContext ? "Context" : "Content";
html = aTheme.html[html];
replacements = messageReplacements;
let meRegExp = /^((<[^>]+>)*)\/me /;
- // We must test originalMessage here as aMsg.message loses its /me
+ // We must test decodedMessage here as aMsg.message loses its /me
// in the following, so if getHTMLForMessage is called a second time for
// the same aMsg (e.g. because it follows the unread ruler), the test
// would fail otherwise.
- if (meRegExp.test(aMsg.originalMessage)) {
+ if (meRegExp.test(aMsg.decodedMessage)) {
aMsg.message = aMsg.message.replace(meRegExp, "$1");
let actionMessageTemplate = "* %message% *";
if (hasMetadataKey(aTheme, "ActionMessageTemplate"))
actionMessageTemplate = getMetadata(aTheme, "ActionMessageTemplate");
html = html.replace(/%message%/g, actionMessageTemplate);
}
}
@@ -894,17 +894,17 @@ SelectedMessage.prototype = {
let html, replacements;
if (msg.system) {
replacements = statusReplacements;
html = getLocalizedPrefWithDefault("systemMessagesTemplate",
"%time% - %message%");
}
else {
replacements = messageReplacements;
- if (/^(<[^>]+>)*\/me /.test(msg.originalMessage)) {
+ if (/^(<[^>]+>)*\/me /.test(msg.decodedMessage)) {
html = getLocalizedPrefWithDefault("actionMessagesTemplate",
"%time% * %sender% %message%");
}
else {
html = getLocalizedPrefWithDefault("contentMessagesTemplate",
"%time% - %sender%: %message%");
}
}
diff --git a/chat/modules/jsProtoHelper.jsm b/chat/modules/jsProtoHelper.jsm
--- a/chat/modules/jsProtoHelper.jsm
+++ b/chat/modules/jsProtoHelper.jsm
@@ -424,17 +424,16 @@ const GenericMessagePrototype = {
}
};
function Message(aWho, aMessage, aObject) {
this._init(aWho, aMessage, aObject);
}
Message.prototype = GenericMessagePrototype;
-
const GenericConversationPrototype = {
__proto__: ClassInfo("prplIConversation", "generic conversation object"),
get wrappedJSObject() this,
get DEBUG() this._account.DEBUG,
get LOG() this._account.LOG,
get WARN() this._account.WARN,
get ERROR() this._account.ERROR,
@@ -467,17 +466,18 @@ const GenericConversationPrototype = {
try {
observer.observe(aSubject, aTopic, aData);
} catch(e) {
this.ERROR(e);
}
}
},
- sendMsg: function (aMsg) {
+ prepareForSending: function(aOm, aCount) null,
+ sendMsg: function(aMsg) {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
sendTyping: function(aString) Ci.prplIConversation.NO_TYPING_LIMIT,
close: function() {
Services.obs.notifyObservers(this, "closing-conversation", null);
Services.conversations.removeConversation(this);
},
diff --git a/chat/protocols/irc/irc.js b/chat/protocols/irc/irc.js
--- a/chat/protocols/irc/irc.js
+++ b/chat/protocols/irc/irc.js
@@ -129,19 +129,19 @@ const 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);
},
- sendMsg: function(aMessage) {
+ prepareForSending: function(aOm, aCount) {
// Split the message by line breaks and send each one individually.
- let messages = aMessage.split(/[\r\n]+/);
+ let messages = aOm.message.split(/[\r\n]+/);
let maxLength = this.getMaxMessageLength();
// Attempt to smartly split a string into multiple lines (based on the
// maximum number of characters the message can contain).
for (let i = 0; i < messages.length; ++i) {
let message = messages[i];
let length = this._account.countBytes(message);
@@ -154,34 +154,37 @@ const GenericIRCConversation = {
// Remove the current message and insert the two new ones. If no space was
// found, cut the first message to the maximum length and start the second
// message one character after that. If a space was found, exclude it.
messages.splice(i, 1, message.substr(0, index == -1 ? maxLength : index),
message.substr((index + 1) || maxLength));
}
- // Send each message and display it in the conversation.
- for (let message of messages) {
- if (!message.length)
- return;
+ if (aCount)
+ aCount.value = messages.length;
- if (!this._account.sendMessage("PRIVMSG", [this.name, message])) {
- this.writeMessage(this._account._currentServerName,
- _("error.sendMessageFailed"),
- {error: true, system: true});
- break;
- }
+ return messages;
+ },
+ sendMsg: function(aMessage) {
+ if (!aMessage.length)
+ return;
- // Since the server doesn't send us a message back, just assume the
- // message was received and immediately show it.
- this.writeMessage(this._account._nickname, message, {outgoing: true});
+ if (!this._account.sendMessage("PRIVMSG", [this.name, aMessage])) {
+ this.writeMessage(this._account._currentServerName,
+ _("error.sendMessageFailed"),
+ {error: true, system: true});
+ return;
+ }
- this._pendingMessage = true;
- }
+ // Since the server doesn't send us a message back, just assume the
+ // message was received and immediately show it.
+ this.writeMessage(this._account._nickname, aMessage, {outgoing: true});
+
+ this._pendingMessage = true;
},
// IRC doesn't support typing notifications, but it does have a maximum
// message length.
sendTyping: function(aString) {
let longestLineLength =
Math.max.apply(null, aString.split("\n").map(this._account.countBytes,
this._account));
return this.getMaxMessageLength() - longestLineLength;
diff --git a/chat/protocols/xmpp/xmpp.jsm b/chat/protocols/xmpp/xmpp.jsm
--- a/chat/protocols/xmpp/xmpp.jsm
+++ b/chat/protocols/xmpp/xmpp.jsm
@@ -28,16 +28,21 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyServiceGetter(this, "imgTools",
"@mozilla.org/image/tools;1",
"imgITools");
XPCOMUtils.defineLazyGetter(this, "_", function()
l10nHelper("chrome://chat/locale/xmpp.properties")
);
+XPCOMUtils.defineLazyGetter(this, "TXTToHTML", function() {
+ let cs = Cc["@mozilla.org/txttohtmlconv;1"].getService(Ci.mozITXTToHTMLConv);
+ return function(aTXT) cs.scanTXT(aTXT, cs.kEntities);
+});
+
/* This is an ordered list, used to determine chat buddy flags:
* index < member -> noFlags
* index = member -> voiced
* moderator -> halfOp
* admin -> op
* owner -> founder
*/
const kRoles = ["outcast", "visitor", "participant", "member", "moderator",
@@ -233,32 +238,35 @@ const XMPPConversationPrototype = {
_targetResource: "",
get to() {
let to = this.buddy.userName;
if (this._targetResource)
to += "/" + this._targetResource;
return to;
},
+ prepareForSending: function(aOm, aCount) {
+ if (aCount)
+ aCount.value = 1;
+ return [TXTToHTML(om.message)];
+ },
+
/* Called when the user enters a chat message */
- sendMsg: function (aMsg) {
+ sendMsg: function(aMsg) {
this._cancelTypingTimer();
let cs = this.shouldSendTypingNotifications ? "active" : null;
let s = Stanza.message(this.to, aMsg, cs);
this._account.sendStanza(s);
let who;
if (this._account._connection)
who = this._account._connection._jid.jid;
if (!who)
who = this._account.name;
let alias = this.account.alias || this.account.statusInfo.displayName;
- let msg = Cc["@mozilla.org/txttohtmlconv;1"]
- .getService(Ci.mozITXTToHTMLConv)
- .scanTXT(aMsg, Ci.mozITXTToHTMLConv.kEntities);
- this.writeMessage(who, msg, {outgoing: true, _alias: alias});
+ this.writeMessage(who, aMsg, {outgoing: true, _alias: alias});
delete this._typingState;
},
/* 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 = {};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment