@jsxc/jsxc
Version:
Real-time XMPP chat application with video calls, file transfer and encrypted communication
190 lines (153 loc) • 6.59 kB
text/typescript
import Account from './Account';
import DiscoInfo from './DiscoInfo';
import PersistentMap from './util/PersistentMap';
import JID from './JID';
import Contact from './Contact';
import Log from '@util/Log';
import Client from './Client';
import Form from './connection/Form';
import { IDiscoInfoRepository } from './DiscoInfoRepository.interface';
import { IJID } from './JID.interface';
export default class implements IDiscoInfoRepository {
private jidVersionMap: PersistentMap;
constructor(private account: Account) {
this.jidVersionMap = new PersistentMap(Client.getStorage(), 'capabilities');
}
public addRelation(jid: IJID, version: string): void;
public addRelation(jid: IJID, discoInfo: DiscoInfo): void;
public addRelation(jid: IJID, value: string | DiscoInfo) {
if (value instanceof DiscoInfo) {
this.jidVersionMap.set(jid.full, value.getCapsVersion());
} else if (typeof value === 'string') {
this.jidVersionMap.set(jid.full, value);
}
}
public getDiscoInfo(jid: IJID) {
let version = this.jidVersionMap.get(jid.full);
if (!version) {
throw new Error('Found no disco version');
}
return new DiscoInfo(version);
}
public getCapableResources(contact: Contact, features: string[]): Promise<string[]>;
public getCapableResources(contact: Contact, features: string): Promise<string[]>;
public getCapableResources(contact: Contact, features): Promise<string[]> {
let resources = contact.getResources();
if (!features) {
return Promise.resolve(resources);
}
let promises = [];
for (let resource of resources) {
//@REVIEW
promises.push(
new Promise(resolve => {
let jid = new JID(contact.getJid().bare + '/' + resource);
this.hasFeature(jid, features).then(hasSupport => {
resolve(hasSupport ? resource : undefined);
});
//@REVIEW do we need a timer?
})
);
}
return Promise.all(promises).then(capableResources => {
return capableResources.filter(resource => typeof resource === 'string');
});
}
public hasFeature(jid: IJID, features: string[]): Promise<boolean>;
public hasFeature(jid: IJID, feature: string): Promise<boolean>;
public hasFeature(discoInfo: DiscoInfo, features: string[]): Promise<boolean>;
public hasFeature(discoInfo: DiscoInfo, feature: string): Promise<boolean>;
public hasFeature() {
let features = arguments[1] instanceof Array ? arguments[1] : [arguments[1]];
let capabilitiesPromise;
if (arguments[0] instanceof JID) {
let jid: JID = arguments[0];
capabilitiesPromise = this.getCapabilities(jid);
} else if (arguments[0] instanceof DiscoInfo) {
capabilitiesPromise = Promise.resolve(arguments[0]);
} else if (typeof arguments[0] === 'undefined') {
let serverJid = this.account.getConnection().getServerJID();
capabilitiesPromise = this.getCapabilities(serverJid);
} else {
return Promise.reject('Wrong parameters');
}
return capabilitiesPromise.then((capabilities: DiscoInfo) => {
return capabilities.hasFeature(features);
});
}
public getCapabilities(jid: IJID): Promise<DiscoInfo | void> {
let jidVersionMap = this.jidVersionMap;
let version = jidVersionMap.get(jid.full);
if (!version || !DiscoInfo.exists(version)) {
return this.requestDiscoInfo(jid).then(discoInfo => {
if (version && version !== discoInfo.getCapsVersion()) {
Log.warn(
`Caps version doesn't match for ${
jid.full
}. Expected: ${version}. Actual: ${discoInfo.getCapsVersion()}.`
);
} else if (!version) {
this.addRelation(jid, discoInfo);
}
return discoInfo;
});
}
return new Promise<DiscoInfo>(resolve => {
checkCaps(resolve);
});
function checkCaps(cb) {
let version = jidVersionMap.get(jid.full);
if (version && DiscoInfo.exists(version)) {
cb(new DiscoInfo(version));
} else {
setTimeout(() => {
checkCaps(cb);
}, 200);
}
}
}
public requestDiscoInfo(jid: IJID, node?: string) {
let connection = this.account.getConnection();
//@REVIEW why does the request fail if we send a node attribute?
return connection.getDiscoService().getDiscoInfo(jid).then(this.processDiscoInfo);
}
private processDiscoInfo(stanza: Element) {
let queryElement = $(stanza).find('query');
// let node = queryElement.attr('node') || '';
// let from = new JID($(stanza).attr('from'));
//@TODO verify response is valid: https://xmpp.org/extensions/xep-0115.html#ver-proc
let capabilities: { [name: string]: any } = {};
for (let childNode of Array.from(queryElement.get(0).childNodes)) {
let nodeName = childNode.nodeName.toLowerCase();
if (typeof capabilities[nodeName] === 'undefined') {
capabilities[nodeName] = [];
}
if (nodeName === 'feature') {
capabilities[nodeName].push($(childNode).attr('var'));
} else if (nodeName === 'identity') {
capabilities[nodeName].push({
category: $(childNode).attr('category') || '',
type: $(childNode).attr('type') || '',
name: $(childNode).attr('name') || '',
lang: $(childNode).attr('xml:lang') || '',
});
//@TODO test required arguments
}
//@TODO handle extended information
}
if (typeof capabilities.identity === 'undefined' || capabilities.identity.length === 0) {
return Promise.reject('Disco info response is invalid. Missing identity.');
}
let forms = queryElement
.find('x[xmlns="jabber:x:data"]')
.get()
.map(element => {
return Form.fromXML(element);
});
// if (typeof capabilities['feature'] === 'undefined' || capabilities['feature'].indexOf('http://jabber.org/protocol/disco#info') < 0) {
// return Promise.reject('Disco info response is unvalid. Doesnt support disco.');
// }
let discoInfo = new DiscoInfo(capabilities.identity, capabilities.feature, forms);
return Promise.resolve(discoInfo);
}
}