@converse/headless
Version:
Converse.js Headless build
420 lines (381 loc) • 18.1 kB
JavaScript
/**
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import converse from '../../shared/api/public.js';
import _converse from '../../shared/_converse.js';
import api from '../../shared/api/index.js';
import log from '@converse/log';
import { parseErrorStanza } from '../../shared/parsers.js';
import { RSM } from '../../shared/rsm.js';
import { parseStanzaForPubSubConfig } from './parsers.js';
const { Strophe, stx, Stanza } = converse.env;
export default {
/**
* @typedef {import('strophe.js').Builder} Builder
* @typedef {import('strophe.js').Stanza} Stanza
* @typedef {import('./types').PubSubConfigOptions} PubSubConfigOptions
*
* The "pubsub" namespace groups methods relevant to PubSub
* @namespace _converse.api.pubsub
* @memberOf _converse.api
*/
pubsub: {
config: {
/**
* Fetches the configuration for a PubSub node
* @method _converse.api.pubsub.config.get
* @param {string} jid - The JID of the pubsub service where the node resides
* @param {string} node - The node to configure
* @returns {Promise<import('./types').PubSubConfigOptions>}
*/
async get(jid, node) {
if (!node) throw new Error('api.pubsub.config.get: Node value required');
const bare_jid = _converse.session.get('bare_jid');
const full_jid = _converse.session.get('jid');
const entity_jid = jid || bare_jid;
const stanza = stx`
<iq xmlns="jabber:client"
from="${full_jid}"
type="get"
to="${entity_jid}">
<pubsub xmlns="${Strophe.NS.PUBSUB}#owner"><configure node="${node}"/></pubsub>
</iq>`;
let response;
try {
response = await api.sendIQ(stanza);
} catch (error) {
throw await parseErrorStanza(error);
}
return parseStanzaForPubSubConfig(response);
},
/**
* Configures a PubSub node
* @method _converse.api.pubsub.config.set
* @param {string} jid The JID of the pubsub service where the node resides
* @param {string} node The node to configure
* @param {PubSubConfigOptions} config The configuration options
* @returns {Promise<import('./types').PubSubConfigOptions>}
*/
async set(jid, node, config) {
if (!node) throw new Error('api.pubsub.config.set: Node value required');
const bare_jid = _converse.session.get('bare_jid');
const entity_jid = jid || bare_jid;
const new_config = {
...(await api.pubsub.config.get(entity_jid, node)),
...config,
};
const stanza = stx`
<iq xmlns="jabber:client"
from="${bare_jid}"
type="set"
to="${entity_jid}">
<pubsub xmlns="${Strophe.NS.PUBSUB}#owner">
<configure node="${node}">
<x xmlns="${Strophe.NS.XFORM}" type="submit">
<field var="FORM_TYPE" type="hidden">
<value>${Strophe.NS.PUBSUB}#node_config</value>
</field>
${Object.entries(new_config).map(
([k, v]) => stx`<field var="pubsub#${k}"><value>${v}</value></field>`,
)}
</x>
</configure>
</pubsub>
</iq>`;
try {
await api.sendIQ(stanza);
} catch (error) {
throw await parseErrorStanza(error);
}
return new_config;
},
},
/**
* Publishes an item to a PubSub node
* @method _converse.api.pubsub.publish
* @param {string} jid The JID of the pubsub service where the node resides.
* @param {string} node The node being published to
* @param {Builder|Stanza|(Builder|Stanza)[]} item The XML element(s) being published
* @param {PubSubConfigOptions} options The publisher options
* (see https://xmpp.org/extensions/xep-0060.html#publisher-publish-options)
* @param {boolean} strict_options Indicates whether the publisher
* options are a strict requirement or not. If they're NOT
* strict, then Converse will publish to the node even if
* the publish options precondition cannot be met.
* @returns {Promise<void|Element>}
*/
async publish(jid, node, item, options, strict_options = true) {
if (!node) throw new Error('api.pubsub.publish: node value required');
if (!item) throw new Error('api.pubsub.publish: item value required');
const bare_jid = _converse.session.get('bare_jid');
const entity_jid = jid || bare_jid;
const stanza = stx`
<iq xmlns="jabber:client"
from="${bare_jid}"
type="set"
to="${entity_jid}">
<pubsub xmlns="${Strophe.NS.PUBSUB}">
<publish node="${node}">${item}</publish>
${
options
? stx`<publish-options>
<x xmlns="${Strophe.NS.XFORM}" type="submit">
<field var="FORM_TYPE" type="hidden">
<value>${Strophe.NS.PUBSUB}#publish-options</value>
</field>
${Object.entries(options).map(
([k, v]) => stx`<field var="pubsub#${k}"><value>${v}</value></field>`,
)}
</x></publish-options>`
: ''
}
</pubsub>
</iq>`;
if (entity_jid === bare_jid) {
// This is PEP, check for support
const supports_pep =
(await api.disco.getIdentity('pubsub', 'pep', bare_jid)) ||
(await api.disco.getIdentity('pubsub', 'pep', Strophe.getDomainFromJid(bare_jid)));
if (!supports_pep) {
log.warn(`api.pubsub.publish: Not publishing via PEP because it's not supported!`);
log.warn(stanza);
return;
}
}
// Check for #publish-options support.
const supports_publish_options =
(await api.disco.supports(Strophe.NS.PUBSUB + '#publish-options', entity_jid)) ||
(entity_jid === bare_jid &&
// XEP-0223 says we need to check the server for support
// (although Prosody returns it on the bare jid)
(await api.disco.supports(
Strophe.NS.PUBSUB + '#publish-options',
Strophe.getDomainFromJid(entity_jid),
)));
if (!supports_publish_options && strict_options) {
log.warn(`api.pubsub.publish: #publish-options not supported, refusing to publish item.`);
log.warn(stanza);
return;
}
try {
await api.sendIQ(stanza);
} catch (iq) {
const e = await parseErrorStanza(iq);
if (
e.name === 'conflict' &&
/** @type {import('shared/errors').StanzaError} */ (e).extra[Strophe.NS.PUBSUB_ERROR] ===
'precondition-not-met'
) {
// Manually configure the node if we can't set it via publish-options
await api.pubsub.config.set(entity_jid, node, options);
try {
await api.sendIQ(stanza);
} catch (e) {
log.error(e);
if (!strict_options) {
// The publish-options precondition couldn't be met.
// We re-publish but without publish-options.
const el = stanza.tree();
el.querySelector('publish-options').outerHTML = '';
log.warn(`api.pubsub.publish: #publish-options precondition-not-met, publishing anyway.`);
await api.sendIQ(el);
}
}
} else {
throw iq;
}
}
},
/**
* Retracts (deletes) an item from a PubSub node (XEP-0060 § 7.2).
* @method _converse.api.pubsub.retract
* @param {string} jid - The JID of the pubsub service where the node
* resides. Pass a falsy value to retract from your own PEP service.
* @param {string} node - The node to retract the item from
* @param {string} id - The id of the item to retract
* @param {object} [options]
* @param {boolean} [options.notify=true] - Whether to ask the server to
* notify subscribers of the retraction.
* @returns {Promise<void>}
*/
async retract(jid, node, id, options = {}) {
if (!node) throw new Error('api.pubsub.retract: node value required');
if (!id) throw new Error('api.pubsub.retract: id value required');
const { notify = true } = options;
const bare_jid = _converse.session.get('bare_jid');
const entity_jid = jid || bare_jid;
const stanza = stx`
<iq xmlns="jabber:client" from="${bare_jid}" type="set" to="${entity_jid}">
<pubsub xmlns="${Strophe.NS.PUBSUB}">
<retract node="${node}" notify="${notify ? 'true' : 'false'}">
<item id="${id}"/>
</retract>
</pubsub>
</iq>`;
try {
await api.sendIQ(stanza);
} catch (error) {
throw await parseErrorStanza(error);
}
},
/**
* Creates a PubSub node at a given service
* @param {string} jid - The PubSub service JID
* @param {string} node - The node to create
* @param {PubSubConfigOptions} config The configuration options
* @returns {Promise<void>}
*/
async create(jid, node, config) {
if (!node) throw new Error('api.pubsub.create: node value required');
const own_jid = _converse.state.session.get('jid');
const iq = stx`
<iq xmlns="jabber:client"
type="set"
from="${own_jid}"
to="${jid}">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<create node="${node}"/>
<configure>
<x xmlns="${Strophe.NS.XFORM}" type="submit">
<field var="FORM_TYPE" type="hidden">
<value>${Strophe.NS.PUBSUB}#node_config</value>
</field>
${Object.entries(config).map(
([k, v]) => stx`<field var="pubsub#${k}"><value>${v}</value></field>`,
)}
</x>
</configure>
</pubsub>
</iq>`;
try {
await api.sendIQ(iq);
} catch (error) {
throw await parseErrorStanza(error);
}
},
/**
* Subscribes the local user to a PubSub node.
* @method _converse.api.pubsub.subscribe
* @param {string} jid - PubSub service JID.
* @param {string} node - The node to subscribe to
* @returns {Promise<void>}
*/
async subscribe(jid, node) {
if (!node) throw new Error('api.pubsub.subscribe: node value required');
const service = jid || (await api.disco.entities.find('http://jabber.org/protocol/pubsub'));
const own_jid = _converse.session.get('jid');
const iq = stx`
<iq type="set" from="${own_jid}" to="${service}" xmlns="jabber:client">
<pubsub xmlns="${Strophe.NS.PUBSUB}">
<subscribe node="${node}" jid="${own_jid}"/>
</pubsub>
</iq>`;
try {
await api.sendIQ(iq);
} catch (error) {
throw await parseErrorStanza(error);
}
},
/**
* Unsubscribes the local user from a PubSub node.
* @method _converse.api.pubsub.unsubscribe
* @param {string} jid - The PubSub service JID
* @param {string} node - The node to unsubscribe from
* @returns {Promise<void>}
*/
async unsubscribe(jid, node) {
if (!node) throw new Error('api.pubsub.unsubscribe: node value required');
const own_jid = _converse.session.get('jid');
const iq = stx`
<iq type="set" from="${own_jid}" to="${jid}" xmlns="jabber:client">
<pubsub xmlns="${Strophe.NS.PUBSUB}">
<unsubscribe node="${node}" jid="${own_jid}"/>
</pubsub>
</iq>`;
try {
await api.sendIQ(iq);
} catch (error) {
throw await parseErrorStanza(error);
}
},
/**
* Retrieves the subscriptions for the local user.
* @method _converse.api.pubsub.subscriptions
* @param {string} [jid] - The PubSub service JID.
* @param {string} [node] - The node to retrieve subscriptions from.
* @returns {Promise<import('./types').PubSubSubscription[]>}
*/
async subscriptions(jid, node) {
const service = jid || (await api.disco.entities.find(Strophe.NS.PUBSUB));
const own_jid = _converse.session.get('jid');
const iq = stx`
<iq xmlns="jabber:client"
type="get"
from="${own_jid}"
to="${service}">
<pubsub xmlns="${Strophe.NS.PUBSUB}">
<subscriptions${node ? ` node="${node}"` : ''}/>
</pubsub>
</iq>`;
const response = await api.sendIQ(iq);
const subs_el = response.querySelector('pubsub subscriptions');
if (!subs_el) return [];
const subs = Array.from(subs_el.querySelectorAll('subscription')).map((el) => ({
node: el.getAttribute('node'),
jid: el.getAttribute('jid'),
subscription: el.getAttribute('subscription'),
subid: el.hasAttribute('subid') ? el.getAttribute('subid') : undefined,
}));
return subs;
},
items: {
/**
* Retrieves items from a PubSub node (XEP-0060 § 6.5 "Retrieve Items").
*
* Supports requesting the most recent N items (`max_items`), specific
* item ids (`item_ids`), and paging through large result sets with
* XEP-0059 Result Set Management (`rsm`).
*
* @method _converse.api.pubsub.items.get
* @param {string|null} jid - The JID of the pubsub service where the node
* resides. Pass a falsy value to query your own PEP service (bare JID).
* @param {string} node - The node to retrieve items from
* @param {import('./types').PubSubItemsOptions} [options]
* @returns {Promise<import('./types').PubSubItemsResult>}
*/
async get(jid, node, options = {}) {
if (!node) throw new Error('api.pubsub.items.get: node value required');
const bare_jid = _converse.session.get('bare_jid');
const full_jid = _converse.session.get('jid');
const entity_jid = jid || bare_jid;
const { max_items, item_ids, rsm: rsm_options } = options;
const rsm = rsm_options ? new RSM(rsm_options) : null;
const stanza = stx`
<iq xmlns="jabber:client"
type="get"
from="${full_jid}"
to="${entity_jid}">
<pubsub xmlns="${Strophe.NS.PUBSUB}">
<items node="${node}"${max_items ? Stanza.unsafeXML(` max_items="${max_items}"`) : ''}>
${item_ids?.map((id) => stx`<item id="${id}"/>`) ?? ''}
</items>
${rsm ? Stanza.fromString(rsm.toString()) : ''}
</pubsub>
</iq>`;
let response;
try {
response = await api.sendIQ(stanza);
} catch (error) {
throw await parseErrorStanza(error);
}
const items = Array.from(response.querySelectorAll('pubsub > items > item'));
const set = response.querySelector('pubsub > set');
return {
items,
rsm: set ? new RSM({ ...(rsm_options ?? {}), xml: set }) : undefined,
};
},
},
},
};