Converse converse.js

Source: headless/plugins/disco/api.js

  1. import isObject from "lodash-es/isObject";
  2. import log from "@converse/headless/log.js";
  3. import { _converse, api, converse } from "@converse/headless/core.js";
  4. import { getOpenPromise } from '@converse/openpromise';
  5. const { Strophe, $iq } = converse.env;
  6. export default {
  7. /**
  8. * The XEP-0030 service discovery API
  9. *
  10. * This API lets you discover information about entities on the
  11. * XMPP network.
  12. *
  13. * @namespace api.disco
  14. * @memberOf api
  15. */
  16. disco: {
  17. /**
  18. * @namespace api.disco.stream
  19. * @memberOf api.disco
  20. */
  21. stream: {
  22. /**
  23. * @method api.disco.stream.getFeature
  24. * @param { String } name The feature name
  25. * @param { String } xmlns The XML namespace
  26. * @example _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver')
  27. */
  28. async getFeature (name, xmlns) {
  29. await api.waitUntil('streamFeaturesAdded');
  30. if (!name || !xmlns) {
  31. throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature");
  32. }
  33. if (_converse.stream_features === undefined && !api.connection.connected()) {
  34. // Happens during tests when disco lookups happen asynchronously after teardown.
  35. const msg = `Tried to get feature ${name} ${xmlns} but _converse.stream_features has been torn down`;
  36. log.warn(msg);
  37. return;
  38. }
  39. return _converse.stream_features.findWhere({'name': name, 'xmlns': xmlns});
  40. }
  41. },
  42. /**
  43. * @namespace api.disco.own
  44. * @memberOf api.disco
  45. */
  46. own: {
  47. /**
  48. * @namespace api.disco.own.identities
  49. * @memberOf api.disco.own
  50. */
  51. identities: {
  52. /**
  53. * Lets you add new identities for this client (i.e. instance of Converse)
  54. * @method api.disco.own.identities.add
  55. *
  56. * @param { String } category - server, client, gateway, directory, etc.
  57. * @param { String } type - phone, pc, web, etc.
  58. * @param { String } name - "Converse"
  59. * @param { String } lang - en, el, de, etc.
  60. *
  61. * @example _converse.api.disco.own.identities.clear();
  62. */
  63. add (category, type, name, lang) {
  64. for (var i=0; i<_converse.disco._identities.length; i++) {
  65. if (_converse.disco._identities[i].category == category &&
  66. _converse.disco._identities[i].type == type &&
  67. _converse.disco._identities[i].name == name &&
  68. _converse.disco._identities[i].lang == lang) {
  69. return false;
  70. }
  71. }
  72. _converse.disco._identities.push({category: category, type: type, name: name, lang: lang});
  73. },
  74. /**
  75. * Clears all previously registered identities.
  76. * @method api.disco.own.identities.clear
  77. * @example _converse.api.disco.own.identities.clear();
  78. */
  79. clear () {
  80. _converse.disco._identities = []
  81. },
  82. /**
  83. * Returns all of the identities registered for this client
  84. * (i.e. instance of Converse).
  85. * @method api.disco.identities.get
  86. * @example const identities = api.disco.own.identities.get();
  87. */
  88. get () {
  89. return _converse.disco._identities;
  90. }
  91. },
  92. /**
  93. * @namespace api.disco.own.features
  94. * @memberOf api.disco.own
  95. */
  96. features: {
  97. /**
  98. * Lets you register new disco features for this client (i.e. instance of Converse)
  99. * @method api.disco.own.features.add
  100. * @param { String } name - e.g. http://jabber.org/protocol/caps
  101. * @example _converse.api.disco.own.features.add("http://jabber.org/protocol/caps");
  102. */
  103. add (name) {
  104. for (var i=0; i<_converse.disco._features.length; i++) {
  105. if (_converse.disco._features[i] == name) { return false; }
  106. }
  107. _converse.disco._features.push(name);
  108. },
  109. /**
  110. * Clears all previously registered features.
  111. * @method api.disco.own.features.clear
  112. * @example _converse.api.disco.own.features.clear();
  113. */
  114. clear () {
  115. _converse.disco._features = []
  116. },
  117. /**
  118. * Returns all of the features registered for this client (i.e. instance of Converse).
  119. * @method api.disco.own.features.get
  120. * @example const features = api.disco.own.features.get();
  121. */
  122. get () {
  123. return _converse.disco._features;
  124. }
  125. }
  126. },
  127. /**
  128. * Query for information about an XMPP entity
  129. *
  130. * @method api.disco.info
  131. * @param { string } jid The Jabber ID of the entity to query
  132. * @param { string } [node] A specific node identifier associated with the JID
  133. * @returns {promise} Promise which resolves once we have a result from the server.
  134. */
  135. info (jid, node) {
  136. const attrs = {xmlns: Strophe.NS.DISCO_INFO};
  137. if (node) {
  138. attrs.node = node;
  139. }
  140. const info = $iq({
  141. 'from': _converse.connection.jid,
  142. 'to':jid,
  143. 'type':'get'
  144. }).c('query', attrs);
  145. return api.sendIQ(info);
  146. },
  147. /**
  148. * Query for items associated with an XMPP entity
  149. *
  150. * @method api.disco.items
  151. * @param { string } jid The Jabber ID of the entity to query for items
  152. * @param { string } [node] A specific node identifier associated with the JID
  153. * @returns {promise} Promise which resolves once we have a result from the server.
  154. */
  155. items (jid, node) {
  156. const attrs = {'xmlns': Strophe.NS.DISCO_ITEMS};
  157. if (node) {
  158. attrs.node = node;
  159. }
  160. return api.sendIQ(
  161. $iq({
  162. 'from': _converse.connection.jid,
  163. 'to':jid,
  164. 'type':'get'
  165. }).c('query', attrs)
  166. );
  167. },
  168. /**
  169. * Namespace for methods associated with disco entities
  170. *
  171. * @namespace api.disco.entities
  172. * @memberOf api.disco
  173. */
  174. entities: {
  175. /**
  176. * Get the corresponding `DiscoEntity` instance.
  177. *
  178. * @method api.disco.entities.get
  179. * @param { string } jid The Jabber ID of the entity
  180. * @param { boolean } [create] Whether the entity should be created if it doesn't exist.
  181. * @example _converse.api.disco.entities.get(jid);
  182. */
  183. async get (jid, create=false) {
  184. await api.waitUntil('discoInitialized');
  185. if (!jid) {
  186. return _converse.disco_entities;
  187. }
  188. if (_converse.disco_entities === undefined) {
  189. // Happens during tests when disco lookups happen asynchronously after teardown.
  190. log.warn(`Tried to look up entity ${jid} but _converse.disco_entities has been torn down`);
  191. return;
  192. }
  193. const entity = _converse.disco_entities.get(jid);
  194. if (entity || !create) {
  195. return entity;
  196. }
  197. return api.disco.entities.create({ jid });
  198. },
  199. /**
  200. * Return any disco items advertised on this entity
  201. *
  202. * @method api.disco.entities.items
  203. * @param { string } jid The Jabber ID of the entity for which we want to fetch items
  204. * @example api.disco.entities.items(jid);
  205. */
  206. items (jid) {
  207. return _converse.disco_entities.filter(e => e.get('parent_jids')?.includes(jid));
  208. },
  209. /**
  210. * Create a new disco entity. It's identity and features
  211. * will automatically be fetched from cache or from the
  212. * XMPP server.
  213. *
  214. * Fetching from cache can be disabled by passing in
  215. * `ignore_cache: true` in the options parameter.
  216. *
  217. * @method api.disco.entities.create
  218. * @param { object } data
  219. * @param { string } data.jid - The Jabber ID of the entity
  220. * @param { string } data.parent_jid - The Jabber ID of the parent entity
  221. * @param { string } data.name
  222. * @param { object } [options] - Additional options
  223. * @param { boolean } [options.ignore_cache]
  224. * If true, fetch all features from the XMPP server instead of restoring them from cache
  225. * @example _converse.api.disco.entities.create({ jid }, {'ignore_cache': true});
  226. */
  227. create (data, options) {
  228. return _converse.disco_entities.create(data, options);
  229. }
  230. },
  231. /**
  232. * @namespace api.disco.features
  233. * @memberOf api.disco
  234. */
  235. features: {
  236. /**
  237. * Return a given feature of a disco entity
  238. *
  239. * @method api.disco.features.get
  240. * @param { string } feature The feature that might be
  241. * supported. In the XML stanza, this is the `var`
  242. * attribute of the `<feature>` element. For
  243. * example: `http://jabber.org/protocol/muc`
  244. * @param { string } jid The JID of the entity
  245. * (and its associated items) which should be queried
  246. * @returns {promise} A promise which resolves with a list containing
  247. * _converse.Entity instances representing the entity
  248. * itself or those items associated with the entity if
  249. * they support the given feature.
  250. * @example
  251. * api.disco.features.get(Strophe.NS.MAM, _converse.bare_jid);
  252. */
  253. async get (feature, jid) {
  254. if (!jid) throw new TypeError('You need to provide an entity JID');
  255. const entity = await api.disco.entities.get(jid, true);
  256. if (_converse.disco_entities === undefined && !api.connection.connected()) {
  257. // Happens during tests when disco lookups happen asynchronously after teardown.
  258. log.warn(`Tried to get feature ${feature} for ${jid} but _converse.disco_entities has been torn down`);
  259. return [];
  260. }
  261. const promises = [
  262. entity.getFeature(feature),
  263. ...api.disco.entities.items(jid).map(i => i.getFeature(feature))
  264. ];
  265. const result = await Promise.all(promises);
  266. return result.filter(isObject);
  267. },
  268. /**
  269. * Returns true if an entity with the given JID, or if one of its
  270. * associated items, supports a given feature.
  271. *
  272. * @method api.disco.features.has
  273. * @param { string } feature The feature that might be
  274. * supported. In the XML stanza, this is the `var`
  275. * attribute of the `<feature>` element. For
  276. * example: `http://jabber.org/protocol/muc`
  277. * @param { string } jid The JID of the entity
  278. * (and its associated items) which should be queried
  279. * @returns {Promise} A promise which resolves with a boolean
  280. * @example
  281. * api.disco.features.has(Strophe.NS.MAM, _converse.bare_jid);
  282. */
  283. async has (feature, jid) {
  284. if (!jid) throw new TypeError('You need to provide an entity JID');
  285. const entity = await api.disco.entities.get(jid, true);
  286. if (_converse.disco_entities === undefined && !api.connection.connected()) {
  287. // Happens during tests when disco lookups happen asynchronously after teardown.
  288. log.warn(`Tried to check if ${jid} supports feature ${feature}`);
  289. return false;
  290. }
  291. if (await entity.getFeature(feature)) {
  292. return true;
  293. }
  294. const result = await Promise.all(api.disco.entities.items(jid).map(i => i.getFeature(feature)));
  295. return result.map(isObject).includes(true);
  296. }
  297. },
  298. /**
  299. * Used to determine whether an entity supports a given feature.
  300. *
  301. * @method api.disco.supports
  302. * @param { string } feature The feature that might be
  303. * supported. In the XML stanza, this is the `var`
  304. * attribute of the `<feature>` element. For
  305. * example: `http://jabber.org/protocol/muc`
  306. * @param { string } jid The JID of the entity
  307. * (and its associated items) which should be queried
  308. * @returns {promise} A promise which resolves with `true` or `false`.
  309. * @example
  310. * if (await api.disco.supports(Strophe.NS.MAM, _converse.bare_jid)) {
  311. * // The feature is supported
  312. * } else {
  313. * // The feature is not supported
  314. * }
  315. */
  316. supports (feature, jid) {
  317. return api.disco.features.has(feature, jid);
  318. },
  319. /**
  320. * Refresh the features, fields and identities associated with a
  321. * disco entity by refetching them from the server
  322. * @method api.disco.refresh
  323. * @param { string } jid The JID of the entity whose features are refreshed.
  324. * @returns {promise} A promise which resolves once the features have been refreshed
  325. * @example
  326. * await api.disco.refresh('room@conference.example.org');
  327. */
  328. async refresh (jid) {
  329. if (!jid) {
  330. throw new TypeError('api.disco.refresh: You need to provide an entity JID');
  331. }
  332. await api.waitUntil('discoInitialized');
  333. let entity = await api.disco.entities.get(jid);
  334. if (entity) {
  335. entity.features.reset();
  336. entity.fields.reset();
  337. entity.identities.reset();
  338. if (!entity.waitUntilFeaturesDiscovered.isPending) {
  339. entity.waitUntilFeaturesDiscovered = getOpenPromise()
  340. }
  341. entity.queryInfo();
  342. } else {
  343. // Create it if it doesn't exist
  344. entity = await api.disco.entities.create({ jid }, {'ignore_cache': true});
  345. }
  346. return entity.waitUntilFeaturesDiscovered;
  347. },
  348. /**
  349. * @deprecated Use {@link api.disco.refresh} instead.
  350. * @method api.disco.refreshFeatures
  351. */
  352. refreshFeatures (jid) {
  353. return api.refresh(jid);
  354. },
  355. /**
  356. * Return all the features associated with a disco entity
  357. *
  358. * @method api.disco.getFeatures
  359. * @param { string } jid The JID of the entity whose features are returned.
  360. * @returns {promise} A promise which resolves with the returned features
  361. * @example
  362. * const features = await api.disco.getFeatures('room@conference.example.org');
  363. */
  364. async getFeatures (jid) {
  365. if (!jid) {
  366. throw new TypeError('api.disco.getFeatures: You need to provide an entity JID');
  367. }
  368. await api.waitUntil('discoInitialized');
  369. let entity = await api.disco.entities.get(jid, true);
  370. entity = await entity.waitUntilFeaturesDiscovered;
  371. return entity.features;
  372. },
  373. /**
  374. * Return all the service discovery extensions fields
  375. * associated with an entity.
  376. *
  377. * See [XEP-0129: Service Discovery Extensions](https://xmpp.org/extensions/xep-0128.html)
  378. *
  379. * @method api.disco.getFields
  380. * @param { string } jid The JID of the entity whose fields are returned.
  381. * @example
  382. * const fields = await api.disco.getFields('room@conference.example.org');
  383. */
  384. async getFields (jid) {
  385. if (!jid) {
  386. throw new TypeError('api.disco.getFields: You need to provide an entity JID');
  387. }
  388. await api.waitUntil('discoInitialized');
  389. let entity = await api.disco.entities.get(jid, true);
  390. entity = await entity.waitUntilFeaturesDiscovered;
  391. return entity.fields;
  392. },
  393. /**
  394. * Get the identity (with the given category and type) for a given disco entity.
  395. *
  396. * For example, when determining support for PEP (personal eventing protocol), you
  397. * want to know whether the user's own JID has an identity with
  398. * `category='pubsub'` and `type='pep'` as explained in this section of
  399. * XEP-0163: https://xmpp.org/extensions/xep-0163.html#support
  400. *
  401. * @method api.disco.getIdentity
  402. * @param { string } The identity category.
  403. * In the XML stanza, this is the `category`
  404. * attribute of the `<identity>` element.
  405. * For example: 'pubsub'
  406. * @param { string } type The identity type.
  407. * In the XML stanza, this is the `type`
  408. * attribute of the `<identity>` element.
  409. * For example: 'pep'
  410. * @param { string } jid The JID of the entity which might have the identity
  411. * @returns {promise} A promise which resolves with a map indicating
  412. * whether an identity with a given type is provided by the entity.
  413. * @example
  414. * api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid).then(
  415. * function (identity) {
  416. * if (identity) {
  417. * // The entity DOES have this identity
  418. * } else {
  419. * // The entity DOES NOT have this identity
  420. * }
  421. * }
  422. * ).catch(e => log.error(e));
  423. */
  424. async getIdentity (category, type, jid) {
  425. const e = await api.disco.entities.get(jid, true);
  426. if (e === undefined && !api.connection.connected()) {
  427. // Happens during tests when disco lookups happen asynchronously after teardown.
  428. const msg = `Tried to look up category ${category} for ${jid} but _converse.disco_entities has been torn down`;
  429. log.warn(msg);
  430. return;
  431. }
  432. return e.getIdentity(category, type);
  433. }
  434. }
  435. }