123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686 |
- // Converse.js
- // http://conversejs.org
- //
- // Copyright (c) 2013-2018, the Converse developers
- // Licensed under the Mozilla Public License (MPLv2)
- /* This is a Converse plugin which add support for XEP-0030: Service Discovery */
- (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.session(
- b64_sha1(`converse.dataforms-{this.get('jid')}`)
- );
- this.features = new Backbone.Collection();
- this.features.browserStorage = new Backbone.BrowserStorage.session(
- b64_sha1(`converse.features-${this.get('jid')}`)
- );
- this.features.on('add', this.onFeatureAdded, this);
- this.fields = new Backbone.Collection();
- this.fields.browserStorage = new Backbone.BrowserStorage.session(
- b64_sha1(`converse.fields-${this.get('jid')}`)
- );
- this.fields.on('add', this.onFieldAdded, this);
- this.identities = new Backbone.Collection();
- this.identities.browserStorage = new Backbone.BrowserStorage.session(
- b64_sha1(`converse.identities-${this.get('jid')}`)
- );
- this.fetchFeatures();
- this.items = new _converse.DiscoEntities();
- this.items.browserStorage = new Backbone.BrowserStorage.session(
- 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);
- },
- onFieldAdded (field) {
- field.entity = this;
- _converse.emit('discoExtensionFieldDiscovered', field);
- },
- fetchFeatures () {
- if (this.features.browserStorage.records.length === 0) {
- this.queryInfo();
- } else {
- this.features.fetch({
- add: true,
- success: () => {
- this.waitUntilFeaturesDiscovered.resolve(this);
- this.trigger('featuresDiscovered');
- }
- });
- this.identities.fetch({add: true});
- }
- },
- queryInfo () {
- _converse.api.disco.info(this.get('jid'), null)
- .then(stanza => this.onInfo(stanza))
- .catch(iq => {
- this.waitUntilFeaturesDiscovered.resolve(this);
- _converse.log(iq, Strophe.LogLevel.ERROR);
- });
- },
- 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))) {
- const entity = _converse.disco_entities.get(jid);
- if (entity) {
- this.items.add(entity);
- } else {
- 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')).then(stanza => this.onDiscoItems(stanza));
- },
- 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')
- });
- });
- // XEP-0128 Service Discovery Extensions
- _.forEach(sizzle('x[type="result"][xmlns="jabber:x:data"] field', stanza), field => {
- this.fields.create({
- 'var': field.getAttribute('var'),
- 'value': _.get(field.querySelector('value'), 'textContent'),
- 'from': stanza.getAttribute('from')
- });
- });
- this.waitUntilFeaturesDiscovered.resolve(this);
- 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.own.identities.add('client', 'web', 'Converse');
- _converse.api.disco.own.features.add(Strophe.NS.BOSH);
- _converse.api.disco.own.features.add(Strophe.NS.CHATSTATES);
- _converse.api.disco.own.features.add(Strophe.NS.DISCO_INFO);
- _converse.api.disco.own.features.add(Strophe.NS.ROSTERX); // Limited support
- if (_converse.message_carbons) {
- _converse.api.disco.own.features.add(Strophe.NS.CARBONS);
- }
- _converse.emit('addClientFeatures');
- return this;
- }
- function initStreamFeatures () {
- _converse.stream_features = new Backbone.Collection();
- _converse.stream_features.browserStorage = new Backbone.BrowserStorage.session(
- b64_sha1(`converse.stream-features-${_converse.bare_jid}`)
- );
- _converse.stream_features.fetch({
- success (collection) {
- if (collection.length === 0 && _converse.connection.features) {
- _.forEach(
- _converse.connection.features.childNodes,
- (feature) => {
- _converse.stream_features.create({
- 'name': feature.nodeName,
- 'xmlns': feature.getAttribute('xmlns')
- });
- });
- }
- }
- });
- _converse.emit('streamFeaturesAdded');
- }
- 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.session(
- 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('sessionInitialized', initStreamFeatures);
- _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});
- }
- iqresult.c('query', attrs);
- _.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;
- }
- _.extend(_converse.api, {
- /**
- * The XEP-0030 service discovery API
- *
- * This API lets you discover information about entities on the
- * XMPP network.
- *
- * @namespace _converse.api.disco
- * @memberOf _converse.api
- */
- 'disco': {
- /**
- * @namespace _converse.api.disco.stream
- * @memberOf _converse.api.disco
- */
- 'stream': {
- /**
- * @method _converse.api.disco.stream.getFeature
- * @param {String} name The feature name
- * @param {String} xmlns The XML namespace
- * @example _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver')
- */
- 'getFeature': function (name, xmlns) {
- if (_.isNil(name) || _.isNil(xmlns)) {
- throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature");
- }
- return _converse.stream_features.findWhere({'name': name, 'xmlns': xmlns});
- }
- },
- /**
- * @namespace _converse.api.disco.own
- * @memberOf _converse.api.disco
- */
- 'own': {
- /**
- * @namespace _converse.api.disco.own.identities
- * @memberOf _converse.api.disco.own
- */
- 'identities': {
- /**
- * Lets you add new identities for this client (i.e. instance of Converse)
- * @method _converse.api.disco.own.identities.add
- *
- * @param {String} category - server, client, gateway, directory, etc.
- * @param {String} type - phone, pc, web, etc.
- * @param {String} name - "Converse"
- * @param {String} lang - en, el, de, etc.
- *
- * @example _converse.api.disco.own.identities.clear();
- */
- add (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});
- },
- /**
- * Clears all previously registered identities.
- * @method _converse.api.disco.own.identities.clear
- * @example _converse.api.disco.own.identities.clear();
- */
- clear () {
- plugin._identities = []
- },
- /**
- * Returns all of the identities registered for this client
- * (i.e. instance of Converse).
- * @method _converse.api.disco.identities.get
- * @example const identities = _converse.api.disco.own.identities.get();
- */
- get () {
- return plugin._identities;
- }
- },
- /**
- * @namespace _converse.api.disco.own.features
- * @memberOf _converse.api.disco.own
- */
- 'features': {
- /**
- * Lets you register new disco features for this client (i.e. instance of Converse)
- * @method _converse.api.disco.own.features.add
- * @param {String} name - e.g. http://jabber.org/protocol/caps
- * @example _converse.api.disco.own.features.add("http://jabber.org/protocol/caps");
- */
- add (name) {
- for (var i=0; i<plugin._features.length; i++) {
- if (plugin._features[i] == name) { return false; }
- }
- plugin._features.push(name);
- },
- /**
- * Clears all previously registered features.
- * @method _converse.api.disco.own.features.clear
- * @example _converse.api.disco.own.features.clear();
- */
- clear () {
- plugin._features = []
- },
- /**
- * Returns all of the features registered for this client (i.e. instance of Converse).
- * @method _converse.api.disco.own.features.get
- * @example const features = _converse.api.disco.own.features.get();
- */
- get () {
- return plugin._features;
- }
- }
- },
- /**
- * Query for information about an XMPP entity
- *
- * @method _converse.api.disco.info
- * @param {string} jid The Jabber ID of the entity to query
- * @param {string} [node] A specific node identifier associated with the JID
- * @returns {promise} Promise which resolves once we have a result from the server.
- */
- 'info' (jid, node) {
- 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);
- return _converse.api.sendIQ(info);
- },
- /**
- * Query for items associated with an XMPP entity
- *
- * @method _converse.api.disco.items
- * @param {string} jid The Jabber ID of the entity to query for items
- * @param {string} [node] A specific node identifier associated with the JID
- * @returns {promise} Promise which resolves once we have a result from the server.
- */
- 'items' (jid, node) {
- const attrs = {'xmlns': Strophe.NS.DISCO_ITEMS};
- if (node) {
- attrs.node = node;
- }
- return _converse.api.sendIQ(
- $iq({
- 'from': _converse.connection.jid,
- 'to':jid,
- 'type':'get'
- }).c('query', attrs)
- );
- },
- /**
- * Namespace for methods associated with disco entities
- *
- * @namespace _converse.api.disco.entities
- * @memberOf _converse.api.disco
- */
- 'entities': {
- /**
- * Get the the corresponding `DiscoEntity` instance.
- *
- * @method _converse.api.disco.entities.get
- * @param {string} jid The Jabber ID of the entity
- * @param {boolean} [create] Whether the entity should be created if it doesn't exist.
- * @example _converse.api.disco.entities.get(jid);
- */
- 'get' (jid, create=false) {
- return _converse.api.waitUntil('discoInitialized')
- .then(() => {
- if (_.isNil(jid)) {
- return _converse.disco_entities;
- }
- const entity = _converse.disco_entities.get(jid);
- if (entity || !create) {
- return entity;
- }
- return _converse.disco_entities.create({'jid': jid});
- });
- }
- },
- /**
- * Used to determine whether an entity supports a given feature.
- *
- * @method _converse.api.disco.supports
- * @param {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`
- * @param {string} jid The JID of the entity
- * (and its associated items) which should be queried
- * @returns {promise} 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.
- *
- * @example
- * _converse.api.disco.supports(Strophe.NS.MAM, _converse.bare_jid)
- * .then(value => {
- * // `value` is a map with two keys, `supported` and `feature`.
- * if (value.supported) {
- * // The feature is supported
- * } else {
- * // The feature is not supported
- * }
- * }).catch(() => {
- * _converse.log(
- * "Error or timeout while checking for feature support",
- * Strophe.LogLevel.ERROR
- * );
- * });
- */
- 'supports' (feature, jid) {
- if (_.isNil(jid)) {
- throw new TypeError('api.disco.supports: You need to provide an entity JID');
- }
- return _converse.api.waitUntil('discoInitialized')
- .then(() => _converse.api.disco.entities.get(jid, true))
- .then(entity => entity.waitUntilFeaturesDiscovered)
- .then(entity => {
- const promises = _.concat(
- entity.items.map(item => item.hasFeature(feature)),
- entity.hasFeature(feature)
- );
- return Promise.all(promises);
- }).then(result => f.filter(f.isObject, result));
- },
- /**
- * Refresh the features (and fields and identities) associated with a
- * disco entity by refetching them from the server
- *
- * @method _converse.api.disco.refreshFeatures
- * @param {string} jid The JID of the entity whose features are refreshed.
- * @returns {promise} A promise which resolves once the features have been refreshed
- * @example
- * await _converse.api.disco.refreshFeatures('room@conference.example.org');
- */
- 'refreshFeatures' (jid) {
- if (_.isNil(jid)) {
- throw new TypeError('api.disco.refreshFeatures: You need to provide an entity JID');
- }
- return _converse.api.waitUntil('discoInitialized')
- .then(() => _converse.api.disco.entities.get(jid, true))
- .then(entity => {
- entity.features.reset();
- entity.fields.reset();
- entity.identities.reset();
- entity.waitUntilFeaturesDiscovered = utils.getResolveablePromise()
- entity.queryInfo();
- return entity.waitUntilFeaturesDiscovered;
- })
- .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
- },
- /**
- * Return all the features associated with a disco entity
- *
- * @method _converse.api.disco.getFeatures
- * @param {string} jid The JID of the entity whose features are returned.
- * @returns {promise} A promise which resolves with the returned features
- * @example
- * const features = await _converse.api.disco.getFeatures('room@conference.example.org');
- */
- 'getFeatures' (jid) {
- if (_.isNil(jid)) {
- throw new TypeError('api.disco.getFeatures: You need to provide an entity JID');
- }
- return _converse.api.waitUntil('discoInitialized')
- .then(() => _converse.api.disco.entities.get(jid, true))
- .then(entity => entity.waitUntilFeaturesDiscovered)
- .then(entity => entity.features)
- .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
- },
- /**
- * Return all the service discovery extensions fields
- * associated with an entity.
- *
- * See [XEP-0129: Service Discovery Extensions](https://xmpp.org/extensions/xep-0128.html)
- *
- * @method _converse.api.disco.getFields
- * @param {string} jid The JID of the entity whose fields are returned.
- * @example
- * const fields = await _converse.api.disco.getFields('room@conference.example.org');
- */
- 'getFields' (jid) {
- if (_.isNil(jid)) {
- throw new TypeError('api.disco.getFields: You need to provide an entity JID');
- }
- return _converse.api.waitUntil('discoInitialized')
- .then(() => _converse.api.disco.entities.get(jid, true))
- .then(entity => entity.waitUntilFeaturesDiscovered)
- .then(entity => entity.fields)
- .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
- },
- /**
- * Get the identity (with the given category and type) for a given disco entity.
- *
- * For example, when determining support for PEP (personal eventing protocol), you
- * want to know whether the user's own JID has an identity with
- * `category='pubsub'` and `type='pep'` as explained in this section of
- * XEP-0163: https://xmpp.org/extensions/xep-0163.html#support
- *
- * @method _converse.api.disco.getIdentity
- * @param {string} The identity category.
- * In the XML stanza, this is the `category`
- * attribute of the `<identity>` element.
- * For example: 'pubsub'
- * @param {string} type The identity type.
- * In the XML stanza, this is the `type`
- * attribute of the `<identity>` element.
- * For example: 'pep'
- * @param {string} jid The JID of the entity which might have the identity
- * @returns {promise} A promise which resolves with a map indicating
- * whether an identity with a given type is provided by the entity.
- * @example
- * _converse.api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid).then(
- * function (identity) {
- * if (_.isNil(identity)) {
- * // The entity DOES NOT have this identity
- * } else {
- * // The entity DOES have this identity
- * }
- * }
- * ).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
- */
- 'getIdentity' (category, type, jid) {
- return _converse.api.disco.entities.get(jid, true).then(e => e.getIdentity(category, type));
- }
- }
- });
- }
- });
- }));
|