xmpp.chapril.org-conversejs/src/converse-disco.js

413 lines
19 KiB
JavaScript
Raw Normal View History

// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//
/* This is a Converse.js plugin which add support for XEP-0030: Service Discovery */
/*global Backbone, define, window */
(function (root, factory) {
define(["converse-core", "sizzle"], factory);
}(this, function (converse, sizzle) {
const { Backbone, Promise, Strophe, $iq, b64_sha1, utils, _, f } = converse.env;
converse.plugins.add('converse-disco', {
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
// Promises exposed by this plugin
_converse.api.promises.add('discoInitialized');
_converse.DiscoEntity = Backbone.Model.extend({
/* A Disco Entity is a JID addressable entity that can be queried
* for features.
*
* See XEP-0030: https://xmpp.org/extensions/xep-0030.html
*/
idAttribute: 'jid',
initialize () {
this.waitUntilFeaturesDiscovered = utils.getResolveablePromise();
this.dataforms = new Backbone.Collection();
this.dataforms.browserStorage = new Backbone.BrowserStorage[_converse.storage](
b64_sha1(`converse.dataforms-{this.get('jid')}`)
);
this.features = new Backbone.Collection();
this.features.browserStorage = new Backbone.BrowserStorage[_converse.storage](
b64_sha1(`converse.features-${this.get('jid')}`)
);
this.features.on('add', this.onFeatureAdded, this);
this.identities = new Backbone.Collection();
this.identities.browserStorage = new Backbone.BrowserStorage[_converse.storage](
b64_sha1(`converse.identities-${this.get('jid')}`)
);
this.fetchFeatures();
this.items = new _converse.DiscoEntities();
this.items.browserStorage = new Backbone.BrowserStorage[_converse.storage](
b64_sha1(`converse.disco-items-${this.get('jid')}`)
);
this.items.fetch();
},
getIdentity (category, type) {
/* Returns a Promise which resolves with a map indicating
* whether a given identity is provided.
*
* Parameters:
* (String) category - The identity category
* (String) type - The identity type
*/
const entity = this;
return new Promise((resolve, reject) => {
function fulfillPromise () {
const model = entity.identities.findWhere({
'category': category,
'type': type
});
resolve(model);
}
entity.waitUntilFeaturesDiscovered
.then(fulfillPromise)
.catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
});
},
hasFeature (feature) {
/* Returns a Promise which resolves with a map indicating
* whether a given feature is supported.
*
* Parameters:
* (String) feature - The feature that might be supported.
*/
const entity = this;
return new Promise((resolve, reject) => {
function fulfillPromise () {
if (entity.features.findWhere({'var': feature})) {
resolve(entity);
} else {
resolve();
}
}
entity.waitUntilFeaturesDiscovered
.then(fulfillPromise)
.catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
});
},
onFeatureAdded (feature) {
feature.entity = this;
_converse.emit('serviceDiscovered', feature);
},
fetchFeatures () {
if (this.features.browserStorage.records.length === 0) {
this.queryInfo();
} else {
this.features.fetch({
add: true,
success: () => {
this.waitUntilFeaturesDiscovered.resolve();
this.trigger('featuresDiscovered');
}
});
this.identities.fetch({add: true});
}
},
queryInfo () {
_converse.api.disco.info(this.get('jid'), null, this.onInfo.bind(this));
},
onDiscoItems (stanza) {
_.each(sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"] item`, stanza), (item) => {
if (item.getAttribute("node")) {
// XXX: ignore nodes for now.
// See: https://xmpp.org/extensions/xep-0030.html#items-nodes
return;
}
const jid = item.getAttribute('jid');
if (_.isUndefined(this.items.get(jid))) {
this.items.create({'jid': jid});
}
});
},
queryForItems () {
if (_.isEmpty(this.identities.where({'category': 'server'}))) {
// Don't fetch features and items if this is not a
// server or a conference component.
return;
}
_converse.api.disco.items(this.get('jid'), null, this.onDiscoItems.bind(this));
},
onInfo (stanza) {
_.forEach(stanza.querySelectorAll('identity'), (identity) => {
this.identities.create({
'category': identity.getAttribute('category'),
'type': identity.getAttribute('type'),
'name': identity.getAttribute('name')
});
});
_.each(sizzle(`x[type="result"][xmlns="${Strophe.NS.XFORM}"]`, stanza), (form) => {
const data = {};
_.each(form.querySelectorAll('field'), (field) => {
data[field.getAttribute('var')] = {
'value': _.get(field.querySelector('value'), 'textContent'),
'type': field.getAttribute('type')
};
});
this.dataforms.create(data);
});
if (stanza.querySelector(`feature[var="${Strophe.NS.DISCO_ITEMS}"]`)) {
this.queryForItems();
}
_.forEach(stanza.querySelectorAll('feature'), (feature) => {
this.features.create({
'var': feature.getAttribute('var'),
'from': stanza.getAttribute('from')
});
});
this.waitUntilFeaturesDiscovered.resolve();
this.trigger('featuresDiscovered');
}
});
_converse.DiscoEntities = Backbone.Collection.extend({
model: _converse.DiscoEntity,
fetchEntities () {
return new Promise((resolve, reject) => {
this.fetch({
add: true,
success: resolve,
error () {
reject (new Error("Could not fetch disco entities"));
}
});
});
}
});
function addClientFeatures () {
// See http://xmpp.org/registrar/disco-categories.html
_converse.api.disco.addIdentity('client', 'web', 'Converse.js');
_converse.api.disco.addFeature(Strophe.NS.BOSH);
_converse.api.disco.addFeature(Strophe.NS.CHATSTATES);
_converse.api.disco.addFeature(Strophe.NS.DISCO_INFO);
_converse.api.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
if (_converse.message_carbons) {
_converse.api.disco.addFeature(Strophe.NS.CARBONS);
}
_converse.emit('addClientFeatures');
return this;
}
function initializeDisco () {
addClientFeatures();
_converse.connection.addHandler(onDiscoInfoRequest, Strophe.NS.DISCO_INFO, 'iq', 'get', null, null);
_converse.disco_entities = new _converse.DiscoEntities();
_converse.disco_entities.browserStorage = new Backbone.BrowserStorage[_converse.storage](
b64_sha1(`converse.disco-entities-${_converse.bare_jid}`)
);
_converse.disco_entities.fetchEntities().then((collection) => {
if (collection.length === 0 || !collection.get(_converse.domain)) {
// If we don't have an entity for our own XMPP server,
// create one.
_converse.disco_entities.create({'jid': _converse.domain});
}
_converse.emit('discoInitialized');
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
}
_converse.api.listen.on('reconnected', initializeDisco);
_converse.api.listen.on('connected', initializeDisco);
_converse.api.listen.on('beforeTearDown', () => {
if (_converse.disco_entities) {
_converse.disco_entities.each((entity) => {
entity.features.reset();
entity.features.browserStorage._clear();
});
_converse.disco_entities.reset();
_converse.disco_entities.browserStorage._clear();
}
});
const plugin = this;
plugin._identities = [];
plugin._features = [];
function onDiscoInfoRequest (stanza) {
const node = stanza.getElementsByTagName('query')[0].getAttribute('node');
const attrs = {xmlns: Strophe.NS.DISCO_INFO};
if (node) { attrs.node = node; }
const iqresult = $iq({'type': 'result', 'id': stanza.getAttribute('id')});
const from = stanza.getAttribute('from');
if (from !== null) {
iqresult.attrs({'to': from});
}
_.each(plugin._identities, (identity) => {
const attrs = {
'category': identity.category,
'type': identity.type
};
if (identity.name) {
attrs.name = identity.name;
}
if (identity.lang) {
attrs['xml:lang'] = identity.lang;
}
iqresult.c('identity', attrs).up();
});
_.each(plugin._features, (feature) => {
iqresult.c('feature', {'var': feature}).up();
});
_converse.connection.send(iqresult.tree());
return true;
}
/* We extend the default converse.js API to add methods specific to service discovery */
_.extend(_converse.api, {
'disco': {
'info' (jid, node, callback, errback, timeout) {
const attrs = {xmlns: Strophe.NS.DISCO_INFO};
if (node) {
attrs.node = node;
}
const info = $iq({
'from': _converse.connection.jid,
'to':jid,
'type':'get'
}).c('query', attrs);
_converse.connection.sendIQ(info, callback, errback, timeout);
},
'items' (jid, node, callback, errback, timeout) {
const attrs = {'xmlns': Strophe.NS.DISCO_ITEMS};
if (node) {
attrs.node = node;
}
const items = $iq({
'from': _converse.connection.jid,
'to':jid,
'type':'get'
}).c('query', attrs);
_converse.connection.sendIQ(items, callback, errback, timeout);
},
'entities': {
'get' (entity_jid, create=false) {
return _converse.api.waitUntil('discoInitialized').then(() => {
if (_.isNil(entity_jid)) {
return _converse.disco_entities;
}
const entity = _converse.disco_entities.get(entity_jid);
if (entity || !create) {
return entity;
}
return _converse.disco_entities.create({'jid': entity_jid});
});
}
},
'supports' (feature, entity_jid) {
/* Returns a Promise which resolves with a list containing
* _converse.Entity instances representing the entity
* itself or those items associated with the entity if
* they support the given feature.
*
* Parameters:
* (String) feature - The feature that might be
* supported. In the XML stanza, this is the `var`
* attribute of the `<feature>` element. For
* example: 'http://jabber.org/protocol/muc'
* (String) entity_jid - The JID of the entity
* (and its associated items) which should be queried
*/
if (_.isNil(entity_jid)) {
throw new TypeError('disco.supports: You need to provide an entity JID');
}
return _converse.api.waitUntil('discoInitialized').then(() => {
return new Promise((resolve, reject) => {
_converse.api.disco.entities.get(entity_jid, true).then((entity) => {
entity.waitUntilFeaturesDiscovered.then(() => {
const promises = _.concat(
entity.items.map((item) => item.hasFeature(feature)),
entity.hasFeature(feature)
);
Promise.all(promises).then((result) => {
resolve(f.filter(f.isObject, result));
}).catch(reject);
}).catch(reject);
})
});
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
},
'addIdentity' (category, type, name, lang) {
for (var i=0; i<plugin._identities.length; i++) {
if (plugin._identities[i].category == category &&
plugin._identities[i].type == type &&
plugin._identities[i].name == name &&
plugin._identities[i].lang == lang) {
return false;
}
}
plugin._identities.push({category: category, type: type, name: name, lang: lang});
},
'addFeature' (name) {
for (var i=0; i<plugin._features.length; i++) {
if (plugin._features[i] == name) { return false; }
}
plugin._features.push(name);
},
'getIdentity' (category, type, entity_jid) {
/* Returns a Promise which resolves with a map indicating
* whether an identity with a given type is provided by
* the entity.
*
* Parameters:
* (String) category - The identity category.
* In the XML stanza, this is the `category`
* attribute of the `<identity>` element.
* For example: 'pubsub'
* (String) type - The identity type.
* In the XML stanza, this is the `type`
* attribute of the `<identity>` element.
* For example: 'pep'
* (String) entity_jid - The JID of the entity which might have the identity
*/
return new Promise((resolve, reject) => {
_converse.api.waitUntil('discoInitialized').then(() => {
_converse.api.disco.entities.get(entity_jid, true)
.then((entity) => resolve(entity.getIdentity(category, type)));
})
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
}
}
});
}
});
}));