2
0
Эх сурвалжийг харах

Add support for fetching a node's config

JC Brand 6 сар өмнө
parent
commit
7b2dd9e9d2

+ 1 - 0
karma.conf.js

@@ -37,6 +37,7 @@ module.exports = function(config) {
       { pattern: "src/headless/plugins/muc/tests/pruning.js", type: 'module' },
       { pattern: "src/headless/plugins/muc/tests/registration.js", type: 'module' },
       { pattern: "src/headless/plugins/ping/tests/ping.js", type: 'module' },
+      { pattern: "src/headless/plugins/pubsub/tests/config.js", type: 'module' },
       { pattern: "src/headless/plugins/roster/tests/presence.js", type: 'module' },
       { pattern: "src/headless/plugins/smacks/tests/smacks.js", type: 'module' },
       { pattern: "src/headless/plugins/status/tests/status.js", type: 'module' },

+ 3 - 3
src/headless/plugins/bookmarks/tests/bookmarks.js

@@ -59,7 +59,7 @@ describe("A bookmark", function () {
             () => IQ_stanzas.filter(s => sizzle('publish[node="urn:xmpp:bookmarks:1"]', s).length).pop());
 
         expect(sent_stanza).toEqualStanza(stx`
-            <iq from="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+            <iq from="${bare_jid}" to="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="urn:xmpp:bookmarks:1">
                         <item id="${muc1_jid}">
@@ -101,7 +101,7 @@ describe("A bookmark", function () {
             () => IQ_stanzas.filter(s => sizzle('publish[node="urn:xmpp:bookmarks:1"] conference[name="Balcony"]', s).length).pop());
 
         expect(sent_stanza).toEqualStanza(stx`
-            <iq from="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+            <iq from="${bare_jid}" to="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="urn:xmpp:bookmarks:1">
                         <item id="${muc2_jid}">
@@ -152,7 +152,7 @@ describe("A bookmark", function () {
             () => IQ_stanzas.filter(s => sizzle('publish[node="urn:xmpp:bookmarks:1"] conference[name="Garden"]', s).length).pop());
 
         expect(sent_stanza).toEqualStanza(stx`
-            <iq xmlns="jabber:client" type="set" from="${bare_jid}" id="${sent_stanza.getAttribute('id')}">
+            <iq xmlns="jabber:client" type="set" from="${bare_jid}" to="${bare_jid}" id="${sent_stanza.getAttribute('id')}">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="urn:xmpp:bookmarks:1">
                         <item id="${muc2_jid}">

+ 2 - 2
src/headless/plugins/bookmarks/tests/deprecated.js

@@ -31,7 +31,7 @@ describe("A bookmark", function () {
             () => IQ_stanzas.filter(s => sizzle('item[id="current"]', s).length).pop());
 
         expect(sent_stanza).toEqualStanza(stx`
-            <iq from="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+            <iq from="${bare_jid}" to="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="storage:bookmarks">
                         <item id="current">
@@ -75,7 +75,7 @@ describe("A bookmark", function () {
             () => IQ_stanzas.filter(s => sizzle('item[id="current"] conference[name="Balcony"]', s).length).pop());
 
         expect(sent_stanza).toEqualStanza(stx`
-            <iq from="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+            <iq from="${bare_jid}" to="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="storage:bookmarks">
                         <item id="current">

+ 79 - 9
src/headless/plugins/pubsub/api.js

@@ -6,6 +6,7 @@ import converse from '../../shared/api/public.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import log from '../../log.js';
+import { parseStanzaForPubSubConfig } from './parsers.js';
 
 const { Strophe, stx } = converse.env;
 
@@ -13,20 +14,88 @@ 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.configure
+             * @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) {
+                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}"><configure node="${node}"/></pubsub>
+                    </iq>`;
+
+                let response;
+                try {
+                    response = await api.sendIQ(stanza);
+                } catch (error) {
+                    throw error;
+                }
+                return parseStanzaForPubSubConfig(response);
+            },
+
+            /**
+             * Configures a PubSub node
+             * @method _converse.api.pubsub.configure
+             * @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<void|Element>}
+             */
+            async set(jid, node, config) {
+                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}">
+                        <configure node="${node}">
+                            <x xmlns="${Strophe.NS.XFORM}" type="submit">
+                                <field var="FORM_TYPE" type="hidden">
+                                    <value>${Strophe.NS.PUBSUB}#nodeconfig</value>
+                                </field>
+                                ${Object.entries(config).map(([k, v]) => stx`<field var="${k}"><value>${v}</value></field>`)}
+                            </x>
+                        </configure>
+                    </pubsub>
+                    </iq>`;
+
+                try {
+                    const response = await api.sendIQ(stanza);
+                    return response;
+                } catch (error) {
+                    throw error;
+                }
+            },
+        },
+
         /**
          * Publshes 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 {import('./types').PubSubConfigOptions} options The publisher options
+         * @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
@@ -42,7 +111,7 @@ export default {
                 <iq xmlns="jabber:client"
                     from="${bare_jid}"
                     type="set"
-                    ${entity_jid !== bare_jid ? `to="${entity_jid}"` : ''}>
+                    to="${entity_jid}">
                 <pubsub xmlns="${Strophe.NS.PUBSUB}">
                     <publish node="${node}">${item}</publish>
                     ${
@@ -73,14 +142,15 @@ export default {
             }
 
             // Check for #publish-options support.
-            // XEP-0223 says we need to check the server for support,
-            // but Prosody returns it on the bare jid.
             const supports_publish_options =
                 (await api.disco.supports(Strophe.NS.PUBSUB + '#publish-options', entity_jid)) ||
-                (await api.disco.supports(
-                    Strophe.NS.PUBSUB + '#publish-options',
-                    Strophe.getDomainFromJid(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) {
                 if (strict_options) {

+ 16 - 0
src/headless/plugins/pubsub/parsers.js

@@ -0,0 +1,16 @@
+import { parseXForm } from '../../shared/parsers.js';
+
+/**
+ * @param {Element} iq - An IQ result stanza
+ * @returns {import('./types').PubSubConfigOptions}
+ */
+export function parseStanzaForPubSubConfig(iq) {
+    return parseXForm(iq).fields.reduce((acc, f) => {
+        if (f.var.startsWith('pubsub#')) {
+            const key = f.var.replace(/^pubsub#/, '');
+            const value = (f.type === 'boolean') ? f.checked : (f.value ?? null);
+            acc[key] = value;
+        }
+        return acc;
+    }, {});
+}

+ 162 - 0
src/headless/plugins/pubsub/tests/config.js

@@ -0,0 +1,162 @@
+/* global mock, converse */
+const { Strophe, sizzle, stx, u } = converse.env;
+
+describe('The pubsub API', function () {
+    it(
+        "can be used to fetch a nodes's configuration settings",
+        mock.initConverse([], {}, async function (_converse) {
+            await mock.waitForRoster(_converse, 'current', 0);
+            const { api } = _converse;
+            const sent_stanzas = api.connection.get().sent_stanzas;
+            const own_jid = _converse.session.get('jid');
+
+            const node = 'princely_musings';
+            const pubsub_jid = 'pubsub.shakespeare.lit';
+            const promise = api.pubsub.config.get(pubsub_jid, node);
+            const sent_stanza = await u.waitUntil(() =>
+                sent_stanzas.filter((iq) => sizzle('pubsub configure', iq)).pop()
+            );
+
+            const response = stx`
+            <iq type='result'
+                xmlns="jabber:client"
+                from='${pubsub_jid}'
+                to='${own_jid}'
+                id="${sent_stanza.getAttribute('id')}">
+            <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
+                <configure node='${node}'>
+                <x xmlns='jabber:x:data' type='form'>
+                    <field var='FORM_TYPE' type='hidden'>
+                    <value>http://jabber.org/protocol/pubsub#node_config</value>
+                    </field>
+                    <field var='pubsub#title' type='text-single'
+                        label='A friendly name for the node'/>
+                    <field var='pubsub#deliver_notifications' type='boolean'
+                        label='Whether to deliver event notifications'>
+                    <value>true</value>
+                    </field>
+                    <field var='pubsub#deliver_payloads' type='boolean'
+                        label='Whether to deliver payloads with event notifications'>
+                    <value>true</value>
+                    </field>
+                    <field var='pubsub#notify_config' type='boolean'
+                        label='Notify subscribers when the node configuration changes'>
+                    <value>0</value>
+                    </field>
+                    <field var='pubsub#notify_delete' type='boolean'
+                        label='Notify subscribers when the node is deleted'>
+                    <value>false</value>
+                    </field>
+                    <field var='pubsub#notify_retract' type='boolean'
+                        label='Notify subscribers when items are removed from the node'>
+                    <value>false</value>
+                    </field>
+                    <field var='pubsub#notify_sub' type='boolean'
+                        label='Notify owners about new subscribers and unsubscribes'>
+                    <value>0</value>
+                    </field>
+                    <field var='pubsub#persist_items' type='boolean'
+                        label='Persist items to storage'>
+                    <value>1</value>
+                    </field>
+                    <field var='pubsub#max_items' type='text-single'
+                        label='Max # of items to persist. \`max\` for no specific limit other than a server imposed maximum.'>
+                    <value>10</value>
+                    </field>
+                    <field var='pubsub#item_expire' type='text-single'
+                        label='Time after which to automatically purge items. \`max\` for no specific limit other than a server imposed maximum.'>
+                    <value>604800</value>
+                    </field>
+                    <field var='pubsub#subscribe' type='boolean'
+                        label='Whether to allow subscriptions'>
+                    <value>1</value>
+                    </field>
+                    <field var='pubsub#access_model' type='list-single'
+                        label='Specify the subscriber model'>
+                    <option><value>authorize</value></option>
+                    <option><value>open</value></option>
+                    <option><value>presence</value></option>
+                    <option><value>roster</value></option>
+                    <option><value>whitelist</value></option>
+                    <value>open</value>
+                    </field>
+                    <field var='pubsub#roster_groups_allowed' type='list-multi'
+                        label='Roster groups allowed to subscribe'>
+                    <option><value>friends</value></option>
+                    <option><value>courtiers</value></option>
+                    <option><value>servants</value></option>
+                    <option><value>enemies</value></option>
+                    </field>
+                    <field var='pubsub#publish_model' type='list-single'
+                        label='Specify the publisher model'>
+                    <option><value>publishers</value></option>
+                    <option><value>subscribers</value></option>
+                    <option><value>open</value></option>
+                    <value>publishers</value>
+                    </field>
+                    <field var='pubsub#purge_offline' type='boolean'
+                        label='Purge all items when the relevant publisher goes offline?'>
+                    <value>0</value>
+                    </field>
+                    <field var='pubsub#max_payload_size' type='text-single'
+                        label='Max Payload size in bytes'>
+                    <value>1028</value>
+                    </field>
+                    <field var='pubsub#send_last_published_item' type='list-single'
+                        label='When to send the last published item'>
+                    <option label='Never'><value>never</value></option>
+                    <option label='When a new subscription is processed'><value>on_sub</value></option>
+                    <option label='When a new subscription is processed and whenever a subscriber comes online'>
+                        <value>on_sub_and_presence</value>
+                    </option>
+                    <value>never</value>
+                    </field>
+                    <field var='pubsub#presence_based_delivery' type='boolean'
+                        label='Deliver event notifications only to available users'>
+                    <value>0</value>
+                    </field>
+                    <field var='pubsub#notification_type' type='list-single'
+                        label='Specify the delivery style for event notifications'>
+                    <option><value>normal</value></option>
+                    <option><value>headline</value></option>
+                    <value>headline</value>
+                    </field>
+                    <field var='pubsub#type' type='text-single'
+                        label='Specify the semantic type of payload data to be provided at this node.'>
+                    <value>urn:example:e2ee:bundle</value>
+                    </field>
+                    <field var='pubsub#dataform_xslt' type='text-single' label='Payload XSLT'/>
+                </x>
+                </configure>
+            </pubsub>
+            </iq>`;
+
+            _converse.api.connection.get()._dataRecv(mock.createRequest(response));
+
+            const result = await promise;
+            expect(result).toEqual({
+                access_model: null,
+                dataform_xslt: null,
+                deliver_notifications: true,
+                deliver_payloads: true,
+                item_expire: '604800',
+                max_items: '10',
+                max_payload_size: '1028',
+                notification_type: null,
+                notify_config: false,
+                notify_delete: false,
+                notify_retract: false,
+                notify_sub: false,
+                persist_items: true,
+                presence_based_delivery: false,
+                publish_model: null,
+                purge_offline: false,
+                roster_groups_allowed: null,
+                send_last_published_item: null,
+                subscribe: true,
+                title: null,
+                type: 'urn:example:e2ee:bundle',
+            });
+        })
+    );
+});

+ 20 - 20
src/headless/plugins/pubsub/types.ts

@@ -1,42 +1,42 @@
 export type PubSubConfigOptions = {
-    access_model: 'authorize' | 'open' | 'presence' | 'roster' | 'whitelist';
+    access_model?: 'authorize' | 'open' | 'presence' | 'roster' | 'whitelist';
     // Payload XSLT
     dataform_xslt?: string;
-    deliver_notifications: boolean;
+    deliver_notifications?: boolean;
     // Whether to deliver payloads with event notifications
-    deliver_payloads: boolean;
+    deliver_payloads?: boolean;
     // Time after which to automatically purge items. `max` for no specific limit other than a server imposed maximum.
-    item_expire: string;
+    item_expire?: string;
     // Max # of items to persist. `max` for no specific limit other than a server imposed maximum.
-    max_items: string;
+    max_items?: string;
     // Max Payload size in bytes
-    max_payload_size: string;
-    notification_type: 'normal' | 'headline';
+    max_payload_size?: string;
+    notification_type?: 'normal' | 'headline';
     // Notify subscribers when the node configuration changes
-    notify_config: boolean;
+    notify_config?: boolean;
     // Notify subscribers when the node is deleted
-    notify_delete: boolean;
+    notify_delete?: boolean;
     // Notify subscribers when items are removed from the node
-    notify_retract: boolean;
+    notify_retract?: boolean;
     // <field var='notify_sub' type='boolean'
-    notify_sub: boolean;
+    notify_sub?: boolean;
     // <field var='persist_items' type='boolean'
-    persist_items: boolean;
+    persist_items?: boolean;
     // Deliver event notifications only to available users
-    presence_based_delivery: boolean;
-    publish_model: 'publishers' | 'subscribers' | 'open';
+    presence_based_delivery?: boolean;
+    publish_model?: 'publishers' | 'subscribers' | 'open';
     // Purge all items when the relevant publisher goes offline?
-    purge_offline: boolean;
-    roster_groups_allowed: string[];
+    purge_offline?: boolean;
+    roster_groups_allowed?: string[];
     // When to send the last published item
     // - Never
     // - When a new subscription is processed
     // - When a new subscription is processed and whenever a subscriber comes online
-    send_last_published_item: 'never' | 'on_sub' | 'on_sub_and_presence';
+    send_last_published_item?: 'never' | 'on_sub' | 'on_sub_and_presence';
     // Whether to allow subscriptions
-    subscribe: boolean;
+    subscribe?: boolean;
     // A friendly name for the node'/>
-    title: string;
+    title?: string;
     // Specify the semantic type of payload data to be provided at this node.
-    type: string;
+    type?: string;
 };

+ 6 - 7
src/headless/plugins/smacks/utils.js

@@ -5,8 +5,7 @@ import log from '../../log.js';
 import { getOpenPromise } from '@converse/openpromise';
 import { isTestEnv } from '../../utils/session.js';
 
-const { Strophe } = converse.env;
-const u = converse.env.utils;
+const { Strophe, u, stx } = converse.env;
 
 function isStreamManagementSupported () {
     if (api.connection.isType('bosh') && !isTestEnv()) {
@@ -51,7 +50,7 @@ function handleAck (el) {
 function sendAck () {
     if (_converse.session.get('smacks_enabled')) {
         const h = _converse.session.get('num_stanzas_handled');
-        const stanza = u.toStanza(`<a xmlns="${Strophe.NS.SM}" h="${h}"/>`);
+        const stanza = stx`<a xmlns="${Strophe.NS.SM}" h="${h}"/>`;
         api.send(stanza);
     }
     return true;
@@ -155,7 +154,7 @@ function resendUnackedStanzas () {
     // service worker or handling IQ[type="result"] stanzas
     // differently, more like push stanzas, so that they don't need
     // explicit handlers.
-    stanzas.forEach(s => api.send(s));
+    stanzas.forEach((s) => api.send(u.toStanza(s)));
 }
 
 /**
@@ -180,7 +179,7 @@ async function sendResumeStanza () {
 
     const previous_id = _converse.session.get('smacks_stream_id');
     const h = _converse.session.get('num_stanzas_handled');
-    const stanza = u.toStanza(`<resume xmlns="${Strophe.NS.SM}" h="${h}" previd="${previous_id}"/>`);
+    const stanza = stx`<resume xmlns="${Strophe.NS.SM}" h="${h}" previd="${previous_id}"/>`;
     api.send(stanza);
     connection.flush();
     await promise;
@@ -197,7 +196,7 @@ export async function sendEnableStanza () {
         connection._addSysHandler(el => promise.resolve(onFailedStanza(el)), Strophe.NS.SM, 'failed');
 
         const resume = api.connection.isType('websocket') || isTestEnv();
-        const stanza = u.toStanza(`<enable xmlns="${Strophe.NS.SM}" resume="${resume}"/>`);
+        const stanza = stx`<enable xmlns="${Strophe.NS.SM}" resume="${resume}"/>`;
         api.send(stanza);
         connection.flush();
         await promise;
@@ -246,7 +245,7 @@ export function onStanzaSent (stanza) {
             const num = _converse.session.get('num_stanzas_since_last_ack') + 1;
             if (num % max_unacked === 0) {
                 // Request confirmation of sent stanzas
-                api.send(u.toStanza(`<r xmlns="${Strophe.NS.SM}"/>`));
+                api.send(stx`<r xmlns="${Strophe.NS.SM}"/>`);
             }
             _converse.session.save({ 'num_stanzas_since_last_ack': num });
         }

+ 22 - 29
src/headless/shared/api/send.js

@@ -1,17 +1,16 @@
-/**
- * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder
- */
 import _converse from '../_converse.js';
 import log from '../../log.js';
-import { Strophe, toStanza } from 'strophe.js';
+import { Strophe } from 'strophe.js';
 import { TimeoutError } from '../errors.js';
 
 export default {
     /**
+     * @typedef {import('strophe.js').Builder} Builder
+     *
      * Allows you to send XML stanzas.
      * @method _converse.api.send
-     * @param {Element|Strophe.Builder} stanza
-     * @return {void}
+     * @param {Element|Builder} stanza
+     * @returns {void}
      * @example
      * const msg = converse.env.$msg({
      *     'from': 'juliet@example.com/balcony',
@@ -20,31 +19,26 @@ export default {
      * });
      * _converse.api.send(msg);
      */
-    send (stanza) {
+    send(stanza) {
         const { api } = _converse;
         if (!api.connection.connected()) {
             log.warn("Not sending stanza because we're not connected!");
             log.warn(Strophe.serialize(stanza));
             return;
         }
-        if (typeof stanza === 'string') {
-            stanza = toStanza(stanza);
-        } else if (stanza?.tree) {
-            stanza = stanza.tree();
-        }
-
-        if (stanza.tagName === 'iq') {
-            return api.sendIQ(stanza);
+        const el = stanza instanceof Element ? stanza : stanza.tree();
+        if (el.tagName === 'iq') {
+            return api.sendIQ(el);
         } else {
-            api.connection.get().send(stanza);
-            api.trigger('send', stanza);
+            api.connection.get().send(el);
+            api.trigger('send', el);
         }
     },
 
     /**
      * Send an IQ stanza
      * @method _converse.api.sendIQ
-     * @param {Element|Strophe.Builder} stanza
+     * @param {Element|Builder} stanza
      * @param {number} [timeout] - The default timeout value is taken from
      *  the `stanza_timeout` configuration setting.
      * @param {boolean} [reject=true] - Whether an error IQ should cause the promise
@@ -54,9 +48,8 @@ export default {
      *  If the IQ stanza being sent is of type `result` or `error`, there's
      *  nothing to wait for, so an already resolved promise is returned.
      */
-    sendIQ (stanza, timeout, reject=true) {
+    sendIQ(stanza, timeout, reject = true) {
         const { api } = _converse;
-
         if (!api.connection.connected()) {
             throw new Error("Not sending IQ stanza because we're not connected!");
         }
@@ -64,26 +57,26 @@ export default {
         const connection = api.connection.get();
 
         let promise;
-        stanza = stanza.tree?.() ?? stanza;
-        if (['get', 'set'].includes(stanza.getAttribute('type'))) {
+        const el = stanza instanceof Element ? stanza : stanza.tree();
+        if (['get', 'set'].includes(el.getAttribute('type'))) {
             timeout = timeout || api.settings.get('stanza_timeout');
             if (reject) {
-                promise = new Promise((resolve, reject) => connection.sendIQ(stanza, resolve, reject, timeout));
+                promise = new Promise((resolve, reject) => connection.sendIQ(el, resolve, reject, timeout));
                 promise.catch((e) => {
                     if (e === null) {
                         throw new TimeoutError(
-                            `Timeout error after ${timeout}ms for the following IQ stanza: ${Strophe.serialize(stanza)}`
+                            `Timeout error after ${timeout}ms for the following IQ stanza: ${Strophe.serialize(el)}`
                         );
                     }
                 });
             } else {
-                promise = new Promise((resolve) => connection.sendIQ(stanza, resolve, resolve, timeout));
+                promise = new Promise((resolve) => connection.sendIQ(el, resolve, resolve, timeout));
             }
         } else {
-            connection.sendIQ(stanza);
+            connection.sendIQ(el);
             promise = Promise.resolve();
         }
-        api.trigger('send', stanza);
+        api.trigger('send', el);
         return promise;
-    }
-}
+    },
+};

+ 3 - 3
src/headless/shared/parsers.js

@@ -480,9 +480,9 @@ export function getInputType(field) {
 }
 
 /**
-* @param {Element} stanza
-* @returns {import('./types').XForm}
-*/
+ * @param {Element} stanza
+ * @returns {import('./types').XForm}
+ */
 export function parseXForm(stanza) {
     const xs = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, stanza);
     if (xs.length > 1) {

+ 20 - 2
src/headless/types/plugins/pubsub/api.d.ts

@@ -1,13 +1,31 @@
 declare namespace _default {
     namespace pubsub {
+        namespace config {
+            /**
+             * Fetches the configuration for a PubSub node
+             * @method _converse.api.pubsub.configure
+             * @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>}
+             */
+            function get(jid: string, node: string): Promise<import("./types").PubSubConfigOptions>;
+            /**
+             * Configures a PubSub node
+             * @method _converse.api.pubsub.configure
+             * @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<void|Element>}
+             */
+            function set(jid: string, node: string, config: import("./types").PubSubConfigOptions): Promise<void | Element>;
+        }
         /**
          * Publshes 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 {import('./types').PubSubConfigOptions} options The publisher options
+         * @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

+ 6 - 0
src/headless/types/plugins/pubsub/parsers.d.ts

@@ -0,0 +1,6 @@
+/**
+ * @param {Element} iq - An IQ result stanza
+ * @returns {import('./types').PubSubConfigOptions}
+ */
+export function parseStanzaForPubSubConfig(iq: Element): import("./types").PubSubConfigOptions;
+//# sourceMappingURL=parsers.d.ts.map

+ 20 - 20
src/headless/types/plugins/pubsub/types.d.ts

@@ -1,24 +1,24 @@
 export type PubSubConfigOptions = {
-    access_model: 'authorize' | 'open' | 'presence' | 'roster' | 'whitelist';
+    access_model?: 'authorize' | 'open' | 'presence' | 'roster' | 'whitelist';
     dataform_xslt?: string;
-    deliver_notifications: boolean;
-    deliver_payloads: boolean;
-    item_expire: string;
-    max_items: string;
-    max_payload_size: string;
-    notification_type: 'normal' | 'headline';
-    notify_config: boolean;
-    notify_delete: boolean;
-    notify_retract: boolean;
-    notify_sub: boolean;
-    persist_items: boolean;
-    presence_based_delivery: boolean;
-    publish_model: 'publishers' | 'subscribers' | 'open';
-    purge_offline: boolean;
-    roster_groups_allowed: string[];
-    send_last_published_item: 'never' | 'on_sub' | 'on_sub_and_presence';
-    subscribe: boolean;
-    title: string;
-    type: string;
+    deliver_notifications?: boolean;
+    deliver_payloads?: boolean;
+    item_expire?: string;
+    max_items?: string;
+    max_payload_size?: string;
+    notification_type?: 'normal' | 'headline';
+    notify_config?: boolean;
+    notify_delete?: boolean;
+    notify_retract?: boolean;
+    notify_sub?: boolean;
+    persist_items?: boolean;
+    presence_based_delivery?: boolean;
+    publish_model?: 'publishers' | 'subscribers' | 'open';
+    purge_offline?: boolean;
+    roster_groups_allowed?: string[];
+    send_last_published_item?: 'never' | 'on_sub' | 'on_sub_and_presence';
+    subscribe?: boolean;
+    title?: string;
+    type?: string;
 };
 //# sourceMappingURL=types.d.ts.map

+ 9 - 8
src/headless/types/shared/api/send.d.ts

@@ -1,9 +1,13 @@
 declare namespace _default {
+    /**
+     * @typedef {import('strophe.js').Builder} Builder
+     * @typedef {import('strophe.js').Stanza} Stanza
+     */
     /**
      * Allows you to send XML stanzas.
      * @method _converse.api.send
-     * @param {Element|Strophe.Builder} stanza
-     * @return {void}
+     * @param {Element|Builder|Stanza} stanza
+     * @returns {void}
      * @example
      * const msg = converse.env.$msg({
      *     'from': 'juliet@example.com/balcony',
@@ -12,11 +16,11 @@ declare namespace _default {
      * });
      * _converse.api.send(msg);
      */
-    function send(stanza: Element | Strophe.Builder): void;
+    function send(stanza: Element | import("strophe.js").Builder | import("strophe.js").Stanza): void;
     /**
      * Send an IQ stanza
      * @method _converse.api.sendIQ
-     * @param {Element|Strophe.Builder} stanza
+     * @param {Element|Builder|Stanza} stanza
      * @param {number} [timeout] - The default timeout value is taken from
      *  the `stanza_timeout` configuration setting.
      * @param {boolean} [reject=true] - Whether an error IQ should cause the promise
@@ -26,10 +30,7 @@ declare namespace _default {
      *  If the IQ stanza being sent is of type `result` or `error`, there's
      *  nothing to wait for, so an already resolved promise is returned.
      */
-    function sendIQ(stanza: Element | Strophe.Builder, timeout?: number, reject?: boolean): Promise<any>;
+    function sendIQ(stanza: Element | import("strophe.js").Builder | import("strophe.js").Stanza, timeout?: number, reject?: boolean): Promise<any>;
 }
 export default _default;
-export namespace Strophe {
-    type Builder = any;
-}
 //# sourceMappingURL=send.d.ts.map

+ 3 - 3
src/headless/types/shared/parsers.d.ts

@@ -132,9 +132,9 @@ export function isArchived(original_stanza: Element): boolean;
  */
 export function getInputType(field: Element): any;
 /**
-* @param {Element} stanza
-* @returns {import('./types').XForm}
-*/
+ * @param {Element} stanza
+ * @returns {import('./types').XForm}
+ */
 export function parseXForm(stanza: Element): import("./types").XForm;
 export class StanzaParseError extends Error {
     /**

+ 10 - 2
src/plugins/bookmark-views/tests/bookmarks.js

@@ -49,7 +49,11 @@ describe("A chat room", function () {
         const sent_stanza = await u.waitUntil(
             () => IQ_stanzas.filter(s => sizzle('iq publish[node="urn:xmpp:bookmarks:1"]', s).length).pop());
         expect(sent_stanza).toEqualStanza(
-            stx`<iq from="romeo@montague.lit" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+        stx`<iq from="${_converse.bare_jid}"
+                    to="${_converse.bare_jid}"
+                    id="${sent_stanza.getAttribute('id')}"
+                    type="set"
+                    xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="urn:xmpp:bookmarks:1">
                         <item id="${view.model.get('jid')}">
@@ -255,7 +259,11 @@ describe("A chat room", function () {
             // only bookmark).
             const sent_stanza = _converse.api.connection.get().IQ_stanzas.pop();
             expect(sent_stanza).toEqualStanza(
-                stx`<iq from="romeo@montague.lit" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+                stx`<iq from="${_converse.bare_jid}"
+                        to="${_converse.bare_jid}"
+                        id="${sent_stanza.getAttribute('id')}"
+                        type="set"
+                        xmlns="jabber:client">
                     <pubsub xmlns="http://jabber.org/protocol/pubsub">
                         <publish node="urn:xmpp:bookmarks:1"/>
                         <publish-options>

+ 10 - 2
src/plugins/bookmark-views/tests/deprecated.js

@@ -45,7 +45,11 @@ describe("A chat room", function () {
         const sent_stanza = await u.waitUntil(
             () => IQ_stanzas.filter(s => sizzle('iq publish[node="storage:bookmarks"]', s).length).pop());
         expect(sent_stanza).toEqualStanza(
-            stx`<iq from="romeo@montague.lit" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+            stx`<iq to="romeo@montague.lit"
+                    from="romeo@montague.lit"
+                    id="${sent_stanza.getAttribute('id')}"
+                    type="set"
+                    xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="storage:bookmarks">
                         <item id="current">
@@ -153,7 +157,11 @@ describe("A chat room", function () {
             // only bookmark).
             const sent_stanza = _converse.api.connection.get().IQ_stanzas.pop();
             expect(sent_stanza).toEqualStanza(
-                stx`<iq from="romeo@montague.lit" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+                stx`<iq from="${_converse.bare_jid}"
+                        to="${_converse.bare_jid}"
+                        id="${sent_stanza.getAttribute('id')}"
+                        type="set"
+                        xmlns="jabber:client">
                     <pubsub xmlns="http://jabber.org/protocol/pubsub">
                         <publish node="storage:bookmarks">
                             <item id="current"><storage xmlns="storage:bookmarks"/></item>

+ 1 - 1
src/plugins/muc-views/tests/muc.js

@@ -556,7 +556,7 @@ describe("Groupchats", function () {
             const room_creation_promise = await _converse.api.rooms.open(muc_jid, {nick});
             await mock.getRoomFeatures(_converse, muc_jid);
             const sent_stanzas = _converse.api.connection.get().sent_stanzas;
-            await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop());
+            await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length));
 
             const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
             await _converse.api.waitUntil('chatRoomViewInitialized');

+ 8 - 5
src/plugins/omemo/tests/omemo.js

@@ -442,7 +442,7 @@ describe("The OMEMO module", function() {
         await u.waitUntil(() => _converse.state.omemo_store);
         iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse), 1000);
         expect(iq_stanza).toEqualStanza(
-            stx`<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" type="set" xmlns="jabber:client">
+            stx`<iq to="${_converse.bare_jid}" from="${_converse.bare_jid}" id="${iq_stanza.getAttribute("id")}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="eu.siacs.conversations.axolotl.bundles:123456789">
                         <item>
@@ -643,7 +643,8 @@ describe("The OMEMO module", function() {
         // Check that our own device is added again, but that removed
         // devices are not added.
         expect(iq_stanza).toEqualStanza(
-            stx`<iq from="romeo@montague.lit"
+            stx`<iq from="${_converse.bare_jid}"
+                    to="${_converse.bare_jid}"
                     id="${iq_stanza.getAttribute(`id`)}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="eu.siacs.conversations.axolotl.devicelist">
@@ -886,7 +887,8 @@ describe("The OMEMO module", function() {
 
         iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse));
         expect(iq_stanza).toEqualStanza(
-            stx`<iq from="romeo@montague.lit"
+            stx`<iq from="${_converse.bare_jid}"
+                    to="${_converse.bare_jid}"
                     id="${iq_stanza.getAttribute("id")}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="eu.siacs.conversations.axolotl.bundles:123456789">
@@ -975,7 +977,8 @@ describe("The OMEMO module", function() {
         // Check that own device was published
         iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse));
         expect(iq_stanza).toEqualStanza(
-            stx`<iq from="romeo@montague.lit"
+            stx`<iq from="${_converse.bare_jid}"
+                    to="${_converse.bare_jid}"
                     id="${iq_stanza.getAttribute(`id`)}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="eu.siacs.conversations.axolotl.devicelist">
@@ -1007,7 +1010,7 @@ describe("The OMEMO module", function() {
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
         const iq_el = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse));
-        expect(iq_el.getAttributeNames().sort().join()).toBe(["from", "id", "type", "xmlns"].sort().join());
+        expect(iq_el.getAttributeNames().sort().join()).toBe(["to", "from", "id", "type", "xmlns"].sort().join());
         expect(iq_el.querySelector('prekeys').childNodes.length).toBe(100);
 
         const signed_prekeys = iq_el.querySelectorAll('signedPreKeyPublic');