new file mode 100644
--- /dev/null
+++ b/chat/Makefile.in
@@ -0,0 +1,68 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Florian QUEZE <florian@instantbird.org>
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH = ..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+PROTOCOLS = \
+ facebook \
+ gtalk \
+ irc \
+ twitter \
+ 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
new file mode 100644
--- /dev/null
+++ b/chat/chat-prefs.js
@@ -0,0 +1,93 @@
+// 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);
new file mode 100644
--- /dev/null
+++ b/chat/components/public/Makefile.in
@@ -0,0 +1,65 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+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
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIAccount.idl
@@ -0,0 +1,295 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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.
+ */
+
+[ptr] native GHashTablePtr(GHashTable);
+
+[scriptable, uuid(ea7ab156-d25e-46cc-93ef-29f77b3c0795)]
+interface prplIChatRoomFieldValues: nsISupports {
+ AUTF8String getValue(in AUTF8String aIdentifier);
+ void setValue(in AUTF8String aIdentifier, in AUTF8String aValue);
+
+ [noscript] readonly attribute GHashTablePtr hashTable;
+};
+
+[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);
+
+ /* 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_NEWLINES
+ // No new lines are allowed in outgoing messages.
+ readonly attribute boolean noNewlines;
+
+ // 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.
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIAccountsService.idl
@@ -0,0 +1,97 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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).
+*/
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imICommandsService.idl
@@ -0,0 +1,110 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2011.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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 <name>.
+ // Format: <command name> <parameters>: <help message>
+ // 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"
+%}
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIContactsService.idl
@@ -0,0 +1,269 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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();
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIConversationsService.idl
@@ -0,0 +1,98 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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);
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imICoreService.idl
@@ -0,0 +1,54 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include "nsISupports.idl"
+#include "imIUserStatusInfo.idl"
+#include "nsISimpleEnumerator.idl"
+#include "prplIProtocol.idl"
+
+[scriptable, uuid(205d4b2b-1ccf-4879-9ef1-f08942566151)]
+interface imICoreService: nsISupports {
+ void init();
+ void quit(); // will emit a prpl-quit notification.
+
+ // returns an enumerator on a pplIProtocol array
+ nsISimpleEnumerator getProtocols();
+
+ prplIProtocol getProtocolById(in AUTF8String aProtocolId);
+
+ readonly attribute imIUserStatusInfo globalUserStatus;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imILogger.idl
@@ -0,0 +1,87 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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(ab38c01c-2245-4279-9437-1d6bcc69d556)]
+interface imILogger: nsISupports {
+ imILog getLogFromFile(in nsIFile aFile);
+ nsISimpleEnumerator getLogsForAccountBuddy(in imIAccountBuddy aAccountBuddy);
+ nsISimpleEnumerator getLogsForBuddy(in imIBuddy aBuddy);
+ nsISimpleEnumerator getLogsForContact(in imIContact aContact);
+ nsISimpleEnumerator getLogsForConversation(in prplIConversation aConversation);
+ nsISimpleEnumerator getSystemLogsForAccount(in imIAccount aAccount);
+ nsISimpleEnumerator getSimilarLogs(in imILog aLog);
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIStatusInfo.idl
@@ -0,0 +1,87 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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();
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imITagsService.idl
@@ -0,0 +1,89 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/imIUserStatusInfo.idl
@@ -0,0 +1,76 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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
+ */
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplIConversation.idl
@@ -0,0 +1,179 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+
+#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;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplIMessage.idl
@@ -0,0 +1,111 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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 ACString 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);
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplIPref.idl
@@ -0,0 +1,69 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplIProtocol.idl
@@ -0,0 +1,133 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2007.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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 PR_TRUE if the separator should be found starting at
+ the end of the string, PR_FALSE otherwise. */
+ readonly attribute boolean reverse;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplIRequest.idl
@@ -0,0 +1,69 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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();
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/public/prplITooltipInfo.idl
@@ -0,0 +1,54 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2008.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#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;
+};
new file mode 100644
--- /dev/null
+++ b/chat/components/src/Makefile.in
@@ -0,0 +1,54 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+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
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imAccounts.js
@@ -0,0 +1,982 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2011.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Romain Bezut <romain@bezut.info>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+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.split(","))
+ 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 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() {
+ /* This will be the first connection with these parameters */
+ this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
+
+ if (this._pendingReconnectForConnectionInfoChange)
+ 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;
+ login.init(passwordURI, null, passwordURI, this.normalizedName, "", "", "");
+ let logins = LoginManager.findLogins({}, passwordURI, null, passwordURI);
+ for each (let l in logins) {
+ if (login.matches(l, true)) {
+ LoginManager.removeLogin(l);
+ break;
+ }
+ }
+ 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._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 &&
+ this.connected)
+ this.prplAccount.disconnect();
+ else if (statusType == Ci.imIStatusInfo.STATUS_OFFLINE &&
+ this._reconnectTimer)
+ 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);
+ }
+
+ this._ensurePrplAccount.connect();
+ },
+ disconnect: function() {
+ if (this._statusObserver) {
+ this.statusInfo.removeObserver(this._statusObserver);
+ delete this._statusObserver;
+ }
+ 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 noNewlines() this._ensurePrplAccount.noNewlines,
+ 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;
+ }
+
+ /* If the application was started offline, disable auto-login */
+ if (!Services.io.manageOfflineStatus && Services.io.offline) {
+ this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_START_OFFLINE;
+ 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() {
+ if (Services.io.offline)
+ throw Cr.NS_ERROR_FAILURE;
+
+ 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]);
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}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imCommands.js
@@ -0,0 +1,254 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2011.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+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) {
+ if (!(this._commands.hasOwnProperty(aName)))
+ 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 prplId = aConversation.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.
+ return cmdArray.some(function (aCmd) aCmd.run(args, aConversation));
+ },
+
+ 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]);
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}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imContacts.js
@@ -0,0 +1,1417 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+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.
+ conn.setGrowthIncrement(512 * 1024, "");
+
+ // 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();
+ 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;
+
+ 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();
+
+ 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);
+ },
+ 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();
+
+ buddy.observe(aAccountBuddy, "account-buddy-moved");
+ ContactsById[buddy.contact.id]._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]);
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}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imConversations.js
@@ -0,0 +1,470 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2009.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+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");
+ Services.obs.notifyObservers(this, "ui-conversation-closed", null);
+ 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 (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.
+
+ this.systemMessage(bundle.GetStringFromName("accountDisconnected"));
+ this.notifyObservers(this, "update-buddy-status");
+ },
+ connected: function() {
+ delete this._disconnected;
+ 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];
+ if (aTopic == "new-text")
+ Services.obs.notifyObservers(aSubject, aTopic, aData);
+ this.notifyObservers(aSubject, aTopic, 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];
+ }
+ 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]);
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}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imCore.js
@@ -0,0 +1,384 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2011.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+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-<timestamp(now)>.<aIconFile.extension>
+ 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 = {
+ _initialized: false,
+ globalUserStatus: null,
+ 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();
+
+ 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]);
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}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/logger.js
@@ -0,0 +1,527 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2010.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+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,
+ format: "txt",
+ _init: function cl_init() {
+ let file = getLogFolderForAccount(this._conv.account, true);
+ let name = this._conv.normalizedName;
+ if (this._conv.isChat && this._conv.account.protocol.id != "prpl-twitter")
+ 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));
+ 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
+ }) + "\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, "<br/>").replace(/<br>/gi, "<br/>");
+ 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 + " <AUTO-REPLY>: " + 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;
+ }
+ }
+};
+
+const dummyConversationLog = {
+ 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(aLineInputStream)
+{
+ let line = {value: ""};
+ let more = aLineInputStream.readLine(line);
+
+ if (!line.value)
+ throw "bad log file";
+
+ let data = JSON.parse(line.value);
+ this.name = data.name;
+ this.title = data.title;
+ this._accountName = data.account;
+ this._protocolName = data.protocol;
+
+ this._messages = [];
+ while (more) {
+ more = aLineInputStream.readLine(line);
+ if (!line.value)
+ break;
+ let data = JSON.parse(line.value);
+ this._messages.push(new LogMessage(data, this));
+ }
+}
+LogConversation.prototype = {
+ __proto__: ClassInfo("imILogConversation", "Log conversation object"),
+ get isChat() false,
+ 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;
+ const regexp = /([0-9]{4})-([0-9]{2})-([0-9]{2}).([0-9]{2})([0-9]{2})([0-9]{2})([+-])([0-9]{2})([0-9]{2}).*\.([a-z]+)$/;
+ let r = aFile.leafName.match(regexp);
+ let date = new Date(r[1], r[2] - 1, r[3], r[4], r[5], r[6]);
+ let offset = r[7] * 60 + r[8];
+ if (r[6] == -1)
+ offset *= -1;
+ this.time = date.valueOf() / 1000; // ignore the timezone offset for now (FIXME)
+ this.format = r[10];
+}
+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);
+ return new LogConversation(lis);
+ }
+};
+
+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 Logger() { }
+Logger.prototype = {
+ _enumerateLogs: function logger__enumerateLogs(aAccount, aNormalizedName) {
+ let file = getLogFolderForAccount(aAccount);
+ file.append(aNormalizedName);
+ if (!file.exists())
+ return EmptyEnumerator;
+
+ return new LogEnumerator([file.directoryEntries]);
+ },
+ getLogFromFile: function logger_getLogFromFile(aFile) new Log(aFile),
+ 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) {
+ let name = aConversation.normalizedName;
+ if (aConversation.isChat &&
+ aConversation.account.protocol.id != "prpl-twitter")
+ name += ".chat";
+ return this._enumerateLogs(aConversation.account, name);
+ },
+ getSystemLogsForAccount: function logger_getSystemLogsForAccount(aAccount)
+ this._enumerateLogs(aAccount, ".system"),
+ getSimilarLogs: function(aLog)
+ new LogEnumerator([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]);
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
new file mode 100644
--- /dev/null
+++ b/chat/components/src/smileProtocolHandler.js
@@ -0,0 +1,76 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Instantbird messenging client, released
+ * 2009.
+ *
+ * The Initial Developer of the Original Code is
+ * Florian QUEZE <florian@instantbird.org>.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+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]);
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}
new file mode 100644
--- /dev/null
+++ b/chat/content/Makefile.in
@@ -0,0 +1,44 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is the Mozilla Browser code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 2002
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH = ../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/chat/content/browserRequest.js
@@ -0,0 +1,175 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is oauthorizer.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Shane Caraveo <shanec@mozillamessaging.com>
+ * Florian Quèze <florian@instantbird.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+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);
+}
new file mode 100644
--- /dev/null
+++ b/chat/content/browserRequest.xul
@@ -0,0 +1,75 @@
+<?xml version="1.0"?>
+<!-- ***** BEGIN LICENSE BLOCK *****
+ - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ -
+ - The contents of this file are subject to the Mozilla Public License Version
+ - 1.1 (the "License"); you may not use this file except in compliance with
+ - the License. You may obtain a copy of the License at
+ - http://www.mozilla.org/MPL/
+ -
+ - Software distributed under the License is distributed on an "AS IS" basis,
+ - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ - for the specific language governing rights and limitations under the
+ - License.
+ -
+ - The Original Code is oauthorizer.
+ -
+ - The Initial Developer of the Original Code is Mozilla.
+ - Portions created by the Initial Developer are Copyright (C) 2010
+ - the Initial Developer. All Rights Reserved.
+ -
+ - Contributor(s):
+ - Shane Caraveo <shanec@mozillamessaging.com>
+ - Florian Quèze <florian@instantbird.org>
+ -
+ - Alternatively, the contents of this file may be used under the terms of
+ - either the GNU General Public License Version 2 or later (the "GPL"), or
+ - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ - in which case the provisions of the GPL or the LGPL are applicable instead
+ - of those above. If you wish to allow use of your version of this file only
+ - under the terms of either the GPL or the LGPL, and not to allow others to
+ - use your version of this file under the terms of the MPL, indicate your
+ - decision by deleting the provisions above and replace them with the notice
+ - and other provisions required by the LGPL or the GPL. If you do not delete
+ - the provisions above, a recipient may use your version of this file under
+ - the terms of any one of the MPL, the GPL or the LGPL.
+ -
+ - ***** END LICENSE BLOCK ***** -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://chat/skin/browserRequest.css" type="text/css"?>
+
+<!DOCTYPE window>
+<window id="browserRequest"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ buttons=","
+ onload="loadRequestedUrl()"
+ onclose="reportUserClosed()"
+ title=""
+ width="800"
+ height="500"
+ orient="vertical">
+
+ <script type="application/javascript" src="chrome://chat/content/browserRequest.js"/>
+
+ <keyset id="mainKeyset">
+ <key id="key_close" key="w" modifiers="accel" oncommand="cancelRequest()"/>
+ <key id="key_close2" keycode="VK_ESCAPE" oncommand="cancelRequest()"/>
+ </keyset>
+ <hbox id="header">
+ <image id="headerImage" src="chrome://chat/skin/prpl-generic/icon32.png"/>
+ <vbox>
+ <label id="headerLabel"/>
+ <description id="headerMessage"/>
+ </vbox>
+ </hbox>
+ <browser type="content" disablehistory="true" src="about:blank" id="requestFrame" flex="1"/>
+ <statusbar>
+ <statusbarpanel id="security-display" crop="end" flex="1"/>
+ <statusbarpanel id="security-status" crop="end" collapsed="true"/>
+ <statusbarpanel class="statusbarpanel-progress" collapsed="true" id="statusbar-status">
+ <progressmeter class="progressmeter-statusbar" id="statusbar-icon" mode="normal" value="0"/>
+ </statusbarpanel>
+ <statusbarpanel id="security-button" class="statusbarpanel-iconic"/>
+ </statusbar>
+</window>
new file mode 100644
--- /dev/null
+++ b/chat/content/convbrowser.xml
@@ -0,0 +1,1036 @@
+<?xml version="1.0"?>
+
+<!-- ***** BEGIN LICENSE BLOCK *****
+ - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ -
+ - The contents of this file are subject to the Mozilla Public License Version
+ - 1.1 (the "License"); you may not use this file except in compliance with
+ - the License. You may obtain a copy of the License at
+ - http://www.mozilla.org/MPL/
+ -
+ - Software distributed under the License is distributed on an "AS IS" basis,
+ - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ - for the specific language governing rights and limitations under the
+ - License.
+ -
+ - The Original Code is this file as it was released on March 28, 2001.
+ -
+ - The Initial Developer of the Original Code is
+ - Peter Annema.
+ - Portions created by the Initial Developer are Copyright (C) 2001
+ - the Initial Developer. All Rights Reserved.
+ -
+ - Contributor(s):
+ - Peter Annema <disttsc@bart.nl> (Original Author of <browser>)
+ - Peter Parente <parente@cs.unc.edu>
+ - Christopher Thomas <cst@yecc.com>
+ - Michael Ventnor <m.ventnor@gmail.com>
+ - Florian Queze <florian@instantbird.org>
+ -
+ - Alternatively, the contents of this file may be used under the terms of
+ - either the GNU General Public License Version 2 or later (the "GPL"), or
+ - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ - in which case the provisions of the GPL or the LGPL are applicable instead
+ - of those above. If you wish to allow use of your version of this file only
+ - under the terms of either the GPL or the LGPL, and not to allow others to
+ - use your version of this file under the terms of the MPL, indicate your
+ - decision by deleting the provisions above and replace them with the notice
+ - and other provisions required by the GPL or the LGPL. If you do not delete
+ - the provisions above, a recipient may use your version of this file under
+ - the terms of any one of the MPL, the GPL or the LGPL.
+ -
+ - ***** END LICENSE BLOCK ***** -->
+
+<bindings id="browserBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="browser">
+ <implementation type="application/javascript"
+ implements="nsIAccessibleProvider, nsIDOMEventListener,
+ nsIWebProgressListener, nsIController,
+ nsISelectionListener, nsIObserver">
+ <property name="accessibleType" readonly="true">
+ <getter>
+ <![CDATA[
+ return Components.interfaces.nsIAccessibleProvider.OuterDoc;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="autoscrollEnabled">
+ <getter>
+ <![CDATA[
+ if (this.getAttribute("autoscroll") == "false")
+ return false;
+
+ var enabled = true;
+ try {
+ enabled = Services.prefs.getBoolPref("general.autoScroll");
+ }
+ catch(ex) {
+ }
+
+ return enabled;
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_theme">null</field>
+
+ <property name="theme"
+ readonly="true"
+ onget="return this._theme || (this._theme = getCurrentTheme());;"/>
+
+ <field name="_conv">null</field>
+
+ <field name="_loadState">0</field>
+
+ <method name="init">
+ <parameter name="aConversation"/>
+ <body>
+ <![CDATA[
+ this._conv = aConversation;
+
+ // init is called when the message style preview is
+ // reloaded so we need to reset _theme.
+ this._theme = null;
+
+ // Prevent ongoing asynchronous message display from continuing.
+ this._messageDisplayPending = false;
+
+ // _loadState is 0 while loading conv.html and 1 while
+ // loading the real conversation HTML.
+ this._loadState = 0;
+
+ const URI = "chrome://chat/content/conv.html";
+ const flag = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ this.webNavigation.loadURI(URI, flag, null, null, null);
+ this.addProgressListener(this);
+
+ if (this._scrollingView)
+ this._autoScrollPopup.hidePopup();
+ ]]>
+ </body>
+ </method>
+
+ <property name="currentURI"
+ onget="return this.webNavigation.currentURI;"
+ readonly="true"/>
+
+ <field name="_docShell">null</field>
+
+ <property name="docShell"
+ onget="return this._docShell || (this._docShell = this.boxObject.QueryInterface(Components.interfaces.nsIContainerBoxObject).docShell);"
+ readonly="true"/>
+
+ <field name="_webNavigation">null</field>
+
+ <property name="webNavigation"
+ onget="return this._webNavigation || (this._webNavigation = this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation));"
+ readonly="true"/>
+
+
+ <field name="_fastFind">null</field>
+ <property name="fastFind"
+ readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._fastFind) {
+ if (!("@mozilla.org/typeaheadfind;1" in Components.classes))
+ return null;
+
+ if (!this.docShell)
+ return null;
+
+ this._fastFind = Components.classes["@mozilla.org/typeaheadfind;1"]
+ .createInstance(Components.interfaces.nsITypeAheadFind);
+ this._fastFind.init(this.docShell);
+ }
+ return this._fastFind;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="webProgress"
+ readonly="true"
+ onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);"/>
+
+ <field name="_contentWindow">null</field>
+
+ <property name="contentWindow"
+ readonly="true"
+ onget="return this._contentWindow || (this._contentWindow = this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow));"/>
+
+ <property name="contentDocument"
+ onget="return this.webNavigation.document;"
+ readonly="true"/>
+
+ <property name="markupDocumentViewer"
+ onget="return this.docShell.contentViewer.QueryInterface(Components.interfaces.nsIMarkupDocumentViewer);"
+ readonly="true"/>
+
+ <field name="magicCopyPref" readonly="true">"messenger.conversations.selections.magicCopyEnabled"</field>
+
+ <property name="magicCopyEnabled"
+ onget="return Services.prefs.getBoolPref(this.magicCopyPref);"
+ readonly="true"/>
+
+ <method name="addProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ <![CDATA[
+ this.webProgress.addProgressListener(aListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ <![CDATA[
+ this.webProgress.removeProgressListener(aListener);
+ ]]>
+ </body>
+ </method>
+
+ <field name="mDestroyed">false</field>
+
+ <constructor>
+ <![CDATA[
+ this.addEventListener("scroll", this.browserScroll);
+ this.addEventListener("resize", this.browserResize);
+ Services.prefs.addObserver(this.magicCopyPref, this, false);
+
+ if (!("cleanupImMarkup" in window))
+ Components.utils.import("resource:///modules/imContentSink.jsm");
+ if (!("smileImMarkup" in window))
+ Components.utils.import("resource:///modules/imSmileys.jsm");
+ if (!("getCurrentTheme" in window))
+ Components.utils.import("resource:///modules/imThemes.jsm");
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ this.destroy();
+ ]]>
+ </destructor>
+
+ <!-- This is necessary because the destructor doesn't always get called when
+ we are removed from a tabbrowser. This will be explicitly called by tabbrowser -->
+ <method name="destroy">
+ <body>
+ <![CDATA[
+ if (this.mDestroyed)
+ return;
+ this.mDestroyed = true;
+ this._messageDisplayPending = false;
+
+ if (this.mDragDropHandler)
+ this.mDragDropHandler.detach();
+ this.mDragDropHandler = null;
+
+ this._fastFind = null;
+
+ if (this._autoScrollNeedsCleanup) {
+ // we polluted the global scope, so clean it up
+ this._autoScrollPopup.parentNode.removeChild(this._autoScrollPopup);
+ }
+
+ Services.prefs.removeObserver(this.magicCopyPref, this);
+ if (this.magicCopyEnabled)
+ this.contentWindow.controllers.removeController(this);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_autoScrollEnabled">true</field>
+
+ <method name="_updateAutoScrollEnabled">
+ <body>
+ <![CDATA[
+ // Enable auto-scroll if the scrollbar is at the bottom.
+ let body = this.contentDocument.getElementsByTagName("body")[0];
+ this._autoScrollEnabled =
+ body.scrollHeight <= body.scrollTop + body.clientHeight + 10;
+ return this._autoScrollEnabled;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_scrollToElement">
+ <parameter name="aElt"/>
+ <body>
+ <![CDATA[
+ aElt.scrollIntoView(true);
+ this._scrollingIntoView = true;
+ ]]>
+ </body>
+ </method>
+
+ <field name="_textModifiers">[smileTextNode]</field>
+
+ <method name="addTextModifier">
+ <parameter name="aModifier"/>
+ <body>
+ <![CDATA[
+ if (this._textModifiers.indexOf(aModifier) == -1)
+ this._textModifiers.push(aModifier);
+ ]]>
+ </body>
+ </method>
+
+ <!-- These variables are reset in onStateChange. -->
+ <field name="_lastMessage">null</field>
+ <field name="_lastMessageIsContext">true</field>
+ <field name="_firstNonContextElt">null</field>
+
+ <field name="_pendingMessages">[]</field>
+ <field name="_messageDisplayPending">false</field>
+ <method name="appendMessage">
+ <parameter name="aMsg"/>
+ <parameter name="aContext"/>
+ <body>
+ <![CDATA[
+ this._pendingMessages.push({msg: aMsg, context: aContext});
+ if (this._messageDisplayPending)
+ return;
+ this._messageDisplayPending = true;
+ Services.tm.mainThread.dispatch(this.displayPendingMessages.bind(this),
+ Ci.nsIEventTarget.DISPATCH_NORMAL);
+ ]]>
+ </body>
+ </method>
+
+ <field name="progressBar">null</field>
+ <!-- These variables are reset in onStateChange. -->
+ <field name="_nextPendingMessageIndex">0</field>
+ <field name="_displayPendingMessagesCalls">0</field>
+ <method name="displayPendingMessages">
+ <body>
+ <![CDATA[
+ if (!this._messageDisplayPending)
+ return;
+
+ let max = this._pendingMessages.length;
+ let begin = Date.now();
+ let i;
+ for (i = this._nextPendingMessageIndex; i < max; ++i) {
+ let msg = this._pendingMessages[i];
+ this.displayMessage(msg.msg, msg.context, i + 1 < max);
+ if (Date.now() - begin > 40)
+ break;
+ }
+ if (i < max - 1) {
+ this._nextPendingMessageIndex = i + 1;
+ if (this.progressBar) {
+ // Show progress bar if after the third call (ca. 120ms)
+ // less than half the messages have been displayed.
+ if (++this._displayPendingMessagesCalls > 2 &&
+ max > 2 * this._nextPendingMessageIndex)
+ this.progressBar.hidden = false;
+ this.progressBar.max = max;
+ this.progressBar.value = this._nextPendingMessageIndex;
+ }
+ Services.tm.mainThread.dispatch(this.displayPendingMessages.bind(this),
+ Ci.nsIEventTarget.DISPATCH_NORMAL);
+ return;
+ }
+ this._messageDisplayPending = false;
+ this._pendingMessages = [];
+ this._nextPendingMessageIndex = 0;
+ this._displayPendingMessagesCalls = 0;
+ if (this.progressBar)
+ this.progressBar.hidden = true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="displayMessage">
+ <parameter name="aMsg"/>
+ <parameter name="aContext"/>
+ <parameter name="aNoAutoScroll"/>
+ <body>
+ <![CDATA[
+ let doc = this.contentDocument;
+ let cs = Components.classes["@mozilla.org/txttohtmlconv;1"]
+ .getService(Ci.mozITXTToHTMLConv);
+ /*
+ * kStructPhrase creates tags for plaintext-markup like *bold*,
+ * /italics/, etc. We always use this; the content filter will
+ * filter it out if the user does not want styling.
+ */
+ let csFlags = cs.kStructPhrase;
+ // Automatically find and link freetext URLs
+ if (!aMsg.noLinkification)
+ csFlags |= cs.kURLs;
+
+ let msg = aMsg.originalMessage;
+
+ // The slash of a leading '/me' should not be used to
+ // format as italic, so we remove the '/me' text before
+ // scanning the HTML, and we add it back later.
+ let meRegExp = /^((<[^>]+>)*)\/me /;
+ let me = false;