@jsxc/jsxc
Version:
Real-time XMPP chat application with video calls, file transfer and encrypted communication
255 lines (191 loc) • 7.1 kB
text/typescript
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<boolean> {
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<IContact[]> {
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<void> {
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>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>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);
}
}
}