2017-07-21 12:38:16 +02:00
|
|
|
// 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 */
|
|
|
|
|
2018-02-09 16:02:56 +01:00
|
|
|
/*global Backbone, define, window */
|
2017-07-21 12:38:16 +02:00
|
|
|
(function (root, factory) {
|
|
|
|
define(["converse-core", "sizzle", "strophe.disco"], factory);
|
|
|
|
}(this, function (converse, sizzle) {
|
|
|
|
|
2018-03-29 16:12:19 +02:00
|
|
|
const { Backbone, Promise, Strophe, b64_sha1, utils, _, f } = converse.env;
|
2017-07-21 12:38:16 +02:00
|
|
|
|
|
|
|
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');
|
|
|
|
|
2018-03-29 16:12:19 +02:00
|
|
|
|
2017-07-21 12:38:16 +02:00
|
|
|
_converse.DiscoEntity = Backbone.Model.extend({
|
|
|
|
/* A Disco Entity is a JID addressable entity that can be queried
|
2017-11-02 23:23:01 +01:00
|
|
|
* for features.
|
|
|
|
*
|
|
|
|
* See XEP-0030: https://xmpp.org/extensions/xep-0030.html
|
|
|
|
*/
|
2017-07-21 17:38:08 +02:00
|
|
|
idAttribute: 'jid',
|
|
|
|
|
|
|
|
initialize () {
|
2017-11-10 15:53:59 +01:00
|
|
|
this.waitUntilFeaturesDiscovered = utils.getResolveablePromise();
|
|
|
|
|
2018-04-17 16:42:20 +02:00
|
|
|
this.dataforms = new Backbone.Collection();
|
|
|
|
this.dataforms.browserStorage = new Backbone.BrowserStorage[_converse.storage](
|
|
|
|
b64_sha1(`converse.dataforms-{this.get('jid')}`)
|
|
|
|
);
|
|
|
|
|
2017-07-21 17:38:08 +02:00
|
|
|
this.features = new Backbone.Collection();
|
|
|
|
this.features.browserStorage = new Backbone.BrowserStorage[_converse.storage](
|
|
|
|
b64_sha1(`converse.features-${this.get('jid')}`)
|
|
|
|
);
|
2018-03-03 11:45:57 +01:00
|
|
|
this.features.on('add', this.onFeatureAdded, this);
|
2017-07-21 17:38:08 +02:00
|
|
|
|
|
|
|
this.identities = new Backbone.Collection();
|
|
|
|
this.identities.browserStorage = new Backbone.BrowserStorage[_converse.storage](
|
|
|
|
b64_sha1(`converse.identities-${this.get('jid')}`)
|
|
|
|
);
|
|
|
|
this.fetchFeatures();
|
2017-11-10 15:53:59 +01:00
|
|
|
|
2018-03-29 16:12:19 +02:00
|
|
|
this.items = new _converse.DiscoEntities();
|
|
|
|
this.items.browserStorage = new Backbone.BrowserStorage[_converse.storage](
|
|
|
|
b64_sha1(`converse.disco-items-${this.get('jid')}`)
|
|
|
|
);
|
2017-11-10 15:53:59 +01:00
|
|
|
},
|
|
|
|
|
2018-02-07 17:23:04 +01:00
|
|
|
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));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-11-10 15:53:59 +01:00
|
|
|
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 () {
|
2018-03-29 16:12:19 +02:00
|
|
|
if (entity.features.findWhere({'var': feature})) {
|
|
|
|
resolve(entity);
|
2017-11-10 15:53:59 +01:00
|
|
|
} else {
|
2018-03-29 16:12:19 +02:00
|
|
|
resolve();
|
2017-11-10 15:53:59 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
entity.waitUntilFeaturesDiscovered
|
|
|
|
.then(fulfillPromise)
|
|
|
|
.catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
|
|
|
|
});
|
2017-07-21 17:38:08 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
onFeatureAdded (feature) {
|
2018-03-03 11:45:57 +01:00
|
|
|
feature.entity = this;
|
2017-07-21 17:38:08 +02:00
|
|
|
_converse.emit('serviceDiscovered', feature);
|
|
|
|
},
|
|
|
|
|
|
|
|
fetchFeatures () {
|
|
|
|
if (this.features.browserStorage.records.length === 0) {
|
|
|
|
this.queryInfo();
|
|
|
|
} else {
|
2017-11-10 15:53:59 +01:00
|
|
|
this.features.fetch({
|
|
|
|
add: true,
|
|
|
|
success: () => {
|
|
|
|
this.waitUntilFeaturesDiscovered.resolve();
|
|
|
|
this.trigger('featuresDiscovered');
|
|
|
|
}
|
|
|
|
});
|
2017-07-21 17:38:08 +02:00
|
|
|
this.identities.fetch({add: true});
|
2017-07-21 12:38:16 +02:00
|
|
|
}
|
2017-07-21 17:38:08 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
queryInfo () {
|
|
|
|
_converse.connection.disco.info(this.get('jid'), null, this.onInfo.bind(this));
|
|
|
|
},
|
|
|
|
|
2018-03-29 16:12:19 +02:00
|
|
|
onDiscoItems (stanza) {
|
|
|
|
_.each(stanza.querySelectorAll('query item'), (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});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-07-21 17:38:08 +02:00
|
|
|
queryForItems () {
|
2017-07-21 17:46:24 +02:00
|
|
|
if (_.isEmpty(this.identities.where({'category': 'server'}))) {
|
2017-07-21 17:38:08 +02:00
|
|
|
// Don't fetch features and items if this is not a
|
|
|
|
// server or a conference component.
|
|
|
|
return;
|
|
|
|
}
|
2018-03-29 16:12:19 +02:00
|
|
|
_converse.connection.disco.items(this.get('jid'), null, this.onDiscoItems.bind(this));
|
2017-07-21 17:38:08 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
onInfo (stanza) {
|
|
|
|
_.forEach(stanza.querySelectorAll('identity'), (identity) => {
|
|
|
|
this.identities.create({
|
|
|
|
'category': identity.getAttribute('category'),
|
2018-02-07 17:23:04 +01:00
|
|
|
'type': identity.getAttribute('type'),
|
|
|
|
'name': identity.getAttribute('name')
|
2017-07-21 17:38:08 +02:00
|
|
|
});
|
|
|
|
});
|
2018-04-17 16:42:20 +02:00
|
|
|
|
|
|
|
_.each(sizzle('x[type="result"][xmlns="jabber:x:data"]', 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);
|
|
|
|
});
|
|
|
|
|
2017-07-21 17:38:08 +02:00
|
|
|
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')
|
|
|
|
});
|
|
|
|
});
|
2017-11-10 15:53:59 +01:00
|
|
|
this.waitUntilFeaturesDiscovered.resolve();
|
2017-08-08 17:35:17 +02:00
|
|
|
this.trigger('featuresDiscovered');
|
2017-07-21 12:38:16 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
_converse.DiscoEntities = Backbone.Collection.extend({
|
|
|
|
model: _converse.DiscoEntity,
|
|
|
|
|
|
|
|
fetchEntities () {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.fetch({
|
|
|
|
add: true,
|
2018-03-29 16:12:19 +02:00
|
|
|
success: resolve,
|
2017-07-21 12:38:16 +02:00
|
|
|
error () {
|
|
|
|
reject (new Error("Could not fetch disco entities"));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-07-21 17:38:08 +02:00
|
|
|
function addClientFeatures () {
|
|
|
|
/* The strophe.disco.js plugin keeps a list of features which
|
|
|
|
* it will advertise to any #info queries made to it.
|
|
|
|
*
|
|
|
|
* See: http://xmpp.org/extensions/xep-0030.html#info
|
|
|
|
*/
|
|
|
|
|
|
|
|
// See http://xmpp.org/registrar/disco-categories.html
|
|
|
|
_converse.connection.disco.addIdentity('client', 'web', 'Converse.js');
|
|
|
|
|
|
|
|
_converse.connection.disco.addFeature(Strophe.NS.BOSH);
|
|
|
|
_converse.connection.disco.addFeature(Strophe.NS.CHATSTATES);
|
|
|
|
_converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
|
|
|
|
_converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
|
|
|
|
if (_converse.message_carbons) {
|
|
|
|
_converse.connection.disco.addFeature(Strophe.NS.CARBONS);
|
2017-07-21 12:38:16 +02:00
|
|
|
}
|
2017-07-21 17:38:08 +02:00
|
|
|
_converse.emit('addClientFeatures');
|
|
|
|
return this;
|
|
|
|
}
|
2017-07-21 12:38:16 +02:00
|
|
|
|
2017-07-21 12:41:16 +02:00
|
|
|
function initializeDisco () {
|
2017-07-21 17:38:08 +02:00
|
|
|
addClientFeatures();
|
2017-07-21 12:38:16 +02:00
|
|
|
_converse.disco_entities = new _converse.DiscoEntities();
|
2018-03-29 16:12:19 +02:00
|
|
|
_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));
|
2017-07-21 12:41:16 +02:00
|
|
|
}
|
|
|
|
_converse.api.listen.on('reconnected', initializeDisco);
|
|
|
|
_converse.api.listen.on('connected', initializeDisco);
|
2017-07-21 12:38:16 +02:00
|
|
|
|
|
|
|
_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();
|
|
|
|
}
|
|
|
|
});
|
2017-11-02 23:23:01 +01:00
|
|
|
|
|
|
|
/* We extend the default converse.js API to add methods specific to service discovery */
|
|
|
|
_.extend(_converse.api, {
|
|
|
|
'disco': {
|
2017-11-10 15:53:59 +01:00
|
|
|
'entities': {
|
|
|
|
'get' (entity_jid, create=false) {
|
2018-03-29 16:12:19 +02:00
|
|
|
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});
|
|
|
|
});
|
2017-11-10 15:53:59 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
'supports' (feature, entity_jid) {
|
2018-03-29 16:12:19 +02:00
|
|
|
/* 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.
|
2017-11-02 23:23:01 +01:00
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (String) feature - The feature that might be
|
2018-03-29 16:12:19 +02:00
|
|
|
* 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
|
2017-11-02 23:23:01 +01:00
|
|
|
*/
|
2018-03-29 16:12:19 +02:00
|
|
|
if (_.isNil(entity_jid)) {
|
|
|
|
throw new TypeError('disco.supports: You need to provide an entity JID');
|
|
|
|
}
|
|
|
|
return _converse.api.waitUntil('discoInitialized').then((entity) => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
_converse.api.disco.entities.get(entity_jid, true).then((entity) => {
|
|
|
|
Promise.all(
|
|
|
|
_.concat(
|
|
|
|
entity.items.map((item) => item.hasFeature(feature)),
|
|
|
|
entity.hasFeature(feature)
|
|
|
|
)
|
|
|
|
).then((result) => {
|
|
|
|
resolve(f.filter(f.isObject, result));
|
|
|
|
}).catch(reject);
|
|
|
|
})
|
|
|
|
});
|
2018-02-07 17:23:04 +01:00
|
|
|
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
|
|
|
|
},
|
|
|
|
|
|
|
|
'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'
|
2018-03-29 16:12:19 +02:00
|
|
|
* (String) entity_jid - The JID of the entity which might have the identity
|
2018-02-07 17:23:04 +01:00
|
|
|
*/
|
2018-03-29 16:12:19 +02:00
|
|
|
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)));
|
|
|
|
})
|
2018-02-07 17:23:04 +01:00
|
|
|
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
|
2017-11-02 23:23:01 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2017-07-21 12:38:16 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}));
|