# HG changeset patch # User Florian Quèze # Date 1352134190 -3600 # Node ID 82fe1991bc36ba5be4a42bf6eb883e215e88df2a # Parent 4a2be997f5392078118603d9c8346cd01b089521 XMPP as a chrome social provider. diff --git a/browser/build.mk b/browser/build.mk --- a/browser/build.mk +++ b/browser/build.mk @@ -15,16 +15,18 @@ endif tier_app_dirs += $(MOZ_BRANDING_DIRECTORY) tier_app_dirs += services ifdef MOZ_WEBAPP_RUNTIME tier_app_dirs += webapprt endif +tier_app_dirs += chat + tier_app_dirs += browser # Never add other tier_app_dirs after browser. They won't get packaged # properly on mac. ################################################ # Parallel build on Windows with GNU make check default:: diff --git a/browser/components/Makefile.in b/browser/components/Makefile.in --- a/browser/components/Makefile.in +++ b/browser/components/Makefile.in @@ -23,16 +23,17 @@ EXTRA_PP_COMPONENTS = \ nsBrowserGlue.js \ $(NULL) EXTRA_JS_MODULES = distribution.js PARALLEL_DIRS = \ about \ certerror \ + chat \ dirprovider \ downloads \ feeds \ places \ preferences \ privatebrowsing \ search \ sessionstore \ diff --git a/browser/components/chat/Makefile.in b/browser/components/chat/Makefile.in new file mode 100644 --- /dev/null +++ b/browser/components/chat/Makefile.in @@ -0,0 +1,20 @@ +# 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/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +PREF_JS_EXPORTS = $(srcdir)/chat-browser-prefs.js + +EXTRA_PP_JS_MODULES = \ + modules/ibCore.jsm \ + modules/ibInterruptions.jsm \ + modules/imWindows.jsm \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/browser/components/chat/chat-browser-prefs.js b/browser/components/chat/chat-browser-prefs.js new file mode 100644 --- /dev/null +++ b/browser/components/chat/chat-browser-prefs.js @@ -0,0 +1,12 @@ +pref("messenger.options.messagesStyle.theme", "browser-messagestyles"); +pref("messenger.options.emoticonsTheme", "browser-emoticons"); +pref("messenger.conversations.textbox.autoResize", true); +pref("messenger.conversations.doubleClickToReply", true); +pref("purple.conversations.im.send_typing", true); +pref("messenger.options.getAttentionOnNewMessages", true); +pref("messenger.accounts.promptOnDelete", true); +pref("messenger.buddies.showOffline", false); +pref("purple.debug.loglevel", 3); +pref("chat.twitter.consumerKey", "7Gzq35FbZLYJQ9n3sJmJw"); +pref("chat.twitter.consumerSecret", "fz0dXr3yul1uncHNf0vITTiIOtSKAVCvopKDrqR1Mo"); +pref("conversation.zoomLevel", "1.0"); diff --git a/browser/components/chat/content/Makefile.in b/browser/components/chat/content/Makefile.in new file mode 100644 --- /dev/null +++ b/browser/components/chat/content/Makefile.in @@ -0,0 +1,34 @@ +# 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/. + +DEPTH = ../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +ifdef WINCE +DEFINES += -DWINCE +endif + +include $(topsrcdir)/config/rules.mk + +# defines used for preference window files +DEFINES += \ + -DMOZ_APP_NAME=$(MOZ_APP_NAME) \ + -DMOZ_MACBUNDLE_NAME=$(MOZ_MACBUNDLE_NAME) \ + $(NULL) + +# ifneq (,$(filter windows gtk2 mac cocoa, $(MOZ_WIDGET_TOOLKIT))) +# DEFINES += -DHAVE_SHELL_SERVICE=1 +# endif + +ifdef MOZ_UPDATER +DEFINES += -DMOZ_UPDATER=1 +endif + +abs_app_license_path = $(shell cd $(srcdir)/../app && pwd)/app-license.html +CHROME_DEPS += $(abs_app_license_path) +DEFINES += -DAPP_LICENSE_BLOCK=$(abs_app_license_path) diff --git a/browser/components/chat/content/account.js b/browser/components/chat/content/account.js new file mode 100644 --- /dev/null +++ b/browser/components/chat/content/account.js @@ -0,0 +1,295 @@ +/* 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/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +Cu.import("resource://gre/modules/Services.jsm"); + +const autoJoinPref = "autoJoin"; + +var account = { + onload: function account_onload() { + this.account = window.arguments[0]; + this.proto = this.account.protocol; + document.getElementById("accountName").value = this.account.name; + document.getElementById("protocolName").value = this.proto.name || this.proto.id; + document.getElementById("protocolIcon").src = + this.proto.iconBaseURI + "icon48.png"; + + let passwordBox = document.getElementById("passwordBox"); + if (this.proto.noPassword) + passwordBox.hidden = true; + else { + try { + // Will throw if we don't have a protocol plugin for the account. + document.getElementById("password").value = this.account.password; + } catch (e) { + passwordBox.hidden = true; + } + } + + document.getElementById("alias").value = this.account.alias; + + let protoId = this.proto.id; + if (protoId == "prpl-irc" || protoId == "prpl-jabber" || + protoId == "prpl-gtalk") { + document.getElementById("optionalSeparator").hidden = false; + document.getElementById("autojoinBox").hidden = false; + var branch = Services.prefs.getBranch("messenger.account." + + this.account.id + "."); + if (branch.prefHasUserValue(autoJoinPref)) { + document.getElementById("autojoin").value = + branch.getCharPref(autoJoinPref); + } + } + +/* FIXME + document.getElementById("newMailNotification").hidden = + !this.proto.newMailNotification; +*/ + + this.prefs = Services.prefs.getBranch("messenger.account." + + this.account.id + ".options."); + this.populateProtoSpecificBox(); + + let proxyVisible = this.proto.usePurpleProxy; + if (proxyVisible) { + this.proxy = this.account.proxyInfo; + this.displayProxyDescription(); + } + document.getElementById("proxyBox").hidden = !proxyVisible; + document.getElementById("proxySeparator").hidden = !proxyVisible; + + Services.obs.addObserver(this, "prpl-quit", false); + window.addEventListener("unload", this.unload); + }, + unload: function account_unload() { + Services.obs.removeObserver(account, "prpl-quit"); + }, + observe: function account_observe(aObject, aTopic, aData) { + if (aTopic == "prpl-quit") { + // libpurple is being uninitialized. Close this dialog. + window.close(); + } + }, + + displayProxyDescription: function account_displayProxyDescription() { + var type = this.proxy.type; + var bundle = document.getElementById("proxiesBundle"); + var proxy; + var result; + if (type == Ci.purpleIProxyInfo.useGlobal) { + proxy = Cc["@instantbird.org/libpurple/core;1"] + .getService(Ci.purpleICoreService).globalProxy; + type = proxy.type; + } + else + proxy = this.proxy; + + if (type == Ci.purpleIProxyInfo.noProxy) + result = bundle.getString("proxies.directConnection"); + + if (type == Ci.purpleIProxyInfo.useEnvVar) + result = bundle.getString("proxies.useEnvironment"); + + if (!result) { + // At this point, we should have either a socks or http proxy + if (type == Ci.purpleIProxyInfo.httpProxy) + result = bundle.getString("proxies.http"); + else if (type == Ci.purpleIProxyInfo.socks4Proxy) + result = bundle.getString("proxies.socks4"); + else if (type == Ci.purpleIProxyInfo.socks5Proxy) + result = bundle.getString("proxies.socks5"); + else + throw "Unknown proxy type"; + + if (result) + result += " "; + + if (proxy.username) + result += proxy.username + "@"; + + result += proxy.host + ":" + proxy.port; + } + + document.getElementById("proxyDescription").textContent = result; + }, + + createTextbox: function account_createTextbox(aType, aValue, aLabel, aName) { + let row = document.createElement("row"); + row.setAttribute("align", "center"); + + var label = document.createElement("label"); + label.textContent = aLabel; + label.setAttribute("control", aName); + row.appendChild(label); + + var textbox = document.createElement("textbox"); + if (aType) + textbox.setAttribute("type", aType); + textbox.setAttribute("value", aValue); + textbox.setAttribute("id", aName); + + row.appendChild(textbox); + return row; + }, + + createMenulist: function account_createMenulist(aList, aLabel, aName) { + let vbox = document.createElement("vbox"); + vbox.setAttribute("flex", "1"); + + var label = document.createElement("label"); + label.setAttribute("value", aLabel); + label.setAttribute("control", aName); + vbox.appendChild(label); + + aList.QueryInterface(Ci.nsISimpleEnumerator); + var menulist = document.createElement("menulist"); + menulist.setAttribute("id", aName); + var popup = menulist.appendChild(document.createElement("menupopup")); + while (aList.hasMoreElements()) { + let elt = aList.getNext(); + let item = document.createElement("menuitem"); + item.setAttribute("label", elt.name); + item.setAttribute("value", elt.value); + popup.appendChild(item); + } + vbox.appendChild(menulist); + return vbox; + }, + + getBool: function account_getBool(aOpt) { + if (this.prefs.prefHasUserValue(aOpt.name)) + return this.prefs.getBoolPref(aOpt.name); + + return aOpt.getBool(); + }, + + getInt: function account_getInt(aOpt) { + if (this.prefs.prefHasUserValue(aOpt.name)) + return this.prefs.getIntPref(aOpt.name); + + return aOpt.getInt(); + }, + + getString: function account_getString(aOpt) { + if (this.prefs.prefHasUserValue(aOpt.name)) + return this.prefs.getComplexValue(aOpt.name, Ci.nsISupportsString).data; + + return aOpt.getString(); + }, + + getListValue: function account_getListValue(aOpt) { + if (this.prefs.prefHasUserValue(aOpt.name)) + return this.prefs.getCharPref(aOpt.name); + + return aOpt.getListDefault(); + }, + + populateProtoSpecificBox: function account_populate() { + let rows = document.getElementById("protoSpecific"); + var id = this.proto.id; + for (let opt in this.getProtoOptions()) { + var text = opt.label; + var name = id + "-" + opt.name; + switch (opt.type) { + case opt.typeBool: + var chk = document.createElement("checkbox"); + if (this.getBool(opt)) + chk.setAttribute("checked", "true"); + chk.setAttribute("label", text); + chk.setAttribute("id", name); + rows.appendChild(chk); + break; + case opt.typeInt: + rows.appendChild(this.createTextbox("number", this.getInt(opt), + text, name)); + break; + case opt.typeString: + rows.appendChild(this.createTextbox(null, this.getString(opt), + text, name)); + break; + case opt.typeList: + rows.appendChild(this.createMenulist(opt.getList(), text, name)); + document.getElementById(name).value = this.getListValue(opt); + break; + default: + throw "unknown preference type " + opt.type; + } + } + if (!rows.firstChild) + document.getElementById("advancedTab").hidden = true; + }, + + getValue: function account_getValue(aId) { + var elt = document.getElementById(aId); + if ("checked" in elt) + return elt.checked; + return elt.value; + }, + + save: function account_save() { + if (!this.proto.noPassword && + !document.getElementById("passwordBox").hidden) { + var password = this.getValue("password"); + if (password != this.account.password) + this.account.password = password; + } + + var alias = this.getValue("alias"); + if (alias != this.account.alias) + this.account.alias = alias; + + let protoId = this.proto.id; + if (protoId == "prpl-irc" || protoId == "prpl-jabber" || + protoId == "prpl-gtalk") { + var branch = Services.prefs.getBranch("messenger.account." + + this.account.id + "."); + var autojoin = this.getValue("autojoin"); + if (autojoin || branch.prefHasUserValue(autoJoinPref)) + branch.setCharPref(autoJoinPref, autojoin); + } + + if (this.proto.usePurpleProxy && + this.account.proxyInfo.key != this.proxy.key) + this.account.proxyInfo = this.proxy; + + for (let opt in this.getProtoOptions()) { + var name = this.proto.id + "-" + opt.name; + var val = this.getValue(name); + switch (opt.type) { + case opt.typeBool: + if (val != this.getBool(opt)) + this.account.setBool(opt.name, val); + break; + case opt.typeInt: + if (val != this.getInt(opt)) + this.account.setInt(opt.name, val); + break; + case opt.typeString: + if (val != this.getString(opt)) + this.account.setString(opt.name, val); + break; + case opt.typeList: + if (val != this.getListValue(opt)) + this.account.setString(opt.name, val); + break; + default: + throw "unknown preference type " + opt.type; + } + } + }, + + getProtoOptions: function account_getProtoOptions() { + let options = this.proto.getOptions(); + while (options.hasMoreElements()) + yield options.getNext(); + }, + + openProxySettings: function account_openProxySettings() { + window.openDialog("chrome://browser/content/chat/proxies.xul", "", + "chrome,modal,titlebar,centerscreen", + this); + this.displayProxyDescription(); + } +}; diff --git a/browser/components/chat/content/account.xml b/browser/components/chat/content/account.xml new file mode 100644 --- /dev/null +++ b/browser/components/chat/content/account.xml @@ -0,0 +1,334 @@ + + + + + + %accountsDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0) { + let [val1, unit1, val2, unit2] = DownloadUtils.convertTimeUnits(date); + if (!val2) + reconnect = bundle.getFormattedString("account.reconnectInSingle", + [val1, unit1]) + else + reconnect = bundle.getFormattedString("account.reconnectInDouble", + [val1, unit1, val2, unit2]) + } + document.getAnonymousElementByAttribute(this, "anonid", "reconnect") + .textContent = reconnect; + return reconnect; + }).bind(this); + if (updateReconnect() && !this.reconnectUpdateInterval) { + this.setAttribute("reconnectPending", "true"); + this.reconnectUpdateInterval = setInterval(updateReconnect, 1000); + gAccountManager.disableCommandItems(); + } + ]]> + + + + + + 0) { + let [val1, unit1, val2, unit2] = DownloadUtils.convertTimeUnits(date); + if (!val2) + value = bundle.getFormattedString("account.connectedForSingle", + [val1, unit1]) + else + value = bundle.getFormattedString("account.connectedForDouble", + [val1, unit1, val2, unit2]) + } + else + value = bundle.getString("account.connectedForSeconds"); + this.connectedLabel.value = value; + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/chat/content/account.xul b/browser/components/chat/content/account.xul new file mode 100644 --- /dev/null +++ b/browser/components/chat/content/account.xul @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + +#endif diff --git a/browser/components/chat/content/instantbird.css b/browser/components/chat/content/instantbird.css new file mode 100644 --- /dev/null +++ b/browser/components/chat/content/instantbird.css @@ -0,0 +1,55 @@ +/* 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/. */ + +conversation { + -moz-binding: url("chrome://browser/content/chat/conversation.xml#conversation"); +} + +.conv-chat:not([chat]) { + display: none; +} + +.statusMessage[editable], +.statusMessageWithDash[editable] { + cursor: text !important; +} + +.statusMessage[editing] { + -moz-appearance: textfield; + -moz-binding: url('chrome://global/content/bindings/textbox.xml#textbox'); +} + +/* +@media all and (min-height: 251px) { + .conv-top-info { + -moz-binding: url("chrome://browser/content/chat/conversation.xml#conv-info-large") !important; + } +} + +@media all and (max-height: 250px) { +*/ + .conv-top-info { + -moz-binding: url("chrome://browser/content/chat/conversation.xml#conv-info-small") !important; + } + + .statusMessage:not([editing]), + .statusMessageWithDash[editing] { + display: none; + } +/*} + +@media all and (max-height: 200px) { + .conv-top-info { + display: none; + } +} +*/ + +.alltargets-item { + -moz-binding: url("chrome://browser/content/chat/conversation.xml#menuitem-target"); +} + +browser[type="content-conversation"] { + -moz-binding: url("chrome://chat/content/convbrowser.xml#browser"); +} diff --git a/browser/components/chat/content/instantbird.js b/browser/components/chat/content/instantbird.js new file mode 100644 --- /dev/null +++ b/browser/components/chat/content/instantbird.js @@ -0,0 +1,181 @@ +/* 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/. */ + + +var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab"; + +var convWindow = { + load: function mo_load() { + Components.utils.import("resource:///modules/imWindows.jsm"); + Conversations.registerWindow(window); + + if ("arguments" in window) { + while (window.arguments[0] instanceof XULElement) { + // swap the given tab with the default dummy conversation tab + // and then close the original tab in the other window. + let tab = window.arguments.shift(); + document.getElementById("conversations").importConversation(tab); + } + } + + window.addEventListener("unload", convWindow.unload); + window.addEventListener("resize", convWindow.onresize); + window.addEventListener("activate", convWindow.onactivate, true); + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow) + .XULBrowserWindow = window.XULBrowserWindow; + }, + unload: function mo_unload() { + Conversations.unregisterWindow(window); + }, + onactivate: function mo_onactivate(aEvent) { + Conversations.onWindowFocus(window); + setTimeout(function () { + // setting the focus to the textbox just after the window is + // activated puts the textbox in an unconsistant state, some + // special characters like ^ don't work, so delay the focus + // operation... + getBrowser().selectedConversation.focus(); + }, 0); + }, + onresize: function mo_onresize(aEvent) { + if (aEvent.originalTarget != window) + return; + + // Resize each textbox (if the splitter has not been used). + let convs = getBrowser().conversations; + for each (let conv in convs) + conv.onConvResize(aEvent); + } +}; + +function getConvWindowURL() "chrome://browser/content/chat/instantbird.xul" + +function getBrowser() +{ + return document.getElementById("conversations"); +} + +// Inspired from the same function in mozilla/browser/base/content/browser.js +function FillInHTMLTooltip(tipElement) +{ + if (tipElement.namespaceURI == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul") + return false; + + var defView = tipElement.ownerDocument && tipElement.ownerDocument.defaultView; + // XXX Work around bug 350679: + // "Tooltips can be fired in documents with no view". + if (!defView) + return false; + + while (tipElement) { + if (tipElement.nodeType == Node.ELEMENT_NODE) { + let titleText = tipElement.getAttribute("title"); + if (titleText && /\S/.test(titleText)) { + let direction = defView.getComputedStyle(tipElement, "") + .getPropertyValue("direction"); + let tipNode = document.getElementById("aHTMLTooltip"); + tipNode.style.direction = direction; + // Per HTML 4.01 6.2 (CDATA section), literal CRs and tabs should be + // replaced with spaces, and LFs should be removed entirely. + // XXX Bug 322270: We don't preserve the result of entities like , + // which should result in a line break in the tooltip, because we can't + // distinguish that from a literal character in the source by this point. + titleText = titleText.replace(/[\r\t]/g, ' ').replace(/\n/g, ''); + tipNode.setAttribute("label", titleText); + return true; + } + // When hovering a link, displaying a tooltip for a parent + // element is confusing, so we prevent it here. + if (tipElement.hasAttribute("href")) + return false; + } + tipElement = tipElement.parentNode; + } + + return false; +} + +// Copied from mozilla/browser/base/content/browser.js (and simplified) +var XULBrowserWindow = { + // Stored Status + status: "", + defaultStatus: "", + jsStatus: "", + jsDefaultStatus: "", + overLink: "", + statusText: "", + + QueryInterface: function (aIID) { + if (aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsIXULBrowserWindow) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + }, + + get statusTextField () { + delete this.statusTextField; + return this.statusTextField = document.getElementById("statusbar-display"); + }, + + setStatus: function (status) { + this.status = status; + this.updateStatusField(); + }, + + setJSStatus: function (status) { + this.jsStatus = status; + this.updateStatusField(); + }, + + setJSDefaultStatus: function (status) { + this.jsDefaultStatus = status; + this.updateStatusField(); + }, + + setDefaultStatus: function (status) { + this.defaultStatus = status; + this.updateStatusField(); + }, + + setOverLink: function (link, b) { + // Encode bidirectional formatting characters. + // (RFC 3987 sections 3.2 and 4.1 paragraph 6) + this.overLink = link.replace(/[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g, + encodeURIComponent); + this.updateStatusField(); + }, + + // Called before links are navigated to, allows us to retarget them if needed. + onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) { + return originalTarget; + }, + + setStatusEnd: function (aStatusEndText, aError) { + let field = document.getElementById("statusbar-display-end"); + field.label = aStatusEndText; + field.hidden = !aStatusEndText; + if (aError) + field.setAttribute("error", "true"); + else + field.removeAttribute("error"); + }, + + updateStatusField: function () { + var text = this.overLink || this.status || this.jsStatus || this.jsDefaultStatus || this.defaultStatus; + + // check the current value so we don't trigger an attribute change + // and cause needless (slow!) UI updates + if (this.statusText != text) { + this.statusTextField.label = text; + this.statusText = text; + } + } +} + +this.addEventListener("load", convWindow.load); diff --git a/browser/components/chat/content/instantbird.xul b/browser/components/chat/content/instantbird.xul new file mode 100644 --- /dev/null +++ b/browser/components/chat/content/instantbird.xul @@ -0,0 +1,137 @@ + + + + + + + + + + + + %instantbirdDTD; + + %brandDTD; + + %textcontextDTD; +]> + + + diff --git a/browser/components/chat/themes/messages/Incoming/Content.html b/browser/components/chat/themes/messages/Incoming/Content.html new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/messages/Incoming/Content.html @@ -0,0 +1,4 @@ +
+

%time% %sender% %message%

+
+
diff --git a/browser/components/chat/themes/messages/Incoming/Context.html b/browser/components/chat/themes/messages/Incoming/Context.html new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/messages/Incoming/Context.html @@ -0,0 +1,4 @@ +
+

%time% %sender% %message%

+
+
diff --git a/browser/components/chat/themes/messages/Incoming/NextContent.html b/browser/components/chat/themes/messages/Incoming/NextContent.html new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/messages/Incoming/NextContent.html @@ -0,0 +1,3 @@ +
+

%time% %message%

+
diff --git a/browser/components/chat/themes/messages/Info.plist b/browser/components/chat/themes/messages/Info.plist new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/messages/Info.plist @@ -0,0 +1,41 @@ + + + + + ActionMessageTemplate + %sender% %message% + + CFBundleDevelopmentRegion + English + + CFBundleGetInfoString + Instantbird PaperSheets Message Style + + CFBundleIdentifier + org.instantbird.papersheets.message.style + + CFBundleInfoDictionaryVersion + 1.0 + + CFBundleName + PaperSheets + + CFBundlePackageType + AdIM + + DefaultBackgroundColor + FFFFFF + + DefaultVariant + default + + DisableCustomBackground + + + MessageViewVersion + 4 + + ShowsUserIcons + + + diff --git a/browser/components/chat/themes/messages/NextStatus.html b/browser/components/chat/themes/messages/NextStatus.html new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/messages/NextStatus.html @@ -0,0 +1,2 @@ +

%time% %message%

+
diff --git a/browser/components/chat/themes/messages/Status.html b/browser/components/chat/themes/messages/Status.html new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/messages/Status.html @@ -0,0 +1,4 @@ +
+

%time% %message%

+
+
diff --git a/browser/components/chat/themes/messages/Variants/White.css b/browser/components/chat/themes/messages/Variants/White.css new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/messages/Variants/White.css @@ -0,0 +1,22 @@ +/* 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/. */ + +div.outgoing { + background: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.1) 15px, rgba(255, 255, 255, 1) 15px, rgba(255, 255, 255, 1)) !important; +} + +div.incoming { + background: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.1) 15px, rgba(255, 255, 255, 1) 15px, rgba(255, 255, 255, 1)) !important; +} + + + +/* used by javascript */ +.outgoing-color { + background-color: rgb(255, 255, 255); +} + +.incoming-color { + background-color: rgb(255, 255, 255); +} diff --git a/browser/components/chat/themes/messages/main.css b/browser/components/chat/themes/messages/main.css new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/messages/main.css @@ -0,0 +1,192 @@ +/* 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/. */ + +body { + margin: 0; + padding: 0; +} + +p { + font-family: sans-serif; + margin: 0; + padding: 0; +} + +div.messages-group { + margin: -15px 0 0 0; + padding: 18px 5px 20px 5px; +} + +div.outgoing { + background: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.1) 15px, rgba(245, 245, 255, 1) 15px, rgba(245, 245, 255, 1)); +} + +div.incoming { + background: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.1) 15px, rgba(255, 245, 245, 1) 15px, rgba(255, 245, 245, 1)); +} + +div.event { + background: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.1) 15px, rgba(255, 255, 240, 1) 15px, rgba(255, 255, 240, 1)); +} + +div.context+div.event { + background: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.05) 15px, rgba(255, 255, 240, 1) 15px, rgba(255, 255, 240, 1)); +} + +div.context:not(:hover)>p { + opacity: 0.55; +} + +div.messages-group:last-child { + padding-bottom: 10px; +} + +div.messages-group>hr { + margin: 3px 50px 0px 20px; + background-color: rgba(0, 0, 0, 0.05); + height: 1px; + border: 0; +} + +span.message-style { + margin: 2px 50px 0px 20px; + display: block; + float: none; +} + +span.date { + color: rgba(0, 0, 0, 0.4); + font-size: smaller; + text-align: right; + float: right; + display: block; +} + +span.date-next { + opacity: 0.4; + margin-top: -6px; + -moz-transition-property: opacity; + -moz-transition-duration: 0.3s; +} + +p:hover > span.date-next { + opacity: 1; +} + +span.pseudo { + font-weight: bold; + float: none; + display: block; +} + +p.outgoing>span.pseudo { + color: rgb(80,80,200); +} + +p.incoming>span.pseudo { + color: rgb(200,80,80); +} + +p.nick>span.message-style { + font-weight: bold; +} + +p.action>span.message-style { + font-style: italic; +} + +p.action>span.message-style:before { + content: "*** "; +} + +p.event { + margin-left: 0px; + min-height: 16px; + background: url('Bitmaps/information.png') no-repeat top left; +} + +p.event>span.message-style { + color: rgba(0, 0, 0, 0.4); +} + +#Chat { + white-space: normal; +} + +p *:-moz-any-link img { + margin-bottom: 1px; + border-bottom: solid 1px; +} + + + +/* used by javascript */ +.outgoing-color { + background-color: rgb(245, 245, 255); +} + +.incoming-color { + background-color: rgb(255, 245, 245); +} + +.event-color { + background-color: rgb(255, 255, 240); +} + +.button { + margin-top: -2px; + margin-left: -4px; + height: 9px; + width: 9px; + cursor: pointer; +} + +.hide +{ + background: url('Bitmaps/plus.png') no-repeat left top; +} + +.show +{ + background: url('Bitmaps/minus.png') no-repeat left top; +} + +.first-event:after { + content: "[...]"; + margin-left: 1em; + color: #5a7ac6; + font-size: smaller; +} + +/* Adapt styles to narrow windows */ +@media all and (max-width: 400px) { + div.messages-group > hr { + margin-right: 0; + } + + span.message-style { + margin-right: 0; + } + + span.date-next { + display: none; + } +} + +@media all and (max-width: 200px) { + span.date { + display: none; + } +} + +/* Adapt styles when the window is very low */ +@media all and (max-height: 200px) { + div.messages-group { + padding-bottom: 8px; + } + + div.messages-group:last-child { + padding-bottom: 8px; + } +} diff --git a/browser/components/chat/themes/newMessage.png b/browser/components/chat/themes/newMessage.png new file mode 100644 index 0000000000000000000000000000000000000000..1343d47544d4ff9747ee9c29e860ddb76a07cafe GIT binary patch literal 1944 zc$@*42WR++P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXe< z4~qf1p_f4&nRha zgrd+^Ap--ZBLyrt4#&8Uk!sJ(Z|*+k-rwhZf8YE29gjCINRrG zoWc3FFj3YQ4M2$sgozB!d8=*P+L%dW#l;Kt&~@!^k@m$(BDAbLInUF8df z(y(pY5`F8|hcp0d7^C_yQTk8-6ae4~f4X4K40BZ8NL`9}Z<2W>({X5DCJt5q3NxmU zz5xQ#0RS{LUB~g`=TKXF7FV0v;PJXB7Bx?wjKqfI2_;MvB1Vs_+EUIEcPA}@B#(xY zrNyXv@erg>nytrprHk*u(f!Bp%;r}S>x`qZp$Tch0J27O;< zN=9g85a~fbe1;CMp&`lZ22t0;s;f_SMx*A=y8Z)ZuvQ!<3Npr4>n?3Wa%#Z4@>^xb z{E`B2DIi50ZLMvHbacS)R^isw-h_$k?AZov+45#Y2rIo#%Fa8T1q{Ibfn?v)1WftL zl9}%8kwK;@0D}V}0%LT`>+apZG#eY64_cP}5`d0`hzz6%-R{3IQMSvaRmEcFY(-Tt zDlZ$BupmqmX555j3YeyVhzkf|!M2^-c6-iQoG}*ToH~gp${0n6C?bS?i3TcHm}q@) zM8@-0+|=${vIv`ZKLZB4+aujRvAaxbnbu)=A4npMw7S3BeM-TxapJ@W*uMQOEc-?=7A%^4>l0Cb04XJWhKj<%iFo40 z%}B{e!|M5K@R#5HxhrvZLRftu{bAtPsHyoQ_Uw5F+t&x?A-D)+FM$&bj94h-S)zm__X;tcI-HemevT?ZY;&5 zal?@oFm4`31DC+*XEqzT*~74W&6nX1xKa82AEUkHQ>ysleM!+`)aU1LZvRu4>8W-`i_&4$;j6u5BV zKX`moDaMWc60$Or!Pw2c+cYgG3Xcr|z^C3=Uo*k^0x3QX^NR9d=!E^1?;hv zVr*7Ag2|rSJ~$GILs3{{Kmf*=;SYEf-0)8Kz?i+x_v#!)`C0Hk^&oUjMQ$h!hD#lA zXB{0eFvi*k1mFty`uy&GiTlHtfGyzG6-><=4#o(au|cO`BoYG=T_02dugBfj^KVk2 zJG$qIs_-G7h9i+U0JIJWKn%veFJ4C7duO0244ML{oS>-$jY((A?Y};`GxG5Xw%vKa`8-^ylUYFzZxsjCQVM$4b z76^D$pU(xK&kdi?4Zq)whK6e}P5T;shSO8eh$aAD0Db_zp?05`?D)$3xMr9y?CcwB0&az2ztWpwzF#vZ z1HjdvCK8E+to>xo&D#86?!%h!c>j^_&F$^&UaeNcXf(oVwZd#RL#x$7sZ>HN7Vl}# z1z)d1p@7flLnsu2-|vUVSS+6KL?V&jJd^|9-*&r= z(P)HzzYm%>o6W!S5C^Xf%5y%SV=|dwFc_fI>74LnGMP_2)CXQy6l=9wVX;_XI-O!T z9HQIpay-O#%tJla>2!Skgff|IPd@AQ8q4Jpv)Swu57mTtN~IFVL-nSf^Li#NXfzt^ zb~|h~8+?3xa6IGj_=JaIkk*cMOq!$^Nkl(}(4-zJgMCO#2~$X| zhoY1f)JiS%p%1hk`XEdbHCIL3U3J%8@7%qe+b)qsR5WmAhBO)%#>b75CxeCBnznQN(sl%OwwAd(Uw@@ZdkSV%srM;Pg zX_csSNrW`okM?HNZ#&QONah3q6cw^Mdjnr_YBz4j?TU_sI+`xq2{KK=w(pr?8NjP{ z-d}3O-s-399nq&fU0?!D^D1(A#*t&bH=tcjJ_>R<-GPdcD$z86s#+XATFLIx3zIBS z{-KS`&v=HH==KNZBQS!3s0^8V0C1OO9YbUN?A6putiYXN&Ocy6$3@<}agQb$zSIeB zr^g>471XDoh05;)W~R{DG}*eTknNxg;v7I|7y=qk<4L?OED@Gb^mI$+kDp*vd{L1| zT^8dN1poQy>ae^>y@DjuFzj;xg=x^_?1V{B!SVVHsvf*VU3D89QhW!Jd3bsvveqoe zLD31-wFYPn%^3c<1V5TWJZFkLCBuxz=}#8mt^+dJ__rEqd<9mM6+ACs>I(n>002ov JPDHLkV1lx*M!En1 diff --git a/browser/components/chat/themes/richlistbox.css b/browser/components/chat/themes/richlistbox.css new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/richlistbox.css @@ -0,0 +1,50 @@ +/* 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/. */ + +/* Mostly copied from the download manager */ + +/* List Items */ +richlistitem { + padding-top: 6px; + padding-bottom: 6px; + -moz-padding-start: 4px; + -moz-padding-end: 4px; + min-height: 25px; +%ifndef XP_MACOSX +%ifdef XP_WIN + border-bottom: 1px solid ThreeDLightShadow; +%else + border-bottom: 1px dotted #C0C0C0; +%endif +%endif +} + +%ifdef XP_MACOSX +richlistitem:not([selected="true"]):nth-child(odd) { + background-color: -moz-oddtreerow; +} +%endif + +%ifdef XP_WIN +richlistitem[selected="true"] { + background-image: url(chrome://mozapps/skin/extensions/itemEnabledFader.png); +} + +richlistitem[selected="true"]:not(:-moz-window-inactive) { + background-color: Highlight; + color: HighlightText; +} +%else +%ifdef XP_MACOSX +richlistitem[selected="true"]:not(:-moz-window-inactive) { + background-color: Highlight; + color: HighlightText; +} +%else +richlistitem[selected="true"] { + background-color: -moz-Dialog; + color: -moz-DialogText; +} +%endif +%endif diff --git a/browser/components/chat/themes/smileys/angry.png b/browser/components/chat/themes/smileys/angry.png new file mode 100644 index 0000000000000000000000000000000000000000..14ea2967f031b5871c516fbb9491ab2ba247f786 GIT binary patch literal 678 zc$@*I0$KfuP)!O z^m&!f-$zI$?-P&yhQ=pYD}kZkMY55!EKiD5QzG3>2?PQX2n0m-7e)T0MB0ld&NfBX zQzC^)k!&Pw1!5@0ncz|YF*^`KbC}_@=b4>(0T|HvXa;30G5hivSe($DH^rI*AY=zh z7@Zh)1?@c1;5VR6^VcqWTRMY2o#>?shznW`b4jNIP_hFBTKbXo{brq|Cts+n)bU&% zV&`WAe|VVj$2ZWN1AT`&K*3DgrRKOA0-J@Ew`&Xs&*LBSV6|E4b$e8ncc`!Ja_vqK z4g}WCv=2-22H$9M-|eNd_ZRo5Ixb1SD9tS$hid>$uR*<>AyK?{SduBW%T=vW!|ioJ z7n28l_Q36RnZliN6_`4`+fJ3_g4cWw&0&Vq)?v2>#%bL?i8K7Fe`OsCb246##{d8T M07*qoM6N<$f^)Yeng9R* diff --git a/browser/components/chat/themes/smileys/confused.png b/browser/components/chat/themes/smileys/confused.png new file mode 100644 index 0000000000000000000000000000000000000000..46cb432cd74541585dcb6a7a6158102139f109ef GIT binary patch literal 691 zc$@*V0!;mhP)+6P#W2hKhWa*H6+8Xd$K zXciW95*^lG5_B9p5yDQSq_j|m2c zdhYwVp6gbG5bVQbun&j;As_%O07)QDRC~3Ts_cl%&(HdXUl+zp4fFZ3$ws)hjd1TV zre~IzPGoS18iy>#`#V=$*Bm!K<^GE&gI*Lq&u_MP+Rq)w(PEWX3t&rDxm0H{^^>mF z%~b?vh-xdQ?FnM|{GP6G2M(tTELJeHr>rE z1?U7jXP}+|r48--Wl-z{+rL6_7Je*%Ug@_05stXQq(Jjk25`b8F1GO%v0QnG)ifLX-}L!b%A2 z1H_AE^n4Y3XeT zo|}jNb2yyy`wO{TjvY1XItd&H8i5+%JKzPz=u7WD zrnRMl4dZ0<^mnQ(7*7g%a+;7P2GN%vR>?I!njK#H_QQBvf{A~ z9`;Re^+>_8N+HzK;R^{s<+E#h2P{?A7D|di0SI#T%hw1Eb;WXn9FPT8BZPy?<7aRB z&k2X?ik{BLOu9B9X`|l}h1oI4~FtB$LU!AB{$7s@4ptn(Opsu^AI?w;KRa z6cI&{g@pwqDT&kRq^hba?~9^HJRYaHPG6?za+}S8fFD5+$Y!&7B@zkBN(=D&{X6_j zCPOB(L6_TXR`&H&+)~&}wEcshP$(2glEm!nEP7pEsoX-SXqR~;xHY)Q+dJ=n8H$O#cC(LmLW6~A+aQ3uoYsfDMqVT!O9Y>E-sTp zCTu&~z6R|py{q|>l2$)l#EVc_ykw*T{2{Q(tPHU&q1EGPf~002ovPDHLk FV1gA!RG9z( diff --git a/browser/components/chat/themes/smileys/cry.png b/browser/components/chat/themes/smileys/cry.png new file mode 100644 index 0000000000000000000000000000000000000000..9ebf7d8988ba0df51b9859d15ee1ffe8d236bc37 GIT binary patch literal 685 zc$@*P0#f~nP)U=G(I!bMP z;cO1yd(O*yo}-8edo&#B1TFzlpczO531Ea+^7-yjWyd@m2>@|4t%t*Y&@}M+z;>#%Y!((5@(3-&k~wuNL7bqWOQS*R zi$nkY8OC1ELlum@nWsNKL+Y!81Pt&SIuLhn9twA&Y0t{1PjTyRj+%xx&b7AC(VB$y z3@D9Eq#{gBO;cQa%k3Kh)bF!kn_b9II4+;MMe@BO8yN|OLJ|svL`vf#zk5Zj@m+fv zksrMxiEfdha9jnVXd3&{0SzGP0h;mp+;Xf@2A(_uY;az>McI=KJbpkysfOaHYxep8 znms@o+lk`Qzmn0ZPw=XAl6G|#EE6E%0Y)e+U8BAMP?&x3lQXfSbo7N$t5nJx zW$xtL;1_(@Y@)p`4J%6kBioQXMK^j%HGZ7cb!rT)0*OGeT+PF|vs|f7(O_qwurNq0 zd3i5kenUqOf}&KKm2J`l7PGKiA?#natF<2L{O)sjeQr6H=|;8x$M$aR<5&F!2}25J TirF(300000NkvXXu0mjf*S{k= diff --git a/browser/components/chat/themes/smileys/embarrassed.png b/browser/components/chat/themes/smileys/embarrassed.png new file mode 100644 index 0000000000000000000000000000000000000000..d80fc10827e5cbcae4c195e26caaf236a8c0670d GIT binary patch literal 676 zc$@*G0$crwP)$=#DJZmH z$ZCi*M67PyxYIwvo&SRy!HrOAX-h)Wtkg;cKQe=utU{?^KQhy1YKS^BcP5#+E|Rum zanlFR;=Jd1&fTb$<~4j`uJZ7ic)2(#ux6|LaNs zbCrir#8*4<=)$L8Juv^yq_@rt3G9-hN7~K#vHX|C8|yy%a9nP?cFkPn-5|Yw`n)Jw z^u~lv_@?zclSmX_zsc@>f^n7GZxalde0g2i`iVOmB+dCON`_-HfpDng!u#jI?;Bsx zy-5C?3m<%R@OF#MpE(*+yIaa&S`>L93`Q`W#e^`=g9gcx%U@qaLi;8?B)43?^d-Rn z_m+6nARd5;AiaKi)zHal-&P6m-YE(J>9Y^h%TY_Y^6P#-Qy{rp&Y-N>+9#&y?69{> zS6}VY-r1i+(t>gpD{_sGm0q>C*VQ{53cClSCkuOYj)XF^CuOsTXx}HX^3-P|g#}{; zfBaHYU8_4hI_Rb2gBtage`+m-$=EB!F}A8at{(W@pRl+vVr0x-r)TT-w(Wa=J2rmA z@N36pa~Za3{P}#s`m;R{EjSwQ+Y#nzw`n!msgwL)w)^n{SM@K6Mf76eEEe4W0000< KMNUMnLSTZEz(E-R diff --git a/browser/components/chat/themes/smileys/grin.png b/browser/components/chat/themes/smileys/grin.png new file mode 100644 index 0000000000000000000000000000000000000000..b0d4196537da459df1ff0176d832f92fdc0b8b5f GIT binary patch literal 672 zc$@*C0$=@!P)TKJE_Q%1K0c!E^F( z|1O^E=6VznVT(fE9-s^G0d61;M1e8@!HgJcrY$zlAl3yK6iJ!skpEpFJ+Mr`gF zP{i9j1*I>b>?1bc0g46hp^uOY!$KU2`Cb+1!eIx40@m~NUup+nG5#1TgJ5o_|MJye zchj&M0h=AZCb|rO4^3lRvT6W66>wuR>-h@f480fxim*PV=Pgb%^!zC(7FZqEVUroa ztw_)_gS*+sP7Uh#|H3N3S{|0>qACz&E$>6q7`^*}rnZa!eC_hvFx~f#Lm>|kRe>?S zWv;V(4||UqvA3L}>2&AcU}Ta?W(2AN*$luKB7y`x0eR<_^1^^j-0qZYVo_?9O)Sd9 ztxhR_8W4&2MS`Ayh-@|tg<0&b#&W%b_b)$_O*^n1Y30ZK3=Q@)CwmX0=F;#zxq?vB zFwhO)0T!(fn;ong)jA`Fa!Jpa%|J0f4BXh-?OJW)SN#Oi0R^pzo^-$f0000?Hj_Aj4@1h^ng?mu zXhB71cjiT|9%oS1befGz441i|KT0IgW%U}>e&AzvT6{!z4~$-$ji+GdjEox=rCm6jJ z>*pD@_=J9~uTC5nSXtmJR_1xie!r@eawy5?dFQ_YX((NVf>X4W00000NkvXXu0mjf DrcJ-@ diff --git a/browser/components/chat/themes/smileys/manga_annoyed.png b/browser/components/chat/themes/smileys/manga_annoyed.png new file mode 100644 index 0000000000000000000000000000000000000000..e7072732a52396dd3b73b5bb4a0a8e54f110db26 GIT binary patch literal 613 zc$@)c0-F7aP)5r00004b3#c}2nYxW zdw)5sUhe2_!NJOOp1HXl6oIF@TM4nrM7J0uaQgL=g1SJ*c8w(33$z_tP|7d|RFd9SS?RVVz zDGf9j2Fc~LA$^vvBM6A+0}bra9~_5r00004b3#c}2nYxW zdKlf_WUHpfvZI_Y#gM=cjW-mceFTyqu zZV*8aJs1cgL=V065Jf@s*jv$qPvv8RGzX3Ts1-tPlut!Qo2x!bn9+sy6P!<}sl zd+5O72Z!JHa}I~^k%SP`gx}oTi#WOK$EK(X=$)?9eouY(Sx z@XGkzeJF~C%Ly_BC!+)>qfmzllOZ@2B{&rY31*uQZi+#ts~+&M-9}NswOca`kIVsZ z;e01oFCXT{?J&a+&07E2eGFV|V@y9lQdi=0YaI_{pa(@^RnIfOm?1o&5q_yLw~%IG zF>ThKYJ{I?%r9kVOviZDa+8SAzKJtLCGq%3hVJfWEOj+wLuG*!|H@xQh`h_<3$@5V zgnXg$xmr+N6IEF#Q1%BhFvFU$b*F!Uat38{Ap-$clTZ|7zZ)dpr?wO)KBUOT4JZjn zCjkNg6(87D4X4VN=@=i!mr&ZRG})~DT*+aia_sl)W@nEID=}D&jp7TPt_C=en8l{r zu{#k0lAqEf7k*&d+lFmVtLd$kIHY3B2)giv5*0xC7J$1nDjIDzuqt3_Fc+3H%(3&v jC@{Dwt_A$ZHCD%OSehT~Rvemx00000NkvXXu0mjfgIg&v diff --git a/browser/components/chat/themes/smileys/manga_smile.png b/browser/components/chat/themes/smileys/manga_smile.png new file mode 100644 index 0000000000000000000000000000000000000000..c20601bbe2f51f977dac330f09c9afe53320e6f4 GIT binary patch literal 698 zc$@*c0!96aP)5Q*ju_Kj+MCdStfg+KZ71h07QT9=k}gX>_v& zhF$Km3nRMePlDjC2*Tv?`qN{%Jy?J;( z&+p-RzK>K`$tif*PIKnnk^8& zH3PuJ(-As-3ahIo@$nfDF!5l7&W;xNp+nKUE(1}HdoeNv)GG-hmjXnF0;rJ$pTFd4 zvB3b5iviSV0wYrZw-+EPiOK#sde08vQb;Xr5Dt652C1(bghL)usSUzC9@wkLPJ3Y2 zgrAE^Nlf-{)7R(1VFMc=*Tvzm*!xSBJ<#X#GN4l|QO;Ko6{|7XIwAq0YEj;;K&b?v z%RrJ{6T8EP_FRYRKYpqg)L!b?oi-?#07)5`CYMD*qP@F~+57WsF7Gsen=3oaKAflh zR2vc!Y-Ita5BKZ~ju8m5^lk<1#Wx(ScD!fYWWR4xHY=#{UV4VRAY(vof6sM?A+dlr z=))JFv{NO$m?OVt;_@r_LmfCjMWem3$S`=_NYr`p+wN{H} g0=V7i?f<;0KdqV*b^1q4dH?_b07*qoM6N<$f-d?q*#H0l diff --git a/browser/components/chat/themes/smileys/manga_stunned.png b/browser/components/chat/themes/smileys/manga_stunned.png new file mode 100644 index 0000000000000000000000000000000000000000..9903039dfdcc35c2f06d312ee061a1ad4445e72e GIT binary patch literal 728 zc$@*)0w?{6P)%fSYH=7b&W{G zz-G0uu`U>V`kBiO1u=`FRN@Uqt(!LpSjx-wwlhUM8eV2;IgP*3j?<~}b8eNfw<{Fy z%;R)>6&4aY@kjt@RbEbZdsw2MnuuA^xhf%~^fC*P5`qk)c#*Dwr&dv|Vd zIR z{0(*v*Y4)iCB*;0 zVkCy75^pG)!!{70Kx?7ck$DbVex{sN1D59@WsCqFxorREulftSF($p>ywR2b0000< KMNUMnLSTYTyFN7l diff --git a/browser/components/chat/themes/smileys/manga_tired.png b/browser/components/chat/themes/smileys/manga_tired.png new file mode 100644 index 0000000000000000000000000000000000000000..3de53a97d6fece2c00dae657f9e944fcfd0b6e6d GIT binary patch literal 556 zc$@(&0@MA8P)ByS%`GeZ5W88 zpxdtdBMOB=p}!!T*|-un7NsFuL9m-7P;N0HgQ}IKb=l z7UE{Ph~Nv3YqRNq6sDmDk{PEOq6qro6`H4bonB_KP3<_o-7K}#pySl;$#94NTAJiyn4dlnv7zAwgg%X2j-5e8r&Bq z8omzzTJw;+!*=U9E(m!2vdMkyzwu17eh9VQ@oYILzdOLDsigOg+tE26}?Pw7vPw)lp=5p8hDD zx`y_305DK5Em^voB@<0~$+U0!EtI=TAWJL3Pm< zi(Y`J2gqVHVt8bzFQZ1*fI8J5W%jmpP6u_=^98UZs8#GGjRrv017xV_ht~J2CDM;p zD6EzU^qpaMvqGr9i}=GKL|=#FPzT7^X-o1v7oy-6SbeoYSL7_AfdDSI3$tZX__9lB zqs*n-5olZR-A?eCqQG^imMMj_6`q`d7Xm#}wmv!u0ZQ|ni5OeQ>&(EY2>`VPuDjL%mfv;_#`a)PRW<2tjq zCji)3Ux3as*e0006YbUVu=fUrolyE3R)HXz#(#xF0|;6GA6A>xSI?1nwhT0&xhnNG z@+6+dL9xSLPLi!Q0G}e#<8Sf#gA8dfz`q}?1MJsevsh4p0{b-y>L3B_mOLM23))qQ z5kS2LP*8y^KenMGjvI7f>2Z}ia=A`? zu_$YYjVmQ+)RIJuRL@GpsF-C;6WRjW5WVlm?RF5o=kE~(R7}`0%RuZeZv*q#wHbz; zavfG1G@DZ6?zWr=v3_r#jEp`7gn=O71J;28kp1&l{RRQ%0L)Z+7V`iA002ovPDHLk FV1ihKBAx&M diff --git a/browser/components/chat/themes/smileys/slant.png b/browser/components/chat/themes/smileys/slant.png new file mode 100644 index 0000000000000000000000000000000000000000..f0622a02c3320f161010a868ea1524df7bb2e796 GIT binary patch literal 700 zc$@*e0z>_YP)&%?yI(kwe7c?nWv5kUA5V{Ci zXciWB6J7RS1pN)+O%UBAB`Ad}qzi>X5p4xgq=++|HO&%r+u2#i?d{^sUpGMyJU1`z z^Sz(5PLs&2I_edS(57ph@esHJpt!)vKDzJ+T8Lac0YO~@M(m#_qy4Drb54fA ze}T;GDZnJwJpIIy&$~ALSJC(4J^!oX$&YpA4h_AslLk$-RJ=zJNFQ(qu}Xb z=;odJSqchop(zL}OMMO?j;ius@TmZC1qkEz*yfa{7<)Dfm{6Iq%`#JrJ$(ei3xz3L z?DhbJg`AAOC)^#ULAA|M8i6uMYoFULkUp@spbC@>_%@qXfHWl|j;iu`e1YHDO+vlx zv_~}@il~dNI_u*r8vtqNy<55y%NnM+h2~?8)T)yC*E!OUzf#B>xP2}f4*P781Zomi zbFgjQ-wDY$nm$OI4gwHhteY&RevnOl=TFfhI^0dy`J=Fwhu`@L5}9iU5=Kzh(Aq$V zJ!d6sj(OPI1uI^TlE*WJ?Uo&e~^3t0000xu8j3lv@Rf(vHGL zhJpp1byoBTc@jhiAvMYphj_SCN#N9X)BLeS6R3pY`fNWdofujhkflxd|LWrb$BjxW3lSB)B>pPMMbc_u!dXr z`zM041fywruY)n1F2|X`2`si&Q1qaqA@rbc#(mBMc9#Pf(<)E(4xy?qIq3zLmL03P>h=p3ks7534IsEZk6lLE7vfOJKgr=*@lrVk-}zG7iRmMsMBO zdz*vYTd>(7wLT~VepHqJf>{OdD}Wb+Q4?ophS1Y-pa`W|O{n4=p(hVPGC^)eBO8nW zUP(;#e!$z~$Es@LC{4f)h;oD4dJtwPZ$kyh7vS4+L;)h?3-#8;Hb~S~RoxXNB5`7G z60{y`=JfUBuu}ktD8Mw?%w1Yq0ZFR{q`zzud!69>yA&;5O?dh`=(^~Hs(`Hw!1TUJ zp5t&0<8-k$|AUW@moZrMc=|i&?mLQVGigaHz(x|X$q9ndkpm9H?sEJpS&(EITc@?| zGJ#fcBL$nun!{?|ChThm#xSYF*zDRm3`QuGv>CTDP%KOU_XtMQb#FJ~&{Y8|Km?dR bd{zGdu&3Tdg8MhQ00000NkvXXu0mjfv!p8j diff --git a/browser/components/chat/themes/smileys/smile.png b/browser/components/chat/themes/smileys/smile.png new file mode 100644 index 0000000000000000000000000000000000000000..074d1cc0167b828f2e1a8465f8ba8037e9f177f2 GIT binary patch literal 682 zc$@*M0#*HqP)U8UN^2;HMhbD!%%TOcu_z@dX_{2hq)sN8wBuq@n=V}R zz}-CF_u$-fp0F&7U7C)J0K-5O2m?7F3#3SBZ#%DwfADl9xLj;LevzqNY)Ed-ojpiA z)<^nNo{zJ?P+E1yhPvPN2j%M|wBimt9SOF!tc5!tjKGasXYlTm?FTJj%bx0blZOxH zxP9KW>U49(N8Bd8V3w$q;0>(EE6xEfN()=#)f4D zAS|q@!8yVM(Wa1)JY5LI_uqwCMg>#fM7B&=Ud%dxEPr$)xeM)S4w_YHnXpO6m|#}X zp68I1E~x7OSqG3}t&Bj>)9)p7cb>}f#tygzmHY;od-L=h@gfkgUIs|*RQiY~&5*a(e@pvDU3b3}FbGu;(7Vw7x1bS&~v?wmFQCrdJ4ypvh z`%yejD6PQn!YURW4QqEBxQ|Os;`7^a$O<$Xw#W4{82Th|b9c8pwTEBz7gy)}RAL@e QkpKVy07*qoM6N<$g7~>3YybcN diff --git a/browser/components/chat/themes/smileys/sp_laugh.png b/browser/components/chat/themes/smileys/sp_laugh.png new file mode 100644 index 0000000000000000000000000000000000000000..40d84ea7d62ab5b6b9c7ac650566274db6745cef GIT binary patch literal 669 zc$@*90%HA%P)fF&C-!3NZq;=5) z&&`+TdHFss&#Q?De_*1sALs+Zz!@L~%mQ&Db8p>J?Z7y!rJ(j0lx)~4 z!fFn-i(|kIB6BNM^F(JO8ea%n0YX90waP#N_V(!rEH6X$=P!iwMCMlX{U5{#T7j9I zmvm+m2LTTUbVNx1*koqK3x*W{VgR0dUGMr)>b^8LG5>9il-^n?@VFuIG9Eq-Ipyi zT?4-X(5C}olmcM*QG$`j30hD4h>!K+_tg@A(MxNykC7({hMy)XA?p=DSd;ON0`2X` zsr6Jc>ifjKtV7&&AT__C11U;&Rp=JG-^4}R21t2Q7z2xf9 zIoK`&%zA-1tGOsAP7-XZ!8m(?(8Zpk#_10@xi_#Ukk0|c5fNm(V^lsxr2KtQCU5mf zekm<(r6^-QDkPzkNoSJh;w5b=i1fmN z$K{;IIq%*lq?GKaoG}9=fjAHY%0LOolQQ29Q^l4zXGB&jn@?XXwJvu?LR>h1lHIzjN;#qX+!zLJ9N~nvG2!KU(Jg z#bDK^iL<24%0T>@Xf)07DELN!Hb{3!JOVNVZWAmEYSug{^Qw>wlQZc08&1by?-b_B z8Nk7qD1iF|#4*gT(;$Kvr@laQ3BHxVv9I`mB!|PG2?$+c_F4jf@8u`Z%|al-?Dbpy zUJ+WKU@Q#3){+_!N7va^g>-!I!E@Dl3odv~#gRbU1}OP}JnQwl96SUF zo~LvFjCwPhf}Vs%9U#9w=kaKoBT*1243=bIUG`nnELc}_*7~#zHLHqcfhWO3VPLxl zbfpshwyHn|{@QKeAwfOOSXhn2AAn9rWo*>JvFCxz&fOlaUHqy)^$_KvNQDCd00000 LNkvXXu0mjfgfIsT diff --git a/browser/components/chat/themes/smileys/theme.js b/browser/components/chat/themes/smileys/theme.js new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/smileys/theme.js @@ -0,0 +1,25 @@ +{ + "smileys": [ + {"filename": "smile.png", "textCodes": [":-)", ":)", "(-:", "(:"]}, + {"filename": "grin.png", "textCodes": [":-D", ":D"]}, + {"filename": "wink.png", "textCodes": [";-)", ";)"]}, + {"filename": "cry.png", "textCodes": [":'("]}, + {"filename": "shocked.png", "textCodes": [":-o", ":-O"]}, + {"filename": "confused.png", "textCodes": [":-S", ":S", ":-s", ":s"]}, + {"filename": "slant.png", "textCodes": [":-/"]}, + {"filename": "slant2.png", "textCodes": [":-\\"], "hidden": true}, + {"filename": "angry.png", "textCodes": ["x-("]}, + {"filename": "sad.png", "textCodes": [":-(", ":(", ")-:", "):"]}, + {"filename": "cool.png", "textCodes": ["B-)", "8-)"]}, + {"filename": "tongue.png", "textCodes": [":-P", ":P", ":-p", ":p"]}, + {"filename": "embarrassed.png", "textCodes": [":-]", ":]"]}, + {"filename": "heart.png", "textCodes": ["<3"]}, + {"filename": "straight_face.png", "textCodes": [":-|"]}, + {"filename": "manga_smile.png", "textCodes": ["^^"]}, + {"filename": "manga_embarrassed.png", "textCodes": ["^^'"]}, + {"filename": "manga_tired.png", "textCodes": ["-_-"]}, + {"filename": "manga_annoyed.png", "textCodes": ["-_-'", "--'"]}, + {"filename": "manga_stunned.png", "textCodes": ["o_o", "O_O"]}, + {"filename": "sp_laugh.png", "textCodes": ["XD", "xD"]} + ] +} diff --git a/browser/components/chat/themes/smileys/tongue.png b/browser/components/chat/themes/smileys/tongue.png new file mode 100644 index 0000000000000000000000000000000000000000..225b661df76c22499c28e1bea8091ca90be29708 GIT binary patch literal 670 zc$@*A0%84$P)<-xOF2ejD%PN zV}S+U)kQSA>8AgqyCS0Qk`fw)b9hsWpy?`gF3FOS}9?z{Id(sS5qT&M#rqe@#fo<3RP z!MM0?HVGF9%elYs8@%2KC%jG* zUOoqC_=l2E{{_MT^0zS%MErwap|l9!GoY#0%|M8L7nlUFUuE)U5P%<73O&Wf+6f>`0fFMG7-hiz( z0H2`Gjx6CD3eYV<7ylnv6WCE1WkqejPz6Yv zfds|EJ)C_w+!CAFGII|X5G_`^MUn08Dq6n6@RidzB7M-*VXFX;*vr}Njd0Wpf?;bT zR-}+9;2Idjxv9dG1j81{D^N6YHnry$C9kZbDA3TM3C?jRXO4JrHrg%6Oa{22tOL=1 zb{lw%C`IUX8FBpVf`uBKuR(tvG&Kf9_wRP64)Ck~0QWcXUDTJU3;+NC07*qoM6N<$ Eg2tsL5dZ)H diff --git a/browser/components/chat/themes/smileys/wink.png b/browser/components/chat/themes/smileys/wink.png new file mode 100644 index 0000000000000000000000000000000000000000..556c2a5c72849e90fe4c34d4163174c08e72af06 GIT binary patch literal 680 zc$@*K0$2TsP)Z`SR-lDv^0(RYLi_LTeY!X zeBos-^UOOR?Jw+*$5fWBG z?Cl^)*ffZkAQkB1FL!geH^B7tDP*Y60FrL<(-RZu`eTNpE+}x#UaHcMwLAujhuGjJ z*nSOPve2lVlfXE^04>{L>goc^D`lcXe(uklK?(udX)av648Te@4V5?G55U^$xCG+p zI#LR5-58}m;$`Jq1t|rz`L}v>fVc#rs2VuXZ+gLo!HCZVZJ?3n;p{z7JWzS=!m0)k zl|Yt8Gmg|@?~-x$jdqUMz$`dD)K-h^R$CnL3xKQuGE{5WyEb3bc{lNZcX4*gyuJGo zpW%gi4Im?c6dNT33aw_F#XC#1s_hOq|GvbFn--B+7$Jai2_W^WB#$u6Nx~+d7jk4C zETMY4@kb7^mHR<$v&rbxFhgSlu%3sF{2U4Ed}qS~rhyTr)wIdID^TCsLGRV*J07H4 z>w@()*vuDk7$IR54tCq4PvQ@_&!KA2Y`SMGm!MIb1Fm&;d%ynUSN#M)^WGeH+@0(I O00007M6G0F~&vjx!9ylZf#A|~hBo<`D|9?fq3L#*F z1TqkZrxYhH7Ci=!A22;t_IM=Ul_hsg)#>i)t}dAwZC0o00KD1&G_4Il(`r4y_A)*y zX$v?44uKvp1bza2GaH2Zin<3Ri4^cX;4ZMWVCV(#+RVO%_>BONv<8*K1CZ1O9sm!4o6aDs zY6J{OM{VEChKmL+1d!AN9+76d$sVeaj@(yfb~*j$TmVVOq-NDRX-G==QuRACvyygz zXRC@TLtW;?HE(O2u%xbafc7Mn-ryyG;-sn5CS0XTH~91l^jP27AFI>}oCffA*;b)a zD{>kj`f^%^{<#9J0GMpaw-KPjM4W8|fQfG42B0BzW_}cE3&5Wh_zl|Ke%m+eE{p&G N002ovPDHLkV1fki9`yhK diff --git a/browser/components/chat/themes/viewlog.css b/browser/components/chat/themes/viewlog.css new file mode 100644 --- /dev/null +++ b/browser/components/chat/themes/viewlog.css @@ -0,0 +1,76 @@ +/* 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/. */ + +%ifdef XP_MACOSX +#logList { + -moz-appearance: none; + background-color: #d2d8e2; + border: 0px; + margin: 0 0; +} + +#logList:-moz-window-inactive { + background-color: #e8e8e8; +} + +splitter { + -moz-border-end: none !important; + -moz-border-start: 1px solid #404040; + min-width: 1px; + width: 1px; +} + +splitter:-moz-window-inactive { + border-color: rgba(0,0,0,0.35); +} +%else +%ifdef XP_WIN +#logList { + -moz-appearance: none; + border: none; + margin: 0 0; +} +%else +#browser { + -moz-appearance: listbox; +} + +#logList { + margin: 0 0; +} +%endif +%endif + +#corruptLogScreen { + background-color: -moz-Dialog; + overflow: auto; +} + +#corruptLogBox { + background-color: -moz-Field; + color: -moz-FieldText; + border: 1px solid ThreeDShadow; + border-radius: 10px; + padding: 1.1em; + -moz-padding-start: 20px; + margin-left: 1em; + margin-right: 1em; +} + +#corruptLogImage { +%ifdef XP_MACOSX + list-style-image: url("chrome://global/skin/icons/error-large.png"); +%else + list-style-image: url("chrome://global/skin/icons/error-48.png"); +%endif + -moz-margin-end: 1em; +} + +#corruptLogInnerBox { + max-width: 25em; +} + +#corruptLogDesc { + font-size: large; +} diff --git a/browser/components/chat/themes/voice.png b/browser/components/chat/themes/voice.png new file mode 100644 index 0000000000000000000000000000000000000000..43cc90eea0bff35daf698119be6da11c61a546f6 GIT binary patch literal 733 zc$@*<0wVp1P)7Xwjlw1ZA&HBF!{vx~VY2aWn}cDxx5UnIwr?n2H&u z)uyC^TD5DD3)@79;=<@LK5pYU&fK|w-|s{kOm!~@&iT(d-~avRA%Hdj7L2YTz{XZH z(liU`$cl!I&;AuRP(yXbjQD8_{%EF>fzkQk2qb3BXkcmsz_AG+lLZJ3ZYMys8TCE; z4Pb$^3(uT={2&GXNL3x0R-;u(Zv4+u{Y7 zDq6q|J&8*h7w*wDEQ}7RZojWd)RWd*;lzw3;V(@hG=YQ4X+BkA zkFDVqT_+X5dn(6-#5h?uvQB-rj?2=8Vvocl_4ri*pGaXzvv5`DXh$03Qp0ij#uUI3 zl?qBSWx=64kton$NFAR~?L(PjaQ6+qYiU%h*hpGwxScvV0EMjCyaG^$kYYWtD3L-e z&xP__0Q4>AyHJv_u|mdqbV&v9k;-1m1iNvVjU5pggfc}a-TVYc95K?q#Y8`2CMZYA zNchqds|h%;SCNHr!~~-RhZ1op*NY2Yo1k{qbnwiPS#uEISn6?cROA9_^&}q zr%=b+-|^rf^A53)0gm!;0}VsXL$VS^s;9-wHOlUr{l)IDSf<*>_+^grlk6^ZuWw=_ z4?mfa=E-d0UzVwBa2I21XgoF}fsJj1q7WP@H=|A8{O diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -527,16 +527,47 @@ ; ANGLE GLES-on-D3D rendering library #ifdef MOZ_ANGLE_RENDERER @BINPATH@/libEGL.dll @BINPATH@/libGLESv2.dll @BINPATH@/@MOZ_D3DX9_DLL@ @BINPATH@/@MOZ_D3DCOMPILER_DLL@ #endif +; Instant messaging +@BINPATH@/@PREF_DIR@/chat-prefs.js +@BINPATH@/@PREF_DIR@/chat-browser-prefs.js +@BINPATH@/chrome/chat@JAREXT@ +@BINPATH@/chrome/chat.manifest +@BINPATH@/components/chat.xpt +@BINPATH@/components/imAccounts.js +@BINPATH@/components/imAccounts.manifest +@BINPATH@/components/imCommands.js +@BINPATH@/components/imCommands.manifest +@BINPATH@/components/imContacts.js +@BINPATH@/components/imContacts.manifest +@BINPATH@/components/imConversations.js +@BINPATH@/components/imConversations.manifest +@BINPATH@/components/imCore.js +@BINPATH@/components/imCore.manifest +@BINPATH@/components/facebook.js +@BINPATH@/components/facebook.manifest +@BINPATH@/components/gtalk.js +@BINPATH@/components/gtalk.manifest +; @BINPATH@/components/twitter.js +; @BINPATH@/components/twitter.manifest +; @BINPATH@/components/irc.js +; @BINPATH@/components/irc.manifest +@BINPATH@/components/xmpp.js +@BINPATH@/components/xmpp.manifest +@BINPATH@/components/smileProtocolHandler.js +@BINPATH@/components/smileProtocolHandler.manifest +@BINPATH@/components/logger.js +@BINPATH@/components/logger.manifest + ; [Browser Chrome Files] @BINPATH@/chrome/browser@JAREXT@ @BINPATH@/chrome/browser.manifest @BINPATH@/chrome/pdfjs.manifest @BINPATH@/chrome/pdfjs/* @BINPATH@/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/install.rdf @BINPATH@/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/icon.png #ifdef SHIP_FEEDBACK diff --git a/browser/locales/en-US/chrome/browser/chat/account.dtd b/browser/locales/en-US/chrome/browser/chat/account.dtd new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/account.dtd @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/browser/locales/en-US/chrome/browser/chat/accountWizard.dtd b/browser/locales/en-US/chrome/browser/chat/accountWizard.dtd new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/accountWizard.dtd @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/locales/en-US/chrome/browser/chat/accountWizard.properties b/browser/locales/en-US/chrome/browser/chat/accountWizard.properties new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/accountWizard.properties @@ -0,0 +1,21 @@ +# 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/. + +# LOCALIZATION NOTE +# The selected protocols below should be the most relevant +# to the target language / location and comma delimited. +# Exceeding 4 protocols may cause scrolling. A list of the +# available protocols can be found at +# https://wiki.instantbird.org/Protocol_Identifiers +topProtocol.list=prpl-gtalk,prpl-facebook,prpl-twitter,prpl-aim,prpl-msn,prpl-yahoo + +# LOCALIZATION NOTE +# These are the descriptions of the top protocols specified above. +# A description should be provided for each protocol ID listed above. +topProtocol.prpl-gtalk.description=Talk to your Gmail contacts +topProtocol.prpl-facebook.description=Communicate with Facebook friends +topProtocol.prpl-twitter.description=Stay up to date with your Twitter timeline +topProtocol.prpl-aim.description=Chat with your buddies on AOL Instant Messenger +topProtocol.prpl-msn.description=Microsoft Windows Live Messenger (formerly MSN) +topProtocol.prpl-yahoo.description=Chat with friends using Yahoo! Messenger diff --git a/browser/locales/en-US/chrome/browser/chat/accounts.dtd b/browser/locales/en-US/chrome/browser/chat/accounts.dtd new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/accounts.dtd @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/locales/en-US/chrome/browser/chat/accounts.properties b/browser/locales/en-US/chrome/browser/chat/accounts.properties new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/accounts.properties @@ -0,0 +1,36 @@ +# 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/. + +# %S is replaced by the name of a protocol +protoOptions=%S Options +accountProtocol=Protocol: +accountUsername=Username: +accountColon=%S: +accountUsernameInfo=Please enter the username for your %S account. +accountUsernameInfoWithDescription=Please enter the username (%S) for your %S account. + +account.connection.error=Error: %S +account.connection.errorUnknownPrpl= No '%S' protocol plugin. +account.connection.errorEnteringPasswordRequired=Entering a password is required to connect this account. +account.connection.errorCrashedAccount=A crash occurred while connecting this account. +account.connection.progress=Connecting: %S… +account.connecting=Connecting… +account.connectedForSeconds=Connected for a few seconds. +account.connectedForDouble=Connected for %S %S and %S %S. +account.connectedForSingle=Connected for about %S %S. +account.reconnectInDouble=Reconnection in %S %S and %S %S. +account.reconnectInSingle=Reconnection in %S %S. +account.deletePrompt.title=Delete Account? +account.deletePrompt.message=Are you sure you want to delete this account? +account.deletePrompt.checkbox=Do ¬ ask next time +account.deletePrompt.button=&Delete + +accountsManager.notification.button.accessKey=C +accountsManager.notification.button.label=Connect Now +accountsManager.notification.userDisabled.label=You have disabled automatic connections. +accountsManager.notification.safeMode.label=Automatic Connection Settings have been ignored because the application is currently running in Safe-Mode. +accountsManager.notification.startOffline.label=Automatic Connection Settings have been ignored because the application was started in Offline Mode. +accountsManager.notification.crash.label=The last run exited unexpectedly while connecting. Automatic Connections have been disabled to give you an opportunity to Edit your Settings. +accountsManager.notification.singleCrash.label=A previous run exited unexpectedly while connecting a new or edited account. It has not been connected so that you can Edit its Settings.;A previous run exited unexpectedly while connecting #1 new or edited accounts. They have not been connected so that you can Edit their Settings. +accountsManager.notification.other.label=Automatic connection has been disabled. diff --git a/browser/locales/en-US/chrome/browser/chat/addbuddy.dtd b/browser/locales/en-US/chrome/browser/chat/addbuddy.dtd new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/addbuddy.dtd @@ -0,0 +1,8 @@ + + + + + + diff --git a/browser/locales/en-US/chrome/browser/chat/buddytooltip.properties b/browser/locales/en-US/chrome/browser/chat/buddytooltip.properties new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/buddytooltip.properties @@ -0,0 +1,9 @@ +# 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/. + +buddy.screenname=Buddy +buddy.loggedIn=Logged In +buddy.account=Account +contact.tags=Tags +conversation.topic=Topic diff --git a/browser/locales/en-US/chrome/browser/chat/core.properties b/browser/locales/en-US/chrome/browser/chat/core.properties new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/core.properties @@ -0,0 +1,14 @@ +# 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/. + +startupFailure.title=Instantbird - Start up failure +startupFailure.apologize=Instantbird encountered a serious error and cannot start, we apologize for the inconvenience. +startupFailure.update=An updated version will probably be available shortly to fix the problem. + +startupFailure.purplexpcomFileError=Description: The file "instantbird.xpt" is missing or corrupted. +startupFailure.xpcomRegistrationError=Description: XPCOM registration of the core component failed. +startupFailure.purplexpcomInitError=An exception occurred while initializing the core component: %S + +startupFailure.buttonUpdate=Check for Updates +startupFailure.buttonClose=Close Instantbird diff --git a/browser/locales/en-US/chrome/browser/chat/instantbird.dtd b/browser/locales/en-US/chrome/browser/chat/instantbird.dtd new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/instantbird.dtd @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/locales/en-US/chrome/browser/chat/instantbird.properties b/browser/locales/en-US/chrome/browser/chat/instantbird.properties new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/instantbird.properties @@ -0,0 +1,128 @@ +# 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/. + +# LOCALIZATION NOTE (contextMenuSearchText): %1$S is the search engine, +# %2$S is the selection string. +contextMenuSearchText=Search %1$S for "%2$S" +contextMenuSearchText.accesskey=S +contextMenuSearchWith=Search "%S" with… + +newTagPromptTitle=New Tag +newTagPromptMessage=Please enter the name of the new tag: + +#LOCALIZATION NOTE +# this is used in the addBuddies dialog if the list of existing groups is empty +defaultGroup=Contacts + +#LOCALIZATION NOTE This string appears in a notification bar at the +# top of the Contacts window when someone added the user to his/her +# contact list, to request the permission from the user to share +# status information with this potential new contact. +# %S is replaced with the user name of the potential new contact. +buddy.authRequest.label=%S wants to chat with you +buddy.authRequest.allow.label=Allow +buddy.authRequest.allow.accesskey=A +buddy.authRequest.deny.label=Deny +buddy.authRequest.deny.accesskey=D + +#LOCALIZATION NOTE +# %S here will be replaced by the alias (or username) of a buddy about +# to be removed from the buddy list. +buddy.deletePrompt.title=Delete %S? + +#LOCALIZATION NOTE +# The first parameter of this string will by the name of a buddy (either +# the alias followed by the username between parenthesis if an alias +# is set, or only the username otherwise). +# The second parameter will be the name of the protocol on which this +# buddy is removed (for example: AIM, MSN, Google Talk). +# +# Please find a wording that will keep the username as close as +# possible to the beginning of the string, because this is the +# important information that an user should see when looking quickly +# at this prompt. +buddy.deletePrompt.message=%S will be permanently removed from your %S buddy list if you continue. + +#LOCALIZATION NOTE +# the & symbol indicates the position of the character that should be +# used as the accesskey for this button. +buddy.deletePrompt.button=&Delete + +group.otherContacts.name=Other Contacts +#LOCALIZATION NOTE +# %S here will be replaced by the tag name +group.hidePrompt.title=Hide %S? + +#LOCALIZATION NOTE %S here will be replaced by the tag name. +# The translation for 'Visible Tags…' here should match the translation of +# visibleTagsCmd.label in instantbird.dtd +# The translation for 'Other Contacts' should match group.otherContacts.name +group.hidePrompt.message=The tag '%S' will no longer be visible. Use the 'Visible Tags…' context menu item to show it again.\n\nContacts that have no visible tag will be displayed in the 'Other Contacts' special group at the bottom of the list. + +#LOCALIZATION NOTE +# The following strings are used as textual representation of contacts, that +# screenreaders can read out loud for visually impaired users. +# %1$S is the displayname of the contact +# %2$S is the status text of this contact +# %3$S is a list of the contact's buddies, joined by contact.buddyListSeparator +# Example for the compact/collapsed state of a contact: "John Doe (I'm away)" +contact.collapsed=%1$S (%2$S) +# Example for the expanded state: "John Doe (I'm away): John Doe, JohnDoe@gmail.com" +contact.expanded=%1$S (%2$S): %3$S +# The trailing space here is intentional. +contact.buddySeparator=, + +#LOCALIZATION NOTE +# the & symbol indicates the position of the character that should be +# used as the accesskey for this button. +group.hidePrompt.button=&Hide + +group.hidePrompt.checkbox=Show next time + +displayNameEmptyText=Display Name +userIconFilePickerTitle=Select the new icon… + +isTyping=%S is typing. +hasStoppedTyping=%S has stopped typing. + +noTopic=No topic message for this room. + +#LOCALIZATION NOTE +# This will appear in a tooltip when hovering the protocol icon in the toolbar +# at the top of multi-user chats. +# %1$S is the conversation name (IRC example: #instantbird) +# %2$S is the account name (IRC example: user@irc.mozilla.org) +# %3$S is the protocol name (example: IRC) +targetTooltipChat=%1$S via %2$S (%3$S) +#LOCALIZATION NOTE +# same tooltip text for single user conversations: +# %1$S is the conversation name (AIM screenname, ICQ number, ...) +# %2$S is the protocol name (example: AIM) +targetTooltipIM=%1$S (%2$S) +#LOCALIZATION NOTE +# This will be appended to targetTooltipIM if the conversation is with a contact +# and it's possible to talk to another buddy of the same contact. +targetChangeable=\nClick to change + +#LOCALIZATION NOTE +# this is used as part of the title of the window displaying conversation logs +# %S will be replaced by the name of a buddy of the name of a chat room. +logs=%S logs + +#LOCALIZATION NOTE +# This is shown when an unknown command (/foo) is attempted. %S is the command. +unknownCommand=%S is not a supported command. Type /help to see the list of commands. + +#LOCALIZATION NOTE +# Shown when the user attempts to send a message while the network (necko) is offline. +networkOffline=Your account is disconnected because there is no network connection. + +#LOCALIZATION NOTE +# This is shown when the user attempts to send a message while the status is offline. +statusOffline=Your account is disconnected because your status is currently set to offline. + +#LOCALIZATION NOTE +# This is shown when the user attempts to send a message to a disconnected account. +# %1$S is the name of the protocol of the account, %2$S the name of the account. +accountDisconnected=Your %1$S account %2$S is disconnected. diff --git a/browser/locales/en-US/chrome/browser/chat/joinChat.dtd b/browser/locales/en-US/chrome/browser/chat/joinChat.dtd new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/joinChat.dtd @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/browser/locales/en-US/chrome/browser/chat/mintrayr.dtd b/browser/locales/en-US/chrome/browser/chat/mintrayr.dtd new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/mintrayr.dtd @@ -0,0 +1,9 @@ + + + + + + + diff --git a/browser/locales/en-US/chrome/browser/chat/quitDialog.properties b/browser/locales/en-US/chrome/browser/chat/quitDialog.properties new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/quitDialog.properties @@ -0,0 +1,9 @@ +# 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/. + +dialogTitle=Unread message(s) +message=You have unread message(s) in #1 conversation, do you really want to continue?;You have unread messages in #1 conversations, do you really want to continue? +checkbox=Do ¬ ask next time +quitButton=&Quit +restartButton=&Restart diff --git a/browser/locales/en-US/chrome/browser/chat/region.properties b/browser/locales/en-US/chrome/browser/chat/region.properties new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/region.properties @@ -0,0 +1,7 @@ +# 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/. + +# Search engine order (order displayed in the search bar dropdown)s +browser.search.order.1=Google +browser.search.order.2=Yahoo diff --git a/browser/locales/en-US/chrome/browser/chat/tabbrowser.dtd b/browser/locales/en-US/chrome/browser/chat/tabbrowser.dtd new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/tabbrowser.dtd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/browser/locales/en-US/chrome/browser/chat/tabbrowser.properties b/browser/locales/en-US/chrome/browser/chat/tabbrowser.properties new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/tabbrowser.properties @@ -0,0 +1,8 @@ +# 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/. + +tabs.closeWarningTitle=Confirm close +tabs.closeWarningMessage=You are about to close #1 conversation with unread messages. Are you sure you want to continue?;You are about to close #1 conversations with unread messages. Are you sure you want to continue? +tabs.closeButton=Close conversation;Close conversations +tabs.closeWarningPromptMe=Warn me when I attempt to close multiple conversations with unread messages diff --git a/browser/locales/en-US/chrome/browser/chat/updates.properties b/browser/locales/en-US/chrome/browser/chat/updates.properties new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/chat/updates.properties @@ -0,0 +1,21 @@ +# 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/. + +# Check for Updates +# LOCALIZATION NOTE (updatesItem_*): these are alternative labels for Check for Update item in Help menu. +# Which one is used depends on Update process state. +# This is copied from mozilla/browser/locales/en-US/chrome/browser/browser.properties + +updatesItem_default=Check for Updates… +updatesItem_defaultFallback=Check for Updates… +updatesItem_default.accesskey=C +updatesItem_downloading=Downloading %S… +updatesItem_downloadingFallback=Downloading Update… +updatesItem_downloading.accesskey=D +updatesItem_resume=Resume Downloading %S… +updatesItem_resumeFallback=Resume Downloading Update… +updatesItem_resume.accesskey=D +updatesItem_pending=Apply Downloaded Update Now… +updatesItem_pendingFallback=Apply Downloaded Update Now… +updatesItem_pending.accesskey=D diff --git a/browser/locales/jar.mn b/browser/locales/jar.mn --- a/browser/locales/jar.mn +++ b/browser/locales/jar.mn @@ -99,16 +99,30 @@ locale/browser/syncBrand.dtd (%chrome/browser/syncBrand.dtd) locale/browser/syncSetup.dtd (%chrome/browser/syncSetup.dtd) locale/browser/syncSetup.properties (%chrome/browser/syncSetup.properties) locale/browser/syncGenericChange.properties (%chrome/browser/syncGenericChange.properties) locale/browser/syncKey.dtd (%chrome/browser/syncKey.dtd) locale/browser/syncQuota.dtd (%chrome/browser/syncQuota.dtd) locale/browser/syncQuota.properties (%chrome/browser/syncQuota.properties) #endif + locale/browser/chat/account.dtd (%chrome/browser/chat/account.dtd) + locale/browser/chat/accounts.dtd (%chrome/browser/chat/accounts.dtd) + locale/browser/chat/accounts.properties (%chrome/browser/chat/accounts.properties) + locale/browser/chat/accountWizard.dtd (%chrome/browser/chat/accountWizard.dtd) + locale/browser/chat/accountWizard.properties (%chrome/browser/chat/accountWizard.properties) + locale/browser/chat/addbuddy.dtd (%chrome/browser/chat/addbuddy.dtd) + locale/browser/chat/buddytooltip.properties (%chrome/browser/chat/buddytooltip.properties) + locale/browser/chat/core.properties (%chrome/browser/chat/core.properties) + locale/browser/chat/instantbird.dtd (%chrome/browser/chat/instantbird.dtd) + locale/browser/chat/instantbird.properties (%chrome/browser/chat/instantbird.properties) + locale/browser/chat/joinChat.dtd (%chrome/browser/chat/joinChat.dtd) + locale/browser/chat/proxies.dtd (%chrome/browser/chat/proxies.dtd) + locale/browser/chat/proxies.properties (%chrome/browser/chat/proxies.properties) + locale/browser/chat/updates.properties (%chrome/browser/chat/updates.properties) % locale browser-region @AB_CD@ %locale/browser-region/ locale/browser-region/region.properties (%chrome/browser-region/region.properties) # the following files are browser-specific overrides locale/browser/netError.dtd (%chrome/overrides/netError.dtd) locale/browser/appstrings.properties (%chrome/overrides/appstrings.properties) locale/browser/downloads/settingsChange.dtd (%chrome/overrides/settingsChange.dtd) % override chrome://global/locale/netError.dtd chrome://browser/locale/netError.dtd % override chrome://global/locale/appstrings.properties chrome://browser/locale/appstrings.properties diff --git a/browser/makefiles.sh b/browser/makefiles.sh --- a/browser/makefiles.sh +++ b/browser/makefiles.sh @@ -9,16 +9,17 @@ add_makefiles " browser/Makefile browser/app/Makefile browser/app/profile/extensions/Makefile browser/base/Makefile browser/components/Makefile browser/components/about/Makefile browser/components/build/Makefile browser/components/certerror/Makefile +browser/components/chat/Makefile browser/components/dirprovider/Makefile browser/components/downloads/Makefile browser/components/downloads/src/Makefile browser/components/feeds/Makefile browser/components/feeds/public/Makefile browser/components/feeds/src/Makefile browser/components/migration/Makefile browser/components/migration/public/Makefile diff --git a/chat/Makefile.in b/chat/Makefile.in new file mode 100644 --- /dev/null +++ b/chat/Makefile.in @@ -0,0 +1,37 @@ +# 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/. + +DEPTH = .. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +PROTOCOLS = \ + facebook \ + gtalk \ + irc \ + twitter \ + vkontakte \ + xmpp \ + $(NULL) + +ifdef MOZ_DEBUG +PROTOCOLS += jsTest +endif + +PREF_JS_EXPORTS = $(srcdir)/chat-prefs.js + +PARALLEL_DIRS = \ + components/public \ + components/src \ + modules \ + content \ + themes \ + locales \ + $(foreach proto,$(PROTOCOLS),protocols/$(proto)) \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/chat/chat-prefs.js b/chat/chat-prefs.js new file mode 100644 --- /dev/null +++ b/chat/chat-prefs.js @@ -0,0 +1,97 @@ +/* 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/. */ + +// What to do when starting up +// 0 = do not connect / show the account manager +// 1 = connect automatically +// Other values will be added later, for example to start minimized +pref("messenger.startup.action", 1); + +pref("messenger.accounts", ""); + +// Should the accounts service stored in the password manager the +// passwords that are currently stored in the preferences? +pref("messenger.accounts.convertOldPasswords", false); + +// The intervals in seconds between automatic reconnection attempts. +// The last value will be reused for the rest of the reconnection attempts. +// A value of 0 means that there will be no more reconnection attempts. +pref("messenger.accounts.reconnectTimer", "1,5,30,60,90,300,600,1200,3600"); + +// List of tags ids whose contacts should be shown in the special +// "Other contacts" group. +pref("messenger.buddies.hiddenTags", ""); + +// 1 accepts invitations automatically, +// 0 ignores the invitations, +// -1 rejects the invitations. +pref("messenger.conversations.autoAcceptChatInvitations", 1); + +// Indicates whether the core should always close conversations closed +// by the UI or if they can be put on hold instead. +pref("messenger.conversations.alwaysClose", false); + +pref("messenger.conversations.selections.magicCopyEnabled", true); +pref("messenger.conversations.selections.ellipsis", "chrome://chat/locale/conversations.properties"); +pref("messenger.conversations.selections.systemMessagesTemplate", "chrome://chat/locale/conversations.properties"); +pref("messenger.conversations.selections.contentMessagesTemplate", "chrome://chat/locale/conversations.properties"); +pref("messenger.conversations.selections.actionMessagesTemplate", "chrome://chat/locale/conversations.properties"); + +pref("messenger.conversations.textbox.autoResize", true); +pref("messenger.conversations.textbox.defaultMaxLines", 5); + +pref("messenger.conversations.sendFormat", true); + +// this preference changes how we filter incoming messages +// 0 = no formattings +// 1 = basic formattings (bold, italic, underlined) +// 2 = permissive mode (colors, font face, font size, ...) +pref("messenger.options.filterMode", 2); + +// use "none" to disable +pref("messenger.options.emoticonsTheme", "default"); +pref("messenger.options.messagesStyle.theme", "bubbles"); +pref("messenger.options.messagesStyle.variant", "default"); +pref("messenger.options.messagesStyle.showHeader", false); +pref("messenger.options.messagesStyle.combineConsecutive", true); +// if the time interval in seconds between two messages is longer than +// this value, the messages will not be combined +pref("messenger.options.messagesStyle.combineConsecutiveInterval", 300); // 5 minutes + +pref("messenger.status.reportIdle", true); +pref("messenger.status.timeBeforeIdle", 300); // 5 minutes +pref("messenger.status.awayWhenIdle", true); +pref("messenger.status.defaultIdleAwayMessage", "chrome://chat/locale/status.properties"); +pref("messenger.status.userIconFileName", ""); +pref("messenger.status.userDisplayName", ""); + +// Default message used when quitting IRC. This is overridable per account. +pref("chat.irc.defaultQuitMessage", ""); + +// loglevel is the minimum severity level that a libpurple message +// must have to be reported in the Error Console. +// +// The possible values are: +// 0 Show all libpurple messages (PURPLE_DEBUG_ALL) +// 1 Very verbose (PURPLE_DEBUG_MISC) +// 2 Verbose (PURPLE_DEBUG_INFO) +// 3 Show warnings (PURPLE_DEBUG_WARNING) +// 4 Show errors (PURPLE_DEBUG_ERROR) +// 5 Show only fatal errors (PURPLE_DEBUG_FATAL) + +// Setting the loglevel to a value smaller than 2 will cause messages +// with an INFO or MISC severity to be displayed as warnings so that +// their file URL is clickable +#ifndef DEBUG +// By default, show only warning and errors +pref("purple.debug.loglevel", 3); +#else +// On debug builds, show warning, errors and debug information. +pref("purple.debug.loglevel", 2); +#endif + +pref("purple.logging.format", "json"); +pref("purple.logging.log_chats", true); +pref("purple.logging.log_ims", true); +pref("purple.logging.log_system", true); diff --git a/chat/components/public/Makefile.in b/chat/components/public/Makefile.in new file mode 100644 --- /dev/null +++ b/chat/components/public/Makefile.in @@ -0,0 +1,33 @@ +# 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/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = chat + +XPIDLSRCS = \ + imIAccount.idl \ + imIAccountsService.idl \ + imICommandsService.idl \ + imIContactsService.idl \ + imIConversationsService.idl \ + imICoreService.idl \ + imILogger.idl \ + imIStatusInfo.idl \ + imITagsService.idl \ + imIUserStatusInfo.idl \ + prplIConversation.idl \ + prplIMessage.idl \ + prplIPref.idl \ + prplIProtocol.idl \ + prplIRequest.idl \ + prplITooltipInfo.idl \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/chat/components/public/imIAccount.idl b/chat/components/public/imIAccount.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/imIAccount.idl @@ -0,0 +1,260 @@ +/* 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 "imIUserStatusInfo.idl" + +interface imITag; +interface imIBuddy; +interface imIAccountBuddy; +interface imIAccount; +interface prplIProtocol; +interface purpleIProxyInfo; + +/* + * Used to join chat rooms. + */ + +[scriptable, uuid(7e91accd-f04c-4787-9954-c7db4fb235fb)] +interface prplIChatRoomFieldValues: nsISupports { + AUTF8String getValue(in AUTF8String aIdentifier); + void setValue(in AUTF8String aIdentifier, in AUTF8String aValue); +}; + +[scriptable, uuid(19dff981-b125-4a70-bc1a-efc783d07137)] +interface prplIChatRoomField: nsISupports { + readonly attribute AUTF8String label; + readonly attribute AUTF8String identifier; + readonly attribute boolean required; + + const short TYPE_TEXT = 0; + const short TYPE_PASSWORD = 1; + const short TYPE_INT = 2; + + readonly attribute short type; + readonly attribute long min; + readonly attribute long max; +}; + + +/* + * This interface should be implemented by the protocol plugin. + */ +[scriptable, uuid(fb1b29cb-63ba-4335-9f44-63aea3f616a3)] +interface prplIAccount: nsISupports { + readonly attribute imIAccount imAccount; + + // observe should only be called by the imIAccount + // implementation to report user status changes that affect this account. + void observe(in nsISupports aObj, in string aEvent, + [optional] in wstring aData); + + // This should only be called by the imIAccountsService + // implementation, never directly. It will call + // imIContactsService.accountBuddyRemoved on each buddy of the + // account and close all prplIConversation instances of the account. + void remove(); + + /* Uninitialize the prplIAccount instance. This is typically done + automatically at shutdown (by the core service) or as part of + the 'remove' method. */ + void unInit(); + + void connect(); + void disconnect(); + + prplIConversation createConversation(in AUTF8String aName); + + // Used when the user wants to add a buddy to the buddy list + void addBuddy(in imITag aTag, in AUTF8String aName); + + // Used while loading the buddy list at startup. + imIAccountBuddy loadBuddy(in imIBuddy aBuddy, in imITag aTag); + + /* Request more info on a buddy (typically a chat buddy). + * The result (if any) will be provided by a user-info-received + * notification dispatched through the observer service: + * - aSubject will be an nsISimpleEnumerator of prplITooltipInfo. + * - aData will be aBuddyName. + */ + void requestBuddyInfo(in AUTF8String aBuddyName); + + readonly attribute boolean canJoinChat; + nsISimpleEnumerator getChatRoomFields(); + prplIChatRoomFieldValues getChatRoomDefaultFieldValues([optional] in AUTF8String aDefaultChatName); + /* + * Create a new chat conversation if it doesn't already exist. + */ + void joinChat(in prplIChatRoomFieldValues aComponents); + + readonly attribute AUTF8String normalizedName; + + attribute purpleIProxyInfo proxyInfo; + + // protocol specific options: those functions set the protocol + // specific options for the PurpleAccount + void setBool(in string aName, in boolean aVal); + void setInt(in string aName, in long aVal); + void setString(in string aName, in AUTF8String aVal); + + /* When a connection error occurred, this value indicates the type of error */ + readonly attribute short connectionErrorReason; + + /* Possible connection error reasons: + ERROR_NETWORK_ERROR and ERROR_ENCRYPTION_ERROR are not fatal and + should enable the automatic reconnection feature. */ + const short NO_ERROR = -1; + const short ERROR_NETWORK_ERROR = 0; + const short ERROR_INVALID_USERNAME = 1; + const short ERROR_AUTHENTICATION_FAILED = 2; + const short ERROR_AUTHENTICATION_IMPOSSIBLE = 3; + const short ERROR_NO_SSL_SUPPORT = 4; + const short ERROR_ENCRYPTION_ERROR = 5; + const short ERROR_NAME_IN_USE = 6; + const short ERROR_INVALID_SETTINGS = 7; + const short ERROR_CERT_NOT_PROVIDED = 8; + const short ERROR_CERT_UNTRUSTED = 9; + const short ERROR_CERT_EXPIRED = 10; + const short ERROR_CERT_NOT_ACTIVATED = 11; + const short ERROR_CERT_HOSTNAME_MISMATCH = 12; + const short ERROR_CERT_FINGERPRINT_MISMATCH = 13; + const short ERROR_CERT_SELF_SIGNED = 14; + const short ERROR_CERT_OTHER_ERROR = 15; + const short ERROR_OTHER_ERROR = 16; + + /* From PurpleConnectionFlags */ + + // PURPLE_CONNECTION_HTML + // Connection sends/receives in 'HTML'. + readonly attribute boolean HTMLEnabled; + + // libpurple expects messages to be HTML escaped even when HTML + // isn't enabled. Our js-prpls most likely don't want that behavior. + readonly attribute boolean HTMLEscapePlainText; + + // PURPLE_CONNECTION_NO_BGCOLOR + // Connection does not send/receive background colors. + readonly attribute boolean noBackgroundColors; + + // PURPLE_CONNECTION_AUTO_RESP + // Send auto responses when away. + readonly attribute boolean autoResponses; + + // PURPLE_CONNECTION_FORMATTING_WBFO + // The text buffer must be formatted as a whole. + readonly attribute boolean singleFormatting; + + // PURPLE_CONNECTION_NO_FONTSIZE + // Connection does not send/receive font sizes. + readonly attribute boolean noFontSizes; + + // PURPLE_CONNECTION_NO_URLDESC + // Connection does not support descriptions with links. + readonly attribute boolean noUrlDesc; + + // PURPLE_CONNECTION_NO_IMAGES + // Connection does not support sending of images. + readonly attribute boolean noImages; + + // This is currently used only by Twitter. + readonly attribute long maxMessageLength; +}; + +/* This interface should be implemented by the im core. It inherits +from prplIAccount and in most cases will forward the calls for the +inherited members to a prplIAccount account instance implemented by +the protocol plugin. */ +[scriptable, uuid(20a85b44-e220-4f23-85bf-f8523d1a2b08)] +interface imIAccount: prplIAccount { + /* Check if autologin is enabled for this account, connect it now. */ + void checkAutoLogin(); + + /* Cancel the timer that automatically reconnects the account if it was + disconnected because of a non fatal error. */ + void cancelReconnection(); + + readonly attribute AUTF8String name; + readonly attribute AUTF8String id; + readonly attribute unsigned long numericId; + readonly attribute prplIProtocol protocol; + readonly attribute prplIAccount prplAccount; + + // Save account specific preferences to disk. + void save(); + + attribute boolean autoLogin; + + /* This is the value when the preference firstConnectionState is not set. + It indicates that the account has already been successfully connected at + least once with the current parameters. */ + const short FIRST_CONNECTION_OK = 0; + /* Set when the account has never had a successful connection + with the current parameters */ + const short FIRST_CONNECTION_UNKNOWN = 1; + /* Set when the account is trying to connect for the first time + with the current parameters (removed after a successsful connection) */ + const short FIRST_CONNECTION_PENDING = 2; + /* Set at startup when the previous state was pending */ + const short FIRST_CONNECTION_CRASHED = 4; + + attribute short firstConnectionState; + + // Passwords are stored in the toolkit Password Manager. + attribute AUTF8String password; + + attribute AUTF8String alias; + + /* While an account is connecting, this attribute contains a message + indicating the current step of the connection */ + readonly attribute AUTF8String connectionStateMsg; + + /* Number of the reconnection attempt + * 0 means that no automatic reconnection currently pending + * n means the nth reconnection attempt is pending + */ + readonly attribute unsigned short reconnectAttempt; + + /* Time stamp of the next reconnection attempt */ + readonly attribute long long timeOfNextReconnect; + + /* Time stamp of the last connection (value not reliable if not connected) */ + readonly attribute long long timeOfLastConnect; + + /* Additional possible connection error reasons: + * (Use a big enough number that it can't conflict with error + * codes used in prplIAccount). + */ + const short ERROR_UNKNOWN_PRPL = 42; + const short ERROR_CRASHED = 43; + const short ERROR_MISSING_PASSWORD = 44; + + /* A message describing the connection error */ + readonly attribute AUTF8String connectionErrorMessage; + + /* Info about the connection state and flags */ + const short STATE_DISCONNECTED = 0; + const short STATE_CONNECTED = 1; + const short STATE_CONNECTING = 2; + const short STATE_DISCONNECTING = 3; + + readonly attribute short connectionState; + + /* The following 4 properties use the above connectionState value. */ + readonly attribute boolean disconnected; + readonly attribute boolean connected; + readonly attribute boolean connecting; + readonly attribute boolean disconnecting; + + /* The imIUserStatusInfo instance this account should observe for + status changes. When this is null (the default value), the + account will observe the global status. */ + attribute imIUserStatusInfo observedStatusInfo; + // Same as above, but never null (it fallbacks to the global status info). + attribute imIUserStatusInfo statusInfo; + + // imIAccount also implements an observe method but this + // observe should only be called by the prplIAccount + // implementations to report connection status changes. +}; diff --git a/chat/components/public/imIAccountsService.idl b/chat/components/public/imIAccountsService.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/imIAccountsService.idl @@ -0,0 +1,64 @@ +/* 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 "nsISimpleEnumerator.idl" +#include "imIAccount.idl" + +[scriptable, uuid(b3b6459a-5c26-47b8-8e9c-ba838b6f632a)] +interface imIAccountsService: nsISupports { + void initAccounts(); + void unInitAccounts(); + + /* This attribute is set to AUTOLOGIN_ENABLED by default. It can be set to + any other value before the initialization of this service to prevent + accounts with autoLogin enabled from being connected when libpurple is + initialized. + Any value other than the ones listed below will disable autoLogin and + display a generic message in the Account Manager. */ + attribute short autoLoginStatus; + + const short AUTOLOGIN_ENABLED = 0; + const short AUTOLOGIN_USER_DISABLED = 1; + const short AUTOLOGIN_SAFE_MODE = 2; + const short AUTOLOGIN_CRASH = 3; + const short AUTOLOGIN_START_OFFLINE = 4; + + /* The method should be used to connect all accounts with autoLogin enabled. + Some use cases: + - if the autologin was disabled at startup + - after a loss of internet connectivity that disconnected all accounts. + */ + void processAutoLogin(); + + imIAccount getAccountById(in AUTF8String aAccountId); + + /* will throw NS_ERROR_FAILURE if not found */ + imIAccount getAccountByNumericId(in unsigned long aAccountId); + + nsISimpleEnumerator getAccounts(); + + /* will fire the event account-added */ + imIAccount createAccount(in AUTF8String aName, in AUTF8String aPrpl); + + /* will fire the event account-removed */ + void deleteAccount(in AUTF8String aAccountId); +}; + +/* + account related notifications sent to nsIObserverService: + - account-added: a new account has been created + - account-removed: the account has been deleted + - account-connecting: the account is being connected + - account-connected: the account is now connected + - account-connect-error: the account is disconnect with an error. + (before account-disconnecting) + - account-disconnecting: the account is being disconnected + - account-disconnected: the account is now disconnected + - account-updated: when some settings have changed + - account-list-updated: when the list of account is reordered. + These events can be watched using an nsIObserver. + The associated imIAccount will be given as a parameter + (except for account-list-updated). +*/ diff --git a/chat/components/public/imICommandsService.idl b/chat/components/public/imICommandsService.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/imICommandsService.idl @@ -0,0 +1,77 @@ +/* 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" +interface prplIConversation; + +[scriptable, uuid(dae17fae-0d04-4d89-a817-637ef433383f)] +interface imICommand: nsISupports { + readonly attribute AUTF8String name; + + // Help message displayed when the user types /help . + // Format: : + // Example: "help <name>: show the help message for the <name> + // command, or the list of possible commands when used without + // parameter." + readonly attribute AUTF8String helpString; + + const short CONTEXT_IM = 1; + const short CONTEXT_CHAT = 2; + const short CONTEXT_ALL = CONTEXT_IM | CONTEXT_CHAT; + readonly attribute long usageContext; + + const short PRIORITY_LOW = -1000; + const short PRIORITY_DEFAULT = 0; + const short PRIORITY_PRPL = 1000; + const short PRIORITY_HIGH = 4000; + // Any integer value is usable as a priority. + // 0 is the default priority. + // < 0 is lower priority. + // > 0 is higher priority. + // Commands registered by protocol plugins will usually use PRIORITY_PRPL. + readonly attribute long priority; + + // Will return true if the command handled the message (it should not be sent). + // The leading slash, the command name and the following space are not included + // in the aMessage parameter. + boolean run(in AUTF8String aMessage, + [optional] in prplIConversation aConversation); +}; + +[scriptable, uuid(467709a0-0bed-4f44-9bdc-13f78b9eaeba)] +interface imICommandsService: nsISupports { + void initCommands(); + void unInitCommands(); + + // Commands registered without a protocol id will work for all protocols. + // Registering several commands of the same name with the same + // protocol id or no protocol id will replace the former command + // with the latter. + void registerCommand(in imICommand aCommand, + [optional] in AUTF8String aPrplId); + + // aPrplId should be the same as what was used for the command registration. + void unregisterCommand(in AUTF8String aCommandName, + [optional] in AUTF8String aPrplId); + + void listCommandsForConversation( + [optional] in prplIConversation aConversation, + [optional] out unsigned long commandCount, + [retval, array, size_is(commandCount)] out imICommand commands); + + void listCommandsForProtocol(in AUTF8String aPrplId, + [optional] out unsigned long commandCount, + [retval, array, size_is(commandCount)] out imICommand commands); + + // Will return true if a command handled the message (it should not be sent). + // The aConversation parameters is required to execute protocol specific + // commands. Application global commands will work without it. + boolean executeCommand(in AUTF8String aMessage, + [optional] in prplIConversation aConversation); +}; + +%{ C++ +#define IM_COMMANDS_SERVICE_CONTRACTID \ + "@mozilla.org/chat/commands-service;1" +%} diff --git a/chat/components/public/imIContactsService.idl b/chat/components/public/imIContactsService.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/imIContactsService.idl @@ -0,0 +1,236 @@ +/* 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 "imIStatusInfo.idl" +#include "imITagsService.idl" +#include "nsISupports.idl" +#include "nsIObserver.idl" +#include "nsISimpleEnumerator.idl" + +interface imIContact; +interface imIBuddy; +interface imIAccountBuddy; +interface imIAccount; +interface prplIProtocol; + +[scriptable, uuid(f1619b49-310b-47aa-ab1c-238aba084c62)] +interface imIContactsService: nsISupports { + void initContacts(); + void unInitContacts(); + + imIContact getContactById(in long aId); + imIBuddy getBuddyById(in long aId); + imIBuddy getBuddyByNameAndProtocol(in AUTF8String aNormalizedName, + in prplIProtocol aPrpl); + + // These 3 functions are called by the protocol plugins when + // synchronizing the buddy list with the server stored list, + // or after user operations have been performed. + void accountBuddyAdded(in imIAccountBuddy aAccountBuddy); + void accountBuddyRemoved(in imIAccountBuddy aAccountBuddy); + void accountBuddyMoved(in imIAccountBuddy aAccountBuddy, + in imITag aOldTag, in imITag aNewTag); + + // These methods are called by the imIAccountsService implementation + // to keep the accounts table in sync with accounts stored in the + // preferences. + + // Called when an account is created or loaded to store the new + // account or ensure it doesn't conflict with an existing account + // (to detect database corruption). + // Will throw if a stored account has the id aId but a different + // username or prplId. + void storeAccount(in unsigned long aId, in AUTF8String aUserName, + in AUTF8String aPrplId); + // Check if an account id already exists in the database. + boolean accountIdExists(in unsigned long aId); + // Called when deleting an account to remove it from blist.sqlite. + void forgetAccount(in unsigned long aId); +}; + +[scriptable, uuid(f585b0df-f6ad-40d5-9de4-c58b14af13e4)] +interface imIContact: imIStatusInfo { + // The id will be positive if the contact is real (stored in the + // SQLite database) and negative if the instance is a dummy contact + // holding only a single buddy without aliases or additional tags. + readonly attribute long id; + attribute AUTF8String alias; + + void getTags([optional] out unsigned long tagCount, + [retval, array, size_is(tagCount)] out imITag tags); + + // Will do nothing if the contact already has aTag. + void addTag(in imITag aTag); + // Will throw if the contact doesn't have aTag or doesn't have any other tag. + void removeTag(in imITag aTag); + + readonly attribute imIBuddy preferredBuddy; + void getBuddies([optional] out unsigned long buddyCount, + [retval, array, size_is(buddyCount)] out imIBuddy buddies); + + // Move all the buddies of aContact into the current contact, + // and copy all its tags. + void mergeContact(in imIContact aContact); + + // Change the position of aBuddy in the current contact. + // The new position is the current position of aBeforeBuddy if it is + // specified, or at the end otherwise. + void moveBuddyBefore(in imIBuddy aBuddy, [optional] in imIBuddy aBeforeBuddy); + + // Remove aBuddy from its current contact and append it to the list + // of buddies of the current contact. + // aBuddy should not already be attached to the current contact. + void adoptBuddy(in imIBuddy aBuddy); + + // Returns a new contact that contains only aBuddy, and has the same + // list of tags. + // Will throw if aBuddy is not a buddy of the contact. + imIContact detachBuddy(in imIBuddy aBuddy); + + // remove the contact from the buddy list. Will also remove the + // associated buddies. + void remove(); + + void addObserver(in nsIObserver aObserver); + void removeObserver(in nsIObserver aObserver); + /* Observers will be notified of changes related to the contact. + * aSubject will point to the imIContact object + * (with some exceptions for contact-moved-* notifications). + * + * Fired notifications: + * contact-availability-changed + * when either statusType or availabilityDetails has changed. + * contact-signed-on + * contact-signed-off + * contact-status-changed + * when either statusType or statusText has changed. + * contact-display-name-changed + * when the alias (or serverAlias of the most available buddy if + * no alias is set) has changed. + * The old display name is provided in aData. + * contact-preferred-buddy-changed + * The buddy that would be favored to start a conversation has changed. + * contact-moved, contact-moved-in, contact-moved-out + * contact-moved is notified through the observer service + * contact-moved-in is notified to + * - the contact observers (aSubject is the new tag) + * - the new tag (aSubject is the contact instance) + * contact-moved-out is notified to + * - the contact observers (aSubject is the old tag) + * - the old tag (aSubject is the contact instance) + * contact-no-longer-dummy + * When a real contact is created to replace a dummy contact. + * The old (negative) id will be given in aData. + * See also the comment above the 'id' attribute. + * contact-icon-changed + * + * Observers will also receive all the (forwarded) notifications + * from the linked buddies (imIBuddy instances) and their account + * buddies (imIAccountBuddy instances). + */ + + // Exposed for add-on authors. All internal calls will come from the + // imIContact implementation itself so it wasn't required to expose this. + // This can be used to dispatch custom notifications to the + // observers of the contact and its tags. + // The notification will also be forwarded to the observer service. + void notifyObservers(in nsISupports aObj, in string aEvent, + [optional] in wstring aData); +}; + + +[scriptable, uuid(fea582a0-3839-4d80-bcab-0ff82ae8f97f)] +interface imIBuddy: imIStatusInfo { + readonly attribute long id; + readonly attribute prplIProtocol protocol; + readonly attribute AUTF8String userName; // may be formatted + readonly attribute AUTF8String normalizedName; // normalized userName + // The optional server alias is in displayName (inherited from imIStatusInfo) + // displayName = serverAlias || userName. + + readonly attribute imIContact contact; + readonly attribute imIAccountBuddy preferredAccountBuddy; + void getAccountBuddies([optional] out unsigned long accountBuddyCount, + [retval, array, size_is(accountBuddyCount)] out imIAccountBuddy accountBuddies); + + // remove the buddy from the buddy list. If the contact becomes empty, it will be removed too. + void remove(); + + void addObserver(in nsIObserver aObserver); + void removeObserver(in nsIObserver aObserver); + /* Observers will be notified of changes related to the buddy. + * aSubject will point to the imIBuddy object. + * Fired notifications: + * buddy-availability-changed + * when either statusType or availabilityDetails has changed. + * buddy-signed-on + * buddy-signed-off + * buddy-status-changed + * when either statusType or statusText has changed. + * buddy-display-name-changed + * when the serverAlias has changed. + * The old display name is provided in aData. + * buddy-preferred-account-changed + * The account that would be favored to start a conversation has changed. + * buddy-icon-changed + * + * Observers will also receive all the (forwarded) notifications + * from the linked account buddies (imIAccountBuddy instances). + */ + + // Exposed for add-on authors. All internal calls will come from the + // imIBuddy implementation itself so it wasn't required to expose this. + // This can be used to dispatch custom notifications to the + // observers of the buddy, its contact and its tags. + // The contact will forward the notifications to the observer service. + void notifyObservers(in nsISupports aObj, in string aEvent, + [optional] in wstring aData); + + // observe should only be called by the imIAccountBuddy + // implementations to report changes. + void observe(in nsISupports aObj, in string aEvent, + [optional] in wstring aData); +}; + +/* imIAccountBuddy implementations can send notifications to their buddy: + * + * For all of them, aSubject points to the imIAccountBuddy object. + * + * Supported notifications: + * account-buddy-availability-changed + * when either statusType or availabilityDetails has changed. + * account-buddy-signed-on + * account-buddy-signed-off + * account-buddy-status-changed + * when either statusType or statusText has changed. + * account-buddy-display-name-changed + * when the serverAlias has changed. + * The old display name is provided in aData. + * account-buddy-icon-changed + * + * All notifications (even unsupported ones) will be forwarded to the contact, + * its tags and nsObserverService. + */ +[scriptable, uuid(114d24ff-56a1-4fd6-9822-4992efb6e036)] +interface imIAccountBuddy: imIStatusInfo { + // The setter is for internal use only. buddy will be set by the + // Contacts service when accountBuddyAdded is called on this + // instance of imIAccountBuddy. + attribute imIBuddy buddy; + readonly attribute imIAccount account; + // Setting the tag will move the buddy to a different group on the + // server-stored buddy list. + attribute imITag tag; + readonly attribute AUTF8String userName; + readonly attribute AUTF8String normalizedName; + attribute AUTF8String serverAlias; + + // remove the buddy from the buddy list of this account. + void remove(); + + // Called by the contacts service during its uninitialization to + // notify that all references kept to imIBuddy or imIAccount + // instances should be released now. + void unInit(); +}; diff --git a/chat/components/public/imIConversationsService.idl b/chat/components/public/imIConversationsService.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/imIConversationsService.idl @@ -0,0 +1,66 @@ +/* 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 "imIContactsService.idl" + +interface prplIMessage; + +[scriptable, uuid(a09faf46-bb9d-402f-b460-89f8d7827ff1)] +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. + void systemMessage(in AUTF8String aMessage, [optional] in boolean aIsError); + + attribute prplIConversation target; + + // Number of unread messages (all messages, including system + // messages are counted). + readonly attribute unsigned long unreadMessageCount; + // Number of unread incoming messages targeted at the user (= IMs or + // message containing the user's nick in MUCs). + readonly attribute unsigned long unreadTargetedMessageCount; + // Number of unread incoming messages (both targeted and untargeted + // messages are counted). + readonly attribute unsigned long unreadIncomingMessageCount; + // Reset all unread message counts. + void markAsRead(); + + // Call this to give the core an opportunity to close an inactive + // 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); +}; + +[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 + // conversation and set it. + void addConversation(in prplIConversation aConversation); + void removeConversation(in prplIConversation aConversation); + + void getUIConversations([optional] out unsigned long conversationCount, + [retval, array, size_is(conversationCount)] out imIConversation conversations); + imIConversation getUIConversation(in prplIConversation aConversation); + imIConversation getUIConversationByContactId(in long aId); + + nsISimpleEnumerator getConversations(); + prplIConversation getConversationById(in unsigned long aId); + prplIConversation getConversationByNameAndAccount(in AUTF8String aName, + in imIAccount aAccount, + in boolean aIsChat); +}; diff --git a/chat/components/public/imICoreService.idl b/chat/components/public/imICoreService.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/imICoreService.idl @@ -0,0 +1,29 @@ +/* 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 "imIUserStatusInfo.idl" +#include "nsISimpleEnumerator.idl" +#include "prplIProtocol.idl" + +[scriptable, uuid(205d4b2b-1ccf-4879-9ef1-f08942566151)] +interface imICoreService: nsISupports { + readonly attribute boolean initialized; + + // This will emit a prpl-init notification. After this point the 'initialized' + // attribute will be 'true' and it's safe to access the services for accounts, + // contacts, conversations and commands. + void init(); + + // This will emit a prpl-quit notification. This is the last opportunity to + // use the aforementioned services before they are uninitialized. + void quit(); + + // returns an enumerator on a pplIProtocol array + nsISimpleEnumerator getProtocols(); + + prplIProtocol getProtocolById(in AUTF8String aProtocolId); + + readonly attribute imIUserStatusInfo globalUserStatus; +}; diff --git a/chat/components/public/imILogger.idl b/chat/components/public/imILogger.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/imILogger.idl @@ -0,0 +1,57 @@ +/* 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 "nsISimpleEnumerator.idl" +#include "nsIFile.idl" + +interface imIAccount; +interface imIAccountBuddy; +interface imIBuddy; +interface imIContact; +interface prplIConversation; +interface prplIMessage; + +[scriptable, uuid(5bc06f3b-33a1-412b-a4d8-4fc7ba4c962b)] +interface imILogConversation: nsISupports { + readonly attribute AUTF8String title; + readonly attribute AUTF8String name; + + // Simplified account implementation: + // - alias will always be empty + // - name (always the normalizedName) + // - statusInfo will return Services.core.globalUserStatus + // - 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); +}; + +[scriptable, uuid(164ff6c3-ca64-4880-b8f3-67eb1817955f)] +interface imILog: nsISupports { + readonly attribute AUTF8String path; + readonly attribute PRTime time; + readonly attribute AUTF8String format; + // Will return null if the log format isn't json. + imILogConversation getConversation(); +}; + +[scriptable, uuid(327ba58c-ee9c-4d1c-9216-fd505c45a3e0)] +interface imILogger: nsISupports { + imILog getLogFromFile(in nsIFile aFile, [optional] in boolean aGroupByDay); + nsIFile getLogFileForOngoingConversation(in prplIConversation aConversation); + nsISimpleEnumerator getLogsForAccountBuddy(in imIAccountBuddy aAccountBuddy); + nsISimpleEnumerator getLogsForBuddy(in imIBuddy aBuddy); + nsISimpleEnumerator getLogsForContact(in imIContact aContact); + nsISimpleEnumerator getLogsForConversation(in prplIConversation aConversation, + [optional] in boolean aGroupByDay); + nsISimpleEnumerator getSystemLogsForAccount(in imIAccount aAccount); + nsISimpleEnumerator getSimilarLogs(in imILog aLog, + [optional] in boolean aGroupByDay); +}; diff --git a/chat/components/public/imIStatusInfo.idl b/chat/components/public/imIStatusInfo.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/imIStatusInfo.idl @@ -0,0 +1,54 @@ +/* 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 "nsISimpleEnumerator.idl" +#include "prplIConversation.idl" + +[scriptable, uuid(f13dc4fc-5334-45cb-aa58-a92851955e55)] +interface imIStatusInfo: nsISupports { + // Name suitable for display in the UI. Can either be the username, + // the server side alias, or the user set local alias of the contact. + readonly attribute AUTF8String displayName; + readonly attribute AUTF8String buddyIconFilename; + + const short STATUS_UNKNOWN = 0; + const short STATUS_OFFLINE = 1; + const short STATUS_INVISIBLE = 2; + const short STATUS_MOBILE = 3; + const short STATUS_IDLE = 4; + const short STATUS_AWAY = 5; + const short STATUS_UNAVAILABLE = 6; + const short STATUS_AVAILABLE = 7; + + // numerical value used to compare the availability of two buddies + // based on their current status. + // Use it only for immediate comparisons, do not store the value, + // it can change between versions for a same status of the buddy. + readonly attribute long statusType; + + readonly attribute boolean online; // (statusType > STATUS_OFFLINE) + readonly attribute boolean available; // (statusType == STATUS_AVAILABLE) + readonly attribute boolean idle; // (statusType == STATUS_IDLE) + readonly attribute boolean mobile; // (statusType == STATUS_MOBILE) + + readonly attribute AUTF8String statusText; + + // Gives more detail to compare the availability of two buddies with the same + // status type. + // Example: 2 buddies may have been idle for various amounts of times. + readonly attribute long availabilityDetails; + + // True if the buddy is online or if the account supports sending + // offline messages to the buddy. + readonly attribute boolean canSendMessage; + + // enumerator of purpleTooltipInfo components + nsISimpleEnumerator getTooltipInfo(); + + // Will select the buddy automatically based on availability, and + // the account (if needed) based on the account order in the account + // manager. + prplIConversation createConversation(); +}; diff --git a/chat/components/public/imITagsService.idl b/chat/components/public/imITagsService.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/imITagsService.idl @@ -0,0 +1,56 @@ +/* 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 "nsIObserver.idl" + +interface imIContact; + +[scriptable, uuid(c211e5e2-f0a4-4a86-9e4c-3f6b905628a5)] +interface imITag: nsISupports { + readonly attribute long id; + attribute AUTF8String name; + + // Get an array of all the contacts associated with this tag. + // Contacts can either "have the tag" (added by user action) or + // have inherited the tag because it was the server side group for + // one of the AccountBuddy of the contact. + void getContacts([optional] out unsigned long contactCount, + [retval, array, size_is(contactCount)] out imIContact contacts); + + void addObserver(in nsIObserver aObserver); + void removeObserver(in nsIObserver aObserver); + /* Observers will be notified of changes related to the contacts + * that have the tag: contact-*, buddy-*, account-buddy-* + * notifications forwarded respectively from the imIContact, + * imIBuddy and imIAccountBuddy instances. + */ + + // Exposed for add-on authors. All internal calls will come from the + // imITag implementation itself so it wasn't required to expose this. + // This can be used to dispatch custom notifications to the + // observers of the tag. + void notifyObservers(in nsISupports aObj, in string aEvent, + [optional] in wstring aData); +}; + +[scriptable, uuid(f799a9c2-23f2-4fd1-96fb-515bad238f8c)] +interface imITagsService: nsISupports { + // Create a new tags or return the existing one if it already exists + imITag createTag(in AUTF8String aName); + // Get an existing tag by (numeric) id. Returns null if not found. + imITag getTagById(in long aId); + // Get an existing tag by name (will do an SQL query). Returns null + // if not found. + imITag getTagByName(in AUTF8String aName); + // Get an array of all existing tags. + void getTags([optional] out unsigned long tagCount, + [retval, array, size_is(tagCount)] out imITag tags); + + boolean isTagHidden(in imITag aTag); + void hideTag(in imITag aTag); + void showTag(in imITag aTag); + + readonly attribute imITag otherContactsTag; +}; diff --git a/chat/components/public/imIUserStatusInfo.idl b/chat/components/public/imIUserStatusInfo.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/imIUserStatusInfo.idl @@ -0,0 +1,43 @@ +/* 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 "nsIObserver.idl" + +//forward declarations +interface nsILocalFile; +interface nsIFileURL; + +[scriptable, uuid(817918fa-1f4b-4254-9cdb-f906da91c45d)] +interface imIUserStatusInfo: nsISupports { + + readonly attribute AUTF8String statusText; + + // See imIStatusInfo for the values. + readonly attribute short statusType; + + // Only works with STATUS_OFFLINE, STATUS_UNAVAILABLE, STATUS_AWAY, + // STATUS_AVAILABLE and STATUS_INVISIBLE. + // - When called with the status type STATUS_UNSET, only the status + // message will be changed. + // - When called with STATUS_OFFLINE, the aMessage parameter is ignored. + void setStatus(in short aStatus, in AUTF8String aMessage); + + /* Will fire an user-icon-changed notificaton. */ + void setUserIcon(in nsILocalFile aIconFile); + + nsIFileURL getUserIcon(); + + /* The setter will fire an user-display-name-changed notificaton. */ + attribute AUTF8String displayName; + + void addObserver(in nsIObserver aObserver); + void removeObserver(in nsIObserver aObserver); + /* Observers will receive the following notifications: + * status-changed (when either the status type or text has changed) + * user-icon-changed + * user-display-name-changed + * idle-time-changed + */ +}; diff --git a/chat/components/public/prplIConversation.idl b/chat/components/public/prplIConversation.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/prplIConversation.idl @@ -0,0 +1,146 @@ +/* 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 "nsISimpleEnumerator.idl" +#include "nsIObserver.idl" + +interface imIAccountBuddy; +interface imIAccount; +interface nsIURI; +interface nsIDOMDocument; + +/* + * This is the XPCOM purple conversation component, a proxy for PurpleConversation. + */ + +[scriptable, uuid(74fca337-b8e7-4e5d-81cd-8aba3dba9208)] +interface prplIConversation: nsISupports { + + /* Indicate if this conversation implements prplIConvIM or prplIConvChat */ + readonly attribute boolean isChat; + + /* The account used for this conversation */ + readonly attribute imIAccount account; + + /* The name of the conversation, typically in English */ + readonly attribute AUTF8String name; + + /* The normalized name of the conversation (suitable for a log file name) */ + readonly attribute AUTF8String normalizedName; + + /* The title of the conversation, typically localized */ + readonly attribute AUTF8String title; + + /* 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); + + /* Send information about the current typing state to the server. + aLength should contain the length of the content currently in the text field. + A length value of 0 will make the method send a NOT_TYPING message. */ + void sendTyping(in unsigned long aLength); + + /* Un-initialize the conversation. Will be called by + purpleCoreService::RemoveConversation when the conversation is + closed or by purpleCoreService::Quit while exiting. */ + void unInit(); + + /* When the conversation is closed from the UI. */ + void close(); + + /* Method to add or remove an observer */ + void addObserver(in nsIObserver aObserver); + void removeObserver(in nsIObserver aObserver); + + /* Observers will be all receive new-text notifications. + aSubject will contain the message (prplIMessage) */ +}; + +[scriptable, uuid(0c072a80-103a-4992-b249-8e442b5f0d46)] +interface prplIConvIM: prplIConversation { + + /* The buddy at the remote end of the conversation */ + readonly attribute imIAccountBuddy buddy; + + /* The remote buddy is not currently typing */ + const short NOT_TYPING = 0; + + /* The remote buddy is currently typing */ + const short TYPING = 1; + + /* The remote buddy started typing, but has stopped typing */ + const short TYPED = 2; + + /* The typing state of the remote buddy. + The value is NOT_TYPING, TYPING or TYPED. */ + readonly attribute short typingState; +}; + +[scriptable, uuid(c7f11466-f479-4f12-a581-b99713b8ecc0)] +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 starting a + private conversation or calling 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; + + /* Whether the protocol plugin can set a topic. Doesn't check that + the user has the necessary rights in the current conversation. */ + readonly attribute boolean topicSettable; + + /* The nick seen by other people in the room */ + readonly attribute AUTF8String nick; + + /* This is true when we left the chat but kept the conversation open */ + readonly attribute boolean left; + + /* Observers will receive chat-buddy-add, chat-buddy-update, + chat-buddy-remove and chat-update-topic notifications. + + aSubject will be of type: + nsISimpleEnumerator of prplIConvChatBuddy for chat-buddy-add, + nsISimpleEnumerator of nsISupportsString for chat-buddy-remove, + prplIConvChatBuddy for chat-buddy-update, + null for chat-update-topic. + + aData will contain the old nick for chat-buddy-update if the name + has changed. + */ +}; + +/* This represents a participant in a chat room */ +[scriptable, uuid(33f6ef81-1d23-484e-b4e0-14fffa0c4392)] +interface prplIConvChatBuddy: nsISupports { + + /* The name of the buddy */ + readonly attribute AUTF8String name; + + /* The alias (FIXME: can this be non-null if buddy is null?) */ + readonly attribute AUTF8String alias; + + /* Indicates if this chat buddy corresponds to a buddy in our buddy list */ + readonly attribute boolean buddy; + + /* PurpleConvChatBuddyFlags flags; (ops, voice etc.) */ + readonly attribute boolean noFlags; + readonly attribute boolean voiced; + readonly attribute boolean halfOp; + readonly attribute boolean op; + readonly attribute boolean founder; + readonly attribute boolean typing; +}; diff --git a/chat/components/public/prplIMessage.idl b/chat/components/public/prplIMessage.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/prplIMessage.idl @@ -0,0 +1,78 @@ +/* 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 "nsIRunnable.idl" +#include "prplIConversation.idl" + +/* + * An action that the user may perform in relation to a particular message. + */ +[scriptable, uuid(7e470f0e-d948-4d9a-b8dc-4beecf6554b9)] +interface prplIMessageAction: nsIRunnable +{ + /* The protocol plugins need to provide a localized label suitable + for being shown in the user interface (for example as a context + menu item). */ + readonly attribute AUTF8String label; +}; + +[scriptable, uuid(d9f0ca7f-ee59-4657-a3dd-f458c204ca45)] +interface prplIMessage: nsISupports { + /* The uniqueness of the message id is only guaranteed across + messages of a conversation, not across all messages created + during the execution of the application. */ + readonly attribute unsigned long id; + readonly attribute AUTF8String who; + readonly attribute AUTF8String alias; + readonly attribute AUTF8String originalMessage; + attribute AUTF8String message; + readonly attribute AUTF8String iconURL; + readonly attribute PRTime time; + readonly attribute prplIConversation conversation; + + /* Holds the sender color for Chats. + Empty string by default, it is set by the conversation binding. */ + attribute AUTF8String color; + + /* PURPLE_MESSAGE_SEND = 0x0001, /**< Outgoing message. */ + readonly attribute boolean outgoing; + /* PURPLE_MESSAGE_RECV = 0x0002, /**< Incoming message. */ + readonly attribute boolean incoming; + /* PURPLE_MESSAGE_SYSTEM = 0x0004, /**< System message. */ + readonly attribute boolean system; + /* PURPLE_MESSAGE_AUTO_RESP = 0x0008, /**< Auto response. */ + readonly attribute boolean autoResponse; + /* PURPLE_MESSAGE_ACTIVE_ONLY = 0x0010, /**< Hint to the UI that this + message should not be + shown in conversations + which are only open for + internal UI purposes + (e.g. for contact-aware + conversions). */ + /* PURPLE_MESSAGE_NICK = 0x0020, /**< Contains your nick. */ + readonly attribute boolean containsNick; + /* PURPLE_MESSAGE_NO_LOG = 0x0040, /**< Do not log. */ + readonly attribute boolean noLog; + /* PURPLE_MESSAGE_ERROR = 0x0200, /**< Error message. */ + readonly attribute boolean error; + /* PURPLE_MESSAGE_DELAYED = 0x0400, /**< Delayed message. */ + readonly attribute boolean delayed; + /* PURPLE_MESSAGE_RAW = 0x0800, /**< "Raw" message - don't + apply formatting */ + readonly attribute boolean noFormat; + /* PURPLE_MESSAGE_IMAGES = 0x1000, /**< Message contains images */ + readonly attribute boolean containsImages; + /* PURPLE_MESSAGE_NOTIFY = 0x2000, /**< Message is a notification */ + readonly attribute boolean notification; + /* PURPLE_MESSAGE_NO_LINKIFY = 0x4000 /**< Message should not be auto-linkified */ + readonly attribute boolean noLinkification; + + /* An array of actions the user may perform on this message. + The first action will be the 'default' and may be performed + automatically when the message is double clicked. + 'Reply' is usually a good default action. */ + void getActions([optional] out unsigned long actionCount, + [retval, array, size_is(actionCount)] out prplIMessageAction actions); +}; diff --git a/chat/components/public/prplIPref.idl b/chat/components/public/prplIPref.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/prplIPref.idl @@ -0,0 +1,36 @@ +/* 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 "nsISimpleEnumerator.idl" + +/* + * This is a proxy for libpurple PurpleAccountOption + */ + +[scriptable, uuid(e781563f-9088-4a96-93e3-4fb6f5ce6a77)] +interface prplIPref: nsISupports { + const short typeBool = 1; + const short typeInt = 2; + const short typeString = 3; + const short typeList = 4; + + readonly attribute AUTF8String name; + readonly attribute AUTF8String label; + readonly attribute short type; + readonly attribute boolean masked; + + boolean getBool(); + long getInt(); + AUTF8String getString(); + // enumerator of prplIKeyValuePair + nsISimpleEnumerator getList(); + AUTF8String getListDefault(); +}; + +[scriptable, uuid(8fc16882-ba8e-432a-999f-0d4dc104234b)] +interface prplIKeyValuePair: nsISupports { + readonly attribute AUTF8String name; + readonly attribute AUTF8String value; +}; diff --git a/chat/components/public/prplIProtocol.idl b/chat/components/public/prplIProtocol.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/prplIProtocol.idl @@ -0,0 +1,100 @@ +/* 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 "nsISimpleEnumerator.idl" +#include "imIAccount.idl" + +[scriptable, uuid(7d302db0-3813-4c51-8372-c7eb5fc9f3d3)] +interface prplIProtocol: nsISupports { + // aId is the prpl id, this method is used so that classes + // implementing several protocol plugins can know which protocol is + // desired for this instance. + void init(in AUTF8String aId); + + readonly attribute AUTF8String name; + readonly attribute AUTF8String id; + readonly attribute AUTF8String normalizedName; + + // returns a chrome URI pointing to a folder that contains the files: + // icon.png icon32.png and icon48.png + readonly attribute AUTF8String iconBaseURI; + + // returns an enumerator of prplIPref + nsISimpleEnumerator getOptions(); + + // returns an enumerator of prplIUsernameSplit + nsISimpleEnumerator getUsernameSplit(); + + // descriptive text to put in the username input box when it is empty + readonly attribute AUTF8String usernameEmptyText; + + // Use this function to avoid attempting to create duplicate accounts + boolean accountExists(in AUTF8String aName); + + // These should all be stuff that some plugins can do and others can't. + + // OPT_PROTO_UNIQUE_CHATNAME Use a unique name, not an alias, for + // chat rooms. + // XMPP lets you choose what name you want for chat. So it + // shouldn't be pulling the alias for when you're in chat; it gets + // annoying. + readonly attribute boolean uniqueChatName; + + // OPT_PROTO_CHAT_TOPIC Chat rooms have topics. + // IRC and XMPP support this. + readonly attribute boolean chatHasTopic; + + // OPT_PROTO_NO_PASSWORD Don't require passwords for sign-in. + // Zephyr doesn't require passwords, so there's no need for a + // password prompt. + readonly attribute boolean noPassword; + + // OPT_PROTO_MAIL_CHECK Notify on new mail. + // MSN and Yahoo notify you when you have new mail. + readonly attribute boolean newMailNotification; + + // OPT_PROTO_IM_IMAGE Images in IMs. + // Oscar lets you send images in direct IMs. + readonly attribute boolean imagesInIM; + + // OPT_PROTO_PASSWORD_OPTIONAL Allow passwords to be optional. + // Passwords in IRC are optional, and are needed for certain + // functionality. + readonly attribute boolean passwordOptional; + + // OPT_PROTO_USE_POINTSIZE Allows font size to be specified in sane + // point size. + // Probably just XMPP and Y!M + readonly attribute boolean usePointSize; + + // OPT_PROTO_REGISTER_NOSCREENNAME Set the Register button active + // when screenname is not given. + // Gadu-Gadu doesn't need a screenname to register new account. + readonly attribute boolean registerNoScreenName; + + // OPT_PROTO_SLASH_COMMANDS_NATIVE Indicates that slash commands + // are native to this protocol. + // Used as a hint that unknown commands should not be sent as messages. + readonly attribute boolean slashCommandsNative; + + // Indicates if the protocol plugin can use a purpleIProxy for the + // account, or uses the Mozilla proxy settings for all accounts. + readonly attribute boolean usePurpleProxy; + + // Get the protocol specific part of an already initialized + // imIAccount instance. + prplIAccount getAccount(in imIAccount aImAccount); +}; + +[scriptable, uuid(20c4971a-f7c2-4781-8e85-69fee7b83a3d)] +interface prplIUsernameSplit: nsISupports { + readonly attribute AUTF8String label; + readonly attribute AUTF8String defaultValue; + readonly attribute char separator; + + /* reverse is true if the separator should be found starting at + the end of the string, false otherwise. */ + readonly attribute boolean reverse; +}; diff --git a/chat/components/public/prplIRequest.idl b/chat/components/public/prplIRequest.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/prplIRequest.idl @@ -0,0 +1,36 @@ +/* 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" + +interface imIAccount; +interface nsIDOMWindow; +interface nsIWebProgress; + +/* This interface is for use in the browser-request notification, to + let protocol plugins open a browser window. This is an unfortunate + necessity for protocols that require an OAuth authentication. */ +[scriptable, uuid(b89dbb38-0de4-11e0-b3d0-0002e304243c)] +interface prplIRequestBrowser: nsISupports { + readonly attribute AUTF8String promptText; + readonly attribute imIAccount account; + readonly attribute AUTF8String url; + void cancelled(); + void loaded(in nsIDOMWindow aWindow, + in nsIWebProgress aWebProgress); +}; + +/* This interface is used for buddy authorization requests, when the + user needs to confirm if a remote contact should be allowed to see + his presence information. It is implemented by the aSubject + parameter of the buddy-authorization-request and + buddy-authorization-request-canceled notifications. +*/ +[scriptable, uuid(a55c1e24-17cc-4ddc-8c64-3bc315a3c3b1)] +interface prplIBuddyRequest: nsISupports { + readonly attribute imIAccount account; + readonly attribute AUTF8String userName; + void grant(); + void deny(); +}; diff --git a/chat/components/public/prplITooltipInfo.idl b/chat/components/public/prplITooltipInfo.idl new file mode 100644 --- /dev/null +++ b/chat/components/public/prplITooltipInfo.idl @@ -0,0 +1,21 @@ +/* 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" + +/* + * This interface provides access to the content of a + * PurpleNotifyUserInfoEntry structure. + */ + +[scriptable, uuid(dd535741-4b04-49ca-8df6-08f8577fe52b)] +interface prplITooltipInfo: nsISupports { + const short pair = 0; + const short sectionBreak = 1; + const short sectionHeader = 2; + + readonly attribute short type; + readonly attribute AUTF8String label; + readonly attribute AUTF8String value; +}; diff --git a/chat/components/src/Makefile.in b/chat/components/src/Makefile.in new file mode 100644 --- /dev/null +++ b/chat/components/src/Makefile.in @@ -0,0 +1,22 @@ +# 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/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +EXTRA_COMPONENTS = \ + imAccounts.js imAccounts.manifest \ + imCommands.js imCommands.manifest \ + imContacts.js imContacts.manifest \ + imConversations.js imConversations.manifest \ + imCore.js imCore.manifest \ + logger.js logger.manifest \ + smileProtocolHandler.js smileProtocolHandler.manifest \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/chat/components/src/imAccounts.js b/chat/components/src/imAccounts.js new file mode 100644 --- /dev/null +++ b/chat/components/src/imAccounts.js @@ -0,0 +1,967 @@ +/* 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/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; +Cu.import("resource:///modules/imXPCOMUtils.jsm"); +Cu.import("resource:///modules/imServices.jsm"); + +const kPrefAutologinPending = "messenger.accounts.autoLoginPending"; +const kPrefMessengerAccounts = "messenger.accounts"; +const kPrefAccountPrefix = "messenger.account."; +const kAccountKeyPrefix = "account"; +const kAccountOptionPrefPrefix = "options."; +const kPrefAccountName = "name"; +const kPrefAccountPrpl = "prpl"; +const kPrefAccountAutoLogin = "autoLogin"; +const kPrefAccountAutoJoin = "autoJoin"; +const kPrefAccountAlias = "alias"; +const kPrefAccountFirstConnectionState = "firstConnectionState"; + +const kPrefConvertOldPasswords = "messenger.accounts.convertOldPasswords"; +const kPrefAccountPassword = "password"; + +XPCOMUtils.defineLazyGetter(this, "LoginManager", function() + Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager) +); + +XPCOMUtils.defineLazyGetter(this, "_", function() + l10nHelper("chrome://chat/locale/accounts.properties") +); + +var gUserCanceledMasterPasswordPrompt = false; +var gConvertingOldPasswords = false; + +var SavePrefTimer = { + saveNow: function() { + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + } + Services.prefs.savePrefFile(null); + }, + _timer: null, + unInitTimer: function() { + if (this._timer) + this.saveNow(); + }, + initTimer: function() { + if (!this._timer) + this._timer = setTimeout(this.saveNow.bind(this), 5000); + } +}; + +var AutoLoginCounter = { + _count: 0, + startAutoLogin: function() { + ++this._count; + if (this._count != 1) + return; + Services.prefs.setIntPref(kPrefAutologinPending, Date.now() / 1000); + SavePrefTimer.saveNow(); + }, + finishedAutoLogin: function() { + --this._count; + if (this._count != 0) + return; + Services.prefs.deleteBranch(kPrefAutologinPending); + SavePrefTimer.initTimer(); + } +}; + +function UnknownProtocol(aPrplId) +{ + this.id = aPrplId; +} +UnknownProtocol.prototype = { + __proto__: ClassInfo("prplIProtocol", "Unknown protocol"), + get name() "", + get normalizedName() this.name, + get iconBaseURI() "chrome://chat/skin/prpl-unknown/", + getOptions: function() EmptyEnumerator, + getUsernameSplit: function() EmptyEnumerator, + get usernameEmptyText() "", + + getAccount: function(aKey, aName) { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }, + accountExists: function() { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }, + + // false seems an acceptable default for all options + // (they should never be called anyway). + get uniqueChatName() false, + get chatHasTopic() false, + get noPassword() false, + get newMailNotification() false, + get imagesInIM() false, + get passwordOptional() true, + get usePointSize() true, + get registerNoScreenName() false, + get slashCommandsNative() false, + get usePurpleProxy() false +}; + +// aName and aPrplId are provided as parameter only if this is a new +// account that doesn't exist in the preferences. In this case, these +// 2 values should be stored. +function imAccount(aKey, aName, aPrplId) +{ + if (aKey.indexOf(kAccountKeyPrefix) != 0) + throw Cr.NS_ERROR_INVALID_ARG; + + this.id = aKey; + this.numericId = parseInt(aKey.substr(kAccountKeyPrefix.length)); + this.prefBranch = Services.prefs.getBranch(kPrefAccountPrefix + aKey + "."); + + if (aName) { + this.name = aName; + let str = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + str.data = aName; + this.prefBranch.setComplexValue(kPrefAccountName, Ci.nsISupportsString, + str); + + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN; + } + else { + this.name = this.prefBranch.getComplexValue(kPrefAccountName, + Ci.nsISupportsString).data; + } + + let prplId = aPrplId; + if (prplId) + this.prefBranch.setCharPref(kPrefAccountPrpl, prplId); + else + prplId = this.prefBranch.getCharPref(kPrefAccountPrpl); + + // Get the protocol plugin, or fallback to an UnknownProtocol instance. + this.protocol = Services.core.getProtocolById(prplId); + if (!this.protocol) { + this.protocol = new UnknownProtocol(prplId); + this._connectionErrorReason = Ci.imIAccount.ERROR_UNKNOWN_PRPL; + return; + } + + // Ensure the account is correctly stored in blist.sqlite. + Services.contacts.storeAccount(this.numericId, this.name, prplId); + + // Get the prplIAccount from the protocol plugin. + this.prplAccount = this.protocol.getAccount(this); + + // Send status change notifications to the account. + this.observedStatusInfo = null; // (To execute the setter). + + // If we have never finished the first connection attempt for this account, + // mark the account as having caused a crash. + if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_PENDING) + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_CRASHED; + + // Try to convert old passwords stored in the preferences. + // Don't try too hard if the user has canceled a master password prompt: + // we don't want to display several of theses prompts at startup. + if (gConvertingOldPasswords && !this.protocol.noPassword) { + try { + let password = this.prefBranch.getComplexValue(kPrefAccountPassword, + Ci.nsISupportsString).data; + if (password && !this.password) + this.password = password; + } catch (e) { /* No password saved in the prefs for this account. */ } + } + + // Check for errors that should prevent connection attempts. + if (this._passwordRequired && !this.password) + this._connectionErrorReason = Ci.imIAccount.ERROR_MISSING_PASSWORD; + else if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_CRASHED) + this._connectionErrorReason = Ci.imIAccount.ERROR_CRASHED; +} + +imAccount.prototype = { + __proto__: ClassInfo(["imIAccount", "prplIAccount"], "im account object"), + + name: "", + id: "", + numericId: 0, + protocol: null, + prplAccount: null, + connectionState: Ci.imIAccount.STATE_DISCONNECTED, + connectionStateMsg: "", + connectionErrorMessage: "", + _connectionErrorReason: Ci.prplIAccount.NO_ERROR, + get connectionErrorReason() { + if (this._connectionErrorReason != Ci.prplIAccount.NO_ERROR && + (this._connectionErrorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD || + !this._password)) + return this._connectionErrorReason; + else + return this.prplAccount.connectionErrorReason; + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic == "account-connect-progress") + this.connectionStateMsg = aData; + else if (aTopic == "account-connecting") { + if (this.prplAccount.connectionErrorReason != Ci.prplIAccount.NO_ERROR) { + delete this.connectionErrorMessage; + if (this.timeOfNextReconnect - Date.now() > 1000) { + // This is a manual reconnection, reset the auto-reconnect stuff + this.timeOfLastConnect = 0; + this.cancelReconnection(); + } + } + if (this.firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK) + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_PENDING; + this.connectionState = Ci.imIAccount.STATE_CONNECTING; + } + else if (aTopic == "account-connected") { + this.connectionState = Ci.imIAccount.STATE_CONNECTED; + this._finishedAutoLogin(); + this.timeOfLastConnect = Date.now(); + if (this.firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK) + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_OK; + delete this.connectionStateMsg; + + if (this.canJoinChat && + this.prefBranch.prefHasUserValue(kPrefAccountAutoJoin)) { + let autojoin = this.prefBranch.getCharPref(kPrefAccountAutoJoin); + if (autojoin) { + for each (let room in autojoin.trim().split(/,\s*/)) { + if (room) + this.joinChat(this.getChatRoomDefaultFieldValues(room)); + } + } + } + } + else if (aTopic == "account-disconnecting") { + this.connectionState = Ci.imIAccount.STATE_DISCONNECTING; + this.connectionErrorMessage = aData; + delete this.connectionStateMsg; + this._finishedAutoLogin(); + + let firstConnectionState = this.firstConnectionState; + if (firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK && + firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_CRASHED) + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN; + + let connectionErrorReason = this.prplAccount.connectionErrorReason; + if (connectionErrorReason != Ci.prplIAccount.NO_ERROR) { + if (connectionErrorReason == Ci.prplIAccount.ERROR_NETWORK_ERROR || + connectionErrorReason == Ci.prplIAccount.ERROR_ENCRYPTION_ERROR) + this._startReconnectTimer(); + this._sendNotification("account-connect-error"); + } + } + else if (aTopic == "account-disconnected") + this.connectionState = Ci.imIAccount.STATE_DISCONNECTED; + else + throw Cr.NS_ERROR_UNEXPECTED; + this._sendNotification(aTopic, aData); + }, + + _observedStatusInfo: null, + get observedStatusInfo() this._observedStatusInfo, + _statusObserver: null, + set observedStatusInfo(aUserStatusInfo) { + if (!this.prplAccount) + return; + if (this._statusObserver) + this.statusInfo.removeObserver(this._statusObserver); + this._observedStatusInfo = aUserStatusInfo; + if (this._statusObserver) + this.statusInfo.addObserver(this._statusObserver); + }, + get statusInfo() this._observedStatusInfo || Services.core.globalUserStatus, + + reconnectAttempt: 0, + timeOfLastConnect: 0, + timeOfNextReconnect: 0, + _reconnectTimer: null, + _startReconnectTimer: function() { + if (Services.io.offline) { + Cu.reportError("_startReconnectTimer called while offline"); + return; + } + + /* If the last successful connection is older than 10 seconds, reset the + number of reconnection attemps. */ + const kTimeBeforeSuccessfulConnection = 10; + if (this.timeOfLastConnect && + this.timeOfLastConnect + kTimeBeforeSuccessfulConnection * 1000 < Date.now()) { + delete this.reconnectAttempt; + delete this.timeOfLastConnect; + } + + let timers = + Services.prefs.getCharPref("messenger.accounts.reconnectTimer").split(","); + let delay = timers[Math.min(this.reconnectAttempt, timers.length - 1)]; + let msDelay = parseInt(delay) * 1000; + ++this.reconnectAttempt; + this.timeOfNextReconnect = Date.now() + msDelay; + this._reconnectTimer = setTimeout(this.connect.bind(this), msDelay); + }, + + _sendNotification: function(aTopic, aData) { + Services.obs.notifyObservers(this, aTopic, aData); + }, + + get firstConnectionState() { + try { + return this.prefBranch.getIntPref(kPrefAccountFirstConnectionState); + } catch (e) { + return Ci.imIAccount.FIRST_CONNECTION_OK; + } + }, + set firstConnectionState(aState) { + if (aState == Ci.imIAccount.FIRST_CONNECTION_OK) + this.prefBranch.deleteBranch(kPrefAccountFirstConnectionState); + else { + this.prefBranch.setIntPref(kPrefAccountFirstConnectionState, aState); + // We want to save this pref immediately when trying to connect. + if (aState == Ci.imIAccount.FIRST_CONNECTION_PENDING) + SavePrefTimer.saveNow(); + else + SavePrefTimer.initTimer(); + } + }, + + _pendingReconnectForConnectionInfoChange: false, + _connectionInfoChanged: function() { + // The next connection will be the first connection with these parameters. + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN; + + // We want to attempt to reconnect with the new settings only if a + // previous attempt failed or a connection attempt is currently + // pending (so we can return early if the account is currently + // connected or disconnected without error). + // The code doing the reconnection attempt is wrapped within an + // executeSoon call so that when multiple settings are changed at + // once we don't attempt to reconnect until they are all saved. + // If a reconnect attempt is already scheduled, we can also return early. + if (this._pendingReconnectForConnectionInfoChange || this.connected || + (this.disconnected && + this.connectionErrorReason == Ci.prplIAccount.NO_ERROR)) + return; + + this._pendingReconnectForConnectionInfoChange = true; + executeSoon(function () { + delete this._pendingReconnectForConnectionInfoChange; + // If the connection parameters have changed while we were + // trying to connect, cancel the ongoing connection attempt and + // try again with the new parameters. + if (this.connecting) { + this.disconnect(); + this.connect(); + return; + } + // If the account was disconnected because of a non-fatal + // connection error, retry now that we have new parameters. + let errorReason = this.connectionErrorReason; + if (this.disconnected && + errorReason != Ci.prplIAccount.NO_ERROR && + errorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD && + errorReason != Ci.imIAccount.ERROR_CRASHED && + errorReason != Ci.imIAccount.ERROR_UNKNOWN_PRPL) { + this.connect(); + } + }.bind(this)); + }, + + get normalizedName() this._ensurePrplAccount.normalizedName, + + _sendUpdateNotification: function() { + this._sendNotification("account-updated"); + }, + + set alias(val) { + if (val) { + let str = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + str.data = val; + this.prefBranch.setComplexValue(kPrefAccountAlias, Ci.nsISupportsString, + str); + } + else + this.prefBranch.deleteBranch(kPrefAccountAlias); + this._sendUpdateNotification(); + }, + get alias() { + try { + return this.prefBranch.getComplexValue(kPrefAccountAlias, + Ci.nsISupportsString).data; + } catch (e) { + return ""; + } + }, + + _password: "", + get password() { + if (this._password) + return this._password; + + // Avoid prompting the user for the master password more than once at startup. + if (gUserCanceledMasterPasswordPrompt) + return ""; + + let passwordURI = "im://" + this.protocol.id; + let logins; + try { + logins = LoginManager.findLogins({}, passwordURI, null, passwordURI); + } catch (e) { + this._handleMasterPasswordException(e); + return ""; + } + let normalizedName = this.normalizedName; + for each (let login in logins) { + if (login.username == normalizedName) { + this._password = login.password; + if (this._connectionErrorReason == Ci.imIAccount.ERROR_MISSING_PASSWORD) { + // We have found a password for an account marked as missing password, + // re-check all others accounts missing a password. But first, + // remove the error on our own account to avoid re-checking it. + delete this._connectionErrorReason; + gAccountsService._checkIfPasswordStillMissing(); + } + return this._password; + } + } + return ""; + }, + _checkIfPasswordStillMissing: function() { + if (this._connectionErrorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD || + !this.password) + return; + + delete this._connectionErrorReason; + this._sendUpdateNotification(); + }, + get _passwordRequired() + !this.protocol.noPassword && !this.protocol.passwordOptional, + set password(aPassword) { + this._password = aPassword; + if (gUserCanceledMasterPasswordPrompt) + return; + let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"] + .createInstance(Ci.nsILoginInfo); + let passwordURI = "im://" + this.protocol.id; + newLogin.init(passwordURI, null, passwordURI, this.normalizedName, + aPassword, "", ""); + try { + let logins = LoginManager.findLogins({}, passwordURI, null, passwordURI); + let saved = false; + for each (let login in logins) { + if (newLogin.matches(login, true)) { + if (aPassword) + LoginManager.modifyLogin(login, newLogin); + else + LoginManager.removeLogin(login); + saved = true; + break; + } + } + if (!saved && aPassword) + LoginManager.addLogin(newLogin); + } catch (e) { + this._handleMasterPasswordException(e); + } + + this._connectionInfoChanged(); + if (aPassword && + this._connectionErrorReason == Ci.imIAccount.ERROR_MISSING_PASSWORD) + this._connectionErrorReason = Ci.imIAccount.NO_ERROR; + else if (!aPassword && this._passwordRequired) + this._connectionErrorReason = Ci.imIAccount.ERROR_MISSING_PASSWORD; + this._sendUpdateNotification(); + }, + _handleMasterPasswordException: function(aException) { + if (aException.result != Components.results.NS_ERROR_ABORT) + throw aException; + + gUserCanceledMasterPasswordPrompt = true; + executeSoon(function () { gUserCanceledMasterPasswordPrompt = false; }); + }, + + get autoLogin() { + let autoLogin = true; + try { + autoLogin = this.prefBranch.getBoolPref(kPrefAccountAutoLogin); + } catch (e) { } + return autoLogin; + }, + set autoLogin(val) { + this.prefBranch.setBoolPref(kPrefAccountAutoLogin, val); + SavePrefTimer.initTimer(); + this._sendUpdateNotification(); + }, + _autoLoginPending: false, + checkAutoLogin: function() { + // No auto-login if: the account has an error at the imIAccount level + // (unknown protocol, missing password, first connection crashed), + // the account is already connected or connecting, or autoLogin is off. + if (this._connectionErrorReason != Ci.prplIAccount.NO_ERROR || + this.connecting || this.connected || !this.autoLogin) + return; + + this._autoLoginPending = true; + AutoLoginCounter.startAutoLogin(); + try { + this.connect(); + } catch (e) { + Cu.reportError(e); + this._finishedAutoLogin(); + } + }, + _finishedAutoLogin: function() { + if (!this.hasOwnProperty("_autoLoginPending")) + return; + delete this._autoLoginPending; + AutoLoginCounter.finishedAutoLogin(); + }, + + // Delete the account (from the preferences, mozStorage, and call unInit). + remove: function() { + let login = Cc["@mozilla.org/login-manager/loginInfo;1"] + .createInstance(Ci.nsILoginInfo); + let passwordURI = "im://" + this.protocol.id; + // The password is stored with the normalizedName. If the protocol + // plugin is missing, we can't access the normalizedName, but in + // lots of cases this.name is equivalent. + let name = this.prplAccount ? this.normalizedName : this.name; + login.init(passwordURI, null, passwordURI, name, "", "", ""); + let logins = LoginManager.findLogins({}, passwordURI, null, passwordURI); + for each (let l in logins) { + if (login.matches(l, true)) { + LoginManager.removeLogin(l); + break; + } + } + if (this.prplAccount) + this.prplAccount.remove(); + this.unInit(); + Services.contacts.forgetAccount(this.numericId); + this.prefBranch.deleteBranch(""); + }, + unInit: function() { + // remove any pending reconnection timer. + this.cancelReconnection(); + + // remove any pending autologin preference used for crash detection. + this._finishedAutoLogin(); + + // If the first connection was pending on quit, we set it back to unknown. + if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_PENDING) + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN; + + // and make sure we cleanup the save pref timer. + SavePrefTimer.unInitTimer(); + + if (this.prplAccount) + this.prplAccount.unInit(); + + delete this.protocol; + delete this.prplAccount; + }, + + get _ensurePrplAccount() { + if (this.prplAccount) + return this.prplAccount; + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + connect: function() { + if (!this.prplAccount) + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + + if (this._passwordRequired) { + // If the previous connection attempt failed because we have a wrong password, + // clear the passwor cache so that if there's no password in the password + // manager the user gets prompted again. + if (this.connectionErrorReason == Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED) + delete this._password; + + let password = this.password; + if (!password) { + let prompts = Services.prompt; + let shouldSave = {value: false}; + password = {value: ""}; + if (!prompts.promptPassword(null, _("passwordPromptTitle", this.name), + _("passwordPromptText", this.name), + password, _("passwordPromptSaveCheckbox"), + shouldSave)) + return; + + if (shouldSave.value) + this.password = password.value; + else + this._password = password.value; + } + } + + if (!this._statusObserver) { + this._statusObserver = { + observe: (function(aSubject, aTopic, aData) { + // Disconnect or reconnect the account automatically, otherwise notify + // the prplAccount instance. + let statusType = aSubject.statusType; + if (statusType == Ci.imIStatusInfo.STATUS_OFFLINE) { + if (this.connected || this.connecting) + this.prplAccount.disconnect(); + this.cancelReconnection(); + } + else if (statusType > Ci.imIStatusInfo.STATUS_OFFLINE && + this.disconnected) + this.prplAccount.connect(); + else if (this.connected) + this.prplAccount.observe(aSubject, aTopic, aData); + }).bind(this) + }; + + this.statusInfo.addObserver(this._statusObserver); + } + + if (!Services.io.offline && + this.statusInfo.statusType > Ci.imIStatusInfo.STATUS_OFFLINE && + this.disconnected) + this.prplAccount.connect(); + }, + disconnect: function() { + if (this._statusObserver) { + this.statusInfo.removeObserver(this._statusObserver); + delete this._statusObserver; + } + if (!this.disconnected) + this._ensurePrplAccount.disconnect(); + }, + + get disconnected() this.connectionState == Ci.imIAccount.STATE_DISCONNECTED, + get connected() this.connectionState == Ci.imIAccount.STATE_CONNECTED, + get connecting() this.connectionState == Ci.imIAccount.STATE_CONNECTING, + get disconnecting() this.connectionState == Ci.imIAccount.STATE_DISCONNECTING, + + cancelReconnection: function() { + if (this._reconnectTimer) { + clearTimeout(this._reconnectTimer); + delete this._reconnectTimer; + } + delete this.reconnectAttempt; + delete this.timeOfNextReconnect; + }, + createConversation: function(aName) + this._ensurePrplAccount.createConversation(aName), + addBuddy: function(aTag, aName) { + this._ensurePrplAccount.addBuddy(aTag, aName); + }, + loadBuddy: function(aBuddy, aTag) + this._ensurePrplAccount.loadBuddy(aBuddy, aTag), // FIXME for unknown proto + requestBuddyInfo: function(aBuddyName) { + this._ensurePrplAccount.requestBuddyInfo(aBuddyName); + }, + getChatRoomFields: function() this._ensurePrplAccount.getChatRoomFields(), + getChatRoomDefaultFieldValues: function(aDefaultChatName) + this._ensurePrplAccount.getChatRoomDefaultFieldValues(aDefaultChatName), + get canJoinChat() this.prplAccount ? this.prplAccount.canJoinChat : false, + joinChat: function(aComponents) { + this._ensurePrplAccount.joinChat(aComponents); + }, + setBool: function(aName, aVal) { + this.prefBranch.setBoolPref(kAccountOptionPrefPrefix + aName, aVal); + this._connectionInfoChanged(); + if (this.prplAccount) + this.prplAccount.setBool(aName, aVal); + SavePrefTimer.initTimer(); + }, + setInt: function(aName, aVal) { + this.prefBranch.setIntPref(kAccountOptionPrefPrefix + aName, aVal); + this._connectionInfoChanged(); + if (this.prplAccount) + this.prplAccount.setInt(aName, aVal); + SavePrefTimer.initTimer(); + }, + setString: function(aName, aVal) { + let str = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + str.data = aVal; + this.prefBranch.setComplexValue(kAccountOptionPrefPrefix + aName, + Ci.nsISupportsString, str); + this._connectionInfoChanged(); + if (this.prplAccount) + this.prplAccount.setString(aName, aVal); + SavePrefTimer.initTimer(); + }, + save: function() { SavePrefTimer.saveNow(); }, + + get HTMLEnabled() this._ensurePrplAccount.HTMLEnabled, + get HTMLEscapePlainText() this._ensurePrplAccount.HTMLEscapePlainText, + get noBackgroundColors() this._ensurePrplAccount.noBackgroundColors, + get autoResponses() this._ensurePrplAccount.autoResponses, + get singleFormatting() this._ensurePrplAccount.singleFormatting, + get noFontSizes() this._ensurePrplAccount.noFontSizes, + get noUrlDesc() this._ensurePrplAccount.noUrlDesc, + get noImages() this._ensurePrplAccount.noImages, + get maxMessageLength() this._ensurePrplAccount.maxMessageLength, + + get proxyInfo() this._ensurePrplAccount.proxyInfo, + set proxyInfo(val) { + this._ensurePrplAccount.proxyInfo = val; + this._connectionInfoChanged(); + } +}; + +var gAccountsService = null; + +function AccountsService() { } +AccountsService.prototype = { + initAccounts: function() { + this._initAutoLoginStatus(); + this._accounts = []; + this._accountsById = {}; + gAccountsService = this; + gConvertingOldPasswords = + Services.prefs.getBoolPref(kPrefConvertOldPasswords); + let accountList = this._accountList; + for each (let account in (accountList ? accountList.split(",") : [])) { + try { + account.trim(); + if (!account) + throw Cr.NS_ERROR_INVALID_ARG; + let newAccount = new imAccount(account); + this._accounts.push(newAccount); + this._accountsById[newAccount.numericId] = newAccount; + } catch (e) { + Cu.reportError(e); + dump(e + " " + e.toSource() + "\n"); + } + } + // If the user has canceled a master password prompt, we haven't + // been able to save any password, so the old password conversion + // still needs to happen. + if (gConvertingOldPasswords && !gUserCanceledMasterPasswordPrompt) + Services.prefs.setBoolPref(kPrefConvertOldPasswords, false); + + this._prefObserver = this.observe.bind(this); + Services.prefs.addObserver(kPrefMessengerAccounts, this._prefObserver, false); + }, + + _observingAccountListChange: true, + _prefObserver: null, + observe: function(aSubject, aTopic, aData) { + if (aTopic != "nsPref:changed" || aData != kPrefMessengerAccounts || + !this._observingAccountListChange) + return; + + this._accounts = + this._accountList.split(",").map(String.trim) + .filter(function (k) k.indexOf(kAccountKeyPrefix) == 0) + .map(function (k) parseInt(k.substr(kAccountKeyPrefix.length))) + .map(this.getAccountByNumericId, this) + .filter(function (a) a); + + Services.obs.notifyObservers(this, "account-list-updated", null); + }, + + get _accountList() Services.prefs.getCharPref(kPrefMessengerAccounts), + set _accountList(aNewList) { + this._observingAccountListChange = false; + Services.prefs.setCharPref(kPrefMessengerAccounts, aNewList); + delete this._observingAccountListChange; + }, + + unInitAccounts: function() { + for each (let account in this._accounts) + account.unInit(); + gAccountsService = null; + delete this._accounts; + delete this._accountsById; + Services.prefs.removeObserver(kPrefMessengerAccounts, this._prefObserver); + delete this._prefObserver; + }, + + autoLoginStatus: Ci.imIAccountsService.AUTOLOGIN_ENABLED, + _initAutoLoginStatus: function() { + /* If auto-login is already disabled, do nothing */ + if (this.autoLoginStatus != Ci.imIAccountsService.AUTOLOGIN_ENABLED) + return; + + let prefs = Services.prefs; + if (!prefs.getIntPref("messenger.startup.action")) { + // the value 0 means that we start without connecting the accounts + this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_USER_DISABLED; + return; + } + + /* Disable auto-login if we are running in safe mode */ + if (Services.appinfo.inSafeMode) { + this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_SAFE_MODE; + return; + } + + /* Check if we crashed at the last startup during autologin */ + let autoLoginPending; + if (prefs.getPrefType(kPrefAutologinPending) == prefs.PREF_INVALID || + !(autoLoginPending = prefs.getIntPref(kPrefAutologinPending))) { + // if the pref isn't set, then we haven't crashed: keep autologin enabled + return; + } + + // Last autologin hasn't finished properly. + // For now, assume it's because of a crash. + this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_CRASH; + prefs.deleteBranch(kPrefAutologinPending); + + // If the crash reporter isn't built, we can't know anything more. + if (!("nsICrashReporter" in Ci)) + return; + + try { + // Try to get more info with breakpad + let lastCrashTime = 0; + + /* Locate the LastCrash file */ + let lastCrash = Services.dirsvc.get("UAppData", Ci.nsILocalFile); + lastCrash.append("Crash Reports"); + lastCrash.append("LastCrash"); + if (lastCrash.exists()) { + /* Ok, the file exists, now let's try to read it */ + let is = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + let sis = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + is.init(lastCrash, -1, 0, 0); + sstream.init(sis); + + lastCrashTime = parseInt(sstream.read(lastCrash.fileSize)); + + sstream.close(); + fstream.close(); + } + // The file not existing is totally acceptable, it just means that + // either we never crashed or breakpad is not enabled. + // In this case, lastCrashTime will keep its 0 initialization value. + + /*dump("autoLoginPending = " + autoLoginPending + + ", lastCrash = " + lastCrashTime + + ", difference = " + lastCrashTime - autoLoginPending + "\n");*/ + + if (lastCrashTime < autoLoginPending) { + // the last crash caught by breakpad is older than our last autologin + // attempt. + // If breakpad is currently enabled, we can be confident that + // autologin was interrupted for an exterior reason + // (application killed by the user, power outage, ...) + try { + Services.appinfo.QueryInterface(Ci.nsICrashReporter) + .annotateCrashReport("=", ""); + } catch (e) { + // This should fail with NS_ERROR_INVALID_ARG if breakpad is enabled, + // and NS_ERROR_NOT_INITIALIZED if it is not. + if (e.result != Cr.NS_ERROR_NOT_INITIALIZED) + this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_ENABLED; + } + } + } catch (e) { + // if we failed to get the last crash time, then keep the + // AUTOLOGIN_CRASH value in mAutoLoginStatus and return. + return; + } + }, + + processAutoLogin: function() { + for each (let account in this._accounts) + account.checkAutoLogin(); + + // Make sure autologin is now enabled, so that we don't display a + // message stating that it is disabled and asking the user if it + // should be processed now. + this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_ENABLED; + + // Notify observers so that any message stating that autologin is + // disabled can be removed + Services.obs.notifyObservers(this, "autologin-processed", null); + }, + + _checkingIfPasswordStillMissing: false, + _checkIfPasswordStillMissing: function() { + // Avoid recursion. + if (this._checkingIfPasswordStillMissing) + return; + + this._checkingIfPasswordStillMissing = true; + for each (let account in this._accounts) + account._checkIfPasswordStillMissing(); + delete this._checkingIfPasswordStillMissing; + }, + + getAccountById: function(aAccountId) { + if (aAccountId.indexOf(kAccountKeyPrefix) != 0) + throw Cr.NS_ERROR_INVALID_ARG; + + let id = parseInt(aAccountId.substr(kAccountKeyPrefix.length)); + return this.getAccountByNumericId(id); + }, + + getAccountByNumericId: function(aAccountId) this._accountsById[aAccountId], + getAccounts: function() new nsSimpleEnumerator(this._accounts), + + createAccount: function(aName, aPrpl) { + // Ensure an account with the same name and protocol doesn't already exist. + let prpl = Services.core.getProtocolById(aPrpl); + if (!prpl) + throw Cr.NS_ERROR_UNEXPECTED; + if (prpl.accountExists(aName)) { + Cu.reportError("Attempted to create a duplicate account!"); + throw Cr.NS_ERROR_ALREADY_INITIALIZED; + } + + /* First get a unique id for the new account. */ + let id; + for (id = 1; ; ++id) { + if (this._accountsById.hasOwnProperty(id)) + continue; + + /* id isn't used by a known account, double check it isn't + already used in the sqlite database. This should never + happen, except if we have a corrupted profile. */ + if (!Services.contacts.accountIdExists(id)) + break; + Services.console.logStringMessage("No account " + id + " but there is some data in the buddy list for an account with this number. Your profile may be corrupted."); + } + + /* Actually create the new account. */ + let key = kAccountKeyPrefix + id; + let account = new imAccount(key, aName, aPrpl); + + /* Keep it in the local account lists. */ + this._accounts.push(account); + this._accountsById[id] = account; + + /* Save the account list pref. */ + let list = this._accountList; + this._accountList = list ? list + "," + key : key; + + Services.obs.notifyObservers(account, "account-added", null); + return account; + }, + + deleteAccount: function(aAccountId) { + let account = this.getAccountById(aAccountId); + if (!account) + throw Cr.NS_ERROR_INVALID_ARG; + + let index = this._accounts.indexOf(account); + if (index == -1) + throw Cr.NS_ERROR_UNEXPECTED; + + let id = account.numericId; + account.remove(); + this._accounts.splice(index, 1); + delete this._accountsById[id]; + Services.obs.notifyObservers(account, "account-removed", null); + + /* Update the account list pref. */ + let list = this._accountList; + this._accountList = + list.split(",").filter(function (k) k.trim() != aAccountId).join(","); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.imIAccountsService]), + classDescription: "Accounts", + classID: Components.ID("{a94b5427-cd8d-40cf-b47e-b67671953e70}"), + contractID: "@mozilla.org/chat/accounts-service;1" +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([AccountsService]); diff --git a/chat/components/src/imAccounts.manifest b/chat/components/src/imAccounts.manifest new file mode 100644 --- /dev/null +++ b/chat/components/src/imAccounts.manifest @@ -0,0 +1,2 @@ +component {a94b5427-cd8d-40cf-b47e-b67671953e70} imAccounts.js +contract @mozilla.org/chat/accounts-service;1 {a94b5427-cd8d-40cf-b47e-b67671953e70} diff --git a/chat/components/src/imCommands.js b/chat/components/src/imCommands.js new file mode 100644 --- /dev/null +++ b/chat/components/src/imCommands.js @@ -0,0 +1,242 @@ +/* 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/. */ + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource:///modules/imServices.jsm"); +Cu.import("resource:///modules/imXPCOMUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "_", function() + l10nHelper("chrome://chat/locale/commands.properties") +); + +function CommandsService() { } +CommandsService.prototype = { + initCommands: function() { + this._commands = {}; + // The say command is directly implemented in the UI layer, but has a + // dummy command registered here so it shows up as a command (e.g. when + // using the /help command). + this.registerCommand({ + name: "say", + get helpString() _("sayHelpString"), + usageContext: Ci.imICommand.CONTEXT_ALL, + priority: Ci.imICommand.PRIORITY_HIGH, + run: function(aMsg, aConv) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + }); + + this.registerCommand({ + name: "raw", + get helpString() _("rawHelpString"), + usageContext: Ci.imICommand.CONTEXT_ALL, + priority: Ci.imICommand.PRIORITY_DEFAULT, + run: function(aMsg, aConv) { + aConv.sendMsg(aMsg); + return true; + } + }); + + this.registerCommand({ + // Reference the command service so we can use the internal properties + // directly. + cmdSrv: this, + + name: "help", + get helpString() _("helpHelpString"), + usageContext: Ci.imICommand.CONTEXT_ALL, + priority: Ci.imICommand.PRIORITY_DEFAULT, + run: function(aMsg, aConv) { + let conv = Services.conversations.getUIConversation(aConv); + if (!conv) + return false; + + // Handle when no command is given, list all possible commands that are + // available for this conversation (alphabetically). + if (!aMsg) { + let commands = this.cmdSrv.listCommandsForConversation(aConv, {}); + if (!commands.length) + return false; + + // Concatenate the command names (separated by a comma and space). + let cmds = commands.map(function(aCmd) aCmd.name).sort().join(", "); + let message = _("commands", cmds); + + // Display the message + conv.systemMessage(message); + return true; + } + + // A command name was given, find the commands that match. + let cmdArray = this.cmdSrv._findCommands(aConv, aMsg); + + if (!cmdArray.length) { + // No command that matches. + let message = _("noCommand", aMsg); + conv.systemMessage(message); + return true; + } + + // Only show the help for the one of the highest priority. + let cmd = cmdArray[0]; + + let text = cmd.helpString; + if (!text) + text = _("noHelp", cmd.name); + + // Display the message. + conv.systemMessage(text); + return true; + } + }); + + // Status commands + let status = { + back: "AVAILABLE", + away: "AWAY", + busy: "UNAVAILABLE", + dnd: "UNAVAILABLE", + offline: "OFFLINE" + }; + for (let cmd in status) { + let statusValue = Ci.imIStatusInfo["STATUS_" + status[cmd]]; + this.registerCommand({ + name: cmd, + get helpString() _("statusCommand", this.name, _(this.name)), + usageContext: Ci.imICommand.CONTEXT_ALL, + priority: Ci.imICommand.PRIORITY_HIGH, + run: function(aMsg) { + Services.core.globalUserStatus.setStatus(statusValue, aMsg); + return true; + } + }); + } + }, + unInitCommands: function() { + delete this._commands; + }, + + registerCommand: function(aCommand, aPrplId) { + let name = aCommand.name; + if (!name) + throw Cr.NS_ERROR_INVALID_ARG; + + if (!(this._commands.hasOwnProperty(name))) + this._commands[name] = {}; + this._commands[name][aPrplId || ""] = aCommand; + }, + unregisterCommand: function(aCommandName, aPrplId) { + if (this._commands.hasOwnProperty(aCommandName)) { + let prplId = aPrplId || ""; + let commands = this._commands[aCommandName]; + if (commands.hasOwnProperty(aPrplId)) + delete commands[aPrplId]; + if (!Object.keys(commands).length) + delete this._commands[aCommandName]; + } + }, + listCommandsForConversation: function(aConversation, commandCount) { + let result = []; + let prplId = aConversation && aConversation.account.protocol.id; + for (let name in this._commands) { + let commands = this._commands[name]; + if (commands.hasOwnProperty("")) + result.push(commands[""]); + if (prplId && commands.hasOwnProperty(prplId)) + result.push(commands[prplId]); + } + result = result.filter(this._usageContextFilter(aConversation)); + commandCount.value = result.length; + return result; + }, + // List only the commands for a protocol (excluding the global commands). + listCommandsForProtocol: function(aPrplId, commandCount) { + if (!aPrplId) + throw "You must provide a prpl ID."; + + let result = []; + for (let name in this._commands) { + let commands = this._commands[name]; + if (commands.hasOwnProperty(aPrplId)) + result.push(commands[aPrplId]); + } + commandCount.value = result.length; + return result; + }, + _usageContextFilter: function(aConversation) { + let usageContext = + Ci.imICommand["CONTEXT_" + (aConversation.isChat ? "CHAT" : "IM")]; + return function(c) c.usageContext & usageContext; + }, + _findCommands: function(aConversation, aName) { + // The command doesn't exist, check if the given command is a partial match + // to a single other command. + if (!(this._commands.hasOwnProperty(aName))) { + let commandNames = Object.keys(this._commands); + // Find all full command names that start with the given command. + commandNames = + commandNames.filter(function(aCommand) aCommand.indexOf(aName) == 0); + + // If a single full command name matches the given partial command name, + // return the results for that command name. Otherwise, return an empty + // array (don't assume a certain command). + if (commandNames.length == 1) + return this._findCommands(aConversation, commandNames[0]); + else + return []; + } + + // Get the 2 possible commands (the global and the proto specific) + let cmdArray = []; + let commands = this._commands[aName]; + if (commands.hasOwnProperty("")) + cmdArray.push(commands[""]); + + if (aConversation) { + let account = aConversation.account; + if (account.connected) { + let prplId = account.protocol.id; + if (commands.hasOwnProperty(prplId)) + cmdArray.push(commands[prplId]); + } + } + + // Remove the commands that can't apply in this context. + cmdArray = cmdArray.filter(this._usageContextFilter(aConversation)); + + // Sort the matching commands by priority before returning the array. + return cmdArray.sort(function(a, b) b.priority - a.priority); + }, + executeCommand: function (aMessage, aConversation) { + if (!aMessage) + throw Cr.NS_ERROR_INVALID_ARG; + + let matchResult; + if (aMessage[0] != "/" || + !(matchResult = /^\/([a-z]+)(?: |$)([\s\S]*)/.exec(aMessage))) + return false; + + let [, name, args] = matchResult; + + let cmdArray = this._findCommands(aConversation, name); + if (!cmdArray.length) + return false; + + // cmdArray contains commands sorted by priority, attempt to apply + // them in order until one succeeds. + if (!cmdArray.some(function (aCmd) aCmd.run(args, aConversation))) { + // If they all failed, print help message. + this.executeCommand("/help " + name, aConversation); + } + return true; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.imICommandsService]), + classDescription: "Commands", + classID: Components.ID("{7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23}"), + contractID: "@mozilla.org/chat/commands-service;1" +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandsService]); diff --git a/chat/components/src/imCommands.manifest b/chat/components/src/imCommands.manifest new file mode 100644 --- /dev/null +++ b/chat/components/src/imCommands.manifest @@ -0,0 +1,2 @@ +component {7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23} imCommands.js +contract @mozilla.org/chat/commands-service;1 {7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23} diff --git a/chat/components/src/imContacts.js b/chat/components/src/imContacts.js new file mode 100644 --- /dev/null +++ b/chat/components/src/imContacts.js @@ -0,0 +1,1408 @@ +/* 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/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; +Cu.import("resource:///modules/imXPCOMUtils.jsm"); +Cu.import("resource:///modules/imServices.jsm"); + +var gDBConnection = null; + +function getDBConnection() +{ + const NS_APP_USER_PROFILE_50_DIR = "ProfD"; + let dbFile = Services.dirsvc.get(NS_APP_USER_PROFILE_50_DIR, Ci.nsIFile); + dbFile.append("blist.sqlite"); + + let conn = + Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService) + .openDatabase(dbFile); + if (!conn.connectionReady) + throw Cr.NS_ERROR_UNEXPECTED; + + // Grow blist db in 512KB increments. + try { + conn.setGrowthIncrement(512 * 1024, ""); + } catch (e if e.result == Cr.NS_ERROR_FILE_TOO_BIG) { + Services.console.logStringMessage("Not setting growth increment on " + + "blist.sqlite because the available " + + "disk space is limited"); + } + + // Create tables and indexes. + [ + "CREATE TABLE IF NOT EXISTS accounts (" + + "id INTEGER PRIMARY KEY, " + + "name VARCHAR, " + + "prpl VARCHAR)", + + "CREATE TABLE IF NOT EXISTS contacts (" + + "id INTEGER PRIMARY KEY, " + + "firstname VARCHAR, " + + "lastname VARCHAR, " + + "alias VARCHAR)", + + "CREATE TABLE IF NOT EXISTS buddies (" + + "id INTEGER PRIMARY KEY, " + + "key VARCHAR NOT NULL, " + + "name VARCHAR NOT NULL, " + + "srv_alias VARCHAR, " + + "position INTEGER, " + + "icon BLOB, " + + "contact_id INTEGER)", + "CREATE INDEX IF NOT EXISTS buddies_contactindex " + + "ON buddies (contact_id)", + + "CREATE TABLE IF NOT EXISTS tags (" + + "id INTEGER PRIMARY KEY, " + + "name VARCHAR UNIQUE NOT NULL, " + + "position INTEGER)", + + "CREATE TABLE IF NOT EXISTS contact_tag (" + + "contact_id INTEGER NOT NULL, " + + "tag_id INTEGER NOT NULL)", + "CREATE INDEX IF NOT EXISTS contact_tag_contactindex " + + "ON contact_tag (contact_id)", + "CREATE INDEX IF NOT EXISTS contact_tag_tagindex " + + "ON contact_tag (tag_id)", + + "CREATE TABLE IF NOT EXISTS account_buddy (" + + "account_id INTEGER NOT NULL, " + + "buddy_id INTEGER NOT NULL, " + + "status VARCHAR, " + + "tag_id INTEGER)", + "CREATE INDEX IF NOT EXISTS account_buddy_accountindex " + + "ON account_buddy (account_id)", + "CREATE INDEX IF NOT EXISTS account_buddy_buddyindex " + + "ON account_buddy (buddy_id)" + ].forEach(conn.executeSimpleSQL); + + return conn; +} + +// Wrap all the usage of DBConn inside a transaction that will be +// commited automatically at the end of the event loop spin so that +// we flush buddy list data to disk only once per event loop spin. +var gDBConnWithPendingTransaction = null; +this.__defineGetter__("DBConn", function() { + if (gDBConnWithPendingTransaction) + return gDBConnWithPendingTransaction; + + if (!gDBConnection) { + gDBConnection = getDBConnection(); + function dbClose(aSubject, aTopic, aData) { + Services.obs.removeObserver(dbClose, aTopic); + if (gDBConnection) { + gDBConnection.asyncClose(); + gDBConnection = null; + } + } + Services.obs.addObserver(dbClose, "profile-before-change", false); + } + gDBConnWithPendingTransaction = gDBConnection; + gDBConnection.beginTransaction(); + executeSoon(function() { + gDBConnWithPendingTransaction.commitTransaction(); + gDBConnWithPendingTransaction = null; + }); + return gDBConnection; +}); + +function TagsService() { } +TagsService.prototype = { + get wrappedJSObject() this, + createTag: function(aName) { + // If the tag already exists, we don't want to create a duplicate. + let tag = this.getTagByName(aName); + if (tag) + return tag; + + let statement = DBConn.createStatement("INSERT INTO tags (name, position) VALUES(:name, 0)"); + statement.params.name = aName; + statement.executeStep(); + + tag = new Tag(DBConn.lastInsertRowID, aName); + Tags.push(tag); + return tag; + }, + // Get an existing tag by (numeric) id. Returns null if not found. + getTagById: function(aId) TagsById[aId], + // Get an existing tag by name (will do an SQL query). Returns null + // if not found. + getTagByName: function(aName) { + let statement = DBConn.createStatement("SELECT id FROM tags where name = :name"); + statement.params.name = aName; + if (!statement.executeStep()) + return null; + return this.getTagById(statement.row.id); + }, + // Get an array of all existing tags. + getTags: function(aTagCount) { + if (aTagCount) + aTagCount.value = Tags.length; + return Tags; + }, + + isTagHidden: function(aTag) aTag.id in otherContactsTag._hiddenTags, + hideTag: function(aTag) { otherContactsTag.hideTag(aTag); }, + showTag: function(aTag) { otherContactsTag.showTag(aTag); }, + get otherContactsTag() { + otherContactsTag._initContacts(); + return otherContactsTag; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.imITagsService]), + classDescription: "Tags", + classID: Components.ID("{1fa92237-4303-4384-b8ac-4e65b50810a5}"), + contractID: "@mozilla.org/chat/tags-service;1" +}; + +// TODO move into the tagsService +var Tags = []; +var TagsById = { }; + +function Tag(aId, aName) { + this._id = aId; + this._name = aName; + this._contacts = []; + this._observers = []; + + TagsById[this.id] = this; +} +Tag.prototype = { + get id() this._id, + get name() this._name, + set name(aNewName) { + let statement = DBConn.createStatement("UPDATE tags SET name = :name WHERE id = :id"); + statement.params.name = aNewName; + statement.params.id = this._id; + statement.execute(); + + //FIXME move the account buddies if some use this tag as their group + return aNewName; + }, + getContacts: function(aContactCount) { + let contacts = this._contacts.filter(function(c) !c._empty); + if (aContactCount) + aContactCount.value = contacts.length; + return contacts; + }, + _addContact: function (aContact) { + this._contacts.push(aContact); + }, + _removeContact: function (aContact) { + let index = this._contacts.indexOf(aContact); + if (index != -1) + this._contacts.splice(index, 1); + }, + + addObserver: function(aObserver) { + if (this._observers.indexOf(aObserver) == -1) + this._observers.push(aObserver); + }, + removeObserver: function(aObserver) { + this._observers = this._observers.filter(function(o) o !== aObserver); + }, + notifyObservers: function(aSubject, aTopic, aData) { + for each (let observer in this._observers) + observer.observe(aSubject, aTopic, aData); + }, + + getInterfaces: function(countRef) { + let interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.imITag]; + countRef.value = interfaces.length; + return interfaces; + }, + getHelperForLanguage: function(language) null, + implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT, + flags: 0, + QueryInterface: XPCOMUtils.generateQI([Ci.imITag, Ci.nsIClassInfo]) +}; + + +var otherContactsTag = { + hiddenTagsPref: "messenger.buddies.hiddenTags", + _hiddenTags: {}, + _contactsInitialized: false, + _saveHiddenTagsPref: function() { + Services.prefs.setCharPref(this.hiddenTagsPref, + [id for (id in this._hiddenTags)].join(",")); + }, + showTag: function(aTag) { + let id = aTag.id; + delete this._hiddenTags[id]; + for each (let contact in this._contacts) + if (contact.getTags().some(function(t) t.id == id)) + this._removeContact(contact); + + aTag.notifyObservers(aTag, "tag-shown", null); + Services.obs.notifyObservers(aTag, "tag-shown", null); + this._saveHiddenTagsPref(); + }, + hideTag: function(aTag) { + if (aTag.id < 0 || aTag.id in otherContactsTag._hiddenTags) + return; + + this._hiddenTags[aTag.id] = aTag; + if (this._contactsInitialized) + this._hideTag(aTag); + + aTag.notifyObservers(aTag, "tag-hidden", null); + Services.obs.notifyObservers(aTag, "tag-hidden", null); + this._saveHiddenTagsPref(); + }, + _hideTag: function(aTag) { + for each (let contact in aTag.getContacts()) + if (!(contact.id in this._contacts) && + contact.getTags().every(function(t) t.id in this._hiddenTags, this)) + this._addContact(contact); + }, + observe: function(aSubject, aTopic, aData) { + aSubject.QueryInterface(Ci.imIContact); + if (aTopic == "contact-tag-removed") { + if (!(aSubject.id in this._contacts) && + !(parseInt(aData) in this._hiddenTags) && + aSubject.getTags().every(function(t) t.id in this._hiddenTags, this)) + this._addContact(aSubject); + } + else if (aSubject.id in this._contacts && + (aTopic == "contact-removed" || + (aTopic == "contact-tag-added" && + !(parseInt(aData) in this._hiddenTags)))) + this._removeContact(aSubject); + }, + + _initHiddenTags: function() { + let pref = Services.prefs.getCharPref(this.hiddenTagsPref); + if (!pref) + return; + for each (let tagId in pref.split(",")) + this._hiddenTags[tagId] = TagsById[tagId]; + }, + _initContacts: function() { + if (this._contactsInitialized) + return; + this._observers = []; + this._observer = { + self: this, + observe: function(aSubject, aTopic, aData) { + if (aTopic == "contact-moved-in" && !(aSubject instanceof Contact)) + return; + + this.self.notifyObservers(aSubject, aTopic, aData); + } + }; + this._contacts = {}; + this._contactsInitialized = true; + for each (let tag in this._hiddenTags) + this._hideTag(tag); + Services.obs.addObserver(this, "contact-tag-added", false); + Services.obs.addObserver(this, "contact-tag-removed", false); + Services.obs.addObserver(this, "contact-removed", false); + }, + + // imITag implementation + get id() -1, + get name() "__others__", + set name(aNewName) { throw Cr.NS_ERROR_NOT_AVAILABLE; }, + getContacts: function(aContactCount) { + let contacts = [contact for each (contact in this._contacts)]; + if (aContactCount) + aContactCount.value = contacts.length; + return contacts; + }, + _addContact: function (aContact) { + this._contacts[aContact.id] = aContact; + this.notifyObservers(aContact, "contact-moved-in"); + for each (let observer in ContactsById[aContact.id]._observers) + observer.observe(this, "contact-moved-in", null); + aContact.addObserver(this._observer); + }, + _removeContact: function (aContact) { + delete this._contacts[aContact.id]; + aContact.removeObserver(this._observer); + this.notifyObservers(aContact, "contact-moved-out"); + for each (let observer in ContactsById[aContact.id]._observers) + observer.observe(this, "contact-moved-out", null); + }, + + addObserver: function(aObserver) { + if (this._observers.indexOf(aObserver) == -1) + this._observers.push(aObserver); + }, + removeObserver: function(aObserver) { + this._observers = this._observers.filter(function(o) o !== aObserver); + }, + notifyObservers: function(aSubject, aTopic, aData) { + for each (let observer in this._observers) + observer.observe(aSubject, aTopic, aData); + }, + + getInterfaces: function(countRef) { + let interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.nsIObserver, Ci.imITag]; + countRef.value = interfaces.length; + return interfaces; + }, + getHelperForLanguage: function(language) null, + implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT, + flags: 0, + QueryInterface: XPCOMUtils.generateQI([Ci.imITag, Ci.nsIObserver, Ci.nsIClassInfo]) +}; + + +var ContactsById = { }; +var LastDummyContactId = 0; +function Contact(aId, aAlias) { + // Assign a negative id to dummy contacts that have a single buddy + this._id = aId || --LastDummyContactId; + this._alias = aAlias; + this._tags = []; + this._buddies = []; + this._observers = []; + + ContactsById[this._id] = this; +} +Contact.prototype = { + _id: 0, + get id() this._id, + get alias() this._alias, + set alias(aNewAlias) { + this._ensureNotDummy(); + + let statement = DBConn.createStatement("UPDATE contacts SET alias = :alias WHERE id = :id"); + statement.params.alias = aNewAlias; + statement.params.id = this._id; + statement.executeAsync(); + + let oldDisplayName = this.displayName; + this._alias = aNewAlias; + this._notifyObservers("display-name-changed", oldDisplayName); + for each (let buddy in this._buddies) + for each (let accountBuddy in buddy._accounts) + accountBuddy.serverAlias = aNewAlias; + return aNewAlias; + }, + _ensureNotDummy: function() { + if (this._id >= 0) + return; + + // Create a real contact for this dummy contact + let statement = DBConn.createStatement("INSERT INTO contacts DEFAULT VALUES"); + statement.execute(); + delete ContactsById[this._id]; + let oldId = this._id; + this._id = DBConn.lastInsertRowID; + ContactsById[this._id] = this; + this._notifyObservers("no-longer-dummy", oldId.toString()); + // Update the contact_id for the single existing buddy of this contact + statement = DBConn.createStatement("UPDATE buddies SET contact_id = :id WHERE id = :buddy_id"); + statement.params.id = this._id; + statement.params.buddy_id = this._buddies[0].id; + statement.executeAsync(); + }, + + getTags: function(aTagCount) { + if (aTagCount) + aTagCount.value = this._tags.length; + return this._tags; + }, + addTag: function(aTag, aInherited) { + if (this.hasTag(aTag)) + return; + + if (!aInherited) { + this._ensureNotDummy(); + let statement = + DBConn.createStatement("INSERT INTO contact_tag (contact_id, tag_id) " + + "VALUES(:contactId, :tagId)"); + statement.params.contactId = this.id; + statement.params.tagId = aTag.id; + statement.executeAsync(); + } + + aTag = TagsById[aTag.id]; + this._tags.push(aTag); + aTag._addContact(this); + + aTag.notifyObservers(this, "contact-moved-in"); + for each (let observer in this._observers) + observer.observe(aTag, "contact-moved-in", null); + Services.obs.notifyObservers(this, "contact-tag-added", aTag.id); + }, + /* Remove a tag from the local tags of the contact. */ + _removeTag: function(aTag) { + if (!this.hasTag(aTag) || this._isTagInherited(aTag)) + return; + + this._removeContactTagRow(aTag); + + this._tags = this._tags.filter(function(tag) tag.id != aTag.id); + aTag = TagsById[aTag.id]; + aTag._removeContact(this); + + aTag.notifyObservers(this, "contact-moved-out"); + for each (let observer in this._observers) + observer.observe(aTag, "contact-moved-out", null); + Services.obs.notifyObservers(this, "contact-tag-removed", aTag.id); + }, + _removeContactTagRow: function(aTag) { + let statement = DBConn.createStatement("DELETE FROM contact_tag " + + "WHERE contact_id = :contactId " + + "AND tag_id = :tagId"); + statement.params.contactId = this.id; + statement.params.tagId = aTag.id; + statement.executeAsync(); + }, + hasTag: function(aTag) this._tags.some(function (t) t.id == aTag.id), + _massMove: false, + removeTag: function(aTag) { + if (!this.hasTag(aTag)) + throw "Attempting to remove a tag that the contact doesn't have"; + if (this._tags.length == 1) + throw "Attempting to remove the last tag of a contact"; + + this._massMove = true; + let hasTag = this.hasTag.bind(this); + let newTag = this._tags[this._tags[0].id != aTag.id ? 0 : 1]; + let moved = false; + this._buddies.forEach(function (aBuddy) { + aBuddy._accounts.forEach(function (aAccountBuddy) { + if (aAccountBuddy.tag.id == aTag.id) { + if (aBuddy._accounts.some(function(ab) + ab.account.numericId == aAccountBuddy.account.numericId && + ab.tag.id != aTag.id && hasTag(ab.tag))) { + // A buddy that already has an accountBuddy of the same + // account with another tag of the contact shouldn't be + // moved to newTag, just remove the accountBuddy + // associated to the tag we are removing. + aAccountBuddy.remove(); + moved = true; + } + else { + try { + aAccountBuddy.tag = newTag; + moved = true; + } catch (e) { + // Ignore failures. Some protocol plugins may not implement this. + } + } + } + }); + }); + this._massMove = false; + if (moved) + this._moved(aTag, newTag); + else { + // If we are here, the old tag is not inherited from a buddy, so + // just remove the local tag. + this._removeTag(aTag); + } + }, + _isTagInherited: function(aTag) { + for each (let buddy in this._buddies) + for each (let accountBuddy in buddy._accounts) + if (accountBuddy.tag.id == aTag.id) + return true; + return false; + }, + _moved: function(aOldTag, aNewTag) { + if (this._massMove) + return; + + // Avoid xpconnect wrappers. + aNewTag = aNewTag && TagsById[aNewTag.id]; + aOldTag = aOldTag && TagsById[aOldTag.id]; + + // Decide what we need to do. Return early if nothing to do. + let shouldRemove = + aOldTag && this.hasTag(aOldTag) && !this._isTagInherited(aOldTag); + let shouldAdd = + aNewTag && !this.hasTag(aNewTag) && this._isTagInherited(aNewTag); + if (!shouldRemove && !shouldAdd) + return; + + // Apply the changes. + let tags = this._tags; + if (shouldRemove) { + tags = tags.filter(function(aTag) aTag.id != aOldTag.id); + aOldTag._removeContact(this); + } + if (shouldAdd) { + tags.push(aNewTag); + aNewTag._addContact(this); + } + this._tags = tags; + + // Finally, notify of the changes. + if (shouldRemove) { + aOldTag.notifyObservers(this, "contact-moved-out"); + for each (let observer in this._observers) + observer.observe(aOldTag, "contact-moved-out", null); + Services.obs.notifyObservers(this, "contact-tag-removed", aOldTag.id); + } + if (shouldAdd) { + aNewTag.notifyObservers(this, "contact-moved-in"); + for each (let observer in this._observers) + observer.observe(aNewTag, "contact-moved-in", null); + Services.obs.notifyObservers(this, "contact-tag-added", aNewTag.id); + } + Services.obs.notifyObservers(this, "contact-moved", null); + }, + + getBuddies: function(aBuddyCount) { + if (aBuddyCount) + aBuddyCount.value = this._buddies.length; + return this._buddies; + }, + get _empty() this._buddies.length == 0 || + this._buddies.every(function(b) b._empty), + + mergeContact: function(aContact) { + // Avoid merging the contact with itself or merging into an + // already removed contact. + if (aContact.id == this.id || !(this.id in ContactsById)) + throw Components.results.NS_ERROR_INVALID_ARG; + + this._ensureNotDummy(); + let contact = ContactsById[aContact.id]; // remove XPConnect wrapper + + // Copy all the contact-only tags first, otherwise they would be lost. + for each (let tag in contact.getTags()) + if (!contact._isTagInherited(tag)) + this.addTag(tag); + + // Adopt each buddy. Removing the last one will delete the contact. + for each (let buddy in contact.getBuddies()) + buddy.contact = this; + this._updatePreferredBuddy(); + }, + moveBuddyBefore: function(aBuddy, aBeforeBuddy) { + let buddy = BuddiesById[aBuddy.id]; // remove XPConnect wrapper + let oldPosition = this._buddies.indexOf(buddy); + if (oldPosition == -1) + throw "aBuddy isn't attached to this contact"; + + let newPosition = -1; + if (aBeforeBuddy) + newPosition = this._buddies.indexOf(BuddiesById[aBeforeBuddy.id]); + if (newPosition == -1) + newPosition = this._buddies.length - 1; + + if (oldPosition == newPosition) + return; + + this._buddies.splice(oldPosition, 1); + this._buddies.splice(newPosition, 0, buddy); + this._updatePositions(Math.min(oldPosition, newPosition), + Math.max(oldPosition, newPosition)); + buddy._notifyObservers("position-changed", String(newPosition)); + this._updatePreferredBuddy(buddy); + }, + adoptBuddy: function(aBuddy) { + if (aBuddy.contact.id == this.id) + throw Components.results.NS_ERROR_INVALID_ARG; + + let buddy = BuddiesById[aBuddy.id]; // remove XPConnect wrapper + buddy.contact = this; + this._updatePreferredBuddy(buddy); + }, + _massRemove: false, + _removeBuddy: function(aBuddy) { + if (this._buddies.length == 1) { + if (this._id > 0) { + let statement = + DBConn.createStatement("DELETE FROM contacts WHERE id = :id"); + statement.params.id = this._id; + statement.executeAsync(); + } + this._notifyObservers("removed"); + delete ContactsById[this._id]; + + for each (let tag in this._tags) + tag._removeContact(this); + let statement = + DBConn.createStatement("DELETE FROM contact_tag WHERE contact_id = :id"); + statement.params.id = this._id; + statement.executeAsync(); + + delete this._tags; + delete this._buddies; + delete this._observers; + } + else { + let index = this._buddies.indexOf(aBuddy); + if (index == -1) + throw "Removing an unknown buddy from contact " + this._id; + + this._buddies = this._buddies.filter(function(b) b !== aBuddy); + + // If we are actually removing the whole contact, don't bother updating + // the positions or the preferred buddy. + if (this._massRemove) + return; + + // No position to update if the removed buddy is at the last position. + if (index < this._buddies.length) + this._updatePositions(index); + + if (this._preferredBuddy.id == aBuddy.id) + this._updatePreferredBuddy(); + } + }, + _updatePositions: function(aIndexBegin, aIndexEnd) { + if (aIndexEnd === undefined) + aIndexEnd = this._buddies.length - 1; + if (aIndexBegin > aIndexEnd) + throw "_updatePositions: Invalid indexes"; + + let statement = + DBConn.createStatement("UPDATE buddies SET position = :position " + + "WHERE id = :buddyId"); + for (let i = aIndexBegin; i <= aIndexEnd; ++i) { + statement.params.position = i; + statement.params.buddyId = this._buddies[i].id; + statement.executeAsync(); + } + }, + + detachBuddy: function(aBuddy) { + // Should return a new contact with the same list of tags. + let buddy = BuddiesById[aBuddy.id]; + if (buddy.contact.id != this.id) + throw Components.results.NS_ERROR_INVALID_ARG; + if (buddy.contact._buddies.length == 1) + throw Components.results.NS_ERROR_UNEXPECTED; + + // Save the list of tags, it may be destoyed if the buddy was the last one. + let tags = buddy.contact.getTags(); + + // Create a new dummy contact and use it for the detached buddy. + buddy.contact = new Contact(); + + // The first tag was inherited during the contact setter. + // This will copy the remaining tags. + for each (let tag in tags) + buddy.contact.addTag(tag); + + return buddy.contact; + }, + remove: function() { + this._massRemove = true; + for each (let buddy in this._buddies) + buddy.remove(); + }, + + // imIStatusInfo implementation + _preferredBuddy: null, + get preferredBuddy() { + if (!this._preferredBuddy) + this._updatePreferredBuddy(); + return this._preferredBuddy; + }, + set preferredBuddy(aBuddy) { + let shouldNotify = this._preferredBuddy != null; + let oldDisplayName = + this._preferredBuddy && this._preferredBuddy.displayName; + this._preferredBuddy = aBuddy; + if (shouldNotify) + this._notifyObservers("preferred-buddy-changed"); + if (oldDisplayName && this._preferredBuddy.displayName != oldDisplayName) + this._notifyObservers("display-name-changed", oldDisplayName); + this._updateStatus(); + }, + // aBuddy indicate which buddy's availability has changed. + _updatePreferredBuddy: function(aBuddy) { + if (aBuddy) { + aBuddy = BuddiesById[aBuddy.id]; // remove potential XPConnect wrapper + + if (!this._preferredBuddy) { + this.preferredBuddy = aBuddy; + return; + } + + if (aBuddy.id == this._preferredBuddy.id) { + // The suggested buddy is already preferred, check if its + // availability has changed. + if (aBuddy.statusType > this._statusType || + (aBuddy.statusType == this._statusType && + aBuddy.availabilityDetails >= this._availabilityDetails)) { + // keep the currently preferred buddy, only update the status. + this._updateStatus(); + return; + } + // We aren't sure that the currently preferred buddy should + // still be preferred. Let's go through the list! + } + else { + // The suggested buddy is not currently preferred. If it is + // more available or at a better position, prefer it! + if (aBuddy.statusType > this._statusType || + (aBuddy.statusType == this._statusType && + (aBuddy.availabilityDetails > this._availabilityDetails || + (aBuddy.availabilityDetails == this._availabilityDetails && + this._buddies.indexOf(aBuddy) < this._buddies.indexOf(this.preferredBuddy))))) + this.preferredBuddy = aBuddy; + return; + } + } + + let preferred; + // |this._buddies| is ordered by user preference, so in case of + // equal availability, keep the current value of |preferred|. + for each (let buddy in this._buddies) { + if (!preferred || preferred.statusType < buddy.statusType || + (preferred.statusType == buddy.statusType && + preferred.availabilityDetails < buddy.availabilityDetails)) + preferred = buddy; + } + if (preferred && (!this._preferredBuddy || + preferred.id != this._preferredBuddy.id)) + this.preferredBuddy = preferred; + }, + _updateStatus: function() { + let buddy = this._preferredBuddy; // for convenience + + // Decide which notifications should be fired. + let notifications = []; + if (this._statusType != buddy.statusType || + this._availabilityDetails != buddy.availabilityDetails) + notifications.push("availability-changed"); + if (this._statusType != buddy.statusType || + this._statusText != buddy.statusText) { + notifications.push("status-changed"); + if (this.online && buddy.statusType <= Ci.imIStatusInfo.STATUS_OFFLINE) + notifications.push("signed-off"); + if (!this.online && buddy.statusType > Ci.imIStatusInfo.STATUS_OFFLINE) + notifications.push("signed-on"); + } + + // Actually change the stored status. + [this._statusType, this._statusText, this._availabilityDetails] = + [buddy.statusType, buddy.statusText, buddy.availabilityDetails]; + + // Fire the notifications. + notifications.forEach(function(aTopic) { + this._notifyObservers(aTopic); + }, this); + }, + get displayName() this._alias || this.preferredBuddy.displayName, + get buddyIconFilename() this.preferredBuddy.buddyIconFilename, + _statusType: 0, + get statusType() this._statusType, + get online() this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE, + get available() this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE, + get idle() this.statusType == Ci.imIStatusInfo.STATUS_IDLE, + get mobile() this.statusType == Ci.imIStatusInfo.STATUS_MOBILE, + _statusText: "", + get statusText() this._statusText, + _availabilityDetails: 0, + get availabilityDetails() this._availabilityDetails, + get canSendMessage() this.preferredBuddy.canSendMessage, + //XXX should we list the buddies in the tooltip? + getTooltipInfo: function() this.preferredBuddy.getTooltipInfo(), + createConversation: function() { + let uiConv = Services.conversations.getUIConversationByContactId(this.id); + if (uiConv) + return uiConv.target; + return this.preferredBuddy.createConversation(); + }, + + addObserver: function(aObserver) { + if (this._observers.indexOf(aObserver) == -1) + this._observers.push(aObserver); + }, + removeObserver: function(aObserver) { + if (!this.hasOwnProperty("_observers")) + return; + + this._observers = this._observers.filter(function(o) o !== aObserver); + }, + // internal calls + calls from add-ons + notifyObservers: function(aSubject, aTopic, aData) { + for each (let observer in this._observers) + if ("observe" in observer) // avoid failing on destructed XBL bindings... + observer.observe(aSubject, aTopic, aData); + for each (let tag in this._tags) + tag.notifyObservers(aSubject, aTopic, aData); + Services.obs.notifyObservers(aSubject, aTopic, aData); + }, + _notifyObservers: function(aTopic, aData) { + this.notifyObservers(this, "contact-" + aTopic, aData); + }, + + // This is called by the imIBuddy implementations. + _observe: function(aSubject, aTopic, aData) { + // Forward the notification. + this.notifyObservers(aSubject, aTopic, aData); + + let isPreferredBuddy = + aSubject instanceof Buddy && aSubject.id == this.preferredBuddy.id; + switch (aTopic) { + case "buddy-availability-changed": + this._updatePreferredBuddy(aSubject); + break; + case "buddy-status-changed": + if (isPreferredBuddy) + this._updateStatus(); + break; + case "buddy-display-name-changed": + if (isPreferredBuddy && !this._alias) + this._notifyObservers("display-name-changed", aData); + break; + case "buddy-icon-changed": + if (isPreferredBuddy) + this._notifyObservers("icon-changed"); + break; + case "buddy-added": + // Currently buddies are always added in dummy empty contacts, + // later we may want to check this._buddies.length == 1. + this._notifyObservers("added"); + break; + case "buddy-removed": + this._removeBuddy(aSubject); + } + }, + + getInterfaces: function(countRef) { + let interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.imIContact]; + countRef.value = interfaces.length; + return interfaces; + }, + getHelperForLanguage: function(language) null, + implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT, + flags: 0, + QueryInterface: XPCOMUtils.generateQI([Ci.imIContact, Ci.nsIClassInfo]) +}; + +var BuddiesById = { }; +function Buddy(aId, aKey, aName, aSrvAlias, aContactId) { + this._id = aId; + this._key = aKey; + this._name = aName; + if (aSrvAlias) + this._srvAlias = aSrvAlias; + this._accounts = []; + this._observers = []; + + if (aContactId) + this._contact = ContactsById[aContactId]; + // Avoid failure if aContactId was invalid. + if (!this._contact) + this._contact = new Contact(null, null); + + this._contact._buddies.push(this); + + BuddiesById[this._id] = this; +} +Buddy.prototype = { + get id() this._id, + destroy: function() { + for each (let ab in this._accounts) + ab.unInit(); + delete this._accounts; + delete this._observers; + delete this._preferredAccount; + }, + get protocol() this._accounts[0].account.protocol, + get userName() this._name, + get normalizedName() this._key, + _srvAlias: "", + _contact: null, + get contact() this._contact, + set contact(aContact) /* not in imIBuddy */ { + if (aContact.id == this._contact.id) + throw Components.results.NS_ERROR_INVALID_ARG; + + this._notifyObservers("moved-out-of-contact"); + this._contact._removeBuddy(this); + + this._contact = aContact; + this._contact._buddies.push(this); + + // Ensure all the inherited tags are in the new contact. + for each (let accountBuddy in this._accounts) + this._contact.addTag(TagsById[accountBuddy.tag.id], true); + + let statement = + DBConn.createStatement("UPDATE buddies SET contact_id = :contactId, " + + "position = :position " + + "WHERE id = :buddyId"); + statement.params.contactId = aContact.id > 0 ? aContact.id : 0; + statement.params.position = aContact._buddies.length - 1; + statement.params.buddyId = this.id; + statement.executeAsync(); + + this._notifyObservers("moved-into-contact"); + return aContact; + }, + _hasAccountBuddy: function(aAccountId, aTagId) { + for each (let ab in this._accounts) { + if (ab.account.numericId == aAccountId && ab.tag.id == aTagId) + return true; + } + return false; + }, + getAccountBuddies: function(aAccountBuddyCount) { + if (aAccountBuddyCount) + aAccountBuddyCount.value = this._accounts.length; + return this._accounts; + }, + + _addAccount: function(aAccountBuddy, aTag) { + this._accounts.push(aAccountBuddy); + let contact = this._contact; + if (this._contact._tags.indexOf(aTag) == -1) { + this._contact._tags.push(aTag); + aTag._addContact(contact); + } + + if (!this._preferredAccount) + this._preferredAccount = aAccountBuddy; + }, + get _empty() this._accounts.length == 0, + + remove: function() { + for each (let account in this._accounts) + account.remove(); + }, + + // imIStatusInfo implementation + _preferredAccount: null, + get preferredAccountBuddy() this._preferredAccount, + _isPreferredAccount: function(aAccountBuddy) { + if (aAccountBuddy.account.numericId != this._preferredAccount.account.numericId) + return false; + + // In case we have more than one accountBuddy for the same buddy + // and account (possible if the buddy is in several groups on the + // server), the protocol plugin may be broken and not update all + // instances, so ensure we handle the notifications on the instance + // that is currently being notified of a change: + this._preferredAccount = aAccountBuddy; + + return true; + }, + set preferredAccount(aAccount) { + let oldDisplayName = + this._preferredAccount && this._preferredAccount.displayName; + this._preferredAccount = aAccount; + this._notifyObservers("preferred-account-changed"); + if (oldDisplayName && this._preferredAccount.displayName != oldDisplayName) + this._notifyObservers("display-name-changed", oldDisplayName); + this._updateStatus(); + }, + // aAccount indicate which account's availability has changed. + _updatePreferredAccount: function(aAccount) { + if (aAccount) { + if (aAccount.account.numericId == this._preferredAccount.account.numericId) { + // The suggested account is already preferred, check if its + // availability has changed. + if (aAccount.statusType > this._statusType || + (aAccount.statusType == this._statusType && + aAccount.availabilityDetails >= this._availabilityDetails)) { + // keep the currently preferred account, only update the status. + this._updateStatus(); + return; + } + // We aren't sure that the currently preferred account should + // still be preferred. Let's go through the list! + } + else { + // The suggested account is not currently preferred. If it is + // more available, prefer it! + if (aAccount.statusType > this._statusType || + (aAccount.statusType == this._statusType && + aAccount.availabilityDetails > this._availabilityDetails)) + this.preferredAccount = aAccount; + return; + } + } + + let preferred; + //TODO take into account the order of the account-manager list. + for each (let account in this._accounts) { + if (!preferred || preferred.statusType < account.statusType || + (preferred.statusType == account.statusType && + preferred.availabilityDetails < account.availabilityDetails)) + preferred = account; + } + if (!this._preferredAccount) { + if (preferred) + this.preferredAccount = preferred; + return; + } + if (preferred.account.numericId != this._preferredAccount.account.numericId) + this.preferredAccount = preferred; + else + this._updateStatus(); + }, + _updateStatus: function() { + let account = this._preferredAccount; // for convenience + + // Decide which notifications should be fired. + let notifications = []; + if (this._statusType != account.statusType || + this._availabilityDetails != account.availabilityDetails) + notifications.push("availability-changed"); + if (this._statusType != account.statusType || + this._statusText != account.statusText) { + notifications.push("status-changed"); + if (this.online && account.statusType <= Ci.imIStatusInfo.STATUS_OFFLINE) + notifications.push("signed-off"); + if (!this.online && account.statusType > Ci.imIStatusInfo.STATUS_OFFLINE) + notifications.push("signed-on"); + } + + // Actually change the stored status. + [this._statusType, this._statusText, this._availabilityDetails] = + [account.statusType, account.statusText, account.availabilityDetails]; + + // Fire the notifications. + notifications.forEach(function(aTopic) { + this._notifyObservers(aTopic); + }, this); + }, + get displayName() this._preferredAccount && this._preferredAccount.displayName || + this._srvAlias || this._name, + get buddyIconFilename() this._preferredAccount.buddyIconFilename, + _statusType: 0, + get statusType() this._statusType, + get online() this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE, + get available() this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE, + get idle() this.statusType == Ci.imIStatusInfo.STATUS_IDLE, + get mobile() this.statusType == Ci.imIStatusInfo.STATUS_MOBILE, + _statusText: "", + get statusText() this._statusText, + _availabilityDetails: 0, + get availabilityDetails() this._availabilityDetails, + get canSendMessage() this._preferredAccount.canSendMessage, + //XXX should we list the accounts in the tooltip? + getTooltipInfo: function() this._preferredAccount.getTooltipInfo(), + createConversation: function() this._preferredAccount.createConversation(), + + addObserver: function(aObserver) { + if (this._observers.indexOf(aObserver) == -1) + this._observers.push(aObserver); + }, + removeObserver: function(aObserver) { + this._observers = this._observers.filter(function(o) o !== aObserver); + }, + // internal calls + calls from add-ons + notifyObservers: function(aSubject, aTopic, aData) { + try { + for each (let observer in this._observers) + observer.observe(aSubject, aTopic, aData); + this._contact._observe(aSubject, aTopic, aData); + } catch (e) { + Cu.reportError(e); + } + }, + _notifyObservers: function(aTopic, aData) { + this.notifyObservers(this, "buddy-" + aTopic, aData); + }, + + // This is called by the imIAccountBuddy implementations. + observe: function(aSubject, aTopic, aData) { + // Forward the notification. + this.notifyObservers(aSubject, aTopic, aData); + + switch (aTopic) { + case "account-buddy-availability-changed": + this._updatePreferredAccount(aSubject); + break; + case "account-buddy-status-changed": + if (this._isPreferredAccount(aSubject)) + this._updateStatus(); + break; + case "account-buddy-display-name-changed": + if (this._isPreferredAccount(aSubject)) { + this._srvAlias = + this.displayName != this.userName ? this.displayName : ""; + let statement = + DBConn.createStatement("UPDATE buddies SET srv_alias = :srvAlias " + + "WHERE id = :buddyId"); + statement.params.buddyId = this.id; + statement.params.srvAlias = this._srvAlias; + statement.executeAsync(); + this._notifyObservers("display-name-changed", aData); + } + break; + case "account-buddy-icon-changed": + if (this._isPreferredAccount(aSubject)) + this._notifyObservers("icon-changed"); + break; + case "account-buddy-added": + if (this._accounts.length == 0) { + // Add the new account in the empty buddy instance. + // The TagsById hack is to bypass the xpconnect wrapper. + this._addAccount(aSubject, TagsById[aSubject.tag.id]); + this._updateStatus(); + this._notifyObservers("added"); + } + else { + this._accounts.push(aSubject); + this.contact._moved(null, aSubject.tag); + this._updatePreferredAccount(aSubject); + } + break; + case "account-buddy-removed": + if (this._accounts.length == 1) { + let statement = + DBConn.createStatement("DELETE FROM buddies WHERE id = :id"); + statement.params.id = this.id; + statement.execute(); + + this._notifyObservers("removed"); + + delete BuddiesById[this._id]; + this.destroy(); + } + else { + this._accounts = this._accounts.filter(function (ab) { + return (ab.account.numericId != aSubject.account.numericId || + ab.tag.id != aSubject.tag.id); + }); + if (this._preferredAccount.account.numericId == aSubject.account.numericId && + this._preferredAccount.tag.id == aSubject.tag.id) { + this._preferredAccount = null; + this._updatePreferredAccount(); + } + this.contact._moved(aSubject.tag); + } + break; + } + }, + + getInterfaces: function(countRef) { + let interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.imIBuddy]; + countRef.value = interfaces.length; + return interfaces; + }, + getHelperForLanguage: function(language) null, + implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT, + flags: 0, + QueryInterface: XPCOMUtils.generateQI([Ci.imIBuddy, Ci.nsIClassInfo]) +}; + + +function ContactsService() { } +ContactsService.prototype = { + initContacts: function() { + let statement = DBConn.createStatement("SELECT id, name FROM tags"); + while (statement.executeStep()) + Tags.push(new Tag(statement.getInt32(0), statement.getUTF8String(1))); + + statement = DBConn.createStatement("SELECT id, alias FROM contacts"); + while (statement.executeStep()) + new Contact(statement.getInt32(0), statement.getUTF8String(1)); + + statement = + DBConn.createStatement("SELECT contact_id, tag_id FROM contact_tag"); + while (statement.executeStep()) { + let contact = ContactsById[statement.getInt32(0)]; + let tag = TagsById[statement.getInt32(1)]; + contact._tags.push(tag); + tag._addContact(contact); + } + + statement = DBConn.createStatement("SELECT id, key, name, srv_alias, contact_id FROM buddies ORDER BY position"); + while (statement.executeStep()) + new Buddy(statement.getInt32(0), statement.getUTF8String(1), + statement.getUTF8String(2), statement.getUTF8String(3), + statement.getInt32(4)); + // FIXME is there a way to enforce that all AccountBuddies of a Buddy have the same protocol? + + statement = DBConn.createStatement("SELECT account_id, buddy_id, tag_id FROM account_buddy"); + while (statement.executeStep()) { + let accountId = statement.getInt32(0); + let buddyId = statement.getInt32(1); + let tagId = statement.getInt32(2); + + if (!BuddiesById.hasOwnProperty(buddyId)) { + Cu.reportError("Corrupted database: account_buddy entry for account " + + accountId + " and tag " + tagId + + " references unknown buddy with id " + buddyId); + continue; + } + + let buddy = BuddiesById[buddyId]; + if (buddy._hasAccountBuddy(accountId, tagId)) { + Cu.reportError("Corrupted database: duplicated account_buddy entry: " + + "account_id = " + accountId + ", buddy_id = " + buddyId + + ", tag_id = " + tagId); + continue; + } + + let account = Services.accounts.getAccountByNumericId(accountId); + let tag = TagsById[tagId]; + try { + let accountBuddy = account.loadBuddy(buddy, tag); + if (accountBuddy) + buddy._addAccount(accountBuddy, tag); + } catch (e) { + // FIXME accountBuddy shouldn't be NULL (once imAccounts.js is finished) + // It currently doesn't work right with unknown protocols. + Components.utils.reportError(e); + dump(e + "\n"); + } + } + + otherContactsTag._initHiddenTags(); + }, + unInitContacts: function() { + Tags = []; + TagsById = { }; + // Avoid shutdown leaks caused by references to native components + // implementing imIAccountBuddy. + for each (let buddy in BuddiesById) + buddy.destroy(); + BuddiesById = { }; + ContactsById = { }; + }, + + getContactById: function(aId) ContactsById[aId], + getBuddyById: function(aId) BuddiesById[aId], + getBuddyByNameAndProtocol: function(aNormalizedName, aPrpl) { + let statement = + DBConn.createStatement("SELECT b.id FROM buddies b " + + "JOIN account_buddy ab ON buddy_id = b.id " + + "JOIN accounts a ON account_id = a.id " + + "WHERE b.key = :buddyName and a.prpl = :prplId"); + statement.params.buddyName = aNormalizedName; + statement.params.prplId = aPrpl.id; + if (!statement.executeStep()) + return null; + return BuddiesById[statement.row.id]; + }, + + accountBuddyAdded: function(aAccountBuddy) { + let account = aAccountBuddy.account; + let normalizedName = aAccountBuddy.normalizedName; + let buddy = this.getBuddyByNameAndProtocol(normalizedName, account.protocol); + if (!buddy) { + let statement = + DBConn.createStatement("INSERT INTO buddies " + + "(key, name, srv_alias, position) " + + "VALUES(:key, :name, :srvAlias, 0)"); + let name = aAccountBuddy.userName; + let srvAlias = aAccountBuddy.serverAlias; + statement.params.key = normalizedName; + statement.params.name = name; + statement.params.srvAlias = srvAlias; + statement.execute(); + buddy = + new Buddy(DBConn.lastInsertRowID, normalizedName, name, srvAlias, 0); + } + + // Initialize the 'buddy' field of the imIAccountBuddy instance. + aAccountBuddy.buddy = buddy; + + // Ensure we aren't storing a duplicate entry. + let accountId = account.numericId; + let tagId = aAccountBuddy.tag.id; + if (buddy._hasAccountBuddy(accountId, tagId)) { + Cu.reportError("Attempting to store a duplicate account buddy " + + normalizedName + ", account id = " + accountId + + ", tag id = " + tagId); + return; + } + + // Store the new account buddy. + let statement = + DBConn.createStatement("INSERT INTO account_buddy " + + "(account_id, buddy_id, tag_id) " + + "VALUES(:accountId, :buddyId, :tagId)"); + statement.params.accountId = accountId; + statement.params.buddyId = buddy.id; + statement.params.tagId = tagId; + statement.execute(); + + // Fire the notifications. + buddy.observe(aAccountBuddy, "account-buddy-added"); + }, + accountBuddyRemoved: function(aAccountBuddy) { + let buddy = aAccountBuddy.buddy; + let statement = + DBConn.createStatement("DELETE FROM account_buddy " + + "WHERE account_id = :accountId AND " + + "buddy_id = :buddyId AND " + + "tag_id = :tagId"); + statement.params.accountId = aAccountBuddy.account.numericId; + statement.params.buddyId = buddy.id; + statement.params.tagId = aAccountBuddy.tag.id; + statement.execute(); + + buddy.observe(aAccountBuddy, "account-buddy-removed"); + }, + + accountBuddyMoved: function(aAccountBuddy, aOldTag, aNewTag) { + let buddy = aAccountBuddy.buddy; + let statement = + DBConn.createStatement("UPDATE account_buddy " + + "SET tag_id = :newTagId " + + "WHERE account_id = :accountId AND " + + "buddy_id = :buddyId AND " + + "tag_id = :oldTagId"); + statement.params.accountId = aAccountBuddy.account.numericId; + statement.params.buddyId = buddy.id; + statement.params.oldTagId = aOldTag.id; + statement.params.newTagId = aNewTag.id; + statement.execute(); + + let contact = ContactsById[buddy.contact.id]; + + // aNewTag is now inherited by the contact from an account buddy, so avoid + // keeping direct tag <-> contact links in the contact_tag table. + contact._removeContactTagRow(aNewTag); + + buddy.observe(aAccountBuddy, "account-buddy-moved"); + contact._moved(aOldTag, aNewTag); + }, + + storeAccount: function(aId, aUserName, aPrplId) { + let statement = + DBConn.createStatement("SELECT name, prpl FROM accounts WHERE id = :id"); + statement.params.id = aId; + if (statement.executeStep()) { + if (statement.getUTF8String(0) == aUserName && + statement.getUTF8String(1) == aPrplId) + return; // The account is already stored correctly. + throw Cr.NS_ERROR_UNEXPECTED; // Corrupted database?!? + } + + // Actually store the account. + statement = DBConn.createStatement("INSERT INTO accounts (id, name, prpl) " + + "VALUES(:id, :userName, :prplId)"); + statement.params.id = aId; + statement.params.userName = aUserName; + statement.params.prplId = aPrplId; + statement.execute(); + }, + accountIdExists: function(aId) { + let statement = + DBConn.createStatement("SELECT id FROM accounts WHERE id = :id"); + statement.params.id = aId; + return statement.executeStep(); + }, + forgetAccount: function(aId) { + let statement = + DBConn.createStatement("DELETE FROM accounts WHERE id = :accountId"); + statement.params.accountId = aId; + statement.execute(); + + // removing the account from the accounts table is not enought, + // we need to remove all the associated account_buddy entries too + statement = DBConn.createStatement("DELETE FROM account_buddy " + + "WHERE account_id = :accountId"); + statement.params.accountId = aId; + statement.execute(); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.imIContactsService]), + classDescription: "Contacts", + classID: Components.ID("{8c3725dd-ee26-489d-8135-736015af8c7f}"), + contractID: "@mozilla.org/chat/contacts-service;1" +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([ContactsService, + TagsService]); diff --git a/chat/components/src/imContacts.manifest b/chat/components/src/imContacts.manifest new file mode 100644 --- /dev/null +++ b/chat/components/src/imContacts.manifest @@ -0,0 +1,4 @@ +component {8c3725dd-ee26-489d-8135-736015af8c7f} imContacts.js +contract @mozilla.org/chat/contacts-service;1 {8c3725dd-ee26-489d-8135-736015af8c7f} +component {1fa92237-4303-4384-b8ac-4e65b50810a5} imContacts.js +contract @mozilla.org/chat/tags-service;1 {1fa92237-4303-4384-b8ac-4e65b50810a5} diff --git a/chat/components/src/imConversations.js b/chat/components/src/imConversations.js new file mode 100644 --- /dev/null +++ b/chat/components/src/imConversations.js @@ -0,0 +1,458 @@ +/* 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/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +Cu.import("resource:///modules/imServices.jsm"); +Cu.import("resource:///modules/imStatusUtils.jsm"); +Cu.import("resource:///modules/imXPCOMUtils.jsm"); +Cu.import("resource:///modules/jsProtoHelper.jsm"); + +var gLastUIConvId = 0; +var gLastPurpleConvId = 0; + +XPCOMUtils.defineLazyGetter(this, "bundle", function() + Services.strings.createBundle("chrome://chat/locale/conversations.properties") +); + +function UIConversation(aPurpleConversation) +{ + this._purpleConv = {}; + this.id = ++gLastUIConvId; + this._observers = []; + this._messages = []; + this.changeTargetTo(aPurpleConversation); + let iface = Ci["prplIConv" + (aPurpleConversation.isChat ? "Chat" : "IM")]; + this._interfaces = this._interfaces.concat(iface); + let contact = this.contact; + if (contact) { + // XPConnect will create a wrapper around 'this' here, + // so the list of exposed interfaces shouldn't change anymore. + contact.addObserver(this); + this._observedContact = contact; + } + Services.obs.notifyObservers(this, "new-ui-conversation", null); +} + +UIConversation.prototype = { + __proto__: ClassInfo(["imIConversation", "prplIConversation", "nsIObserver"], + "UI conversation"), + _observedContact: null, + get contact() { + let target = this.target; + if (!target.isChat && target.buddy) + return target.buddy.buddy.contact; + return null; + }, + get target() this._purpleConv[this._currentTargetId], + set target(aPurpleConversation) { + this.changeTargetTo(aPurpleConversation); + }, + _currentTargetId: 0, + changeTargetTo: function(aPurpleConversation) { + let id = aPurpleConversation.id; + if (this._currentTargetId == id) + return; + + if (!(id in this._purpleConv)) { + this._purpleConv[id] = aPurpleConversation; + aPurpleConversation.addObserver(this.observeConv.bind(this, id)); + } + + let shouldNotify = this._currentTargetId; + this._currentTargetId = id; + if (!this.isChat) { + let buddy = this.buddy; + if (buddy) + ({statusType: this.statusType, statusText: this.statusText}) = buddy; + } + if (shouldNotify) { + this.notifyObservers(this, "target-purple-conversation-changed"); + let target = this.target; + let params = [target.title, target.account.protocol.name]; + this.systemMessage(bundle.formatStringFromName("targetChanged", + params, params.length)); + } + }, + // Returns a boolean indicating if the ui-conversation was closed. + // If the conversation was closed, aContactId.value is set to the contact id + // or 0 if no contact was associated with the conversation. + removeTarget: function(aPurpleConversation, aContactId) { + let id = aPurpleConversation.id; + if (!(id in this._purpleConv)) + throw "unknown purple conversation"; + + delete this._purpleConv[id]; + if (this._currentTargetId != id) + return false; + + for (let newId in this._purpleConv) { + this.changeTargetTo(this._purpleConv[newId]); + return false; + } + + if (this._observedContact) { + this._observedContact.removeObserver(this); + aContactId.value = this._observedContact.id; + delete this._observedContact; + } + else + aContactId.value = 0; + + delete this._currentTargetId; + this.notifyObservers(this, "ui-conversation-closed"); + return true; + }, + + _unreadMessageCount: 0, + get unreadMessageCount() this._unreadMessageCount, + _unreadTargetedMessageCount: 0, + get unreadTargetedMessageCount() this._unreadTargetedMessageCount, + _unreadIncomingMessageCount: 0, + get unreadIncomingMessageCount() this._unreadIncomingMessageCount, + markAsRead: function() { + delete this._unreadMessageCount; + delete this._unreadTargetedMessageCount; + delete this._unreadIncomingMessageCount; + this._notifyUnreadCountChanged(); + }, + _lastNotifiedUnreadCount: 0, + _notifyUnreadCountChanged: function() { + if (this._unreadIncomingMessageCount == this._lastNotifiedUnreadCount) + return; + + this._lastNotifiedUnreadCount = this._unreadIncomingMessageCount; + for each (let observer in this._observers) + observer.observe(this, "unread-message-count-changed", + this._unreadIncomingMessageCount.toString()); + }, + getMessages: function(aMessageCount) { + if (aMessageCount) + aMessageCount.value = this._messages.length; + return this._messages; + }, + checkClose: function() { + if (!this._currentTargetId) + return true; // already closed. + + if (!Services.prefs.getBoolPref("messenger.conversations.alwaysClose") && + (this.isChat && !this.left || + !this.isChat && this.unreadIncomingMessageCount != 0)) + return false; + + this.close(); + return true; + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic == "contact-no-longer-dummy") { + let oldId = parseInt(aData); + // gConversationsService is ugly... :( + delete gConversationsService._uiConvByContactId[oldId]; + gConversationsService._uiConvByContactId[aSubject.id] = this; + } + else if (aTopic == "account-buddy-status-changed") { + if (!this._statusUpdatePending && + aSubject.account.id == this.account.id && + aSubject.buddy.id == this.buddy.buddy.id) { + this._statusUpdatePending = true; + Services.tm.mainThread.dispatch(this.updateBuddyStatus.bind(this), + Ci.nsIEventTarget.DISPATCH_NORMAL); + } + } + else if (aTopic == "account-buddy-icon-changed") { + if (!this._statusUpdatePending && + aSubject.account.id == this.account.id && + aSubject.buddy.id == this.buddy.buddy.id) { + this._iconUpdatePending = true; + Services.tm.mainThread.dispatch(this.updateIcon.bind(this), + Ci.nsIEventTarget.DISPATCH_NORMAL); + } + } + }, + + _iconUpdatePending: false, + updateIcon: function() { + delete this._iconUpdatePending; + this.notifyObservers(this, "update-buddy-icon"); + }, + + _statusUpdatePending: false, + updateBuddyStatus: function() { + delete this._statusUpdatePending; + let {statusType: statusType, statusText: statusText} = this.buddy; + + if (("statusType" in this) && this.statusType == statusType && + this.statusText == statusText) + return; + + let wasUnknown = this.statusType == Ci.imIStatusInfo.STATUS_UNKNOWN; + this.statusType = statusType; + this.statusText = statusText; + + this.notifyObservers(this, "update-buddy-status"); + + let msg; + if (statusType == Ci.imIStatusInfo.STATUS_UNKNOWN) + msg = bundle.formatStringFromName("statusUnknown", [this.title], 1); + else { + let status = Status.toLabel(statusType); + let stringId = wasUnknown ? "statusChangedFromUnknown" : "statusChanged"; + if (this._justReconnected) { + stringId = "statusKnown"; + delete this._justReconnected; + } + if (statusText) { + msg = bundle.formatStringFromName(stringId + "WithStatusText", + [this.title, status, statusText], + 3); + } + else + msg = bundle.formatStringFromName(stringId, [this.title, status], 2); + } + this.systemMessage(msg); + }, + + _disconnected: false, + disconnecting: function() { + if (this._disconnected) + return; + + this._disconnected = true; + if (this.contact) + return; // handled by the contact observer. + + if (this.isChat && this.left) + this._wasLeft = true; + else + this.systemMessage(bundle.GetStringFromName("accountDisconnected")); + this.notifyObservers(this, "update-buddy-status"); + }, + connected: function() { + if (this._disconnected) { + delete this._disconnected; + if (!this.isChat) + this._justReconnected = true; + else if (!this._wasLeft) + this.systemMessage(bundle.GetStringFromName("accountReconnected")); + else + delete this._wasLeft; + } + this.notifyObservers(this, "update-buddy-status"); + }, + + observeConv: function(aTargetId, aSubject, aTopic, aData) { + if (aTargetId != this._currentTargetId && + (aTopic == "new-text" || + (aTopic == "update-typing" && + this._purpleConv[aTargetId].typingState == Ci.prplIConvIM.TYPING))) + this.target = this._purpleConv[aTargetId]; + 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); + } + } + }, + + systemMessage: function(aText, aIsError) { + let flags = {system: true, noLog: true, error: !!aIsError}; + (new Message("system", aText, flags)).conversation = this; + }, + + // 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, + sendMsg: function (aMsg) { this.target.sendMsg(aMsg); }, + unInit: function() { + for each (let conv in this._purpleConv) + gConversationsService.forgetConversation(conv); + if (this._observedContact) { + this._observedContact.removeObserver(this); + delete this._observedContact; + } + this._purpleConv = {}; // Prevent .close from failing. + delete this._currentTargetId; + }, + close: function() { + for each (let conv in this._purpleConv) + conv.close(); + if (!this.hasOwnProperty("_currentTargetId")) + return; + delete this._currentTargetId; + this.notifyObservers(this, "ui-conversation-closed"); + Services.obs.notifyObservers(this, "ui-conversation-closed", null); + }, + addObserver: function(aObserver) { + if (this._observers.indexOf(aObserver) == -1) + this._observers.push(aObserver); + }, + removeObserver: function(aObserver) { + this._observers = this._observers.filter(function(o) o !== aObserver); + }, + notifyObservers: function(aSubject, aTopic, aData) { + if (aTopic == "new-text") { + this._messages.push(aSubject); + ++this._unreadMessageCount; + if (aSubject.incoming && !aSubject.system) { + ++this._unreadIncomingMessageCount; + if (!this.isChat || aSubject.containsNick) + ++this._unreadTargetedMessageCount; + } + } + for each (let observer in this._observers) { + if (!observer.observe && this._observers.indexOf(observer) == -1) + continue; // observer removed by a previous call to another observer. + observer.observe(aSubject, aTopic, aData); + } + this._notifyUnreadCountChanged(); + }, + + // prplIConvIM + get buddy() this.target.buddy, + get typingState() this.target.typingState, + sendTyping: function(aLength) { this.target.sendTyping(aLength); }, + + // Chat only + getParticipants: function() this.target.getParticipants(), + get topic() this.target.topic, + set topic(aTopic) { this.target.topic = aTopic; }, + get topicSetter() this.target.topicSetter, + get topicSettable() this.target.topicSettable, + get nick() this.target.nick, + get left() this.target.left +}; + +var gConversationsService; +function ConversationsService() { gConversationsService = this; } +ConversationsService.prototype = { + get wrappedJSObject() this, + + initConversations: function() { + this._uiConv = {}; + this._uiConvByContactId = {}; + this._purpleConversations = []; + Services.obs.addObserver(this, "account-disconnecting", false); + Services.obs.addObserver(this, "account-connected", false); + }, + + unInitConversations: function() { + for each (let UIConv in this._uiConv) + UIConv.unInit(); + delete this._uiConv; + delete this._uiConvByContactId; + // This should already be empty, but just to be sure... + for each (let purpleConv in this._purpleConversations) + purpleConv.unInit(); + delete this._purpleConversations; + Services.obs.removeObserver(this, "account-disconnecting"); + Services.obs.removeObserver(this, "account-connected"); + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic == "account-connected") { + for each (let conv in this._uiConv) { + if (conv.account.id == aSubject.id) + conv.connected(); + } + } + else if (aTopic == "account-disconnecting") { + for each (let conv in this._uiConv) { + if (conv.account.id == aSubject.id) + conv.disconnecting(); + } + } + }, + + addConversation: function(aPurpleConversation) { + // Give an id to the new conversation. + aPurpleConversation.id = ++gLastPurpleConvId; + this._purpleConversations.push(aPurpleConversation); + + // Notify observers. + Services.obs.notifyObservers(aPurpleConversation, "new-conversation", null); + + // Update or create the corresponding UI conversation. + let contactId; + if (!aPurpleConversation.isChat) { + let accountBuddy = aPurpleConversation.buddy; + if (accountBuddy) + contactId = accountBuddy.buddy.contact.id; + } + + if (contactId) { + if (contactId in this._uiConvByContactId) { + let uiConv = this._uiConvByContactId[contactId]; + uiConv.target = aPurpleConversation; + this._uiConv[aPurpleConversation.id] = uiConv; + return; + } + } + + let newUIConv = new UIConversation(aPurpleConversation); + this._uiConv[aPurpleConversation.id] = newUIConv; + if (contactId) + this._uiConvByContactId[contactId] = newUIConv; + }, + removeConversation: function(aPurpleConversation) { + Services.obs.notifyObservers(aPurpleConversation, "conversation-closed", null); + + let uiConv = this.getUIConversation(aPurpleConversation); + let contactId = {}; + if (uiConv.removeTarget(aPurpleConversation, contactId)) { + delete this._uiConv[aPurpleConversation.id]; + if (contactId.value) + delete this._uiConvByContactId[contactId.value]; + Services.obs.notifyObservers(uiConv, "ui-conversation-closed", null); + } + this.forgetConversation(aPurpleConversation); + }, + forgetConversation: function(aPurpleConversation) { + aPurpleConversation.unInit(); + + this._purpleConversations = + this._purpleConversations.filter(function(c) c !== aPurpleConversation); + }, + + getUIConversations: function(aConvCount) { + let rv = Object.keys(this._uiConv).map(function (k) this._uiConv[k], this); + aConvCount.value = rv.length; + return rv; + }, + getUIConversation: function(aPurpleConversation) { + let id = aPurpleConversation.id; + if (id in this._uiConv) + return this._uiConv[id]; + throw "Unknown conversation"; + }, + getUIConversationByContactId: function(aId) + (aId in this._uiConvByContactId) ? this._uiConvByContactId[aId] : null, + + getConversations: function() new nsSimpleEnumerator(this._purpleConversations), + getConversationById: function(aId) { + for each (let conv in this._purpleConversations) + if (conv.id == aId) + return conv; + return null; + }, + getConversationByNameAndAccount: function(aName, aAccount, aIsChat) { + for each (let conv in this._purpleConversations) + if (conv.name == aName && conv.account.numericId == aAccount.numericId && + conv.isChat == aIsChat) + return conv; + return null; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.imIConversationsService]), + classDescription: "Conversations", + classID: Components.ID("{b2397cd5-c76d-4618-8410-f344c7c6443a}"), + contractID: "@mozilla.org/chat/conversations-service;1" +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([ConversationsService]); diff --git a/chat/components/src/imConversations.manifest b/chat/components/src/imConversations.manifest new file mode 100644 --- /dev/null +++ b/chat/components/src/imConversations.manifest @@ -0,0 +1,2 @@ +component {b2397cd5-c76d-4618-8410-f344c7c6443a} imConversations.js +contract @mozilla.org/chat/conversations-service;1 {b2397cd5-c76d-4618-8410-f344c7c6443a} diff --git a/chat/components/src/imCore.js b/chat/components/src/imCore.js new file mode 100644 --- /dev/null +++ b/chat/components/src/imCore.js @@ -0,0 +1,354 @@ +/* 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/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; +Cu.import("resource:///modules/imServices.jsm"); +Cu.import("resource:///modules/imXPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "categoryManager", + "@mozilla.org/categorymanager;1", + "nsICategoryManager"); + +const kQuitApplicationGranted = "quit-application-granted"; +const kProtocolPluginCategory = "im-protocol-plugin"; + +const kPrefReportIdle = "messenger.status.reportIdle"; +const kPrefUserIconFilename = "messenger.status.userIconFileName"; +const kPrefUserDisplayname = "messenger.status.userDisplayName"; +const kPrefTimeBeforeIdle = "messenger.status.timeBeforeIdle"; +const kPrefAwayWhenIdle = "messenger.status.awayWhenIdle"; +const kPrefDefaultMessage = "messenger.status.defaultIdleAwayMessage"; + +const NS_IOSERVICE_GOING_OFFLINE_TOPIC = "network:offline-about-to-go-offline"; +const NS_IOSERVICE_OFFLINE_STATUS_TOPIC = "network:offline-status-changed"; + +function UserStatus() +{ + this._observers = []; + + if (Services.prefs.getBoolPref(kPrefReportIdle)) + this._addIdleObserver(); + Services.prefs.addObserver(kPrefReportIdle, this, false); + + if (Services.io.offline) + this._offlineStatusType = Ci.imIStatusInfo.STATUS_OFFLINE; + Services.obs.addObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC, false); + Services.obs.addObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false); +} +UserStatus.prototype = { + __proto__: ClassInfo("imIUserStatusInfo", "User status info"), + + unInit: function() { + this._observers = []; + Services.prefs.removeObserver(kPrefReportIdle, this); + if (this._observingIdleness) + this._removeIdleObserver(); + Services.obs.removeObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC); + Services.obs.removeObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC); + }, + _observingIdleness: false, + _addIdleObserver: function() { + this._observingIdleness = true; + this._idleService = + Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService); + Services.obs.addObserver(this, "im-sent", false); + + this._timeBeforeIdle = Services.prefs.getIntPref(kPrefTimeBeforeIdle); + if (this._timeBeforeIdle < 0) + this._timeBeforeIdle = 0; + Services.prefs.addObserver(kPrefTimeBeforeIdle, this, false); + if (this._timeBeforeIdle) + this._idleService.addIdleObserver(this, this._timeBeforeIdle); + }, + _removeIdleObserver: function() { + if (this._timeBeforeIdle) + this._idleService.removeIdleObserver(this, this._timeBeforeIdle); + + Services.prefs.removeObserver(kPrefTimeBeforeIdle, this); + delete this._timeBeforeIdle; + + Services.obs.removeObserver(this, "im-sent"); + delete this._idleService; + delete this._observingIdleness; + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic == "nsPref:changed") { + if (aData == kPrefReportIdle) { + let reportIdle = Services.prefs.getBoolPref(kPrefReportIdle); + if (reportIdle && !this._observingIdleness) + this._addIdleObserver(); + else if (!reportIdle && this._observingIdleness) + this._removeIdleObserver(); + } + else if (aData == kPrefTimeBeforeIdle) { + let timeBeforeIdle = Services.prefs.getIntPref(kPrefTimeBeforeIdle); + if (timeBeforeIdle != this._timeBeforeIdle) { + if (this._timeBeforeIdle) + this._idleService.removeIdleObserver(this, this._timeBeforeIdle); + this._timeBeforeIdle = timeBeforeIdle; + if (this._timeBeforeIdle) + this._idleService.addIdleObserver(this, this._timeBeforeIdle); + } + } + else + throw Cr.NS_ERROR_UNEXPECTED; + } + else if (aTopic == NS_IOSERVICE_GOING_OFFLINE_TOPIC) + this.offline = true; + else if (aTopic == NS_IOSERVICE_OFFLINE_STATUS_TOPIC && aData == "online") + this.offline = false; + else + this._checkIdle(); + }, + + _offlineStatusType: Ci.imIStatusInfo.STATUS_AVAILABLE, + set offline(aOffline) { + let statusType = this.statusType; + let statusText = this.statusText; + if (aOffline) + this._offlineStatusType = Ci.imIStatusInfo.STATUS_OFFLINE; + else + delete this._offlineStatusType; + if (this.statusType != statusType || this.statusText != statusText) + this._notifyObservers("status-changed", this.statusText); + }, + + _idleTime: 0, + get idleTime() this._idleTime, + set idleTime(aIdleTime) { + this._idleTime = aIdleTime; + this._notifyObservers("idle-time-changed", aIdleTime); + }, + _idle: false, + _idleStatusText: "", + _idleStatusType: Ci.imIStatusInfo.STATUS_AVAILABLE, + _checkIdle: function() { + let idleTime = Math.floor(this._idleService.idleTime / 1000); + let idle = this._timeBeforeIdle && idleTime >= this._timeBeforeIdle; + if (idle == this._idle) + return; + + let statusType = this.statusType; + let statusText = this.statusText; + this._idle = idle; + if (idle) { + this.idleTime = idleTime; + if (Services.prefs.getBoolPref(kPrefAwayWhenIdle)) { + this._idleStatusType = Ci.imIStatusInfo.STATUS_AWAY; + this._idleStatusText = + Services.prefs.getComplexValue(kPrefDefaultMessage, + Ci.nsIPrefLocalizedString).data; + } + } + else { + this.idleTime = 0; + delete this._idleStatusType; + delete this._idleStatusText; + } + if (this.statusType != statusType || this.statusText != statusText) + this._notifyObservers("status-changed", this.statusText); + }, + + _statusText: "", + get statusText() this._statusText || this._idleStatusText, + _statusType: Ci.imIStatusInfo.STATUS_AVAILABLE, + get statusType() Math.min(this._statusType, this._idleStatusType, this._offlineStatusType), + setStatus: function(aStatus, aMessage) { + if (aStatus != Ci.imIStatusInfo.STATUS_UNKNOWN) + this._statusType = aStatus; + if (aStatus != Ci.imIStatusInfo.STATUS_OFFLINE) + this._statusText = aMessage; + this._notifyObservers("status-changed", aMessage); + }, + + _getProfileDir: function() + Services.dirsvc.get("ProfD", Ci.nsIFile), + setUserIcon: function(aIconFile) { + let folder = this._getProfileDir(); + + let newName = ""; + if (aIconFile) { + // Get the extension (remove trailing dots - invalid Windows extension). + let ext = aIconFile.leafName.replace(/.*(\.[a-z0-9]+)\.*/i, "$1"); + // newName = userIcon-. + newName = "userIcon-" + Math.floor(Date.now() / 1000) + ext; + + // Copy the new icon file to newName in the profile folder. + aIconFile.copyTo(folder, newName); + } + + // Get the previous file name before saving the new file name. + let oldFileName = Services.prefs.getCharPref(kPrefUserIconFilename); + Services.prefs.setCharPref(kPrefUserIconFilename, newName); + + // Now that the new icon has been copied to the profile directory + // and the pref value changed, we can remove the old icon. Ignore + // failures so that we always fire the user-icon-changed notification. + try { + if (oldFileName) { + folder.append(oldFileName); + if (folder.exists()) + folder.remove(false); + } + } catch (e) { + Cu.reportError(e); + } + + this._notifyObservers("user-icon-changed", newName); + }, + getUserIcon: function() { + let filename = Services.prefs.getCharPref(kPrefUserIconFilename); + if (!filename) + return null; // No icon has been set. + + let file = this._getProfileDir(); + file.append(filename); + + if (!file.exists()) { + Services.console.logStringMessage("Invalid userIconFileName preference"); + return null; + } + + return Services.io.newFileURI(file); + }, + + get displayName() Services.prefs.getComplexValue(kPrefUserDisplayname, + Ci.nsISupportsString).data, + set displayName(aDisplayName) { + let str = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + str.data = aDisplayName; + Services.prefs.setComplexValue(kPrefUserDisplayname, Ci.nsISupportsString, + str); + this._notifyObservers("user-display-name-changed", aDisplayName); + }, + + addObserver: function(aObserver) { + if (this._observers.indexOf(aObserver) == -1) + this._observers.push(aObserver); + }, + removeObserver: function(aObserver) { + this._observers = this._observers.filter(function(o) o !== aObserver); + }, + _notifyObservers: function(aTopic, aData) { + for each (let observer in this._observers) + observer.observe(this, aTopic, aData); + } +}; + +var gCoreService; +function CoreService() { gCoreService = this; } +CoreService.prototype = { + globalUserStatus: null, + + _initialized: false, + get initialized() this._initialized, + init: function() { + if (this._initialized) + return; + + Services.obs.addObserver(this, kQuitApplicationGranted, false); + this._initialized = true; + + Services.cmd.initCommands(); + this._protos = {}; + + this.globalUserStatus = new UserStatus(); + this.globalUserStatus.addObserver({ + observe: function(aSubject, aTopic, aData) { + Services.obs.notifyObservers(aSubject, aTopic, aData); + } + }); + + let accounts = Services.accounts; + accounts.initAccounts(); + Services.contacts.initContacts(); + Services.conversations.initConversations(); + Services.obs.notifyObservers(this, "prpl-init", null); + + if (accounts.autoLoginStatus == Ci.imIAccountsService.AUTOLOGIN_ENABLED) + accounts.processAutoLogin(); + }, + observe: function(aObject, aTopic, aData) { + if (aTopic == kQuitApplicationGranted) + this.quit(); + }, + quit: function() { + if (!this._initialized) + throw Cr.NS_ERROR_NOT_INITIALIZED; + + Services.obs.removeObserver(this, kQuitApplicationGranted); + Services.obs.notifyObservers(this, "prpl-quit", null); + + Services.conversations.unInitConversations(); + Services.accounts.unInitAccounts(); + Services.contacts.unInitContacts(); + Services.cmd.unInitCommands(); + + this.globalUserStatus.unInit(); + delete this.globalUserStatus; + delete this._protos; + delete this._initialized; + }, + + getProtocols: function() { + if (!this._initialized) + throw Cr.NS_ERROR_NOT_INITIALIZED; + + let protocols = []; + let entries = categoryManager.enumerateCategory(kProtocolPluginCategory); + while (entries.hasMoreElements()) { + let id = entries.getNext().QueryInterface(Ci.nsISupportsCString).data; + let proto = this.getProtocolById(id); + if (proto) + protocols.push(proto); + } + return new nsSimpleEnumerator(protocols); + }, + + getProtocolById: function(aPrplId) { + if (!this._initialized) + throw Cr.NS_ERROR_NOT_INITIALIZED; + + if (this._protos.hasOwnProperty(aPrplId)) + return this._protos[aPrplId]; + + let cid; + try { + cid = categoryManager.getCategoryEntry(kProtocolPluginCategory, aPrplId); + } catch (e) { + return null; // no protocol registered for this id. + } + + let proto = null; + try { + proto = Cc[cid].createInstance(Ci.prplIProtocol); + } catch (e) { + // This is a real error, the protocol is registered and failed to init. + let error = "failed to create an instance of " + cid + ": " + e; + dump(error + "\n"); + Cu.reportError(error); + } + if (!proto) + return null; + + try { + proto.init(aPrplId); + } catch (e) { + Cu.reportError(e); + return null; + } + + this._protos[aPrplId] = proto; + return proto; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.imICoreService]), + classDescription: "Core", + classID: Components.ID("{073f5953-853c-4a38-bd81-255510c31c2e}"), + contractID: "@mozilla.org/chat/core-service;1" +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([CoreService]); diff --git a/chat/components/src/imCore.manifest b/chat/components/src/imCore.manifest new file mode 100644 --- /dev/null +++ b/chat/components/src/imCore.manifest @@ -0,0 +1,2 @@ +component {073f5953-853c-4a38-bd81-255510c31c2e} imCore.js +contract @mozilla.org/chat/core-service;1 {073f5953-853c-4a38-bd81-255510c31c2e} diff --git a/chat/components/src/logger.js b/chat/components/src/logger.js new file mode 100644 --- /dev/null +++ b/chat/components/src/logger.js @@ -0,0 +1,708 @@ +/* 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/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu, Constructor: CC} = Components; + +Cu.import("resource:///modules/hiddenWindow.jsm"); +Cu.import("resource:///modules/imServices.jsm"); +Cu.import("resource:///modules/imXPCOMUtils.jsm"); +Cu.import("resource:///modules/jsProtoHelper.jsm"); + +XPCOMUtils.defineLazyGetter(this, "logDir", function() { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("logs"); + return file; +}); + +const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", + "init"); +const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1", + "nsIConverterInputStream", + "init"); +const LocalFile = CC("@mozilla.org/file/local;1", + "nsILocalFile", + "initWithPath"); + +const kLineBreak = "@mozilla.org/windows-registry-key;1" in Cc ? "\r\n" : "\n"; + +function getLogFolderForAccount(aAccount, aCreate) +{ + let file = logDir.clone(); + function createIfNotExists(aFile) { + if (aCreate && !aFile.exists()) + aFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0777); + } + createIfNotExists(file); + file.append(aAccount.protocol.normalizedName); + createIfNotExists(file); + file.append(aAccount.normalizedName); + createIfNotExists(file); + return file; +} + +function getNewLogFileName(aFormat) +{ + let date = new Date(); + let dateTime = date.toLocaleFormat("%Y-%m-%d.%H%M%S"); + let offset = date.getTimezoneOffset(); + if (offset < 0) { + dateTime += "+"; + offset *= -1; + } + else + dateTime += "-"; + let minutes = offset % 60; + offset = (offset - minutes) / 60; + function twoDigits(aNumber) + aNumber == 0 ? "00" : aNumber < 10 ? "0" + aNumber : aNumber; + if (!aFormat) + aFormat = "txt"; + return dateTime + twoDigits(offset) + twoDigits(minutes) + "." + aFormat; +} + +/* Conversation logs stuff */ +function ConversationLog(aConversation) +{ + this._conv = aConversation; +} +ConversationLog.prototype = { + _log: null, + file: null, + format: "txt", + _init: function cl_init() { + let file = getLogFolderForAccount(this._conv.account, true); + let name = this._conv.normalizedName; + if (convIsRealMUC(this._conv)) + name += ".chat"; + file.append(name); + if (!file.exists()) + file.create(Ci.nsIFile.DIRECTORY_TYPE, 0777); + if (Services.prefs.getCharPref("purple.logging.format") == "json") + this.format = "json"; + file.append(getNewLogFileName(this.format)); + this.file = file; + let os = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + const PR_WRITE_ONLY = 0x02; + const PR_CREATE_FILE = 0x08; + const PR_APPEND = 0x10; + os.init(file, PR_WRITE_ONLY | PR_CREATE_FILE | PR_APPEND, 0666, 0); + // just to be really sure everything is in UTF8 + let converter = Cc["@mozilla.org/intl/converter-output-stream;1"]. + createInstance(Ci.nsIConverterOutputStream); + converter.init(os, "UTF-8", 0, 0); + this._log = converter; + this._log.writeString(this._getHeader()); + }, + _getHeader: function cl_getHeader() + { + let account = this._conv.account; + if (this.format == "json") { + return JSON.stringify({date: new Date(), + name: this._conv.name, + title: this._conv.title, + account: account.normalizedName, + protocol: account.protocol.normalizedName, + isChat: this._conv.isChat + }) + "\n"; + } + return "Conversation with " + this._conv.name + + " at " + (new Date).toLocaleString() + + " on " + account.name + + " (" + account.protocol.normalizedName + ")" + kLineBreak; + }, + _serialize: function cl_serialize(aString) { + // TODO cleanup once bug 102699 is fixed + let doc = getHiddenHTMLWindow().document; + let div = doc.createElementNS("http://www.w3.org/1999/xhtml", "div"); + div.innerHTML = aString.replace(/\r?\n/g, "
").replace(/
/gi, "
"); + const type = "text/plain"; + let encoder = + Components.classes["@mozilla.org/layout/documentEncoder;1?type=" + type] + .createInstance(Components.interfaces.nsIDocumentEncoder); + encoder.init(doc, type, 0); + encoder.setContainerNode(div); + encoder.setNodeFixup({fixupNode: function(aNode, aSerializeKids) { + if (aNode.localName == "a" && aNode.hasAttribute("href")) { + let url = aNode.getAttribute("href"); + let content = aNode.textContent; + if (url != content) + aNode.textContent = content + " (" + url + ")"; + } + return null; + }}); + return encoder.encodeToString(); + }, + 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, + 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); + if (aMessage.system) + line += msg; + else { + let sender = aMessage.alias || aMessage.who; + if (aMessage.autoResponse) + line += sender + " : " + msg; + else { + if (/^\/me /.test(msg)) + line += "***" + sender + " " + msg.replace(/^\/me /, ""); + else + line += sender + ": " + msg; + } + } + this._log.writeString(line + kLineBreak); + }, + + close: function cl_close() { + if (this._log) { + this._log.close(); + this._log = null; + this.file = null; + } + } +}; + +const dummyConversationLog = { + file: null, + logMessage: function() {}, + close: function() {} +}; + +var gConversationLogs = { }; +function getLogForConversation(aConversation) +{ + let id = aConversation.id; + if (!(id in gConversationLogs)) { + let prefName = + "purple.logging.log_" + (aConversation.isChat ? "chats" : "ims"); + if (Services.prefs.getBoolPref(prefName)) + gConversationLogs[id] = new ConversationLog(aConversation); + else + gConversationLogs[id] = dummyConversationLog; + } + return gConversationLogs[id]; +} + +function closeLogForConversation(aConversation) +{ + let id = aConversation.id; + if (!(id in gConversationLogs)) + return; + gConversationLogs[id].close(); + delete gConversationLogs[id]; +} + +/* System logs stuff */ +function SystemLog(aAccount) +{ + this._init(aAccount); + this._log.writeString("System log for account " + aAccount.name + + " (" + aAccount.protocol.normalizedName + + ") connected at " + + (new Date()).toLocaleFormat("%c") + kLineBreak); +} +SystemLog.prototype = { + _log: null, + _init: function sl_init(aAccount) { + let file = getLogFolderForAccount(aAccount, true); + file.append(".system"); + if (!file.exists()) + file.create(Ci.nsIFile.DIRECTORY_TYPE, 0777); + file.append(getNewLogFileName()); + let os = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + const PR_WRITE_ONLY = 0x02; + const PR_CREATE_FILE = 0x08; + const PR_APPEND = 0x10; + os.init(file, PR_WRITE_ONLY | PR_CREATE_FILE | PR_APPEND, 0666, 0); + // just to be really sure everything is in UTF8 + let converter = Cc["@mozilla.org/intl/converter-output-stream;1"]. + createInstance(Ci.nsIConverterOutputStream); + converter.init(os, "UTF-8", 0, 0); + this._log = converter; + }, + logEvent: function sl_logEvent(aString) { + if (!this._log) + this._init(); + + let date = (new Date()).toLocaleFormat("%x %X"); + this._log.writeString("---- " + aString + " @ " + date + " ----" + kLineBreak); + }, + + close: function sl_close() { + if (this._log) { + this._log.close(); + this._log = null; + } + } +}; + +const dummySystemLog = { + logEvent: function(aString) {}, + close: function() {} +}; + +var gSystemLogs = { }; +function getLogForAccount(aAccount, aCreate) +{ + let id = aAccount.id; + if (aCreate) { + if (id in gSystemLogs) + gSystemLogs[id].close(); + if (!Services.prefs.getBoolPref("purple.logging.log_system")) + return dummySystemLog; + return (gSystemLogs[id] = new SystemLog(aAccount)); + } + + return (id in gSystemLogs) && gSystemLogs[id] || dummySystemLog; +} + +function closeLogForAccount(aAccount) +{ + let id = aAccount.id; + if (!(id in gSystemLogs)) + return; + gSystemLogs[id].close(); + delete gSystemLogs[id]; +} + +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; + +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]; + + this._messages = []; + + // We'll read the name, title, account, and protocol data from the first + // stream, and skip the others. + let firstFile = true; + + for each (let inputStream in aLineInputStreams) { + let line = {value: ""}; + let more = inputStream.readLine(line); + + if (!line.value) + throw "bad log file"; + + if (firstFile) { + let data = JSON.parse(line.value); + this.name = data.name; + this.title = data.title; + this._accountName = data.account; + this._protocolName = data.protocol; + this._isChat = data.isChat; + firstFile = false; + } + + while (more) { + more = inputStream.readLine(line); + if (!line.value) + break; + try { + let data = JSON.parse(line.value); + this._messages.push(new LogMessage(data, this)); + } catch (e) { + // if a message line contains junk, just ignore the error and + // continue reading the conversation. + } + } + } +} +LogConversation.prototype = { + __proto__: ClassInfo("imILogConversation", "Log conversation object"), + get isChat() this._isChat, + get buddy() null, + get account() ({ + alias: "", + name: this._accountName, + normalizedName: this._accountName, + protocol: {name: this._protocolName}, + statusInfo: Services.core.globalUserStatus + }), + getMessages: function(aMessageCount) { + if (aMessageCount) + aMessageCount.value = this._messages.length; + return this._messages; + } +}; + +/* Generic log enumeration stuff */ +function Log(aFile) +{ + this.file = aFile; + this.path = aFile.path; + + let [date, format] = getDateFromFilename(aFile.leafName); + if (!date || !format) { + this.format = "invalid"; + this.time = 0; + return; + } + this.time = date.valueOf() / 1000; + this.format = format; +} +Log.prototype = { + __proto__: ClassInfo("imILog", "Log object"), + getConversation: function() { + if (this.format != "json") + return null; + + const PR_RDONLY = 0x01; + let fis = new FileInputStream(this.file, PR_RDONLY, 0444, + Ci.nsIFileInputStream.CLOSE_ON_EOF); + let lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + try { + return new LogConversation(lis); + } catch (e) { + // If the file contains some junk (invalid JSON), the + // LogConversation code will still read the messages it can parse. + // If the first line of meta data is corrupt, there's really no + // useful data we can extract from the file so the + // LogConversation constructor will throw. + return null; + } + } +}; + +/** + * Takes a properly formatted log file name and extracts the date information + * and filetype, returning the results as an Array. + * + * Filenames are expected to be formatted as: + * + * YYYY-MM-DD.HHmmSS+ZZzz.format + * + * @param aFilename the name of the file + * @returns an Array, where the first element is a Date object for the date + * that the log file represents, and the file type as a string. + */ +function getDateFromFilename(aFilename) { + const kRegExp = /([\d]{4})-([\d]{2})-([\d]{2}).([\d]{2})([\d]{2})([\d]{2})([+-])([\d]{2})([\d]{2}).*\.([A-Za-z]+)$/; + + let r = aFilename.match(kRegExp); + if (!r) + return []; + + // We ignore the timezone offset for now (FIXME) + return [new Date(r[1], r[2] - 1, r[3], r[4], r[5], r[6]), r[10]]; +} + +/** + * Returns true if a Conversation is both a chat conversation, and not + * a Twitter conversation. + */ +function convIsRealMUC(aConversation) { + return (aConversation.isChat && + aConversation.account.protocol.id != "prpl-twitter"); +} + +function LogEnumerator(aEntries) +{ + this._entries = aEntries; +} +LogEnumerator.prototype = { + _entries: [], + hasMoreElements: function() { + while (this._entries.length > 0 && !this._entries[0].hasMoreElements()) + this._entries.shift(); + return this._entries.length > 0; + }, + getNext: function() + new Log(this._entries[0].getNext().QueryInterface(Ci.nsIFile)), + QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]) +}; + +function DailyLogEnumerator(aEntries) { + this._entries = {}; + + for each (let entry in aEntries) { + while (entry.hasMoreElements()) { + let file = entry.getNext(); + if (!(file instanceof Ci.nsIFile)) + continue; + + let [logDate] = getDateFromFilename(file.leafName); + if (!logDate) { + // We'll skip this one, since it's got a busted filename. + continue; + } + + // We want to cluster all of the logs that occur on the same day + // into the same Arrays. We clone the date for the log, reset it to + // the 0th hour/minute/second, and use that to construct an ID for the + // Array we'll put the log in. + let dateForID = new Date(logDate); + dateForID.setHours(0); + dateForID.setMinutes(0); + dateForID.setSeconds(0); + let dayID = dateForID.toISOString(); + + if (!(dayID in this._entries)) + this._entries[dayID] = []; + + this._entries[dayID].push({ + file: file, + time: logDate + }); + } + } + + this._days = Object.keys(this._entries).sort(); + this._index = 0; +} +DailyLogEnumerator.prototype = { + _entries: {}, + _days: [], + _index: 0, + hasMoreElements: function() this._index < this._days.length, + getNext: function() new LogCluster(this._entries[this._days[this._index++]]), + QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]) +}; + +/** + * A LogCluster is a Log representing several log files all at once. The + * constructor expects aEntries, which is an array of objects that each + * have two properties: file and time. The file is the nsIFile for the + * log file, and the time is the Date object extracted from the filename for + * the log file. + */ +function LogCluster(aEntries) { + if (!aEntries.length) + throw new Error("LogCluster was passed an empty Array"); + + // Sort our list of entries for this day in increasing order. + aEntries.sort(function(aLeft, aRight) aLeft.time - aRight.time); + + this._entries = aEntries; + // Calculate the timestamp for the first entry down to the day. + let timestamp = new Date(aEntries[0].time); + timestamp.setHours(0); + timestamp.setMinutes(0); + timestamp.setSeconds(0); + this.time = timestamp.valueOf() / 1000; + // Path is used to uniquely identify a Log, and sometimes used to + // quickly determine which directory a log file is from. We'll use + // the first file's path. + this.path = aEntries[0].file.path; +} +LogCluster.prototype = { + __proto__: ClassInfo("imILog", "LogCluster object"), + format: "json", + + getConversation: function() { + const PR_RDONLY = 0x01; + let streams = []; + for each (let entry in this._entries) { + let fis = new FileInputStream(entry.file, PR_RDONLY, 0444, + Ci.nsIFileInputStream.CLOSE_ON_EOF); + // Pass in 0x0 so that we throw exceptions on unknown bytes. + let lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + streams.push(lis); + } + + try { + return new LogConversation(streams); + } catch (e) { + // If the file contains some junk (invalid JSON), the + // LogConversation code will still read the messages it can parse. + // If the first line of meta data is corrupt, there's really no + // useful data we can extract from the file so the + // LogConversation constructor will throw. + return null; + } + } +}; + +function Logger() { } +Logger.prototype = { + _enumerateLogs: function logger__enumerateLogs(aAccount, aNormalizedName, + aGroupByDay) { + let file = getLogFolderForAccount(aAccount); + file.append(aNormalizedName); + if (!file.exists()) + return EmptyEnumerator; + + let enumerator = aGroupByDay ? DailyLogEnumerator : LogEnumerator; + + return new enumerator([file.directoryEntries]); + }, + getLogFromFile: function logger_getLogFromFile(aFile, aGroupByDay) { + if (aGroupByDay) + return this._getDailyLogFromFile(aFile); + + return new Log(aFile); + }, + _getDailyLogFromFile: function logger_getDailyLogsForFile(aFile) { + let [targetDate] = getDateFromFilename(aFile.leafName); + if (!targetDate) + return null; + + let targetDay = Math.floor(targetDate / (86400 * 1000)); + + // Get the path for the log file - we'll assume that the files relevant + // to our interests are in the same folder. + let path = aFile.path; + let folder = aFile.parent.directoryEntries; + let relevantEntries = []; + // Pick out the files that start within our date range. + while (folder.hasMoreElements()) { + let file = folder.getNext(); + if (!(file instanceof Ci.nsIFile)) + continue; + + let [logTime] = getDateFromFilename(file.leafName); + + let day = Math.floor(logTime / (86400 * 1000)); + if (targetDay == day) { + relevantEntries.push({ + file: file, + time: logTime + }); + } + } + + return new LogCluster(relevantEntries); + }, + getLogFileForOngoingConversation: function logger_getLogFileForOngoingConversation(aConversation) + getLogForConversation(aConversation).file, + getLogsForContact: function logger_getLogsForContact(aContact) { + let entries = []; + aContact.getBuddies().forEach(function (aBuddy) { + aBuddy.getAccountBuddies().forEach(function (aAccountBuddy) { + let file = getLogFolderForAccount(aAccountBuddy.account); + file.append(aAccountBuddy.normalizedName); + if (file.exists()) + entries.push(file.directoryEntries); + }); + }); + return new LogEnumerator(entries); + }, + getLogsForBuddy: function logger_getLogsForBuddy(aBuddy) { + let entries = []; + aBuddy.getAccountBuddies().forEach(function (aAccountBuddy) { + let file = getLogFolderForAccount(aAccountBuddy.account); + file.append(aAccountBuddy.normalizedName); + if (file.exists()) + entries.push(file.directoryEntries); + }); + return new LogEnumerator(entries); + }, + getLogsForAccountBuddy: function logger_getLogsForAccountBuddy(aAccountBuddy) + this._enumerateLogs(aAccountBuddy.account, aAccountBuddy.normalizedName), + getLogsForConversation: function logger_getLogsForConversation(aConversation, + aGroupByDay) { + let name = aConversation.normalizedName; + if (convIsRealMUC(aConversation)) + name += ".chat"; + + return this._enumerateLogs(aConversation.account, name, aGroupByDay); + }, + getSystemLogsForAccount: function logger_getSystemLogsForAccount(aAccount) + this._enumerateLogs(aAccount, ".system"), + getSimilarLogs: function(aLog, aGroupByDay) { + let enumerator = aGroupByDay ? DailyLogEnumerator : LogEnumerator; + return new enumerator([new LocalFile(aLog.path).parent.directoryEntries]); + }, + + observe: function logger_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "profile-after-change": + Services.obs.addObserver(this, "final-ui-startup", false); + break; + case "final-ui-startup": + Services.obs.removeObserver(this, "final-ui-startup"); + ["new-text", "conversation-closed", "conversation-left-chat", + "account-connected", "account-disconnected", + "account-buddy-status-changed"].forEach(function(aEvent) { + Services.obs.addObserver(this, aEvent, false); + }, this); + break; + case "new-text": + if (!aSubject.noLog) { + let log = getLogForConversation(aSubject.conversation); + log.logMessage(aSubject); + } + break; + case "conversation-closed": + case "conversation-left-chat": + closeLogForConversation(aSubject); + break; + case "account-connected": + getLogForAccount(aSubject, true).logEvent("+++ " + aSubject.name + + " signed on"); + break; + case "account-disconnected": + getLogForAccount(aSubject).logEvent("+++ " + aSubject.name + + " signed off"); + closeLogForAccount(aSubject); + break; + case "account-buddy-status-changed": + let status; + if (!aSubject.online) + status = "Offline"; + else if (aSubject.mobile) + status = "Mobile"; + else if (aSubject.idle) + status = "Idle"; + else if (aSubject.available) + status = "Available"; + else + status = "Unavailable"; + + let statusText = aSubject.statusText; + if (statusText) + status += " (\"" + statusText + "\")"; + + let nameText = aSubject.displayName + " (" + aSubject.userName + ")"; + getLogForAccount(aSubject.account).logEvent(nameText + " is now " + status); + break; + default: + throw "Unexpected notification " + aTopic; + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.imILogger]), + classDescription: "Logger", + classID: Components.ID("{fb0dc220-2c7a-4216-9f19-6b8f3480eae9}"), + contractID: "@mozilla.org/chat/logger;1" +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([Logger]); diff --git a/chat/components/src/logger.manifest b/chat/components/src/logger.manifest new file mode 100644 --- /dev/null +++ b/chat/components/src/logger.manifest @@ -0,0 +1,3 @@ +component {fb0dc220-2c7a-4216-9f19-6b8f3480eae9} logger.js +contract @mozilla.org/chat/logger;1 {fb0dc220-2c7a-4216-9f19-6b8f3480eae9} +category profile-after-change Logger @mozilla.org/chat/logger;1 diff --git a/chat/components/src/smileProtocolHandler.js b/chat/components/src/smileProtocolHandler.js new file mode 100644 --- /dev/null +++ b/chat/components/src/smileProtocolHandler.js @@ -0,0 +1,43 @@ +/* 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/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/imSmileys.jsm"); + +const kSmileRegexp = /^smile:\/\//; + +function smileProtocolHandler() { } + +smileProtocolHandler.prototype = { + scheme: "smile", + defaultPort: -1, + protocolFlags: Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_NOAUTH | + Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE | + Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE, + newURI: function SPH_newURI(aSpec, aOriginCharset, aBaseURI) { + let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI); + uri.spec = aSpec; + uri.QueryInterface(Ci.nsIMutable); + uri.mutable = false; + return uri; + }, + newChannel: function SPH_newChannel(aURI) { + let smile = aURI.spec.replace(kSmileRegexp, ""); + let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + let channel = ios.newChannel(getSmileRealURI(smile), null, null); + channel.originalURI = aURI; + return channel; + }, + allowPort: function SPH_allowPort(aPort, aScheme) false, + + classDescription: "Smile Protocol Handler", + classID: Components.ID("{04e58eae-dfbc-4c9e-8130-6d9ef19cbff4}"), + contractID: "@mozilla.org/network/protocol;1?name=smile", + QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]) +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([smileProtocolHandler]); diff --git a/chat/components/src/smileProtocolHandler.manifest b/chat/components/src/smileProtocolHandler.manifest new file mode 100644 --- /dev/null +++ b/chat/components/src/smileProtocolHandler.manifest @@ -0,0 +1,2 @@ +component {04e58eae-dfbc-4c9e-8130-6d9ef19cbff4} smileProtocolHandler.js +contract @mozilla.org/network/protocol;1?name=smile {04e58eae-dfbc-4c9e-8130-6d9ef19cbff4} diff --git a/chat/content/Makefile.in b/chat/content/Makefile.in new file mode 100644 --- /dev/null +++ b/chat/content/Makefile.in @@ -0,0 +1,12 @@ +# 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/. + +DEPTH = ../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +include $(topsrcdir)/config/rules.mk diff --git a/chat/content/browserRequest.js b/chat/content/browserRequest.js new file mode 100644 --- /dev/null +++ b/chat/content/browserRequest.js @@ -0,0 +1,142 @@ +/* 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/. */ + +const wpl = Components.interfaces.nsIWebProgressListener; + +var reporterListener = { + _isBusy: false, + get statusMeter() { + delete this.statusMeter; + return this.statusMeter = document.getElementById("statusbar-icon"); + }, + get securityButton() { + delete this.securityButton; + return this.securityButton = document.getElementById("security-button"); + }, + get securityLabel() { + delete this.securityLabel; + return this.securityLabel = document.getElementById("security-status"); + }, + get securityDisplay() { + delete this.securityDisplay; + return this.securityDisplay = document.getElementById("security-display"); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Components.interfaces.nsIWebProgressListener) || + aIID.equals(Components.interfaces.nsISupportsWeakReference) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + onStateChange: function(/*in nsIWebProgress*/ aWebProgress, + /*in nsIRequest*/ aRequest, + /*in unsigned long*/ aStateFlags, + /*in nsresult*/ aStatus) { + if (aStateFlags & wpl.STATE_START && + aStateFlags & wpl.STATE_IS_NETWORK) { + this.statusMeter.value = 0; + this.statusMeter.parentNode.collapsed = false; + this.securityLabel.collapsed = true; + } + else if (aStateFlags & wpl.STATE_STOP && + aStateFlags & wpl.STATE_IS_NETWORK) { + this.statusMeter.parentNode.collapsed = true; + this.securityLabel.collapsed = false; + } + }, + + onProgressChange: function(/*in nsIWebProgress*/ aWebProgress, + /*in nsIRequest*/ aRequest, + /*in long*/ aCurSelfProgress, + /*in long */aMaxSelfProgress, + /*in long */aCurTotalProgress, + /*in long */aMaxTotalProgress) { + if (aMaxTotalProgress > 0) { + let percentage = (aCurTotalProgress * 100) / aMaxTotalProgress; + this.statusMeter.value = percentage; + } + }, + + onLocationChange: function(/*in nsIWebProgress*/ aWebProgress, + /*in nsIRequest*/ aRequest, + /*in nsIURI*/ aLocation) { + this.securityDisplay.setAttribute('label', aLocation.host); + }, + + onStatusChange: function(/*in nsIWebProgress*/ aWebProgress, + /*in nsIRequest*/ aRequest, + /*in nsresult*/ aStatus, + /*in wstring*/ aMessage) { + }, + + onSecurityChange: function(/*in nsIWebProgress*/ aWebProgress, + /*in nsIRequest*/ aRequest, + /*in unsigned long*/ aState) { + const wpl_security_bits = wpl.STATE_IS_SECURE | + wpl.STATE_IS_BROKEN | + wpl.STATE_IS_INSECURE | + wpl.STATE_SECURE_HIGH | + wpl.STATE_SECURE_MED | + wpl.STATE_SECURE_LOW; + let browser = document.getElementById("requestFrame"); + let level; + + switch (aState & wpl_security_bits) { + case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_HIGH: + level = "high"; + break; + case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_MED: + case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_LOW: + level = "low"; + break; + case wpl.STATE_IS_BROKEN: + level = "broken"; + break; + } + if (level) { + this.securityButton.setAttribute("level", level); + this.securityButton.hidden = false; + this.securityLabel.setAttribute("label", browser.securityUI.tooltipText); + } else { + this.securityButton.hidden = true; + this.securityButton.removeAttribute("level"); + } + this.securityButton.setAttribute("tooltiptext", + browser.securityUI.tooltipText); + } +} + +function cancelRequest() +{ + reportUserClosed(); + window.close(); +} + +function reportUserClosed() +{ + let request = window.arguments[0]; + request.QueryInterface(Components.interfaces.prplIRequestBrowser); + request.cancelled(); +} + +function loadRequestedUrl() +{ + let request = window.arguments[0]; + request.QueryInterface(Components.interfaces.prplIRequestBrowser); + document.getElementById("headerMessage").textContent = request.promptText; + let account = request.account; + document.getElementById("headerLabel").value = + account.protocol.name + " - " + account.name; + document.getElementById("headerImage").src = + account.protocol.iconBaseURI + "icon48.png"; + + let browser = document.getElementById("requestFrame"); + browser.addProgressListener(reporterListener, + Components.interfaces.nsIWebProgress.NOTIFY_ALL); + let url = request.url; + if (url != "") + browser.setAttribute("src", url); + request.loaded(window, browser.webProgress); +} diff --git a/chat/content/browserRequest.xul b/chat/content/browserRequest.xul new file mode 100644 --- /dev/null +++ b/chat/content/browserRequest.xul @@ -0,0 +1,42 @@ + + + + + + + + + + ": "", + "

foo

": "

foo

", + "

foo

": "

foo

" + }; + for each (let [input, expectedOutput] in Iterator(strings)) + do_check_eq(expectedOutput, cleanupImMarkup(input)); +} + +function test_links() { + // http, https, ftp and mailto links should be preserved. + const ok = [ + "http://example.com/", + "https://example.com/", + "ftp://example.com/", + "mailto:foo@example.com" + ]; + for each (let string in ok) { + string = "
foo"; + do_check_eq(string, cleanupImMarkup(string)); + } + + // other links should be removed + const bad = [ + "chrome://global/content/", + "about:", + "about:blank", + "foo://bar/", + "" + ]; + for each (let string in bad) { + do_check_eq("foo", + cleanupImMarkup("foo")); + } + + // keep link titles + let string = "foo"; + do_check_eq(string, cleanupImMarkup(string)); +} + +function test_allModes() { + test_plainText(); + test_paragraphs(); + test_stripScripts(); + test_links(); + // Remove random classes. + do_check_eq("

foo

", cleanupImMarkup("

foo

")); +} + +function test_strictMode() { + Services.prefs.setIntPref(kModePref, kStrictMode); + test_allModes(); + + // check that basic formatting is stipped in strict mode. + for each (let tag in ["div", "em", "strong", "b", "i", "u", "span", "code", + "ul", "li", "ol", "cite", "blockquote"]) + do_check_eq("foo", cleanupImMarkup("<" + tag + ">foo")); + + // check that font settings are removed. + do_check_eq("foo", + cleanupImMarkup("foo")); + do_check_eq("

foo

", + cleanupImMarkup("

foo

")); + + // Discard hr + do_check_eq("foobar", cleanupImMarkup("foo
bar")); + + run_next_test(); +} + +function test_standardMode() { + Services.prefs.setIntPref(kModePref, kStandardMode); + test_allModes(); + + // check that basic formatting is kept in standard mode. + for each (let tag in ["div", "em", "strong", "b", "i", "u", "span", "code", + "ul", "li", "ol", "cite", "blockquote"]) { + let string = "<" + tag + ">foo"; + do_check_eq(string, cleanupImMarkup(string)); + } + + // Keep special allowed classes. + for each (let className in ["moz-txt-underscore", "moz-txt-tag"]) { + let string = "foo"; + do_check_eq(string, cleanupImMarkup(string)); + } + + // Remove font settings + let string = "foo"; + do_check_eq("foo", cleanupImMarkup(string)); + + // Discard hr + do_check_eq("foobar", cleanupImMarkup("foo
bar")); + + const okCSS = [ + "font-style: italic", + "font-weight: bold" + ]; + for each (let css in okCSS) { + let string = "foo"; + do_check_eq(string, cleanupImMarkup(string)); + } + // text-decoration is a shorthand for several -moz-text-decoration properties. + do_check_eq("foo", + cleanupImMarkup("foo")); + + const badCSS = [ + "color: pink;", + "font-family: Times", + "font-size: larger", + "-moz-binding: url('chrome://global/content/bindings/textbox.xml#textbox');", + "display: none", + "visibility: hidden" + ]; + for each (let css in badCSS) { + do_check_eq("foo", + cleanupImMarkup("foo")); + } + // The shorthand 'font' is decomposed to non-shorthand properties, + // and not recomposed as some non-shorthand properties are filtered out. + do_check_eq("foo", + cleanupImMarkup("foo")); + + run_next_test(); +} + +function test_permissiveMode() { + Services.prefs.setIntPref(kModePref, kPermissiveMode); + test_allModes(); + + // Check that all formatting is kept in permissive mode. + for each (let tag in ["div", "em", "strong", "b", "i", "u", "span", "code", + "ul", "li", "ol", "cite", "blockquote"]) { + let string = "<" + tag + ">foo"; + do_check_eq(string, cleanupImMarkup(string)); + } + + // Keep special allowed classes. + for each (let className in ["moz-txt-underscore", "moz-txt-tag"]) { + let string = "foo"; + do_check_eq(string, cleanupImMarkup(string)); + } + + // Keep font settings + const fontAttributes = [ + "face=\"Times\"", + "color=\"pink\"", + "size=\"3\"" + ]; + for each (let fontAttribute in fontAttributes) { + let string = "foo"; + do_check_eq(string, cleanupImMarkup(string)); + } + + // Allow hr + string = "foo
bar"; + do_check_eq(string, cleanupImMarkup(string)); + + // Allow most CSS rules changing the text appearance. + const okCSS = [ + "font-style: italic", + "font-weight: bold", + "color: pink;", + "font-family: Times", + "font-size: larger" + ]; + for each (let css in okCSS) { + let string = "foo"; + do_check_eq(string, cleanupImMarkup(string)); + } + // The shorthand 'font' is decomposed to non-shorthand properties, + // and not recomposed as some non-shorthand properties are filtered out. + do_check_eq("foo", + cleanupImMarkup("foo")); + // text-decoration is a shorthand for several -moz-text-decoration properties. + do_check_eq("foo", + cleanupImMarkup("foo")); + + // But still filter out dangerous CSS rules. + const badCSS = [ + "-moz-binding: url('chrome://global/content/bindings/textbox.xml#textbox');", + "display: none", + "visibility: hidden" + ]; + for each (let css in badCSS) { + do_check_eq("foo", + cleanupImMarkup("foo")); + } + + run_next_test(); +} + +function test_addGlobalAllowedTag() { + Services.prefs.setIntPref(kModePref, kStrictMode); + + // Check that
isn't allowed by default in strict mode. + // Note: we use
instead of to avoid mailnews' content policy + // messing things up. + do_check_eq("", cleanupImMarkup("
")); + + // Allow
without attributes. + addGlobalAllowedTag("hr"); + do_check_eq("
", cleanupImMarkup("
")); + do_check_eq("
", cleanupImMarkup("
")); + removeGlobalAllowedTag("hr"); + + // Allow
with an unfiltered src attribute. + addGlobalAllowedTag("hr", {src: true}); + do_check_eq("
", cleanupImMarkup("
")); + do_check_eq("
", + cleanupImMarkup("
")); + do_check_eq("
", + cleanupImMarkup("
")); + removeGlobalAllowedTag("hr"); + + // Allow
with an src attribute taking only http(s) urls. + addGlobalAllowedTag("hr", {src: function(aValue) /^https?:/.test(aValue)}); + do_check_eq("
", + cleanupImMarkup("
")); + do_check_eq("
", + cleanupImMarkup("
")); + removeGlobalAllowedTag("hr"); + + run_next_test(); +} + +function test_addGlobalAllowedAttribute() { + Services.prefs.setIntPref(kModePref, kStrictMode); + + // Check that id isn't allowed by default in strict mode. + do_check_eq("
", cleanupImMarkup("
")); + + // Allow id unconditionally. + addGlobalAllowedAttribute("id"); + do_check_eq("
", cleanupImMarkup("
")); + removeGlobalAllowedAttribute("id"); + + // Allow id only with numbers. + addGlobalAllowedAttribute("id", function(aId) /^\d+$/.test(aId)); + do_check_eq("
", cleanupImMarkup("
")); + do_check_eq("
", cleanupImMarkup("
")); + removeGlobalAllowedAttribute("id"); + + run_next_test(); +} + +function test_addGlobalAllowedStyleRule() { + // We need at least the standard mode to have the style attribute allowed. + Services.prefs.setIntPref(kModePref, kStandardMode); + + // Check that clear isn't allowed by default in strict mode. + do_check_eq("
", cleanupImMarkup("
")); + + // Allow clear. + addGlobalAllowedStyleRule("clear"); + do_check_eq("
", + cleanupImMarkup("
")); + removeGlobalAllowedStyleRule("clear"); + + run_next_test(); +} + +function test_createDerivedRuleset() { + Services.prefs.setIntPref(kModePref, kStandardMode); + + let rules = createDerivedRuleset(); + + let string = "
"; + do_check_eq("", cleanupImMarkup(string)); + do_check_eq("", cleanupImMarkup(string, rules)); + rules.tags["hr"] = true; + do_check_eq(string, cleanupImMarkup(string, rules)); + + string = "
"; + do_check_eq("
", cleanupImMarkup(string)); + do_check_eq("
", cleanupImMarkup(string, rules)); + rules.attrs["id"] = true; + do_check_eq(string, cleanupImMarkup(string, rules)); + + string = "
"; + do_check_eq("
", cleanupImMarkup(string)); + do_check_eq("
", cleanupImMarkup(string, rules)); + rules.styles["clear"] = true; + do_check_eq(string, cleanupImMarkup(string, rules)); + + run_next_test(); +} diff --git a/chat/modules/test/xpcshell.ini b/chat/modules/test/xpcshell.ini new file mode 100644 --- /dev/null +++ b/chat/modules/test/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +head = +tail = + +[test_filtering.js] diff --git a/chat/protocols/facebook/Makefile.in b/chat/protocols/facebook/Makefile.in new file mode 100644 --- /dev/null +++ b/chat/protocols/facebook/Makefile.in @@ -0,0 +1,17 @@ +# 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/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +EXTRA_COMPONENTS = \ + facebook.js \ + facebook.manifest \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/chat/protocols/facebook/facebook.js b/chat/protocols/facebook/facebook.js new file mode 100644 --- /dev/null +++ b/chat/protocols/facebook/facebook.js @@ -0,0 +1,55 @@ +/* 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/. */ + +const {interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource:///modules/imXPCOMUtils.jsm"); +Cu.import("resource:///modules/jsProtoHelper.jsm"); +Cu.import("resource:///modules/xmpp.jsm"); +Cu.import("resource:///modules/xmpp-session.jsm"); + +XPCOMUtils.defineLazyGetter(this, "_", function() + l10nHelper("chrome://chat/locale/facebook.properties") +); + +function FacebookAccount(aProtoInstance, aImAccount) { + this._init(aProtoInstance, aImAccount); +} +FacebookAccount.prototype = { + __proto__: XMPPAccountPrototype, + get canJoinChat() false, + connect: function() { + if (this.name.indexOf("@") == -1) { + let jid = this.name + "@chat.facebook.com/" + XMPPDefaultResource; + this._jid = this._parseJID(jid); + } + else { + this._jid = this._parseJID(this.name); + if (this._jid.domain != "chat.facebook.com") { + // We can't use this.onError because this._connection doesn't exist. + this.reportDisconnecting(Ci.prplIAccount.ERROR_INVALID_USERNAME, + _("connection.error.useUsernameNotEmailAddress")); + this.reportDisconnected(); + return; + } + } + + this._connection = new XMPPSession("chat.facebook.com", 5222, + "opportunistic_tls", this._jid, + this.imAccount.password, this); + } +}; + +function FacebookProtocol() { +} +FacebookProtocol.prototype = { + __proto__: GenericProtocolPrototype, + get normalizedName() "facebook", + get name() "Facebook Chat", + get iconBaseURI() "chrome://prpl-facebook/skin/", + getAccount: function(aImAccount) new FacebookAccount(this, aImAccount), + classID: Components.ID("{1d1d0bc5-610c-472f-b2cb-4b89857d80dc}") +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([FacebookProtocol]); diff --git a/chat/protocols/facebook/facebook.manifest b/chat/protocols/facebook/facebook.manifest new file mode 100644 --- /dev/null +++ b/chat/protocols/facebook/facebook.manifest @@ -0,0 +1,3 @@ +component {1d1d0bc5-610c-472f-b2cb-4b89857d80dc} facebook.js +contract @mozilla.org/chat/facebook;1 {1d1d0bc5-610c-472f-b2cb-4b89857d80dc} +category im-protocol-plugin prpl-facebook @mozilla.org/chat/facebook;1 diff --git a/chat/protocols/facebook/icons/prpl-facebook-32.png b/chat/protocols/facebook/icons/prpl-facebook-32.png new file mode 100644 index 0000000000000000000000000000000000000000..77e6d358b62d50924610584a77442871038a9057 GIT binary patch literal 1193 zc$@*L1XlZrP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2iXJ> z2{;vzQxf6;00b#XL_t(o!^M|RXdP7)#(($D%zMw5KTXp#Nn=~17(>-XP}Eiw6?IXI z5Cow+7j9g*3)F=uZd?>`qiZ+rvO!e*tP~XrT3Ss1rLCo=ttF|=yqSB?aq;H8 zCYhJa1Ri)F!*CBX_xtX5zH{b2_@9Sa9T?wxaJP-@j5r_u%eIHTZsFa-z0l$C#~FA}XOJu!bnKgchuA&W!=ndRxIOcrSPX=LIi9>LGPP zBlX0Ml)tWAmGiT+u}kH}7Ks%*w1J&@@S%y?z}8WF?%ER0-|3pvfi(;akJ@q7+POc^ z&H%6H2&lN}?e~t@3-d8)>bsh2=i%0(ux)I_o;`DBx)onZ0K!TXk*0T0aitgc0{!)f zH(q{#rzW?sV{09g~$(p zfH8)^==ubV3L@nyKtvhYd`~WZdHyzGklETm6dF_oRj@@0U?fXH@%ZV}*V;~Bp8Es) zr#P^Gsy%=6qhp+&`zPxM1twJ0X1+GkX4f#@U8Ve*Yvu`vpt3On>$(4~u_g0Zfd@pI zDJa?%z_V|E$=Fa2Uwp7{Mf~$`Px0B&Kicyo>#C{>T0p>ikE*9k5yT!ENu@*E^~8;2 z&B`et;4%eli89FVXjv{hq)HI))+dlUkBDH5mcUm;eMj(4rL3wrUr+_k`Ha0Osep6@ z>e$Lv@!qdbAW749-!7v-=Zewh_oRa|&J__zQbc^NV{OC(YbhY&an2Pf;6R1PCdXE* z*AZxFQ#}_S9qeIhVmKH7?erDWLIO!M1&>c|E7gGdduqA(wxM30*fo}m|9Wb!xxw`a z#Em3750q#Gx|TGZ<03*MNpfFblief0FU1KKW%klMTKE%mjdR25Cuc9s6Ih7j6u<)k z*}=L6Uf=oByI&1#9(Z*!};zjVHIOj>dkR+ZP zx0dwpn~Ukf#S=$>Uceim0t^5g7SH zPq7d6K@fcML3}O+!58tNf>!iD5TS@7DoBMY7R5eUf>_Z?3r!mm)Lvq1noD!e?#}#t z*xlqLXU}DGwmtR-!|u-P?tFjq{r$c(yQ4&eyV-Q70kmswTIIHpqx-a{4@+nc3X*!y zoic?AmFs+E1N;8?iPulpw66@IZ6inb6}p`7cRaYGxMj;G-Bs{#9gU+ET7lLoK36yz zl#1$=QYZyVrJxY70s>eohzQ1l6~S7;#@EKmVhN4K7@^_^{QcKuJ3cm64gSC5YJD0!Ast z6Fb-O)UFNe+|tjw{w{ipF4hWV->~2trfyca@^6_VpPpTYDa}$Ha1;@RIoqz`HN5fC z9yYJ4xq@DfdhLtGf*mIes0waDDq9>CwC9lZJWAT=Ge(G z&QDG=J@0c*slf1HiHC*Jo)|4v?P;mAbvUZC7qAwA@do8~|xsg}#1! z1kcsz2FkN-sE=>o8L6rN@SAgd{^P}Ebt=n5mIni+ZV#{C>=T>TDwUyvt84nNHw>u` zNRBzM8cXj#V-n}7LaVa7nSpc_%)o%;z`*`PU!as?Xl)Olz4d(E`<^@WC4pHEHZ-<9 z2COx0GvEg{K86kNs{{c-*u1ZLc?8|RTuz2FdRGaYt|4l@yN6vM)f`g6VnQ$!97|>EQNI#Nd4jnLzDZDtyP;uLQqeI zG=N$%4RO4QT}rTaOA~;NpN*Cp2?I(goqz>!xBlFz+1bLg#iFCmUA)2{ zW0%_o^6p1Jtom8U(X8w5Ve0yvSby#m&`(6G0c;XNE|FKInTgTkPS-O}3q`N5x1c=N z!EqF><3yW%j>6FzM@RpBTt}l+yz!^i;yFr1i4|}DRh!6CY>UL3eaVKOwb;eYK1&c- zu1_z>wHvd+%*5z%pahhG3J@d?Bw^%DUpP5#DsvzBTlbF4_HEn)s>6H9VuRVMXJ;lx zPs~pKG)|PK4l!;86o76bYSsgTz=rs`7(b9Wc}J1QB_JB{8DI*y2K*CWFSzkdArU2W zF<={?7hqATI$;SmAD9Pj#<(hRy|_eRLn1E~QF6=^X3;00000NkvXXu0mjfGmX70 diff --git a/chat/protocols/facebook/icons/prpl-facebook.png b/chat/protocols/facebook/icons/prpl-facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..bc42cf9b0b3251a61786d99628207d2978969cb2 GIT binary patch literal 552 zc$@(!0@wYCP))8iP zGSW4Gj)dT61D@XN(mi()fStX9G>Hk`k!jTcBIW}wbd~{l@bV)+fA69ch>Tnx0u*5? z!uG4XNBTEUt^u(2XlryZ9*ZEs&vh_a^<2c51LxcnV0Ha10G~JSAL;8-5*k2NR&!sW zu1Vbo-vIKm{BMLwy{f7PFti21JJ#=X=K>zyym+|YeE*HSD8~Ug;vMoY*USiGns|q` zMFTh(T0(HVe)nnW;Hm=tVzLC2C1j~a#}RQHp@1NQ_d-!Q%F59n q>;qV>pw}Pd8~s7P(02x`g5F=w(8@aM?(+5k0000P)Z*4j-(LjYn*sgJeRT>!3Ytvl=M>u)@cUwTvy)YQ~`-!#qN7=|&o zxVRXlrKM`loH-N@hsiJu2q7>qFn~lNq3+zdLy1I!;_*0!hK9}wA>OF3uRr}n1a|M< z{k&KQX;5CCYc9d(=#fruO%0IpxZu3K7KD3M59)LOr>ckkYo$087k zL<%g+I_UTNf3RW023=NG#*|W!Qhot=GGUCtG))i@y1Tn|b8|D@yLa!@;Nal;{rmS@ zlM;wTBFwh!zm%1gt>3(PGcm@%Ifs<;k)RWaVHggPrluy{-roK}S6A1|$BrFy9OfjU zWmyfy#l;&cDk`9qLht?0ajSO-MS(n&O!ZC*B&{pZY6v!)jn7gY@aLBxRPY>x5D-GZ z<#J)urcGp7)_Se=VIq1}XIAbU!DmSKGI5hf)gZbQ=HZ{0zGew4&)u7nD+y zYAU`9gEzm6-u{21rTxd~xbt34zhzkvLZGs;l4i}C^-6Vhb%V2Ana~#y(VN@0ZG&ML z7#$tWOqNhmjR}f6PMikyLej&|Y5KRO|$#QF9(_8z%31{jS(Mj}whk3*$WxdeoQuqC9B5Yon9 zulxk3+Fys10!k^2jEunLa$(V;MWnUj>s96Dp7;Q|ZubEI?j}=c z(+cA4x1kOl8jIbD6JQ-3Fm~^Tur;^^D5)X1f)D~NAN?FR?w-MnXBI;$4uiR&wFc)L z<>lq<^5x6law5POn=^a%Y{ofv!uraK&tb<)^Pr@_Hy2FDZ-2NPSiKtZg%>~v4nQtk z2$$X z_h4!;4@ydS^G!S(_Cmu8wskAyy?cehIRu0dAI0PG1w}D*#aW^Z%vtRib#--}>2&&cCr_T#UauFXIhHhw=Z5j(*Pq2#r}?np zJ4+!C9|1~AV60vZ@6n@ROO{~d=ur?W1T%eL|BKUkh1OfrXVlmy= z*m&2r?IjHj4WBw=9==|ys;cI>-R^&`UAuPLiWMuE5CZ-E{TLY;d2C?D82o-e{C+=_ zQaFG9ygqa0%mC+nd0kyyXAa4U?)R#ys$$bL-wFnU8`iB`r{~X~PyPM<=O7&SFDONr>fs#U8N zuUxs3Zr;4<^aTO|ghC-0h5-QJoMT{M0Qc|T*SBxq#>I;l#qjX(@6+jY!@hm{KGBl^ zP53A!B8IUO(pFVf)fx;2%Wc~xr4)z=)22;>&*#J0vuAZzR~IFdNemAUr?l4HQp!## zWvsWi_fL(Djh_Ng09-_*GPWL1#(W5vGQf@!(UOvq5@uNz0)YU6!61}UICSU`VzJmC zlgZ?Ey<2M-?nAmbPSTn~XgsOb+R+ebu1T5IhP&{}IE%C0~IKqwS?bLPyMlt?6S z;lc&k*4Acl&f8+K*iVliKYlYqG#k#GxsQ_UHsmDXxF}F%Wo4+ixcHV~7+gvju( z8Slx=Hvq7Sa31+*G%hmZS3KU%t{mfRjfb**Qr}Pe_xyj_6^0o*gkT06p>i5g;r%!+BA=9(~>sb zWJ7XyH~YBvoXY4nr8S?k4G2dLWW_K0$2&)b3}AC5zSRf)r~tCkwZj7N~wbY4gmNY zfWxL~j(%7S7fiq~3=e?Y0NhGM*E6%Hw6s(%T)0qNe);88TU(2&sw(Ju(zsGe#N%ehUGb0=hqpz>Ip48vGPM6X_T)m7fxZ@*pBG!3?GL&^(^uMaIk2)LHswr!j2>gr-i`z1adA|p5_6=i|JAP|_!V8wcq07yfH?-K>P zc)eZ-A+US*Zgt?m0RnK7X_|i=mw;gyQ;Dc!&YU?@@3`X*p_D=_7E53M!p7Z*#2f&? z?|!@*mrW@-Z~Rt|AFM18dEuLR63-vF4ggT@^W)ZQ|1@qtuh$D**Rg&3cGceA9%kli zOw;Ujujei$p7D4*Q`fFtE0j`*Mx$AoCQC6+`#36Xq?MT=<(N*?FBXfzaU9%o%Ppe3 zyu6f%UQX4bYyyU1+yvmucinZD@OV6h!7BzUmC_3&;2^0oMZi%hm6P_3#bOXbVBNZP z8Z%!D;O;yEMD+bxvu4TZ)2AaAi$N)se!vHz7=j~FC?(Sb+%_py_JFB)DM%n?0hM6^ zM~)Ft%EA6l6QSstyc!dYMp0c|jccyCMiJ3>vIrQ4F`Jo}uUfTAQ%a?S*41xr`V($? zcsIh)1QaVMR=DqpWZS7g2&@#;tFOXYyB5yUrEpfPfNE|Ar&2C6!*P&SW|g*pW4i!d)9JRHzsHmurmSuenz+YS!x_Z{ESr9@X9*^glzoOWK%2E%Q6_iS9 zMM<&^m6ec@2&9xrt*%ZV+bu0=E2!Kcc3f3xJN7fY`|p<^9fop%mGd4$!<5B&I1-5j zhzN7$%n<I+e;)Sc&A`c%5FH&53l>01Ksf*?6%#27ghm56)ctIFtQ89$#N5l93RZhJ z9{hek2_a@@y3i+TYHA=;0X43uY*%vcr4=jSS+QcQZNY-{L@3(^F~hN40;wQ&@?G-N zv+l+xXWlg-1}Puu>mw=rNVpjPUI2$~M!(G3@r?2|@gakBALm&_U5%oU)_~X(I;&)G+ zI6=$|UDwAgD2h3qMw5YQWAxES;e7A`q*BmUt%C2DzXTc@(t$iyPmrMuC6fx)(#QM$ z@fePsdS%@4g%BVjoH=twDW#4BV}3&Su^***RI7M-mJj!-XWCwJeWIUD!#a64T|@##^~0qP=kXgTDlZv4?P6g*N4ca zO;G*)kWxY~s)ZC@2+ap(ptf=j0HD0MK9w{C)-3x4THpRDR()y%%8KjK$;9LFprWDz z?d|Od1OfuU6E3FQ_knu=w5(sh9#f`F!N9-(9LE_aAN+Sj2!ZqU8aIm*h)002EbJ=nf|yNbnP z31)s2z!RovCdTW3zFUj+DI)8Ni;J~2Yu1SQ^XKE-xpN4I&u_<^h@fd2bX|wf=S!=m zX^2E3Xm4-F>#x7AqS2@TunWNVGIt*?iU1{dWNH2S^;h_Oz9)&OY2(I?kWylBaIhfQ zs;VlKm6fHp422j=c(!dL7!2b5_ut2nBS%zscefy-s8Z^;i9}+{Q%^m07ytuco&?~y zA2?jlnYwxN=2IIsZ1}UT>rFnN55vR5d9k4DI{bb=JRT3aySrgo7Q*2$EX#srS!!Tl zfP=xHmKr0flzQ8??U&zs@4c7z?%f+o0oPmtM8p%3$sB`~8T=<9OzoXJlVrABbp}nTM2814^kNGY{If{hno6@4WKLD@Ox?fLkfEfTvVt zp1Wi+iDVJT0WS&=5JHGW^XARd03a5Nr3qA5SEH`34uL=bEiElF5{X!+PoG}9ZQHhg z=FCwhKokI-dp);Jr52l9$O041zPS13o9l>Z-ok|oDH@H!wrx~YRG_A&21P|hICSU` zcJAEC0L~maa^$PKckez4z%v1uyjf12yCQPVU5xIEnbQP_vUX~7j|l*-yzbzRq{ zPoEAcC1%f_4IuN7~?{+`nHWu>riW77lPd};1rK5?qR?JM zK?DEp%M{@2+PaMYJ7aW=KKCp$8ny<>h=0Bi9}*1o6T-M_Q&7! zYu9#2DZg^}RT4(yS==b>z@c5k)ZX51l*{EzKA(TA8HQo^_V#W$ef=NssE)zQ1@M&> zh=&g$78W3cK=3ahgg_W@__Yizhq4qX?I>orvz=2g~OX zo;vk^4soO7^%JiM>9we?wle}6(siTU|?RxX#} z`@Yh3y~-H7SFKjBjE#-CN-6pI7l?=e=tDz8-ED1cy6d`_n3(v_^Ss-oQt3vaP*?#V z0SEvbfJsCg0AGJ50zj5!W#;DQYQ_pC29NuqD`(60vZWiilCqbLM~yAup0MGimaDaP7n7mo5F!D-rF}b zznS^N;}J6>NE3l(-vj&)00-MT7>02_7K@z$IP#J2t7TcIrBdm$Zvaix-srmiOo;89 zC#8gGngYOc-vC5(D1^XdGT9nHRaFqtp?5EE4FnFbGy=mgjx|j)iRj)2pNF+t4N}Ub zO+`_VOePVJ$NdAuVzDz_*H8QktX{7plgao80B|dTO{deX{Q}p(9$*g;IKb9-!6p+z zAR3KctsTd~Y&JtA5&;oGN(pBEzB7Q)X!O%G&5v$!kxHeG)9G})*=$<9Uhl$joXhce ze7Z9LBI@RHxhIQtKA(R&91h>ww(VrI*~i!U>{J8AV)0$4(|I%)4BoFhvvU!a%jJ;} z;>EiEt0VX(rG%=gkW#|7Z77O@P$-0(-VY0b5z!s*HZ#u)g@Rn?w*+8W)+^IA52Tbw ziA3T-CX>+`jmD35yL};qU}h%wD6M|XeA(@G3jpqVLe7au0$>0RfH^bIy*bHS84(eH zFo1BSQu(AP3N;#ym(^dzn3%d@E8^Dh(Ix6wE3v*_6t`2Mxmpc;hEncuU#n=e& dFD&rO=r;q`=+rK*U1b0O002ovPDHLkV1f@IEHwZC diff --git a/chat/protocols/irc/icons/prpl-irc-48.png b/chat/protocols/irc/icons/prpl-irc-48.png new file mode 100644 index 0000000000000000000000000000000000000000..606425fabb7aa1d5e9e490ad5d74bc55c4c94262 GIT binary patch literal 1003 zc$@+20~Gv;P)M5 zo9u$QTeMi(k2LaXy!)N^-Fa_Dv!u1gFvF4#;B83_Bu#1{X;K48W5g-8w6ye@VHn>k zrN-jaWsJQRLVQ^+mw&kVU#tcU!+1VFKR;&McAT>CeSfT4tv(0v?*GP&b0DRZW7{^G z&1Rgk$mjD=N;zTw4Sfxy29hQ%)EOh6&tr0O z5~HJ|ZxCuYp$4+q?02)Xvrh|!LTm#8!1?((Ha9mhJv|+pW~c!vGn2I}3q*vqwKdUhw-o?z&Rar=ufntvNg*la z6Dj2r&+}~0^K7NmJB>!;lhxJL=IQCFmQup^{ogs~1 z#l^)>2f6=I%|I`eO6A8v?t8Qb1|>9w7-PN=!nQ2yx-SR<1VI4PH2c=*`yhk>5&1+E zmAXo6eXmngsE|^sw6d~N8Xq4I0Kha&V`^$DXBY;w*4WzG;>~7L5)lBP_qTi9g^0iy z(*SUCa$<7MzXp(th({8U2GHA-)>=jxpr|hhb8~Z!VHocTA=t>s$ctjJIC^<`iS_mM zzw7mS(eu1twAKUwjIr*9L}7j1>$9`7SG&8re?$cXppC1|O#@IpO#+Z%MnY$Rh%x}` z0R9A!SzcZa4h|0F&dyHr`1ttqTCMimEkWEx)Z0fV_{@#KMW-J`gqtR!a*=u(lTwQ9 z?d@Op_V&Km-`{@?;LVEQZm6RzI!)thp;0~Y^buvCF9&_!|E5-}ecxy_x^Lv6K36CJ z0s7NN-H4};$QtORQMa+47Fkd+Vc;G$5NY80hASfs|4m_6{2}PRq_tY{BBgkX!%&002ovPDHLkV1o1+&{Y5c diff --git a/chat/protocols/irc/icons/prpl-irc.png b/chat/protocols/irc/icons/prpl-irc.png new file mode 100644 index 0000000000000000000000000000000000000000..19d578deda123c41b441aef78f147fdf74bd426d GIT binary patch literal 454 zc$@*o0XhDOP)MLv|g`;Qfl4_kf!O& zVzGEC%kl=mZ5+qnoo+#E9Ti1!2Ve=n039s=K$0Y%0Pgp-1#DAx90$w{B7$Wda`%BX z$x=#vOsCU_!C=5q6v;f#O_pVSBI3sE!>;M1& diff --git a/chat/protocols/irc/irc.js b/chat/protocols/irc/irc.js new file mode 100644 --- /dev/null +++ b/chat/protocols/irc/irc.js @@ -0,0 +1,1416 @@ +/* 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/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource:///modules/imXPCOMUtils.jsm"); +Cu.import("resource:///modules/imServices.jsm"); +Cu.import("resource:///modules/ircUtils.jsm"); +Cu.import("resource:///modules/ircHandlers.jsm"); +Cu.import("resource:///modules/jsProtoHelper.jsm"); +Cu.import("resource:///modules/socket.jsm"); + +/* + * Parses a raw IRC message into an object (see section 2.3 of RFC 2812). This + * returns an object with the following fields: + * rawMessage The initial message string received without any processing. + * command A string that is the command or response code. + * params An array of strings for the parameters. The last parameter is + * stripped of its : prefix. + * If the message is from a user: + * nickname The user's nickname. + * user The user's username, note that this can be undefined. + * host The user's hostname, note that this can be undefined. + * source A "nicely" formatted combination of user & host, which is + * @ or if host is undefined. + * Otherwise if it's from a server: + * servername This is the address of the server as a host (e.g. + * irc.mozilla.org) or an IPv4 address (e.g. 1.2.3.4) or IPv6 + * address (e.g. 3ffe:1900:4545:3:200:f8ff:fe21:67cf). + */ +function ircMessage(aData) { + let message = {rawMessage: aData}; + let temp, prefix; + + // Splits the raw string into four parts (the second is required), the command + // is required. A raw string looks like: + // [":" " "] [" " ]* [":" ] + // : :( | [["!" ] "@" ]) + // : /[^ ]+/ + // : /[^ ]+/ + // : /.+/ + // See http://joshualuckers.nl/2010/01/10/regular-expression-to-match-raw-irc-messages/ + // Note that this expression is slightly more aggressive in matching than RFC + // 2812 would allow. It allows for empty parameters (besides the last + // parameter, which can always be empty), by allowing two spaces in a row. + // (This is for compatibility with Unreal's 432 response, which returns an + // empty first parameter.) It also allows a trailing space after the + // s when no is present (also occurs with Unreal). + if (!(temp = aData.match(/^(?::([^ ]+) )?([^ ]+)((?: +[^: ][^ ]*)*)? ?(?::(.*))?$/))) { + ERROR("Couldn't parse message: \"" + aData + "\""); + return message; + } + + // Assume message is from the server if not specified + prefix = temp[1]; + message.command = temp[2]; + // Space separated parameters. Since we expect a space as the first thing + // here, we want to ignore the first value (which is empty). + message.params = temp[3] ? temp[3].split(" ").slice(1) : []; + // Last parameter can contain spaces or be an empty string. + if (temp[4] != undefined) + message.params.push(temp[4]); + + // The source string can be split into multiple parts as: + // :(server|nickname[[!user]@host] + // If the source contains a . or a :, assume it's a server name. See RFC + // 2812 Section 2.3 definition of servername vs. nickname. + if (prefix && + (temp = prefix.match(/^([^ !@\.:]+)(?:!([^ @]+))?(?:@([^ ]+))?$/))) { + message.nickname = temp[1]; + message.user = temp[2] || null; // Optional + message.host = temp[3] || null; // Optional + if (message.user) + message.source = message.user + "@" + message.host; + else + message.source = message.host; // Note: this can be null! + } + else if (prefix) + message.servername = prefix; + + return message; +} + +// This handles a mode change string for both channels and participants. A mode +// change string is of the form: +// aAddNewMode is true if modes are being added, false otherwise. +// aNewModes is an array of mode characters. +function _setMode(aAddNewMode, aNewModes) { + // Check each mode being added/removed. + for each (let newMode in aNewModes) { + let index = this._modes.indexOf(newMode); + // If the mode is in the list of modes and we want to remove it. + if (index != -1 && !aAddNewMode) + this._modes.splice(index, 1); + // If the mode is not in the list of modes and we want to add it. + else if (index == -1 && aAddNewMode) + this._modes.push(newMode); + } +} + +// This copies all the properties of aBase to aPrototype (which is expected to +// be the prototype of an object). This is necessary because JavaScript does not +// support multiple inheritance and both conversation objects have a lot of +// shared code (but inherit from objects exposing different XPCOM interfaces). +function copySharedBaseToPrototype(aBase, aPrototype) { + for (let property in aBase) + aPrototype[property] = aBase[property]; +} + +// Properties / methods shared by both ircChannel and ircConversation. +const 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, + + sendMsg: function(aMessage) { + // Split the message by line breaks and send each one individually. + let messages = aMessage.split(/[\r\n]+/); + + // Build the shortest possible message that could be sent. + let baseMessage = this._account._nickname + this._account.prefix + + " " + this._account.buildMessage("PRIVMSG", this.name) + + " :\r\n"; + let maxLength = + this._account.maxMessageLength - this._account.countBytes(baseMessage); + + // 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); + // The message is short enough. + if (length <= maxLength) + continue; + + // Find the location of a space before the maximum length. + let index = message.lastIndexOf(" ", maxLength); + + // 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. + messages.forEach(function (aMessage) { + if (!aMessage.length) + return; + + this._account.sendMessage("PRIVMSG", [this.name, aMessage]); + + // 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; + }, this); + }, + + requestBuddyInfo: function(aNick) { + if (!this._observedNicks.length) + Services.obs.addObserver(this, "user-info-received", false); + this._observedNicks.push(this._account.normalize(aNick)); + this._account.requestBuddyInfo(aNick); + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic != "user-info-received") + return; + + let nick = this._account.normalize(aData); + let nickIndex = this._observedNicks.indexOf(nick); + if (nickIndex == -1) + return; + this._observedNicks.splice(nickIndex, 1); + if (!this._observedNicks.length) + Services.obs.removeObserver(this, "user-info-received"); + + // If we are waiting for the conversation name, set it. + if (this._waitingForNick && nick == this.normalizedName) { + if (hasOwnProperty(this._account.whoisInformation, nick)) + this.updateNick(this._account.whoisInformation[nick]["nick"]); + delete this._waitingForNick; + return; + } + + // Otherwise, print the requested whois information. + this._account.writeWhois(this, aData, + aSubject.QueryInterface(Ci.nsISimpleEnumerator)); + }, + + unInitIRCConversation: function() { + this._account.removeConversation(this.name); + if (this._observedNicks.length) + Services.obs.removeObserver(this, "user-info-received"); + } +}; + +function ircChannel(aAccount, aName, aNick) { + this._init(aAccount, aName, aNick); + this._modes = []; + this._observedNicks = []; + this.banMasks = []; +} +ircChannel.prototype = { + __proto__: GenericConvChatPrototype, + _modes: [], + _receivedInitialMode: false, + // For IRC you're not in a channel until the JOIN command is received, open + // all channels (initially) as left. + _left: true, + banMasks: [], + + // Overwrite the writeMessage function to apply CTCP formatting before + // display. + writeMessage: function(aWho, aText, aProperties) { + GenericConvChatPrototype.writeMessage.call(this, aWho, + ctcpFormatToHTML(aText), + aProperties); + }, + + // Stores the prplIChatRoomFieldValues required to join this channel + // to enable later reconnections. If absent, the MUC will not be reconnected + // automatically after disconnections. + _chatRoomFields: null, + + // Section 3.2.2 of RFC 2812. + part: function(aMessage) { + let params = [this.name]; + + // If a valid message was given, use it as the part message. + // Otherwise, fall back to the default part message, if it exists. + let msg = aMessage || this._account.getString("partmsg"); + if (msg) + params.push(msg); + + this._account.sendMessage("PART", params); + + // Remove reconnection information. + delete this._chatRoomFields; + }, + + close: function() { + // Part the room if we're connected. + if (this._account.connected && !this.left) + this.part(); + GenericConvChatPrototype.close.call(this); + }, + + unInit: function() { + this.unInitIRCConversation(); + GenericConvChatPrototype.unInit.call(this); + }, + + getNormalizedChatBuddyName: function(aNick) + this._account.normalize(aNick, this._account.userPrefixes), + + hasParticipant: function(aNick) + hasOwnProperty(this._participants, this.getNormalizedChatBuddyName(aNick)), + + getParticipant: function(aNick, aNotifyObservers) { + let normalizedNick = this.getNormalizedChatBuddyName(aNick); + if (this.hasParticipant(aNick)) + return this._participants[normalizedNick]; + + let participant = new ircParticipant(aNick, this); + this._participants[normalizedNick] = participant; + + // Add the participant to the whois table if it is not already there. + this._account.setWhoisFromNick(participant._name); + + if (aNotifyObservers) { + this.notifyObservers(new nsSimpleEnumerator([participant]), + "chat-buddy-add"); + } + return participant; + }, + updateNick: function(aOldNick, aNewNick) { + let isParticipant = this.hasParticipant(aOldNick); + if (this._account.normalize(aOldNick) == this._account.normalize(this.nick)) { + // If this is the user's nick, change it. + this.nick = aNewNick; + // If the account was disconnected, it's OK the user is not a participant. + if (!isParticipant) + return; + } + else if (!isParticipant) { + ERROR("Trying to rename nick that doesn't exist! " + aOldNick + " to " + + aNewNick); + return; + } + + // Get the original ircParticipant and then remove it. + let participant = this.getParticipant(aOldNick); + this.removeParticipant(aOldNick); + + // Update the nickname and add it under the new nick. + participant._name = aNewNick; + this._participants[this.getNormalizedChatBuddyName(aNewNick)] = participant; + + this.notifyObservers(participant, "chat-buddy-update", aOldNick); + }, + removeParticipant: function(aNick, aNotifyObservers) { + if (!this.hasParticipant(aNick)) + return; + + if (aNotifyObservers) { + let stringNickname = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + stringNickname.data = aNick; + this.notifyObservers(new nsSimpleEnumerator([stringNickname]), + "chat-buddy-remove"); + } + delete this._participants[this.getNormalizedChatBuddyName(aNick)]; + }, + // Use this before joining to avoid errors of trying to re-add an existing + // participant + removeAllParticipants: function() { + let stringNicknames = []; + for (let nickname in this._participants) { + let stringNickname = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + stringNickname.data = this._participants[nickname].name; + stringNicknames.push(stringNickname); + } + this.notifyObservers(new nsSimpleEnumerator(stringNicknames), + "chat-buddy-remove"); + this._participants = {}; + }, + + setMode: function(aNewMode, aModeParams, aSetter) { + const hostMaskExp = /^.+!.+@.+$/; + function getNextParam() { + // If there's no next parameter, throw a warning. + if (!aModeParams.length) { + WARN("Mode parameter expected!"); + return undefined; + } + return aModeParams.pop(); + } + function peekNextParam() { + // Non-destructively gets the next param. + if (!aModeParams.length) + return undefined; + return aModeParams.slice(-1)[0]; + } + + // Are modes being added or removed? + if (aNewMode[0] != "+" && aNewMode[0] != "-") { + WARN("Invalid mode string: " + aNewMode); + return; + } + let addNewMode = aNewMode[0] == "+"; + + // Check each mode being added and update the user. + let channelModes = []; + let userModes = {}; + let msg; + + for (let i = aNewMode.length - 1; i > 0; --i) { + // Since some modes are conflicted between different server + // implementations, check if a participant with that name exists. If this + // is true, then update the mode of the ConvChatBuddy. + if (this._account.memberStatuses.indexOf(aNewMode[i]) != -1 && + aModeParams.length && this.hasParticipant(peekNextParam())) { + // Store the new modes for this nick (so each participant's mode is only + // updated once). + let nick = this._account.normalize(getNextParam()); + if (!hasOwnProperty(userModes, nick)) + userModes[nick] = []; + userModes[nick].push(aNewMode[i]); + + // Don't use this mode as a channel mode. + continue; + } + else if (aNewMode[i] == "k") { + // Channel key. + let newFields = this.name; + if (addNewMode) { + let key = getNextParam(); + // A new channel key was set. + msg = _("message.channelKeyAdded", aSetter, key); + newFields += " " + key; + } else + msg = _("message.channelKeyRemoved", aSetter); + + this.writeMessage(aSetter, msg, {system: true}); + // Store the new fields for reconnect. + this._chatRoomFields = + this._account.getChatRoomDefaultFieldValues(newFields); + } + else if (aNewMode[i] == "b") { + // A banmask was added or removed. + let banMask = getNextParam(); + let msgKey = "message.banMask"; + if (addNewMode) { + this.banMasks.push(banMask); + msgKey += "Added"; + } + else { + this.banMasks = + this.banMasks.filter(function (aBanMask) banMask != aBanMask); + msgKey += "Removed"; + } + this.writeMessage(aSetter, _(msgKey, banMask, aSetter), {system: true}); + } + else if (["e", "I", "l"].indexOf(aNewMode[i]) != -1) { + // TODO The following have parameters that must be accounted for. + getNextParam(); + } + else if (aNewMode[i] == "R" && aModeParams.length && + peekNextParam().match(hostMaskExp)) { + // REOP_LIST takes a mask as a parameter, since R is a conflicted mode, + // try to match the parameter. Implemented by IRCNet. + // TODO The parameter must be acounted for. + getNextParam(); + } + // TODO From RFC 2811: a, i, m, n, q, p, s, r, t, l, e, I. + + // Keep track of the channel modes in the order they were received. + channelModes.unshift(aNewMode[i]); + } + + if (aModeParams.length) + WARN("Unused mode parameters: " + aModeParams.join(", ")); + + // Update the mode of each participant. + for (let nick in userModes) + this.getParticipant(nick).setMode(addNewMode, userModes[nick], aSetter); + + if (!channelModes.length) + return; + + // Store the channel modes. + _setMode.call(this, addNewMode, channelModes); + + // Notify the UI of changes. + msg = _("message.mode", this.name, aNewMode[0] + channelModes.join(""), + aSetter); + this.writeMessage(aSetter, msg, {system: true}); + this.checkTopicSettable(); + + this._receivedInitialMode = true; + }, + + setModesFromRestriction: function(aRestriction) { + // First remove all types from the list of modes. + for each (let mode in this._account.channelRestrictionToModeMap) { + let index = this._modes.indexOf(mode); + this._modes.splice(index, index != -1); + } + + // Add the new mode onto the list. + if (aRestriction in this._account.channelRestrictionToModeMap) { + let mode = this._account.channelRestrictionToModeMap[aRestriction]; + if (mode) + this._modes.push(mode); + } + }, + + get topic() this._topic, // can't add a setter without redefining the getter + set topic(aTopic) { + this._account.sendMessage("TOPIC", [this.name, aTopic]); + }, + _previousTopicSettable: null, + checkTopicSettable: function() { + if (this.topicSettable == this._previousTopicSettable && + this._previousTopicSettable != null) + return; + + this.notifyObservers(this, "chat-update-topic"); + }, + get topicSettable() { + // If we're not in the room yet, we don't exist. + if (!this.hasParticipant(this.nick)) + return false; + + // If the channel mode is +t, hops and ops can set the topic; otherwise + // everyone can. + let participant = this.getParticipant(this.nick); + return this._modes.indexOf("t") == -1 || participant.op || + participant.halfOp; + }, + + get normalizedName() this._account.normalize(this.name) +}; +copySharedBaseToPrototype(GenericIRCConversation, ircChannel.prototype); + +function ircParticipant(aName, aConv) { + this._name = aName; + this._conv = aConv; + this._account = aConv._account; + this._modes = []; + + if (this._name[0] in this._account.userPrefixToModeMap) { + this._modes.push(this._account.userPrefixToModeMap[this._name[0]]); + this._name = this._name.slice(1); + } +} +ircParticipant.prototype = { + __proto__: GenericConvChatBuddyPrototype, + + setMode: function(aAddNewMode, aNewModes, aSetter) { + _setMode.call(this, aAddNewMode, aNewModes); + + // Notify the UI of changes. + let msg = _("message.mode", this.name, + (aAddNewMode ? "+" : "-") + aNewModes.join(""), aSetter); + this._conv.writeMessage(aSetter, msg, {system: true}); + this._conv.notifyObservers(this, "chat-buddy-update"); + + // In case the new mode now lets us edit the topic. + if (this._account.normalize(this.name) == + this._account.normalize(this._account._nickname)) + this._conv.checkTopicSettable(); + }, + + get voiced() this._modes.indexOf("v") != -1, + get halfOp() this._modes.indexOf("h") != -1, + get op() this._modes.indexOf("o") != -1, + get founder() + this._modes.indexOf("O") != -1 || this._modes.indexOf("q") != -1, + get typing() false +}; + +function ircConversation(aAccount, aName) { + this.buddy = aAccount.getBuddy(aName); + let nick = aAccount.normalize(aName); + if (hasOwnProperty(aAccount.whoisInformation, nick)) + aName = aAccount.whoisInformation[nick]["nick"]; + + this._init(aAccount, aName); + this._observedNicks = []; + + // Fetch correctly capitalized name. + // Always request the info as it may be out of date. + this._waitingForNick = true; + this.requestBuddyInfo(aName); +} +ircConversation.prototype = { + __proto__: GenericConvIMPrototype, + + // Overwrite the writeMessage function to apply CTCP formatting before + // display. + writeMessage: function(aWho, aText, aProperties) { + GenericConvIMPrototype.writeMessage.call(this, aWho, + ctcpFormatToHTML(aText), + aProperties); + }, + + get normalizedName() this._account.normalize(this.name), + + unInit: function() { + this.unInitIRCConversation(); + GenericConvIMPrototype.unInit.call(this); + }, + + updateNick: function(aNewNick) { + this._name = aNewNick; + this.notifyObservers(null, "update-conv-title"); + } +}; +copySharedBaseToPrototype(GenericIRCConversation, ircConversation.prototype); + +function ircSocket(aAccount) { + this._account = aAccount; + this._initCharsetConverter(); +} +ircSocket.prototype = { + __proto__: Socket, + delimiter: "\r\n", + connectTimeout: 60, // Failure to connect after 1 minute + readWriteTimeout: 300, // Failure when no data for 5 minutes + _converter: null, + + _initCharsetConverter: function() { + this._converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + try { + this._converter.charset = this._account._encoding; + } catch (e) { + delete this._converter; + ERROR("Failed to set character set to: " + this._account._encoding + " for " + + this._account.name + "."); + } + }, + + // Implement Section 5 of RFC 2812. + onDataReceived: function(aRawMessage) { + DEBUG(aRawMessage); + if (this._converter) { + try { + aRawMessage = this._converter.ConvertToUnicode(aRawMessage); + } catch (e) { + WARN("This message doesn't seem to be " + this._account._encoding + + " encoded: " + aRawMessage); + // Unfortunately, if the unicode converter failed once, + // it will keep failing so we need to reinitialize it. + this._initCharsetConverter(); + } + } + + // Low level dequote: replace quote character \020 followed by 0, n, r or + // \020 with a \0, \n, \r or \020, respectively. Any other character is + // replaced with itself. + const lowDequote = {"0": "\0", "n": "\n", "r": "\r", "\x10": "\x10"}; + aRawMessage = aRawMessage.replace(/\x10./g, + function(aStr) lowDequote[aStr[1]] || aStr[1]); + + // If nothing handled the message, throw an error. + if (!ircHandlers.handleMessage(this._account, new ircMessage(aRawMessage))) + WARN("Unhandled IRC message: " + aRawMessage); + }, + onConnection: function() { + this._account._connectionRegistration.call(this._account); + }, + + // Throw errors if the socket has issues. + onConnectionClosed: function () { + if (!this._account.imAccount || this._account.disconnecting || + this._account.disconnected) + return; + + ERROR("Connection closed by server."); + this._account.gotDisconnected(Ci.prplIAccount.ERROR_NETWORK_ERROR, + _("connection.error.lost")); + }, + onConnectionReset: function () { + ERROR("Connection reset."); + this._account.gotDisconnected(Ci.prplIAccount.ERROR_NETWORK_ERROR, + _("connection.error.lost")); + }, + onConnectionTimedOut: function() { + ERROR("Connection timed out."); + this._account.gotDisconnected(Ci.prplIAccount.ERROR_NETWORK_ERROR, + _("connection.error.timeOut")); + }, + onBadCertificate: function(aNSSErrorMessage) { + ERROR("bad certificate: " + aNSSErrorMessage); + this._account.gotDisconnected(Ci.prplIAccount.ERROR_CERT_OTHER_ERROR, + aNSSErrorMessage); + }, + log: LOG +}; + +function ircAccountBuddy(aAccount, aBuddy, aTag, aUserName) { + this._init(aAccount, aBuddy, aTag, aUserName); +} +ircAccountBuddy.prototype = { + __proto__: GenericAccountBuddyPrototype, + + // Returns a list of imITooltipInfo objects to be displayed when the user + // hovers over the buddy. + getTooltipInfo: function() this._account.getBuddyInfo(this.normalizedName), + + get normalizedName() this._account.normalize(this.userName), + + // Allow sending of messages to buddies even if they are not online since IRC + // does not always provide status information in a timely fashion. (Note that + // this is OK since the server will throw an error if the user is not online.) + get canSendMessage() this.account.connected, + + // Called when the user wants to chat with the buddy. + createConversation: function() this._account.createConversation(this.userName) +}; + +function ircAccount(aProtocol, aImAccount) { + this._buddies = {}; + this._init(aProtocol, aImAccount); + this._conversations = {}; + + // Split the account name into usable parts. + let splitter = aImAccount.name.lastIndexOf("@"); + this._accountNickname = aImAccount.name.slice(0, splitter); + this._server = aImAccount.name.slice(splitter + 1); + + this._nickname = this._accountNickname; + this._requestedNickname = this._nickname; + + // For more information, see where these are defined in the prototype below. + this._isOnQueue = []; + this.pendingIsOnQueue = []; + this.whoisInformation = {}; + this._chatRoomFieldsList = {}; +} +ircAccount.prototype = { + __proto__: GenericAccountPrototype, + _socket: null, + _MODE_WALLOPS: 1 << 2, // mode 'w' + _MODE_INVISIBLE: 1 << 3, // mode 'i' + get _mode() 0, + + // The nickname stored in the account name. + _accountNickname: null, + // The nickname that will be used when connecting. + _requestedNickname: null, + // The prefix minus the nick (!user@host) as returned by the server, this is + // necessary for guessing message lengths. + prefix: null, + + get normalizedName() this.normalize(this.name), + + // Parts of the specification give max lengths, keep track of them since a + // server can overwrite them. The defaults given here are from RFC 2812. + maxNicknameLength: 9, // 1.2.1 Users + maxChannelLength: 50, // 1.3 Channels + maxMessageLength: 512, // 2.3 Messages + maxHostnameLength: 63, // 2.3.1 Message format in Augmented BNF + + // The default prefixes. + userPrefixes: ["@", "!", "%", "+"], + // The default prefixes to modes. + userPrefixToModeMap: {"@": "o", "!": "n", "%": "h", "+": "v"}, + // Modes that have a nickname parameter and affect a participant. See 4.1 + // Member Status of RFC 2811. + memberStatuses: ["a", "h", "o", "O", "q", "v", "!"], + channelPrefixes: ["&", "#", "+", "!"], // 1.3 Channels + channelRestrictionToModeMap: {"@": "s", "*": "p", "=": null}, // 353 RPL_NAMREPLY + + // Handle Scandanavian lower case (optionally remove status indicators). + // See Section 2.2 of RFC 2812: the characters {}|^ are considered to be the + // lower case equivalents of the characters []\~, respectively. + normalize: function(aStr, aPrefixes) { + let str = aStr; + if (aPrefixes && aPrefixes.indexOf(aStr[0]) != -1) + str = str.slice(1); + + return str.replace(/[\x41-\x5E]/g, + function(c) String.fromCharCode(c.charCodeAt(0) + 0x20)); + }, + + isMUCName: function(aStr) { + return (this.channelPrefixes.indexOf(aStr[0]) != -1); + }, + + // Tell the server about status changes. IRC is only away or not away; + // consider the away, idle and unavailable status type to be away. + isAway: false, + observe: function(aSubject, aTopic, aData) { + if (aTopic != "status-changed") + return; + + let {statusType: type, statusText: text} = this.imAccount.statusInfo; + DEBUG("New status received:\ntype = " + type + "\ntext = " + text); + + // Tell the server to mark us as away. + if (type < Ci.imIStatusInfo.STATUS_AVAILABLE) { + // We have to have a string in order to set IRC as AWAY. + if (!text) { + // If no status is given, use the the default idle/away message. + const IDLE_PREF_BRANCH = "messenger.status."; + const IDLE_PREF = "defaultIdleAwayMessage"; + text = Services.prefs.getComplexValue(IDLE_PREF_BRANCH + IDLE_PREF, + Ci.nsIPrefLocalizedString).data; + + if (!text) { + // Get the default value of the localized preference. + text = Services.prefs.getDefaultBranch(IDLE_PREF_BRANCH) + .getComplexValue(IDLE_PREF, + Ci.nsIPrefLocalizedString).data; + } + // The last resort, fallback to a non-localized string. + if (!text) + text = "Away"; + } + this.sendMessage("AWAY", text); // Mark as away. + } + else if (type == Ci.imIStatusInfo.STATUS_AVAILABLE && this.isAway) + this.sendMessage("AWAY"); // Mark as back. + }, + + // The whois information: nicks are used as keys and refer to a map of field + // to value. + whoisInformation: {}, + // Request WHOIS information on a buddy when the user requests more + // information. + requestBuddyInfo: function(aBuddyName) { + if (!this.connected) + return; + + this.removeBuddyInfo(aBuddyName); + this.sendMessage("WHOIS", aBuddyName); + }, + // Request WHOWAS information on a buddy when the user requests more + // information. + requestOfflineBuddyInfo: function(aBuddyName) { + this.removeBuddyInfo(aBuddyName); + this.sendMessage("WHOWAS", aBuddyName); + }, + // Return an nsISimpleEnumerator of imITooltipInfo for a given nick. + getBuddyInfo: function(aNick) { + let nick = this.normalize(aNick); + if (!hasOwnProperty(this.whoisInformation, nick)) + return EmptyEnumerator; + + let whoisInformation = this.whoisInformation[nick]; + if (whoisInformation.serverName && whoisInformation.serverInfo) { + whoisInformation.server = + _("tooltip.serverValue", whoisInformation.serverName, + whoisInformation.serverInfo); + } + + // List of the names of the info to actually show in the tooltip and + // optionally a transform function to apply to the value. Each field here + // maps to tooltip. in irc.properties. + // See the various RPL_WHOIS* results for the options. + let normalizeBool = function(aBool) _(aBool ? "yes" : "no"); + const kFields = { + realname: null, + server: null, + connectedFrom: null, + registered: normalizeBool, + registeredAs: null, + secure: normalizeBool, + away: null, + ircOp: normalizeBool, + idleTime: null, + channels: null + }; + + let tooltipInfo = []; + for (let field in kFields) { + if (whoisInformation.hasOwnProperty(field) && whoisInformation[field]) { + let value = whoisInformation[field]; + if (kFields[field]) + value = kFields[field](value); + tooltipInfo.push(new TooltipInfo(_("tooltip." + field), value)); + } + } + + return new nsSimpleEnumerator(tooltipInfo); + }, + // Remove a WHOIS entry. + removeBuddyInfo: function(aNick) { + let nick = this.normalize(aNick); + if (hasOwnProperty(this.whoisInformation, nick)) + delete this.whoisInformation[nick]; + }, + // Set minimal WHOIS entry containing only the capitalized nick, + // if no WHOIS info exists already. + setWhoisFromNick: function(aNick) { + let nick = this.normalize(aNick); + if (!hasOwnProperty(this.whoisInformation, nick)) + this.whoisInformation[nick] = {"nick": aNick}; + }, + // Copies the fields of aFields into the whois table. If the field already + // exists, that field is ignored (it is assumed that the first server response + // is the most up to date information, as is the case for 312/314). Note that + // the whois info for a nick is reset whenever whois information is requested, + // so the first response from each whois is recorded. + setWhois: function(aNick, aFields) { + let nick = this.normalize(aNick, this.userPrefixes); + // If the nickname isn't in the list yet, add it. + if (!hasOwnProperty(this.whoisInformation, nick)) + this.whoisInformation[nick] = {}; + + // Set non-normalized nickname field. + this.whoisInformation[nick]["nick"] = aNick; + + // Set the WHOIS fields, but only the first time a field is set. + for (let field in aFields) { + if (!this.whoisInformation[nick].hasOwnProperty(field)) + this.whoisInformation[nick][field] = aFields[field]; + } + + return true; + }, + // Write WHOIS information to a conversation. + writeWhois: function(aConv, aNick, aTooltipInfo) { + let nick = this.normalize(aNick); + // RFC 2812 errors 401 and 406 result in there being no entry for the nick. + if (!hasOwnProperty(this.whoisInformation, nick)) { + aConv.writeMessage(null, _("message.unknownNick", nick), {system: true}); + return; + } + // If the nick is offline, tell the user. In that case, it's WHOWAS info. + let msgType = "message.whois"; + if ("offline" in this.whoisInformation[nick]) + msgType = "message.whowas"; + let msg = _(msgType, this.whoisInformation[nick]["nick"]); + while (aTooltipInfo.hasMoreElements()) { + let elt = aTooltipInfo.getNext().QueryInterface(Ci.prplITooltipInfo); + switch (elt.type) { + case Ci.prplITooltipInfo.pair: + case Ci.prplITooltipInfo.sectionHeader: + msg += "\n" + _("message.whoisEntry", elt.label, elt.value); + break; + case Ci.prplITooltipInfo.sectionBreak: + break; + } + } + aConv.writeMessage(null, msg, {system: true}); + }, + + trackBuddy: function(aNick) { + // Put the username as the first to be checked on the next ISON call. + this._isOnQueue.unshift(aNick); + }, + addBuddy: function(aTag, aName) { + let buddy = new ircAccountBuddy(this, null, aTag, aName); + this._buddies[buddy.normalizedName] = buddy; + this.trackBuddy(buddy.userName); + + Services.contacts.accountBuddyAdded(buddy); + }, + // Loads a buddy from the local storage. Called for each buddy locally stored + // before connecting to the server. + loadBuddy: function(aBuddy, aTag) { + let buddy = new ircAccountBuddy(this, aBuddy, aTag); + this._buddies[buddy.normalizedName] = buddy; + this.trackBuddy(buddy.userName); + + return buddy; + }, + hasBuddy: function(aName) + hasOwnProperty(this._buddies, this.normalize(aName, this.userPrefixes)), + // Return an array of buddy names. + getBuddyNames: function() { + let buddies = []; + for each (let buddyName in Object.keys(this._buddies)) + buddies.push(this._buddies[buddyName].userName); + return buddies; + }, + getBuddy: function(aName) { + if (this.hasBuddy(aName)) + return this._buddies[this.normalize(aName, this.userPrefixes)]; + return null; + }, + changeBuddyNick: function(aOldNick, aNewNick) { + let msg; + if (this.normalize(aOldNick) == this.normalize(this._nickname)) { + // Your nickname changed! + this._nickname = aNewNick; + msg = _("message.nick.you", aNewNick); + for each (let conversation in this._conversations) { + // Update the nick for chats, and inform the user in every conversation. + if (conversation.isChat) + conversation.updateNick(aOldNick, aNewNick); + conversation.writeMessage(aOldNick, msg, {system: true}); + } + } + else { + msg = _("message.nick", aOldNick, aNewNick); + for each (let conversation in this._conversations) { + if (conversation.isChat && conversation.hasParticipant(aOldNick)) { + // Update the nick in every chat conversation it is in. + conversation.updateNick(aOldNick, aNewNick); + conversation.writeMessage(aOldNick, msg, {system: true}); + } + } + } + + // Adjust the whois table where necessary. + this.removeBuddyInfo(aOldNick); + this.setWhoisFromNick(aNewNick); + + // If a private conversation is open with that user, change its title. + if (this.hasConversation(aOldNick)) { + // Get the current conversation and rename it. + let conversation = this.getConversation(aOldNick); + + // Remove the old reference to the conversation and create a new one. + this.removeConversation(aOldNick); + this._conversations[this.normalize(aNewNick)] = conversation; + + conversation.updateNick(aNewNick); + conversation.writeMessage(aOldNick, msg, {system: true}); + } + }, + + countBytes: function(aStr) { + // Assume that if it's not UTF-8 then each character is 1 byte. + if (this._encoding != "UTF-8") + return aStr.length; + + // Count the number of bytes in a UTF-8 encoded string. + function charCodeToByteCount(c) { + // UTF-8 stores: + // - code points below U+0080 on 1 byte, + // - code points below U+0800 on 2 bytes, + // - code points U+D800 through U+DFFF are UTF-16 surrogate halves + // (they indicate that JS has split a 4 bytes UTF-8 character + // in two halves of 2 bytes each), + // - other code points on 3 bytes. + return c < 0x80 ? 1 : (c < 0x800 || (c >= 0xD800 && c <= 0xDFFF)) ? 2 : 3; + } + let bytes = 0; + for (let i = 0; i < aStr.length; i++) + bytes += charCodeToByteCount(aStr.charCodeAt(i)); + return bytes; + }, + + // To check if users are online, we need to queue multiple messages. + // An internal queue of all nicks that we wish to know the status of. + _isOnQueue: [], + // The nicks that were last sent to the server that we're waiting for a + // response about. + pendingIsOnQueue: [], + // The time between sending isOn messages (milliseconds). + _isOnDelay: 60 * 1000, + _isOnTimer: null, + // The number of characters that are available to be filled with nicks for + // each ISON message. + _isOnLength: null, + // Generate and send an ISON message to poll for each nick's status. + sendIsOn: function() { + // Add any previously pending queue to the end of the ISON queue. + if (this.pendingIsOnQueue) + this._isOnQueue = this._isOnQueue.concat(this.pendingIsOnQueue); + + // If no buddies, just look again after the timeout. + if (this._isOnQueue.length) { + // Calculate the possible length of names we can send. + if (!this._isOnLength) { + let length = this.countBytes(this.buildMessage("ISON", " ")) + 2; + this._isOnLength = this.maxMessageLength - length + 1; + } + + // Always add the next nickname to the pending queue, this handles a silly + // case where the next nick is greater than or equal to the maximum + // message length. + this.pendingIsOnQueue = [this._isOnQueue.shift()]; + + // Attempt to maximize the characters used in each message, this may mean + // that a specific user gets sent very often since they have a short name! + let buddiesLength = this.countBytes(this.pendingIsOnQueue[0]); + for (let i = 0; i < this._isOnQueue.length; i++) { + // If we can fit the nick, add it to the current buffer. + if ((buddiesLength + this.countBytes(this._isOnQueue[i])) < this._isOnLength) { + // Remove the name from the list and add it to the pending queue. + let nick = this._isOnQueue.splice(i--, 1)[0]; + this.pendingIsOnQueue.push(nick); + + // Keep track of the length of the string, the + 1 is for the spaces. + buddiesLength += this.countBytes(nick) + 1; + + // If we've filled up the message, stop looking for more nicks. + if (buddiesLength >= this._isOnLength) + break; + } + } + + // Send the message. + this.sendMessage("ISON", this.pendingIsOnQueue.join(" ")); + } + + // Call this function again in _isOnDelay seconds. + // This makes the assumption that this._isOnDelay >> the response to ISON + // from the server. + this._isOnTimer = setTimeout(this.sendIsOn.bind(this), this._isOnDelay); + }, + + connect: function() { + this.reportConnecting(); + + // Load preferences. + this._port = this.getInt("port"); + this._ssl = this.getBool("ssl"); + + // Use the display name as the user's real name. + this._realname = this.imAccount.statusInfo.displayName; + this._encoding = this.getString("encoding") || "UTF-8"; + this._showServerTab = this.getBool("showServerTab"); + + // Open the socket connection. + this._socket = new ircSocket(this); + this._socket.connect(this._server, this._port, this._ssl ? ["ssl"] : []); + }, + + // Used to wait for a response from the server. + _quitTimer: null, + // RFC 2812 Section 3.1.7. + quit: function(aMessage) { + this.reportDisconnecting(Ci.prplIAccount.NO_ERROR); + this.sendMessage("QUIT", + aMessage || this.getString("quitmsg") || undefined); + }, + // When the user clicks "Disconnect" in account manager + disconnect: function() { + if (this.disconnected || this.disconnecting) + return; + + this.reportDisconnecting(Ci.prplIAccount.NO_ERROR); + + // If there's no socket, disconnect immediately to avoid waiting 2 seconds. + if (!this._socket || !this._socket.isConnected) { + this.gotDisconnected(); + return; + } + + // Let the server know we're going to disconnect. + this.quit(); + + // Reset original nickname for the next reconnect. + this._requestedNickname = this._accountNickname; + + // Give the server 2 seconds to respond, otherwise just forcefully + // disconnect the socket. This will be cancelled if a response is heard from + // the server. + this._quitTimer = setTimeout(this.gotDisconnected.bind(this), 2 * 1000); + }, + + createConversation: function(aName) this.getConversation(aName), + + // Temporarily stores the prplIChatRoomFieldValues passed to joinChat for + // each channel to enable later reconnections. + _chatRoomFieldsList: {}, + + // aComponents implements prplIChatRoomFieldValues. + joinChat: function(aComponents) { + let channel = aComponents.getValue("channel"); + if (!channel) { + ERROR("joinChat called without a channel name."); + return; + } + let params = [channel]; + let key = aComponents.getValue("password"); + if (key) + params.push(key); + this._chatRoomFieldsList[this.normalize(channel)] = aComponents; + // Send the join command, but don't log the channel key. + this.sendMessage("JOIN", params, + "JOIN " + channel + (key ? " " : "")); + }, + + chatRoomFields: { + "channel": {"label": _("joinChat.channel"), "required": true}, + "password": {"label": _("joinChat.password"), "isPassword": true} + }, + + parseDefaultChatName: function(aDefaultName) { + let params = aDefaultName.trim().split(/\s+/); + let chatFields = {channel: params[0]}; + if (params.length > 1) + chatFields.password = params[1]; + return chatFields; + }, + + // Attributes + get canJoinChat() true, + + hasConversation: function(aConversationName) + hasOwnProperty(this._conversations, this.normalize(aConversationName)), + + // Returns a conversation (creates it if it doesn't exist) + getConversation: function(aName) { + let name = this.normalize(aName); + if (!this.hasConversation(aName)) { + let constructor = this.isMUCName(aName) ? ircChannel : ircConversation; + this._conversations[name] = new constructor(this, aName, this._nickname); + } + return this._conversations[name]; + }, + + removeConversation: function(aConversationName) { + if (this.hasConversation(aConversationName)) + delete this._conversations[this.normalize(aConversationName)]; + }, + + // This builds the message string that will be sent to the server. + buildMessage: function(aCommand, aParams) { + if (!aCommand) { + ERROR("IRC messages must have a command."); + return null; + } + + // Ensure a command is only characters or numbers. + if (!/^[A-Z0-9]+$/i.test(aCommand)) { + ERROR("IRC command invalid: " + aCommand); + return null; + } + + let message = aCommand; + // If aParams is empty, then use an empty array. If aParams is not an array, + // consider it to be a single parameter and put it into an array. + let params = !aParams ? [] : Array.isArray(aParams) ? aParams : [aParams]; + if (params.length) { + if (params.slice(0, -1).some(function(p) p.indexOf(" ") != -1)) { + ERROR("IRC parameters cannot have spaces: " + params.slice(0, -1)); + return null; + } + // Join the parameters with spaces. There are three cases in which the + // last parameter ("trailing" in RFC 2812) must be prepended with a colon: + // 1. If the last parameter contains a space. + // 2. If the first character of the last parameter is a colon. + // 3. If the last parameter is an empty string. + let trailing = params.slice(-1)[0]; + if (!trailing.length || trailing.indexOf(" ") != -1 || trailing[0] == ":") + params.push(":" + params.pop()); + message += " " + params.join(" "); + } + + return message; + }, + + // Shortcut method to build & send a message at once. Use aLoggedData to log + // something different than what is actually sent. + sendMessage: function(aCommand, aParams, aLoggedData) { + this.sendRawMessage(this.buildMessage(aCommand, aParams), aLoggedData); + }, + + // This sends a message over the socket and catches any errors. Use + // aLoggedData to log something different than what is actually sent. + sendRawMessage: function(aMessage, aLoggedData) { + // Low level quoting, replace \0, \n, \r or \020 with \0200, \020n, \020r or + // \020\020, respectively. + const lowQuote = {"\0": "0", "\n": "n", "\r": "r", "\x10": "\x10"}; + const lowRegex = new RegExp("[" + Object.keys(lowQuote).join("") + "]", "g"); + aMessage = aMessage.replace(lowRegex, function(aChar) "\x10" + lowQuote[aChar]); + + if (!this._socket || !this._socket.isConnected) { + this.gotDisconnected(Ci.prplIAccount.ERROR_NETWORK_ERROR, + _("connection.error.lost")); + } + + let length = this.countBytes(aMessage) + 2; + if (length > this.maxMessageLength) { + // Log if the message is too long, but try to send it anyway. + WARN("Message length too long (" + length + " > " + + this.maxMessageLength + "\n" + aMessage); + } + + try { + this._socket.sendString(aMessage, this._encoding, aLoggedData); + } catch (e) { + try { + WARN("Failed to convert " + aMessage + " from Unicode to " + + this._encoding + "."); + this._socket.sendData(aMessage, aLoggedData); + } catch(e) { + ERROR("Socket error: " + e); + this.gotDisconnected(Ci.prplIAccount.ERROR_NETWORK_ERROR, + _("connection.error.lost")); + } + } + }, + + // CTCP messages are \001 []*\001. + sendCTCPMessage: function(aCommand, aParams, aTarget, aIsNotice) { + // Combine the CTCP command and parameters into the single IRC param. + let ircParam = aCommand; + // If aParams is empty, then use an empty array. If aParams is not an array, + // consider it to be a single parameter and put it into an array. + let params = !aParams ? [] : Array.isArray(aParams) ? aParams : [aParams]; + if (params.length) + ircParam += " " + params.join(" "); + + // High/CTCP level quoting, replace \134 or \001 with \134\134 or \134a, + // respectively. This is only done inside the extended data message. + const highQuote = {"\x5C": "\x5C", "\x01": "a"}; + const highRegex = new RegExp("[" + Object.keys(highQuote).join("") + "]", "g"); + ircParam = ircParam.replace(highRegex, function(aChar) "\x5C" + highQuote[aChar]); + + // Add the CTCP tagging. + ircParam = "\x01" + ircParam + "\x01"; + + // Send the IRC message as a NOTICE or PRIVMSG. + this.sendMessage(aIsNotice ? "NOTICE" : "PRIVMSG", [aTarget, ircParam]); + }, + + // Implement section 3.1 of RFC 2812 + _connectionRegistration: function() { + // Send the password message, if provided (section 3.1.1). + if (this.imAccount.password) { + this.sendMessage("PASS", this.imAccount.password, + "PASS "); + } + // Send the nick message (section 3.1.2). + this.sendMessage("NICK", this._requestedNickname); + + // Send the user message (section 3.1.3). + let username; + // Use a custom username in a hidden preference. + if (this.prefs.prefHasUserValue("username")) + username = this.getString("username"); + // But fallback to brandShortName if no username is provided (or is empty). + if (!username) + username = Services.appinfo.name; + this.sendMessage("USER", [username, this._mode.toString(), "*", + this._realname || this._requestedNickname]); + }, + + gotDisconnected: function(aError, aErrorMessage) { + if (!this.imAccount || this.disconnected) + return; + + if (aError === undefined) + aError = Ci.prplIAccount.NO_ERROR; + // If we are already disconnecting, this call to gotDisconnected + // is when the server acknowledges our disconnection. + // Otherwise it's because we lost the connection. + if (!this.disconnecting) + this.reportDisconnecting(aError, aErrorMessage); + this._socket.disconnect(); + delete this._socket; + + clearTimeout(this._isOnTimer); + delete this._isOnTimer; + + // Clean up each conversation: mark as left and remove participant. + for each (let conversation in this._conversations) { + if (conversation.isChat && !conversation.left) { + // Remove the user's nick and mark the conversation as left as that's + // the final known state of the room. + conversation.removeParticipant(this._nickname, true); + conversation.left = true; + } + } + + // Mark all contacts on the account as having an unknown status. + for each (let buddy in this._buddies) + buddy.setStatus(Ci.imIStatusInfo.STATUS_UNKNOWN, ""); + + // Clear whois table. + this.whoisInformation = {}; + + this.reportDisconnected(); + }, + + remove: function() { + for each (let conv in this._conversations) + conv.close(); + delete this._conversations; + for each (let buddy in this._buddies) + buddy.remove(); + delete this._buddies; + }, + + unInit: function() { + // Disconnect if we're online while this gets called. + if (this._socket) { + if (!this.disconnecting) + this.quit(); + this._socket.disconnect(); + } + delete this.imAccount; + clearTimeout(this._isOnTimer); + clearTimeout(this._quitTimer); + } +}; + +function ircProtocol() { + // ircCommands.jsm exports one variable: commands. Import this directly into + // the protocol object. + Cu.import("resource:///modules/ircCommands.jsm", this); + this.registerCommands(); + + // Register the standard handlers. + let tempScope = {}; + Cu.import("resource:///modules/ircBase.jsm", tempScope); + Cu.import("resource:///modules/ircISUPPORT.jsm", tempScope); + Cu.import("resource:///modules/ircCTCP.jsm", tempScope); + Cu.import("resource:///modules/ircDCC.jsm", tempScope); + Cu.import("resource:///modules/ircServices.jsm", tempScope); + + // Extra features. + Cu.import("resource:///modules/ircNonStandard.jsm", tempScope); + Cu.import("resource:///modules/ircWatchMonitor.jsm", tempScope); + + // Register default IRC handlers (IRC base, CTCP). + ircHandlers.registerHandler(tempScope.ircBase); + ircHandlers.registerHandler(tempScope.ircISUPPORT); + ircHandlers.registerHandler(tempScope.ircCTCP); + ircHandlers.registerHandler(tempScope.ircServices); + // Register default ISUPPORT handler (ISUPPORT base). + ircHandlers.registerISUPPORTHandler(tempScope.isupportBase); + // Register default CTCP handlers (CTCP base, DCC). + ircHandlers.registerCTCPHandler(tempScope.ctcpBase); + ircHandlers.registerCTCPHandler(tempScope.ctcpDCC); + // Register default IRC Services handlers (IRC Services base). + ircHandlers.registerServicesHandler(tempScope.servicesBase); + + // Register extra features. + ircHandlers.registerHandler(tempScope.ircNonStandard); + ircHandlers.registerHandler(tempScope.ircWATCH); + ircHandlers.registerISUPPORTHandler(tempScope.isupportWATCH); + ircHandlers.registerHandler(tempScope.ircMONITOR); + ircHandlers.registerISUPPORTHandler(tempScope.isupportMONITOR); +} +ircProtocol.prototype = { + __proto__: GenericProtocolPrototype, + get name() "IRC", + get iconBaseURI() "chrome://prpl-irc/skin/", + get baseId() "prpl-irc", + + usernameSplits: [ + {label: _("options.server"), separator: "@", + defaultValue: "chat.freenode.net", reverse: true} + ], + + options: { + // TODO Default to IRC over SSL. + "port": {label: _("options.port"), default: 6667}, + "ssl": {label: _("options.ssl"), default: false}, + // TODO We should attempt to auto-detect encoding instead. + "encoding": {label: _("options.encoding"), default: "UTF-8"}, + "quitmsg": {label: _("options.quitMessage"), + get default() Services.prefs.getCharPref("chat.irc.defaultQuitMessage")}, + "partmsg": {label: _("options.partMessage"), default: ""}, + "showServerTab": {label: _("options.showServerTab"), default: false} + }, + + get chatHasTopic() true, + get slashCommandsNative() true, + // Passwords in IRC are optional, and are needed for certain functionality. + get passwordOptional() true, + + getAccount: function(aImAccount) new ircAccount(this, aImAccount), + classID: Components.ID("{607b2c0b-9504-483f-ad62-41de09238aec}") +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([ircProtocol]); diff --git a/chat/protocols/irc/irc.manifest b/chat/protocols/irc/irc.manifest new file mode 100644 --- /dev/null +++ b/chat/protocols/irc/irc.manifest @@ -0,0 +1,3 @@ +component {607b2c0b-9504-483f-ad62-41de09238aec} irc.js +contract @mozilla.org/chat/irc;1 {607b2c0b-9504-483f-ad62-41de09238aec} +category im-protocol-plugin prpl-irc @mozilla.org/chat/irc;1 diff --git a/chat/protocols/irc/ircBase.jsm b/chat/protocols/irc/ircBase.jsm new file mode 100644 --- /dev/null +++ b/chat/protocols/irc/ircBase.jsm @@ -0,0 +1,1360 @@ +/* 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/. */ + +/* + * This contains the implementation for the basic Internet Relay Chat (IRC) + * protocol covered by RFCs 2810, 2811, 2812 and 2813 (which obsoletes RFC + * 1459). RFC 2812 covers the client commands and protocol. + * RFC 2810: Internet Relay Chat: Architecture + * http://tools.ietf.org/html/rfc2810 + * RFC 2811: Internet Relay Chat: Channel Management + * http://tools.ietf.org/html/rfc2811 + * RFC 2812: Internet Relay Chat: Client Protocol + * http://tools.ietf.org/html/rfc2812 + * RFC 2813: Internet Relay Chat: Server Protocol + * http://tools.ietf.org/html/rfc2813 + * RFC 1459: Internet Relay Chat Protocol + * http://tools.ietf.org/html/rfc1459 + */ +const EXPORTED_SYMBOLS = ["ircBase"]; + +const {interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource:///modules/imXPCOMUtils.jsm"); +Cu.import("resource:///modules/imServices.jsm"); +Cu.import("resource:///modules/ircHandlers.jsm"); +Cu.import("resource:///modules/ircUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "DownloadUtils", function() { + Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); + return DownloadUtils; +}); + +function privmsg(aAccount, aMessage, aIsNotification) { + let params = {incoming: true}; + if (aIsNotification) + params.notification = true; + aAccount.getConversation(aAccount.isMUCName(aMessage.params[0]) ? + aMessage.params[0] : aMessage.nickname) + .writeMessage(aMessage.nickname || aMessage.servername, + aMessage.params[1], params); + return true; +} + +// Display the message and remove them from the rooms they're in. +function leftRoom(aAccount, aNicks, aChannels, aSource, aReason, aKicked) { + let msgId = "message." + (aKicked ? "kicked" : "parted"); + // If a part message was included, include it. + let reason = aReason ? _(msgId + ".reason", aReason) : ""; + function __(aNick, aYou) { + // If the user is kicked, we need to say who kicked them. + let msgId2 = msgId + (aYou ? ".you" : ""); + if (aKicked) { + if (aYou) + return _(msgId2, aSource, reason); + return _(msgId2, aNick, aSource, reason); + } + if (aYou) + return _(msgId2, reason); + return _(msgId2, aNick, reason); + } + + for each (let channelName in aChannels) { + if (!aAccount.hasConversation(channelName)) + continue; // Handle when we closed the window + let conversation = aAccount.getConversation(channelName); + for each (let nick in aNicks) { + let msg; + if (aAccount.normalize(nick) == aAccount.normalize(aAccount._nickname)) { + msg = __(nick, true); + // If the user left, mark the conversation as no longer being active. + conversation.left = true; + conversation.notifyObservers(conversation, "update-conv-chatleft"); + } + else + msg = __(nick); + + conversation.writeMessage(aSource, msg, {system: true}); + conversation.removeParticipant(nick, true); + } + } + return true; +} + +function writeMessage(aAccount, aMessage, aString, aType) { + let type = {}; + type[aType] = true; + aAccount.getConversation(aMessage.servername) + .writeMessage(aMessage.servername, 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. + if (!aAccount._showServerTab) + return true; + + return writeMessage(aAccount, aMsg, + aMsg.params.slice(1, aNoLastParam ? -1 : undefined).join(" "), + "system"); +} + +function serverErrorMessage(aAccount, aMessage, aError) { + // If we don't want to show messages from the server, just mark it as handled. + if (!aAccount._showServerTab) + return true; + + return writeMessage(aAccount, aMessage, aError, "error") +} + +function conversationErrorMessage(aAccount, aMessage, aError) { + let conv = aAccount.getConversation(aMessage.params[1]); + conv.writeMessage(aMessage.servername, _(aError, aMessage.params[1]), + {error: true, system: true}); + delete conv._pendingMessage; + return true; +} + +// Try a new nick if the previous tried nick is already in use. +function tryNewNick(aAccount, aMessage) { + let nickParts = /^(.+?)(\d*)$/.exec(aMessage.params[1]); + let newNick = nickParts[1]; + + // If there was not a digit at the end of the nick, just append 1. + let newDigits = "1"; + // If there was a digit at the end of the nick, increment it. + if (nickParts[2]) { + newDigits = (parseInt(nickParts[2], 10) + 1).toString(); + // If there were leading 0s, add them back on, after we've incremented (e.g. + // 009 --> 010). + for (let len = nickParts[2].length - newDigits.length; len > 0; --len) + newDigits = "0" + newDigits; + } + // If the nick will be too long, ensure all the digits fit. + if (newNick.length + newDigits.length > aAccount.maxNicknameLength) { + // Handle the silly case of a single letter followed by all nines. + if (newDigits.length == aAccount.maxNicknameLength) + newDigits = newDigits.slice(1); + newNick = newNick.slice(0, aAccount.maxNicknameLength - newDigits.length); + } + // Append the digits. + newNick += newDigits; + + LOG(aMessage.params[1] + " is already in use, trying " + newNick); + aAccount.sendMessage("NICK", newNick); // Nick message. + return true; +} + +// See RFCs 2811 & 2812 (which obsoletes RFC 1459) for a description of these +// commands. +var ircBase = { + // Parameters + name: "RFC 2812", // Name identifier + priority: ircHandlers.DEFAULT_PRIORITY, + isEnabled: function() true, + + // The IRC commands that can be handled. + commands: { + "ERROR": function(aMessage) { + // ERROR + // Client connection has been terminated. + if (!this.disconnecting) { + // We received an ERROR message when we weren't expecting it, this is + // probably the server giving us a ping timeout. + ERROR("Received unexpected ERROR response:\n" + aMessage.params[0]); + this.gotDisconnected(Ci.prplIAccount.ERROR_NETWORK_ERROR, + _("connection.error.lost")); + } + else { + // We received an ERROR message when expecting it (i.e. we've sent a + // QUIT command). + clearTimeout(this._quitTimer); + delete this._quitTimer; + // Notify account manager. + this.gotDisconnected(); + } + return true; + }, + "INVITE": function(aMessage) { + // INVITE + if (Services.prefs.getIntPref("messenger.conversations.autoAcceptChatInvitations") == 1) { + // Auto-accept the invite. + this.joinChat(this.getChatRoomDefaultFieldValues(aMessage.params[1])); + LOG("Received invite for " + aMessage.params[1] + ", auto-accepting."); + } + // Otherwise, just notify the user. + this.getConversation(aMessage.params[1]) + .writeMessage(aMessage.nickname, + _("message.inviteReceived", aMessage.nickname, + aMessage.params[1]), {system: true}); + return true; + }, + "JOIN": function(aMessage) { + // JOIN ( *( "," ) [ *( "," ) ] ) / "0" + // Add the buddy to each channel + for each (let channelName in aMessage.params[0].split(",")) { + let convAlreadyExists = this.hasConversation(channelName); + let conversation = this.getConversation(channelName); + if (this.normalize(aMessage.nickname, this.userPrefixes) == + this.normalize(this._nickname)) { + // If you join, clear the participants list to avoid errors with + // repeated participants. + conversation.removeAllParticipants(); + conversation.left = false; + conversation.notifyObservers(conversation, "update-conv-chatleft"); + + // If the user parted from this room earlier, confirm the rejoin. + // If conversation._chatRoomFields is present, the rejoin was due to + // an automatic reconnection, for which we already notify the user. + if (convAlreadyExists && !conversation._chatRoomFields) { + conversation.writeMessage(aMessage.nickname, _("message.rejoined"), + {system: true}); + } + + // Ensure chatRoomFields information is available for reconnection. + let nName = this.normalize(channelName); + if (hasOwnProperty(this._chatRoomFieldsList, nName)) { + conversation._chatRoomFields = this._chatRoomFieldsList[nName]; + delete this._chatRoomFieldsList[nName]; + } + else { + WARN("Opening a MUC without storing its prplIChatRoomFieldValues first."); + conversation._chatRoomFields = + this.getChatRoomDefaultFieldValues(channelName); + } + } + else { + // Don't worry about adding ourself, RPL_NAMES takes care of that + // case. + conversation.getParticipant(aMessage.nickname, true); + let msg = _("message.join", aMessage.nickname, aMessage.source || ""); + conversation.writeMessage(aMessage.nickname, msg, {system: true, + noLinkification: true}); + } + } + // If the joiner is a buddy, mark as online. + let buddy = this.getBuddy(aMessage.nickname); + if (buddy) + buddy.setStatus(Ci.imIStatusInfo.STATUS_AVAILABLE, ""); + return true; + }, + "KICK": function(aMessage) { + // KICK *( "," ) *( "," ) [] + let comment = aMessage.params.length == 3 ? aMessage.params[2] : null; + // Some servers (moznet) send the kicker as the comment. + if (comment == aMessage.nickname) + comment = null; + return leftRoom(this, aMessage.params[1].split(","), + aMessage.params[0].split(","), aMessage.nickname, + comment, true); + }, + "MODE": function(aMessage) { + // MODE *( ( "+" / "-") *( "i" / "w" / "o" / "O" / "r" ) ) + // MODE *( ( "-" / "+" ) * * ) + if (this.isMUCName(aMessage.params[0])) { + // If the first parameter is a channel name, a channel/participant mode + // was updated. + this.getConversation(aMessage.params[0]) + .setMode(aMessage.params[1], aMessage.params.slice(2), + aMessage.nickname || aMessage.servername); + + return true; + } + + // Otherwise the user's own mode is being returned to them. + // TODO + return false; + }, + "NICK": function(aMessage) { + // NICK + this.changeBuddyNick(aMessage.nickname, aMessage.params[0]); + return true; + }, + "NOTICE": function(aMessage) { + // NOTICE + // If the message doesn't have a nickname, it's from the server, don't + // show it unless the user wants to see it. + if (!aMessage.hasOwnProperty("nickname")) + return serverMessage(this, aMessage); + return privmsg(this, aMessage, true); + }, + "PART": function(aMessage) { + // PART *( "," ) [ ] + return leftRoom(this, [aMessage.nickname], aMessage.params[0].split(","), + aMessage.source || "", + aMessage.params.length == 2 ? aMessage.params[1] : null); + }, + "PING": function(aMessage) { + // PING ] + // Keep the connection alive. + this.sendMessage("PONG", aMessage.params[0]); + return true; + }, + "PRIVMSG": function(aMessage) { + // PRIVMSG + // Display message in conversation + return privmsg(this, aMessage); + }, + "QUIT": function(aMessage) { + // QUIT [ < Quit Message> ] + // Some IRC servers automatically prefix a "Quit: " string. Remove the + // duplication and use a localized version. + let quitMsg = aMessage.params[0] || ""; + if (quitMsg.indexOf("Quit: ") == 0) + quitMsg = quitMsg.slice(6); // "Quit: ".length + // If a quit message was included, show it. + let msg = _("message.quit", aMessage.nickname, + quitMsg.length ? _("message.quit2", quitMsg) : ""); + // Loop over every conversation with the user and display that they quit. + for each (let conversation in this._conversations) { + if (conversation.isChat && + conversation.hasParticipant(aMessage.nickname)) { + conversation.writeMessage(aMessage.servername, msg, {system: true}); + conversation.removeParticipant(aMessage.nickname, true); + } + } + + // Remove from the whois table. + this.removeBuddyInfo(aMessage.nickname); + + // If the leaver is a buddy, mark as offline. + let buddy = this.getBuddy(aMessage.nickname); + if (buddy) + buddy.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, ""); + return true; + }, + "SQUIT": function(aMessage) { + // + return true; + }, + "TOPIC": function(aMessage) { + // TOPIC [ ] + // Show topic as a message. + let source = aMessage.nickname || aMessage.servername; + let conversation = this.getConversation(aMessage.params[0]); + let topic = aMessage.params[1]; + // Set the topic in the conversation and update the UI. + conversation.setTopic(topic ? ctcpFormatToText(topic) : "", source); + return true; + }, + "001": function(aMessage) { // RPL_WELCOME + // Welcome to the Internet Relay Network !@ + this.reportConnected(); + // Check if our nick has changed. + if (aMessage.params[0] != this._nickname) + this.changeBuddyNick(this._nickname, aMessage.params[0]); + // Get our full prefix. + this.prefix = aMessage.params[1].slice( + aMessage.params[1].lastIndexOf(" ") + 1); + // Remove the nick from the prefix. + this.prefix = this.prefix.slice(this.prefix.indexOf("!")); + // If our status is Unavailable, tell the server. + if (this.imAccount.statusInfo.statusType < Ci.imIStatusInfo.STATUS_AVAILABLE) + this.observe(null, "status-changed"); + // Check if any of our buddies are online! + this.sendIsOn(); + // Reconnect channels if they were not parted by the user. + for each (let conversation in this._conversations) { + if (conversation.isChat && conversation._chatRoomFields) + this.joinChat(conversation._chatRoomFields); + } + return serverMessage(this, aMessage); + }, + "002": function(aMessage) { // RPL_YOURHOST + // Your host is , running version + return serverMessage(this, aMessage); + }, + "003": function(aMessage) { // RPL_CREATED + // This server was created + // TODO parse this date and keep it for some reason? Do we care? + return serverMessage(this, aMessage); + }, + "004": function(aMessage) { // RPL_MYINFO + // + // TODO parse the available modes, let the UI respond and inform the user + return serverMessage(this, aMessage); + }, + "005": function(aMessage) { // RPL_BOUNCE + // Try server , port + return serverMessage(this, aMessage); + }, + + /* + * Handle response to TRACE message + */ + "200": function(aMessage) { // RPL_TRACELINK + // Link + // V + // + return serverMessage(this, aMessage); + }, + "201": function(aMessage) { // RPL_TRACECONNECTING + // Try. + return serverMessage(this, aMessage); + }, + "202": function(aMessage) { // RPL_TRACEHANDSHAKE + // H.S. + return serverMessage(this, aMessage); + }, + "203": function(aMessage) { // RPL_TRACEUNKNOWN + // ???? [] + return serverMessage(this, aMessage); + }, + "204": function(aMessage) { // RPL_TRACEOPERATOR + // Oper + return serverMessage(this, aMessage); + }, + "205": function(aMessage) { // RPL_TRACEUSER + // User + return serverMessage(this, aMessage); + }, + "206": function(aMessage) { // RPL_TRACESERVER + // Serv S C @ + // V + return serverMessage(this, aMessage); + }, + "207": function(aMessage) { // RPL_TRACESERVICE + // Service + return serverMessage(this, aMessage); + }, + "208": function(aMessage) { // RPL_TRACENEWTYPE + // 0 + return serverMessage(this, aMessage); + }, + "209": function(aMessage) { // RPL_TRACECLASS + // Class + return serverMessage(this, aMessage); + }, + "210": function(aMessage) { // RPL_TRACERECONNECTION + // Unused. + return serverMessage(this, aMessage); + }, + + /* + * Handle stats messages. + **/ + "211": function(aMessage) { // RPL_STATSLINKINFO + // + //