import ContactProvider from './ContactProvider'; import Log from '@util/Log'; import { ContactSubscription as SUBSCRIPTION, IContact, ContactType } from './Contact.interface'; import JID from './JID'; import Account from './Account'; import Contact from './Contact'; import { IJID } from './JID.interface'; import ContactManager from './ContactManager'; import RoleAllocator from './RoleAllocator'; export default class RosterContactProvider extends ContactProvider { constructor(contactManager: ContactManager, private account: Account) { super(contactManager); account.getConnection().registerHandler( stanza => { this.processUpdateStanza(stanza); return true; }, 'jabber:iq:roster', 'iq', 'set' ); } public getUid() { return 'roster'; } public async add(contact: IContact): Promise { if (contact.getType() !== ContactType.CHAT) { return false; } await this.getService().addContact(contact.getJid(), contact.hasName() ? contact.getName() : undefined); this.registerContact(contact); return true; } public async load(): Promise { let rosterVersion = this.getStorage().getItem('roster', 'version') || ''; try { let rosterService = this.getConnection().getRosterService(); let rosterStanza = await rosterService.getRoster(rosterVersion); return this.processStanza(rosterStanza); } catch (err) { Log.warn(err); } return []; } public createContact(jid: IJID, name?: string): IContact; public createContact(id: string): IContact; public createContact() { let contact = new Contact(this.account, arguments[0], arguments[1]); this.registerContact(contact); return contact; } private registerContact(contact: IContact) { contact.setProvider(this); contact.registerHook('name', displayName => { if (RoleAllocator.get().isMaster()) { this.renameContact(contact.getJid(), displayName, contact.getGroups()); } }); } public deleteContact(jid: IJID): Promise { return this.getService() .removeContact(jid) .then(() => undefined); } private renameContact(jid: IJID, displayName: string, groups: string[]) { this.getService().setDisplayName(jid, displayName, groups); } private getStorage() { return this.account.getStorage(); } private getConnection() { return this.account.getConnection(); } private getService() { return this.getConnection().getRosterService(); } private processStanza(stanzaElement: Element): IContact[] { Log.debug('Load roster', stanzaElement); let storage = this.getStorage(); let stanza = $(stanzaElement); if (stanza.find('query').length === 0) { Log.debug('Use cached roster'); return this.restoreRosterFromCache(); } let cache = []; let contacts: IContact[] = []; let self = this; stanza.find('item').each(function () { let item = $(this); let jid = new JID(item.attr('jid')); let name = item.attr('name') || jid.bare; let subscription = item.attr('subscription'); let groups = item .find('>group') .map((index, groupElement) => $(groupElement).text()) .get(); let contact = self.createContact(jid, name); contact.setSubscription(subscription); contact.setGroups(groups); cache.push(contact.getId()); contacts.push(contact); }); let rosterVersion = $(stanza).find('query').attr('ver'); if (!rosterVersion) { cache = []; } storage.setItem('roster', 'version', rosterVersion); storage.setItem('roster', 'cache', cache); return contacts; } private restoreRosterFromCache(): IContact[] { let storage = this.getStorage(); let cachedRoster = storage.getItem('roster', 'cache') || []; let failedContacts: string[] = []; let contacts: IContact[] = []; for (let id of cachedRoster) { try { let contact = this.createContact(id); contact.clearResources(); contacts.push(contact); } catch (err) { Log.warn('Could not restore contact from cached roster.', err); failedContacts.push(id); } } if (failedContacts.length > 0) { storage.setItem( 'roster', 'cache', cachedRoster.filter(id => failedContacts.indexOf(id) < 0) ); } return contacts; } private processUpdateStanza(stanza: string): boolean { let fromString = $(stanza).attr('from'); let fromJid: IJID; if (fromString) { fromJid = new JID(fromString); } let account = this.account; if (fromJid && fromJid.bare !== account.getJID().bare) { Log.info('Ignore roster change with wrong sender jid.'); return; } Log.debug('Process roster change.'); let itemElement = $(stanza).find('item'); if (itemElement.length !== 1) { Log.info('Ignore roster change with more than one item element.'); return; } let jid = new JID($(itemElement).attr('jid')); let name = $(itemElement).attr('name'); let subscription = $(itemElement).attr('subscription') || 'none'; let groups = $(itemElement) .find('>group') .map((index, groupElement) => $(groupElement).text()) .get(); let contact = account.getContact(jid); if (!contact && subscription === SUBSCRIPTION.REMOVE) { return; } if (!contact) { contact = this.createContact(jid, name); } if (subscription === SUBSCRIPTION.REMOVE) { this.contactManager.deleteFromCache(contact.getId()); contact.delete(); } else { contact.setName(name); contact.setSubscription(subscription); contact.setGroups(groups); this.contactManager.addToCache(contact); } if (subscription === SUBSCRIPTION.FROM || subscription === SUBSCRIPTION.BOTH) { //@TODO Remove pending friendship request from notice list. This can be done via property hook. } let rosterVersion = $(stanza).find('query').attr('ver'); if (rosterVersion) { let storage = account.getStorage(); storage.setItem('roster', 'version', rosterVersion); let cache = storage.getItem('roster', 'cache') || []; if (subscription === SUBSCRIPTION.REMOVE && cache.indexOf(contact.getId()) > -1) { cache = cache.filter(id => id !== contact.getId()); } else if (subscription !== SUBSCRIPTION.REMOVE && cache.indexOf(contact.getId()) < 0) { cache.push(contact.getId()); } storage.setItem('roster', 'cache', cache); } } }