diff options
Diffstat (limited to 'comm/chat/components/src/imContacts.sys.mjs')
-rw-r--r-- | comm/chat/components/src/imContacts.sys.mjs | 1809 |
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", +}; |