summaryrefslogtreecommitdiffstats
path: root/comm/chat/components/src/imContacts.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/components/src/imContacts.sys.mjs')
-rw-r--r--comm/chat/components/src/imContacts.sys.mjs1809
1 files changed, 1809 insertions, 0 deletions
diff --git a/comm/chat/components/src/imContacts.sys.mjs b/comm/chat/components/src/imContacts.sys.mjs
new file mode 100644
index 0000000000..c902cf4623
--- /dev/null
+++ b/comm/chat/components/src/imContacts.sys.mjs
@@ -0,0 +1,1809 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { IMServices } from "resource:///modules/IMServices.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import {
+ executeSoon,
+ ClassInfo,
+ l10nHelper,
+} from "resource:///modules/imXPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "_", () =>
+ l10nHelper("chrome://chat/locale/contacts.properties")
+);
+
+var gDBConnection = null;
+
+function executeAsyncThenFinalize(statement) {
+ statement.executeAsync();
+ statement.finalize();
+}
+
+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 = Services.storage.openDatabase(dbFile);
+ if (!conn.connectionReady) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ // Grow blist db in 512KB increments.
+ try {
+ conn.setGrowthIncrement(512 * 1024, "");
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_FILE_TOO_BIG) {
+ Services.console.logStringMessage(
+ "Not setting growth increment on " +
+ "blist.sqlite because the available " +
+ "disk space is limited"
+ );
+ } else {
+ throw e;
+ }
+ }
+
+ // 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
+// committed 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;
+Object.defineProperty(lazy, "DBConn", {
+ configurable: true,
+ enumerable: true,
+
+ get() {
+ if (gDBConnWithPendingTransaction) {
+ return gDBConnWithPendingTransaction;
+ }
+
+ if (!gDBConnection) {
+ gDBConnection = getDBConnection();
+ Services.obs.addObserver(function dbClose(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(dbClose, aTopic);
+ if (gDBConnection) {
+ gDBConnection.asyncClose();
+ gDBConnection = null;
+ }
+ }, "profile-before-change");
+ }
+ gDBConnWithPendingTransaction = gDBConnection;
+ gDBConnection.beginTransaction();
+ executeSoon(function () {
+ gDBConnWithPendingTransaction.commitTransaction();
+ gDBConnWithPendingTransaction = null;
+ });
+ return gDBConnection;
+ },
+});
+
+export function TagsService() {}
+TagsService.prototype = {
+ get wrappedJSObject() {
+ return this;
+ },
+ get defaultTag() {
+ return this.createTag(lazy._("defaultGroup"));
+ },
+ createTag(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 = lazy.DBConn.createStatement(
+ "INSERT INTO tags (name, position) VALUES(:name, 0)"
+ );
+ try {
+ statement.params.name = aName;
+ statement.executeStep();
+ } finally {
+ statement.finalize();
+ }
+
+ tag = new Tag(lazy.DBConn.lastInsertRowID, aName);
+ Tags.push(tag);
+ return tag;
+ },
+ // Get an existing tag by (numeric) id. Returns null if not found.
+ getTagById: aId => TagsById[aId],
+ // Get an existing tag by name (will do an SQL query). Returns null
+ // if not found.
+ getTagByName(aName) {
+ let statement = lazy.DBConn.createStatement(
+ "SELECT id FROM tags where name = :name"
+ );
+ statement.params.name = aName;
+ try {
+ if (!statement.executeStep()) {
+ return null;
+ }
+ return this.getTagById(statement.row.id);
+ } finally {
+ statement.finalize();
+ }
+ },
+ // Get an array of all existing tags.
+ getTags() {
+ if (Tags.length) {
+ Tags.sort((a, b) =>
+ a.name.toLowerCase().localeCompare(b.name.toLowerCase())
+ );
+ } else {
+ this.defaultTag;
+ }
+
+ return Tags;
+ },
+
+ isTagHidden: aTag => aTag.id in otherContactsTag._hiddenTags,
+ hideTag(aTag) {
+ otherContactsTag.hideTag(aTag);
+ },
+ showTag(aTag) {
+ otherContactsTag.showTag(aTag);
+ },
+ get otherContactsTag() {
+ otherContactsTag._initContacts();
+ return otherContactsTag;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["imITagsService"]),
+ classDescription: "Tags",
+};
+
+// 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 = {
+ __proto__: ClassInfo("imITag", "Tag"),
+ get id() {
+ return this._id;
+ },
+ get name() {
+ return this._name;
+ },
+ set name(aNewName) {
+ let statement = lazy.DBConn.createStatement(
+ "UPDATE tags SET name = :name WHERE id = :id"
+ );
+ try {
+ statement.params.name = aNewName;
+ statement.params.id = this._id;
+ statement.execute();
+ } finally {
+ statement.finalize();
+ }
+
+ // FIXME move the account buddies if some use this tag as their group
+ },
+ getContacts() {
+ return this._contacts.filter(c => !c._empty);
+ },
+ _addContact(aContact) {
+ this._contacts.push(aContact);
+ },
+ _removeContact(aContact) {
+ let index = this._contacts.indexOf(aContact);
+ if (index != -1) {
+ this._contacts.splice(index, 1);
+ }
+ },
+
+ addObserver(aObserver) {
+ if (!this._observers.includes(aObserver)) {
+ this._observers.push(aObserver);
+ }
+ },
+ removeObserver(aObserver) {
+ this._observers = this._observers.filter(o => o !== aObserver);
+ },
+ notifyObservers(aSubject, aTopic, aData) {
+ for (let observer of this._observers) {
+ observer.observe(aSubject, aTopic, aData);
+ }
+ },
+};
+
+var otherContactsTag = {
+ __proto__: ClassInfo(["nsIObserver", "imITag"], "Other Contacts Tag"),
+ hiddenTagsPref: "messenger.buddies.hiddenTags",
+ _hiddenTags: {},
+ _contactsInitialized: false,
+ _saveHiddenTagsPref() {
+ Services.prefs.setCharPref(
+ this.hiddenTagsPref,
+ Object.keys(this._hiddenTags).join(",")
+ );
+ },
+ showTag(aTag) {
+ let id = aTag.id;
+ delete this._hiddenTags[id];
+ let contacts = Object.keys(this._contacts).map(id => this._contacts[id]);
+ for (let contact of contacts) {
+ if (contact.getTags().some(t => t.id == id)) {
+ this._removeContact(contact);
+ }
+ }
+
+ aTag.notifyObservers(aTag, "tag-shown");
+ Services.obs.notifyObservers(aTag, "tag-shown");
+ this._saveHiddenTagsPref();
+ },
+ hideTag(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");
+ Services.obs.notifyObservers(aTag, "tag-hidden");
+ this._saveHiddenTagsPref();
+ },
+ _hideTag(aTag) {
+ for (let contact of aTag.getContacts()) {
+ if (
+ !(contact.id in this._contacts) &&
+ contact.getTags().every(t => t.id in this._hiddenTags)
+ ) {
+ this._addContact(contact);
+ }
+ }
+ },
+ observe(aSubject, aTopic, aData) {
+ aSubject.QueryInterface(Ci.imIContact);
+ if (aTopic == "contact-tag-removed" || aTopic == "contact-added") {
+ if (
+ !(aSubject.id in this._contacts) &&
+ !(parseInt(aData) in this._hiddenTags) &&
+ aSubject.getTags().every(t => t.id in this._hiddenTags)
+ ) {
+ 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() {
+ let pref = Services.prefs.getCharPref(this.hiddenTagsPref);
+ if (!pref) {
+ return;
+ }
+ for (let tagId of pref.split(",")) {
+ this._hiddenTags[tagId] = TagsById[tagId];
+ }
+ },
+ _initContacts() {
+ if (this._contactsInitialized) {
+ return;
+ }
+ this._observers = [];
+ this._observer = {
+ self: this,
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "contact-moved-in" && !(aSubject instanceof Contact)) {
+ return;
+ }
+
+ this.self.notifyObservers(aSubject, aTopic, aData);
+ },
+ };
+ this._contacts = {};
+ this._contactsInitialized = true;
+ for (let id in this._hiddenTags) {
+ let tag = this._hiddenTags[id];
+ this._hideTag(tag);
+ }
+ Services.obs.addObserver(this, "contact-tag-added");
+ Services.obs.addObserver(this, "contact-tag-removed");
+ Services.obs.addObserver(this, "contact-added");
+ Services.obs.addObserver(this, "contact-removed");
+ },
+
+ // imITag implementation
+ get id() {
+ return -1;
+ },
+ get name() {
+ return "__others__";
+ },
+ set name(aNewName) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ },
+ getContacts() {
+ return Object.keys(this._contacts).map(id => this._contacts[id]);
+ },
+ _addContact(aContact) {
+ this._contacts[aContact.id] = aContact;
+ this.notifyObservers(aContact, "contact-moved-in");
+ for (let observer of ContactsById[aContact.id]._observers) {
+ observer.observe(this, "contact-moved-in", null);
+ }
+ aContact.addObserver(this._observer);
+ },
+ _removeContact(aContact) {
+ delete this._contacts[aContact.id];
+ aContact.removeObserver(this._observer);
+ this.notifyObservers(aContact, "contact-moved-out");
+ for (let observer of ContactsById[aContact.id]._observers) {
+ observer.observe(this, "contact-moved-out", null);
+ }
+ },
+
+ addObserver(aObserver) {
+ if (!this._observers.includes(aObserver)) {
+ this._observers.push(aObserver);
+ }
+ },
+ removeObserver(aObserver) {
+ this._observers = this._observers.filter(o => o !== aObserver);
+ },
+ notifyObservers(aSubject, aTopic, aData) {
+ for (let observer of this._observers) {
+ observer.observe(aSubject, aTopic, aData);
+ }
+ },
+};
+
+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 = {
+ __proto__: ClassInfo("imIContact", "Contact"),
+ _id: 0,
+ get id() {
+ return this._id;
+ },
+ get alias() {
+ return this._alias;
+ },
+ set alias(aNewAlias) {
+ this._ensureNotDummy();
+
+ let statement = lazy.DBConn.createStatement(
+ "UPDATE contacts SET alias = :alias WHERE id = :id"
+ );
+ statement.params.alias = aNewAlias;
+ statement.params.id = this._id;
+ executeAsyncThenFinalize(statement);
+
+ let oldDisplayName = this.displayName;
+ this._alias = aNewAlias;
+ this._notifyObservers("display-name-changed", oldDisplayName);
+ for (let buddy of this._buddies) {
+ for (let accountBuddy of buddy._accounts) {
+ accountBuddy.serverAlias = aNewAlias;
+ }
+ }
+ },
+ _ensureNotDummy() {
+ if (this._id >= 0) {
+ return;
+ }
+
+ // Create a real contact for this dummy contact
+ let statement = lazy.DBConn.createStatement(
+ "INSERT INTO contacts DEFAULT VALUES"
+ );
+ try {
+ statement.execute();
+ } finally {
+ statement.finalize();
+ }
+ delete ContactsById[this._id];
+ let oldId = this._id;
+ this._id = lazy.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 = lazy.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;
+ executeAsyncThenFinalize(statement);
+ },
+
+ getTags() {
+ return this._tags;
+ },
+ addTag(aTag, aInherited) {
+ if (this.hasTag(aTag)) {
+ return;
+ }
+
+ if (!aInherited) {
+ this._ensureNotDummy();
+ let statement = lazy.DBConn.createStatement(
+ "INSERT INTO contact_tag (contact_id, tag_id) " +
+ "VALUES(:contactId, :tagId)"
+ );
+ statement.params.contactId = this.id;
+ statement.params.tagId = aTag.id;
+ executeAsyncThenFinalize(statement);
+ }
+
+ aTag = TagsById[aTag.id];
+ this._tags.push(aTag);
+ aTag._addContact(this);
+
+ aTag.notifyObservers(this, "contact-moved-in");
+ for (let observer of 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(aTag) {
+ if (!this.hasTag(aTag) || this._isTagInherited(aTag)) {
+ return;
+ }
+
+ this._removeContactTagRow(aTag);
+
+ this._tags = this._tags.filter(tag => tag.id != aTag.id);
+ aTag = TagsById[aTag.id];
+ aTag._removeContact(this);
+
+ aTag.notifyObservers(this, "contact-moved-out");
+ for (let observer of this._observers) {
+ observer.observe(aTag, "contact-moved-out", null);
+ }
+ Services.obs.notifyObservers(this, "contact-tag-removed", aTag.id);
+ },
+ _removeContactTagRow(aTag) {
+ let statement = lazy.DBConn.createStatement(
+ "DELETE FROM contact_tag " +
+ "WHERE contact_id = :contactId " +
+ "AND tag_id = :tagId"
+ );
+ statement.params.contactId = this.id;
+ statement.params.tagId = aTag.id;
+ executeAsyncThenFinalize(statement);
+ },
+ hasTag(aTag) {
+ return this._tags.some(t => t.id == aTag.id);
+ },
+ _massMove: false,
+ removeTag(aTag) {
+ if (!this.hasTag(aTag)) {
+ throw new Error(
+ "Attempting to remove a tag that the contact doesn't have"
+ );
+ }
+ if (this._tags.length == 1) {
+ throw new Error("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(
+ 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(aTag) {
+ for (let buddy of this._buddies) {
+ for (let accountBuddy of buddy._accounts) {
+ if (accountBuddy.tag.id == aTag.id) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+ _moved(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(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 (let observer of 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 (let observer of this._observers) {
+ observer.observe(aNewTag, "contact-moved-in", null);
+ }
+ Services.obs.notifyObservers(this, "contact-tag-added", aNewTag.id);
+ }
+ Services.obs.notifyObservers(this, "contact-moved");
+ },
+
+ getBuddies() {
+ return this._buddies;
+ },
+ get _empty() {
+ return this._buddies.length == 0 || this._buddies.every(b => b._empty);
+ },
+
+ mergeContact(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.Exception("", Cr.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 (let tag of contact.getTags()) {
+ if (!contact._isTagInherited(tag)) {
+ this.addTag(tag);
+ }
+ }
+
+ // Adopt each buddy. Removing the last one will delete the contact.
+ for (let buddy of contact.getBuddies()) {
+ buddy.contact = this;
+ }
+ this._updatePreferredBuddy();
+ },
+ moveBuddyBefore(aBuddy, aBeforeBuddy) {
+ let buddy = BuddiesById[aBuddy.id]; // remove XPConnect wrapper
+ let oldPosition = this._buddies.indexOf(buddy);
+ if (oldPosition == -1) {
+ throw new Error("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(aBuddy) {
+ if (aBuddy.contact.id == this.id) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let buddy = BuddiesById[aBuddy.id]; // remove XPConnect wrapper
+ buddy.contact = this;
+ this._updatePreferredBuddy(buddy);
+ },
+ _massRemove: false,
+ _removeBuddy(aBuddy) {
+ if (this._buddies.length == 1) {
+ if (this._id > 0) {
+ let statement = lazy.DBConn.createStatement(
+ "DELETE FROM contacts WHERE id = :id"
+ );
+ statement.params.id = this._id;
+ executeAsyncThenFinalize(statement);
+ }
+ this._notifyObservers("removed");
+ delete ContactsById[this._id];
+
+ for (let tag of this._tags) {
+ tag._removeContact(this);
+ }
+ let statement = lazy.DBConn.createStatement(
+ "DELETE FROM contact_tag WHERE contact_id = :id"
+ );
+ statement.params.id = this._id;
+ executeAsyncThenFinalize(statement);
+
+ delete this._tags;
+ delete this._buddies;
+ delete this._observers;
+ } else {
+ let index = this._buddies.indexOf(aBuddy);
+ if (index == -1) {
+ throw new Error("Removing an unknown buddy from contact " + this._id);
+ }
+
+ this._buddies = this._buddies.filter(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(aIndexBegin, aIndexEnd) {
+ if (aIndexEnd === undefined) {
+ aIndexEnd = this._buddies.length - 1;
+ }
+ if (aIndexBegin > aIndexEnd) {
+ throw new Error("_updatePositions: Invalid indexes");
+ }
+
+ let statement = lazy.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();
+ }
+ statement.finalize();
+ },
+
+ detachBuddy(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.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ if (buddy.contact._buddies.length == 1) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ // Save the list of tags, it may be destroyed 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();
+ buddy.contact._notifyObservers("added");
+
+ // The first tag was inherited during the contact setter.
+ // This will copy the remaining tags.
+ for (let tag of tags) {
+ buddy.contact.addTag(tag);
+ }
+
+ return buddy.contact;
+ },
+ remove() {
+ this._massRemove = true;
+ for (let buddy of 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(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 (let buddy of 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() {
+ 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() {
+ return this._alias || this.preferredBuddy.displayName;
+ },
+ get buddyIconFilename() {
+ return this.preferredBuddy.buddyIconFilename;
+ },
+ _statusType: 0,
+ get statusType() {
+ return this._statusType;
+ },
+ get online() {
+ return this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE;
+ },
+ get available() {
+ return this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE;
+ },
+ get idle() {
+ return this.statusType == Ci.imIStatusInfo.STATUS_IDLE;
+ },
+ get mobile() {
+ return this.statusType == Ci.imIStatusInfo.STATUS_MOBILE;
+ },
+ _statusText: "",
+ get statusText() {
+ return this._statusText;
+ },
+ _availabilityDetails: 0,
+ get availabilityDetails() {
+ return this._availabilityDetails;
+ },
+ get canSendMessage() {
+ return this.preferredBuddy.canSendMessage;
+ },
+ // XXX should we list the buddies in the tooltip?
+ getTooltipInfo() {
+ return this.preferredBuddy.getTooltipInfo();
+ },
+ createConversation() {
+ let uiConv = IMServices.conversations.getUIConversationByContactId(this.id);
+ if (uiConv) {
+ return uiConv.target;
+ }
+ return this.preferredBuddy.createConversation();
+ },
+
+ addObserver(aObserver) {
+ if (!this._observers.includes(aObserver)) {
+ this._observers.push(aObserver);
+ }
+ },
+ removeObserver(aObserver) {
+ if (!this.hasOwnProperty("_observers")) {
+ return;
+ }
+
+ this._observers = this._observers.filter(o => o !== aObserver);
+ },
+ // internal calls + calls from add-ons
+ notifyObservers(aSubject, aTopic, aData) {
+ for (let observer of this._observers) {
+ if ("observe" in observer) {
+ // avoid failing on destructed XBL bindings...
+ observer.observe(aSubject, aTopic, aData);
+ }
+ }
+ for (let tag of this._tags) {
+ tag.notifyObservers(aSubject, aTopic, aData);
+ }
+ Services.obs.notifyObservers(aSubject, aTopic, aData);
+ },
+ _notifyObservers(aTopic, aData) {
+ this.notifyObservers(this, "contact-" + aTopic, aData);
+ },
+
+ // This is called by the imIBuddy implementations.
+ _observe(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);
+ }
+ },
+};
+
+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 = {
+ __proto__: ClassInfo("imIBuddy", "Buddy"),
+ get id() {
+ return this._id;
+ },
+ destroy() {
+ for (let ab of this._accounts) {
+ ab.unInit();
+ }
+ delete this._accounts;
+ delete this._observers;
+ delete this._preferredAccount;
+ },
+ get protocol() {
+ return this._accounts[0].account.protocol;
+ },
+ get userName() {
+ return this._name;
+ },
+ get normalizedName() {
+ return this._key;
+ },
+ _srvAlias: "",
+ _contact: null,
+ get contact() {
+ return this._contact;
+ },
+ set contact(aContact) /* not in imIBuddy */ {
+ if (aContact.id == this._contact.id) {
+ throw Components.Exception("", Cr.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 (let accountBuddy of this._accounts) {
+ this._contact.addTag(TagsById[accountBuddy.tag.id], true);
+ }
+
+ let statement = lazy.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;
+ executeAsyncThenFinalize(statement);
+
+ this._notifyObservers("moved-into-contact");
+ },
+ _hasAccountBuddy(aAccountId, aTagId) {
+ for (let ab of this._accounts) {
+ if (ab.account.numericId == aAccountId && ab.tag.id == aTagId) {
+ return true;
+ }
+ }
+ return false;
+ },
+ getAccountBuddies() {
+ return this._accounts;
+ },
+
+ _addAccount(aAccountBuddy, aTag) {
+ this._accounts.push(aAccountBuddy);
+ let contact = this._contact;
+ if (!this._contact._tags.includes(aTag)) {
+ this._contact._tags.push(aTag);
+ aTag._addContact(contact);
+ }
+
+ if (!this._preferredAccount) {
+ this._preferredAccount = aAccountBuddy;
+ }
+ },
+ get _empty() {
+ return this._accounts.length == 0;
+ },
+
+ remove() {
+ for (let account of this._accounts) {
+ account.remove();
+ }
+ },
+
+ // imIStatusInfo implementation
+ _preferredAccount: null,
+ get preferredAccountBuddy() {
+ return this._preferredAccount;
+ },
+ _isPreferredAccount(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(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 (let account of 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() {
+ 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() {
+ return (
+ (this._preferredAccount && this._preferredAccount.displayName) ||
+ this._srvAlias ||
+ this._name
+ );
+ },
+ get buddyIconFilename() {
+ return this._preferredAccount.buddyIconFilename;
+ },
+ _statusType: 0,
+ get statusType() {
+ return this._statusType;
+ },
+ get online() {
+ return this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE;
+ },
+ get available() {
+ return this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE;
+ },
+ get idle() {
+ return this.statusType == Ci.imIStatusInfo.STATUS_IDLE;
+ },
+ get mobile() {
+ return this.statusType == Ci.imIStatusInfo.STATUS_MOBILE;
+ },
+ _statusText: "",
+ get statusText() {
+ return this._statusText;
+ },
+ _availabilityDetails: 0,
+ get availabilityDetails() {
+ return this._availabilityDetails;
+ },
+ get canSendMessage() {
+ return this._preferredAccount.canSendMessage;
+ },
+ // XXX should we list the accounts in the tooltip?
+ getTooltipInfo() {
+ return this._preferredAccount.getTooltipInfo();
+ },
+ createConversation() {
+ return this._preferredAccount.createConversation();
+ },
+
+ addObserver(aObserver) {
+ if (!this._observers.includes(aObserver)) {
+ this._observers.push(aObserver);
+ }
+ },
+ removeObserver(aObserver) {
+ if (!this._observers) {
+ return;
+ }
+ this._observers = this._observers.filter(o => o !== aObserver);
+ },
+ // internal calls + calls from add-ons
+ notifyObservers(aSubject, aTopic, aData) {
+ try {
+ for (let observer of this._observers) {
+ observer.observe(aSubject, aTopic, aData);
+ }
+ this._contact._observe(aSubject, aTopic, aData);
+ } catch (e) {
+ console.error(e);
+ }
+ },
+ _notifyObservers(aTopic, aData) {
+ this.notifyObservers(this, "buddy-" + aTopic, aData);
+ },
+
+ // This is called by the prplIAccountBuddy implementations.
+ observe(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 = lazy.DBConn.createStatement(
+ "UPDATE buddies SET srv_alias = :srvAlias WHERE id = :buddyId"
+ );
+ statement.params.buddyId = this.id;
+ statement.params.srvAlias = this._srvAlias;
+ executeAsyncThenFinalize(statement);
+ 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 = lazy.DBConn.createStatement(
+ "DELETE FROM buddies WHERE id = :id"
+ );
+ try {
+ statement.params.id = this.id;
+ statement.execute();
+ } finally {
+ statement.finalize();
+ }
+ 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;
+ }
+ },
+};
+
+export function ContactsService() {}
+ContactsService.prototype = {
+ initContacts() {
+ let statement = lazy.DBConn.createStatement("SELECT id, name FROM tags");
+ try {
+ while (statement.executeStep()) {
+ Tags.push(new Tag(statement.getInt32(0), statement.getUTF8String(1)));
+ }
+ } finally {
+ statement.finalize();
+ }
+
+ statement = lazy.DBConn.createStatement("SELECT id, alias FROM contacts");
+ try {
+ while (statement.executeStep()) {
+ new Contact(statement.getInt32(0), statement.getUTF8String(1));
+ }
+ } finally {
+ statement.finalize();
+ }
+
+ statement = lazy.DBConn.createStatement(
+ "SELECT contact_id, tag_id FROM contact_tag"
+ );
+ try {
+ while (statement.executeStep()) {
+ let contact = ContactsById[statement.getInt32(0)];
+ let tag = TagsById[statement.getInt32(1)];
+ contact._tags.push(tag);
+ tag._addContact(contact);
+ }
+ } finally {
+ statement.finalize();
+ }
+
+ statement = lazy.DBConn.createStatement(
+ "SELECT id, key, name, srv_alias, contact_id FROM buddies ORDER BY position"
+ );
+ try {
+ 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?
+ }
+ } finally {
+ statement.finalize();
+ }
+
+ statement = lazy.DBConn.createStatement(
+ "SELECT account_id, buddy_id, tag_id FROM account_buddy"
+ );
+ try {
+ while (statement.executeStep()) {
+ let accountId = statement.getInt32(0);
+ let buddyId = statement.getInt32(1);
+ let tagId = statement.getInt32(2);
+
+ let account = IMServices.accounts.getAccountByNumericId(accountId);
+ // If the account was deleted without properly cleaning up the
+ // account_buddy, skip loading this account buddy.
+ if (!account) {
+ continue;
+ }
+
+ if (!BuddiesById.hasOwnProperty(buddyId)) {
+ console.error(
+ "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)) {
+ console.error(
+ "Corrupted database: duplicated account_buddy entry: " +
+ "account_id = " +
+ accountId +
+ ", buddy_id = " +
+ buddyId +
+ ", tag_id = " +
+ tagId
+ );
+ continue;
+ }
+
+ let tag = TagsById[tagId];
+ try {
+ buddy._addAccount(account.loadBuddy(buddy, tag), tag);
+ } catch (e) {
+ console.error(e);
+ dump(e + "\n");
+ }
+ }
+ } finally {
+ statement.finalize();
+ }
+ otherContactsTag._initHiddenTags();
+ },
+ unInitContacts() {
+ Tags = [];
+ TagsById = {};
+ // Avoid shutdown leaks caused by references to native components
+ // implementing prplIAccountBuddy.
+ for (let buddyId in BuddiesById) {
+ let buddy = BuddiesById[buddyId];
+ buddy.destroy();
+ }
+ BuddiesById = {};
+ ContactsById = {};
+ },
+
+ getContactById: aId => ContactsById[aId],
+ // Get an array of all existing contacts.
+ getContacts() {
+ return Object.keys(ContactsById)
+ .filter(id => !ContactsById[id]._empty)
+ .map(id => ContactsById[id]);
+ },
+ getBuddyById: aId => BuddiesById[aId],
+ getBuddyByNameAndProtocol(aNormalizedName, aPrpl) {
+ let statement = lazy.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;
+ try {
+ if (!statement.executeStep()) {
+ return null;
+ }
+ return BuddiesById[statement.row.id];
+ } finally {
+ statement.finalize();
+ }
+ },
+ getAccountBuddyByNameAndAccount(aNormalizedName, aAccount) {
+ let buddy = this.getBuddyByNameAndProtocol(
+ aNormalizedName,
+ aAccount.protocol
+ );
+ if (buddy) {
+ let id = aAccount.id;
+ for (let accountBuddy of buddy.getAccountBuddies()) {
+ if (accountBuddy.account.id == id) {
+ return accountBuddy;
+ }
+ }
+ }
+ return null;
+ },
+
+ accountBuddyAdded(aAccountBuddy) {
+ let account = aAccountBuddy.account;
+ let normalizedName = aAccountBuddy.normalizedName;
+ let buddy = this.getBuddyByNameAndProtocol(
+ normalizedName,
+ account.protocol
+ );
+ if (!buddy) {
+ let statement = lazy.DBConn.createStatement(
+ "INSERT INTO buddies " +
+ "(key, name, srv_alias, position) " +
+ "VALUES(:key, :name, :srvAlias, 0)"
+ );
+ try {
+ 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(
+ lazy.DBConn.lastInsertRowID,
+ normalizedName,
+ name,
+ srvAlias,
+ 0
+ );
+ } finally {
+ statement.finalize();
+ }
+ }
+
+ // Initialize the 'buddy' field of the prplIAccountBuddy 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)) {
+ console.error(
+ "Attempting to store a duplicate account buddy " +
+ normalizedName +
+ ", account id = " +
+ accountId +
+ ", tag id = " +
+ tagId
+ );
+ return;
+ }
+
+ // Store the new account buddy.
+ let statement = lazy.DBConn.createStatement(
+ "INSERT INTO account_buddy " +
+ "(account_id, buddy_id, tag_id) " +
+ "VALUES(:accountId, :buddyId, :tagId)"
+ );
+ try {
+ statement.params.accountId = accountId;
+ statement.params.buddyId = buddy.id;
+ statement.params.tagId = tagId;
+ statement.execute();
+ } finally {
+ statement.finalize();
+ }
+
+ // Fire the notifications.
+ buddy.observe(aAccountBuddy, "account-buddy-added");
+ },
+ accountBuddyRemoved(aAccountBuddy) {
+ let buddy = aAccountBuddy.buddy;
+ let statement = lazy.DBConn.createStatement(
+ "DELETE FROM account_buddy " +
+ "WHERE account_id = :accountId AND " +
+ "buddy_id = :buddyId AND " +
+ "tag_id = :tagId"
+ );
+ try {
+ statement.params.accountId = aAccountBuddy.account.numericId;
+ statement.params.buddyId = buddy.id;
+ statement.params.tagId = aAccountBuddy.tag.id;
+ statement.execute();
+ } finally {
+ statement.finalize();
+ }
+
+ buddy.observe(aAccountBuddy, "account-buddy-removed");
+ },
+
+ accountBuddyMoved(aAccountBuddy, aOldTag, aNewTag) {
+ let buddy = aAccountBuddy.buddy;
+ let statement = lazy.DBConn.createStatement(
+ "UPDATE account_buddy " +
+ "SET tag_id = :newTagId " +
+ "WHERE account_id = :accountId AND " +
+ "buddy_id = :buddyId AND " +
+ "tag_id = :oldTagId"
+ );
+ try {
+ statement.params.accountId = aAccountBuddy.account.numericId;
+ statement.params.buddyId = buddy.id;
+ statement.params.oldTagId = aOldTag.id;
+ statement.params.newTagId = aNewTag.id;
+ statement.execute();
+ } finally {
+ statement.finalize();
+ }
+
+ let contact = ContactsById[buddy.contact.id];
+
+ // aNewTag is now inherited by the contact from an account buddy, so avoid
+ // keeping direct tag <-> contact links in the contact_tag table.
+ contact._removeContactTagRow(aNewTag);
+
+ buddy.observe(aAccountBuddy, "account-buddy-moved");
+ contact._moved(aOldTag, aNewTag);
+ },
+
+ storeAccount(aId, aUserName, aPrplId) {
+ let statement = lazy.DBConn.createStatement(
+ "SELECT name, prpl FROM accounts WHERE id = :id"
+ );
+ statement.params.id = aId;
+ try {
+ if (statement.executeStep()) {
+ if (
+ statement.getUTF8String(0) == aUserName &&
+ statement.getUTF8String(1) == aPrplId
+ ) {
+ // The account is already stored correctly.
+ return;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); // Corrupted database?!?
+ }
+ } finally {
+ statement.finalize();
+ }
+
+ // Actually store the account.
+ statement = lazy.DBConn.createStatement(
+ "INSERT INTO accounts (id, name, prpl) " +
+ "VALUES(:id, :userName, :prplId)"
+ );
+ try {
+ statement.params.id = aId;
+ statement.params.userName = aUserName;
+ statement.params.prplId = aPrplId;
+ statement.execute();
+ } finally {
+ statement.finalize();
+ }
+ },
+ accountIdExists(aId) {
+ let statement = lazy.DBConn.createStatement(
+ "SELECT id FROM accounts WHERE id = :id"
+ );
+ try {
+ statement.params.id = aId;
+ return statement.executeStep();
+ } finally {
+ statement.finalize();
+ }
+ },
+ forgetAccount(aId) {
+ let statement = lazy.DBConn.createStatement(
+ "DELETE FROM accounts WHERE id = :accountId"
+ );
+ try {
+ statement.params.accountId = aId;
+ statement.execute();
+ } finally {
+ statement.finalize();
+ }
+
+ // removing the account from the accounts table is not enough,
+ // we need to remove all the associated account_buddy entries too
+ statement = lazy.DBConn.createStatement(
+ "DELETE FROM account_buddy WHERE account_id = :accountId"
+ );
+ try {
+ statement.params.accountId = aId;
+ statement.execute();
+ } finally {
+ statement.finalize();
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["imIContactsService"]),
+ classDescription: "Contacts",
+};