Forráskód Böngészése

Refactor the pubsub plugin

- Turn it into a folder
- Use the stx tagged template literal
JC Brand 6 hónapja
szülő
commit
acc54271a1

+ 1 - 2
src/headless/index.js

@@ -32,9 +32,8 @@ export { MAMPlaceholderMessage } from './plugins/mam/index.js';
 // XEP-0045 Multi-user chat
 // XEP-0045 Multi-user chat
 export { MUCMessage, MUCMessages, MUC, MUCOccupant, MUCOccupants } from './plugins/muc/index.js';
 export { MUCMessage, MUCMessages, MUC, MUCOccupant, MUCOccupants } from './plugins/muc/index.js';
 
 
-
 import './plugins/ping/index.js'; // XEP-0199 XMPP Ping
 import './plugins/ping/index.js'; // XEP-0199 XMPP Ping
-import './plugins/pubsub.js'; // XEP-0060 Pubsub
+import './plugins/pubsub/index.js'; // XEP-0060 Pubsub
 
 
 // RFC-6121 Contacts Roster
 // RFC-6121 Contacts Roster
 export { RosterContact, RosterContacts, RosterFilter, Presence, Presences } from './plugins/roster/index.js';
 export { RosterContact, RosterContacts, RosterFilter, Presence, Presences } from './plugins/roster/index.js';

+ 49 - 59
src/headless/plugins/bookmarks/collection.js

@@ -1,9 +1,9 @@
 /**
 /**
  * @typedef {import('../muc/muc.js').default} MUC
  * @typedef {import('../muc/muc.js').default} MUC
  */
  */
+import { Stanza } from 'strophe.js';
 import { Collection } from '@converse/skeletor';
 import { Collection } from '@converse/skeletor';
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
-import '../../plugins/muc/index.js';
 import Bookmark from './model.js';
 import Bookmark from './model.js';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
@@ -11,7 +11,7 @@ import converse from '../../shared/api/public.js';
 import log from '../../log.js';
 import log from '../../log.js';
 import { initStorage } from '../../utils/storage.js';
 import { initStorage } from '../../utils/storage.js';
 import { parseStanzaForBookmarks } from './parsers.js';
 import { parseStanzaForBookmarks } from './parsers.js';
-import { Stanza } from 'strophe.js';
+import '../../plugins/muc/index.js';
 
 
 const { Strophe, sizzle, stx } = converse.env;
 const { Strophe, sizzle, stx } = converse.env;
 
 
@@ -96,69 +96,59 @@ class Bookmarks extends Collection {
     }
     }
 
 
     /**
     /**
-     * @returns {Promise<Stanza>}
+     * @param {'urn:xmpp:bookmarks:1'|'storage:bookmarks'} node
+     * @returns {Stanza|Stanza[]}
      */
      */
-    async createPublishNode() {
-        const bare_jid = _converse.session.get('bare_jid');
-        if (await api.disco.supports(`${Strophe.NS.BOOKMARKS2}#compat`, bare_jid)) {
-            return stx`
-                <publish node="${Strophe.NS.BOOKMARKS2}">
-                    ${this.map(
-                        /** @param {MUC} model */ (model) => {
-                            const extensions = model.get('extensions') ?? [];
-                            return stx`<item id="${model.get('jid')}">
-                            <conference xmlns="${Strophe.NS.BOOKMARKS2}"
-                                        name="${model.get('name')}"
-                                        autojoin="${model.get('autojoin')}">
-                                    ${model.get('nick') ? stx`<nick>${model.get('nick')}</nick>` : ''}
-                                    ${model.get('password') ? stx`<password>${model.get('password')}</password>` : ''}
-                                ${
-                                    extensions.length
-                                        ? stx`<extensions>${extensions.map((e) => Stanza.unsafeXML(e))}</extensions>`
-                                        : ''
-                                };
-                                </conference>
-                            </item>`;
-                        }
-                    )}
-                </publish>`;
+    getPublishedItems(node) {
+        if (node === Strophe.NS.BOOKMARKS2) {
+            return this.map(
+                /** @param {MUC} model */ (model) => {
+                    const extensions = model.get('extensions') ?? [];
+                    return stx`<item id="${model.get('jid')}">
+                    <conference xmlns="${Strophe.NS.BOOKMARKS2}"
+                                name="${model.get('name')}"
+                                autojoin="${model.get('autojoin')}">
+                            ${model.get('nick') ? stx`<nick>${model.get('nick')}</nick>` : ''}
+                            ${model.get('password') ? stx`<password>${model.get('password')}</password>` : ''}
+                        ${
+                            extensions.length
+                                ? stx`<extensions>${extensions.map((e) => Stanza.unsafeXML(e))}</extensions>`
+                                : ''
+                        };
+                        </conference>
+                    </item>`;
+                }
+            );
         } else {
         } else {
-            return stx`
-                <publish node="${Strophe.NS.BOOKMARKS}">
-                    <item id="current">
-                        <storage xmlns="${Strophe.NS.BOOKMARKS}">
-                        ${this.map(
-                            /** @param {MUC} model */ (model) =>
-                                stx`<conference name="${model.get('name')}" autojoin="${model.get('autojoin')}"
-                                jid="${model.get('jid')}">
-                                ${model.get('nick') ? stx`<nick>${model.get('nick')}</nick>` : ''}
-                                ${model.get('password') ? stx`<password>${model.get('password')}</password>` : ''}
-                            </conference>`
-                        )}
-                        </storage>
-                    </item>
-                </publish>`;
+            return stx`<item id="current">
+                <storage xmlns="${Strophe.NS.BOOKMARKS}">
+                ${this.map(
+                    /** @param {MUC} model */ (model) =>
+                        stx`<conference name="${model.get('name')}" autojoin="${model.get('autojoin')}"
+                        jid="${model.get('jid')}">
+                        ${model.get('nick') ? stx`<nick>${model.get('nick')}</nick>` : ''}
+                        ${model.get('password') ? stx`<password>${model.get('password')}</password>` : ''}
+                    </conference>`
+                )}
+                </storage>
+            </item>`;
         }
         }
     }
     }
 
 
+    /**
+     * @returns {Promise<void|Element>}
+     */
     async sendBookmarkStanza() {
     async sendBookmarkStanza() {
-        return api.sendIQ(stx`
-            <iq type="set" from="${api.connection.get().jid}" xmlns="jabber:client">
-                <pubsub xmlns="${Strophe.NS.PUBSUB}">
-                    ${await this.createPublishNode()}
-                    <publish-options>
-                        <x xmlns="${Strophe.NS.XFORM}" type="submit">
-                            <field var='FORM_TYPE' type='hidden'>
-                                <value>${Strophe.NS.PUBSUB}#publish-options</value>
-                            </field>
-                            <field var='pubsub#persist_items'><value>true</value></field>
-                            <field var='pubsub#max_items'><value>max</value></field>
-                            <field var='pubsub#send_last_published_item'><value>never</value></field>
-                            <field var='pubsub#access_model'><value>whitelist</value></field>
-                        </x>
-                    </publish-options>
-                </pubsub>
-            </iq>`);
+        const bare_jid = _converse.session.get('bare_jid');
+        const node = (await api.disco.supports(`${Strophe.NS.BOOKMARKS2}#compat`, bare_jid))
+            ? Strophe.NS.BOOKMARKS2
+            : Strophe.NS.BOOKMARKS;
+        return api.pubsub.publish(null, node, this.getPublishedItems(node), {
+            persist_items: true,
+            max_items: 'max',
+            send_last_published_item: 'never',
+            access_model: 'whitelist',
+        });
     }
     }
 
 
     /**
     /**

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

@@ -14,7 +14,7 @@ describe("A chat room", function () {
             const jid = 'theplay@conference.shakespeare.lit';
             const jid = 'theplay@conference.shakespeare.lit';
             const { bookmarks } = _converse.state;
             const { bookmarks } = _converse.state;
             const model = bookmarks.create({
             const model = bookmarks.create({
-                'jid': jid,
+                jid,
                 'autojoin': false,
                 'autojoin': false,
                 'name':  'The Play',
                 'name':  'The Play',
                 'nick': ''
                 'nick': ''
@@ -22,7 +22,7 @@ describe("A chat room", function () {
             expect(_converse.api.rooms.create).not.toHaveBeenCalled();
             expect(_converse.api.rooms.create).not.toHaveBeenCalled();
             bookmarks.remove(model);
             bookmarks.remove(model);
             bookmarks.create({
             bookmarks.create({
-                'jid': jid,
+                jid,
                 'autojoin': true,
                 'autojoin': true,
                 'name':  'Hamlet',
                 'name':  'Hamlet',
                 'nick': ''
                 'nick': ''
@@ -43,7 +43,7 @@ describe("A bookmark", function () {
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.waitUntilBookmarksReturned(_converse);
         await mock.waitUntilBookmarksReturned(_converse);
 
 
-        const jid = _converse.session.get('jid');
+        const bare_jid = _converse.session.get('bare_jid');
         const muc1_jid = 'theplay@conference.shakespeare.lit';
         const muc1_jid = 'theplay@conference.shakespeare.lit';
         const { bookmarks } = _converse.state;
         const { bookmarks } = _converse.state;
 
 
@@ -59,7 +59,7 @@ describe("A bookmark", function () {
             () => IQ_stanzas.filter(s => sizzle('publish[node="urn:xmpp:bookmarks:1"]', s).length).pop());
             () => IQ_stanzas.filter(s => sizzle('publish[node="urn:xmpp:bookmarks:1"]', s).length).pop());
 
 
         expect(sent_stanza).toEqualStanza(stx`
         expect(sent_stanza).toEqualStanza(stx`
-            <iq from="${jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+            <iq from="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="urn:xmpp:bookmarks:1">
                     <publish node="urn:xmpp:bookmarks:1">
                         <item id="${muc1_jid}">
                         <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());
             () => IQ_stanzas.filter(s => sizzle('publish[node="urn:xmpp:bookmarks:1"] conference[name="Balcony"]', s).length).pop());
 
 
         expect(sent_stanza).toEqualStanza(stx`
         expect(sent_stanza).toEqualStanza(stx`
-            <iq from="${jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+            <iq from="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="urn:xmpp:bookmarks:1">
                     <publish node="urn:xmpp:bookmarks:1">
                         <item id="${muc2_jid}">
                         <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());
             () => IQ_stanzas.filter(s => sizzle('publish[node="urn:xmpp:bookmarks:1"] conference[name="Garden"]', s).length).pop());
 
 
         expect(sent_stanza).toEqualStanza(stx`
         expect(sent_stanza).toEqualStanza(stx`
-            <iq xmlns="jabber:client" type="set" from="${jid}" id="${sent_stanza.getAttribute('id')}">
+            <iq xmlns="jabber:client" type="set" from="${bare_jid}" id="${sent_stanza.getAttribute('id')}">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="urn:xmpp:bookmarks:1">
                     <publish node="urn:xmpp:bookmarks:1">
                         <item id="${muc2_jid}">
                         <item id="${muc2_jid}">

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

@@ -15,7 +15,7 @@ describe("A bookmark", function () {
             'storage:bookmarks'
             'storage:bookmarks'
         );
         );
 
 
-        const jid = _converse.session.get('jid');
+        const bare_jid = _converse.session.get('bare_jid');
         const muc1_jid = 'theplay@conference.shakespeare.lit';
         const muc1_jid = 'theplay@conference.shakespeare.lit';
         const { bookmarks } = _converse.state;
         const { bookmarks } = _converse.state;
 
 
@@ -31,7 +31,7 @@ describe("A bookmark", function () {
             () => IQ_stanzas.filter(s => sizzle('item[id="current"]', s).length).pop());
             () => IQ_stanzas.filter(s => sizzle('item[id="current"]', s).length).pop());
 
 
         expect(sent_stanza).toEqualStanza(stx`
         expect(sent_stanza).toEqualStanza(stx`
-            <iq from="${jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+            <iq from="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="storage:bookmarks">
                     <publish node="storage:bookmarks">
                         <item id="current">
                         <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());
             () => IQ_stanzas.filter(s => sizzle('item[id="current"] conference[name="Balcony"]', s).length).pop());
 
 
         expect(sent_stanza).toEqualStanza(stx`
         expect(sent_stanza).toEqualStanza(stx`
-            <iq from="${jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
+            <iq from="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                 <pubsub xmlns="http://jabber.org/protocol/pubsub">
                     <publish node="storage:bookmarks">
                     <publish node="storage:bookmarks">
                         <item id="current">
                         <item id="current">

+ 0 - 93
src/headless/plugins/pubsub.js

@@ -1,93 +0,0 @@
-/**
- * @module converse-pubsub
- * @copyright The Converse.js contributors
- * @license Mozilla Public License (MPLv2)
- * @typedef {import('strophe.js').Builder} Strophe.Builder
- */
-import "./disco/index.js";
-import _converse from '../shared/_converse.js';
-import api from '../shared/api/index.js';
-import converse from '../shared/api/public.js';
-import log from "../log.js";
-
-const { Strophe, $iq } = converse.env;
-
-Strophe.addNamespace('PUBSUB_ERROR', Strophe.NS.PUBSUB+"#errors");
-
-
-converse.plugins.add('converse-pubsub', {
-
-    dependencies: ["converse-disco"],
-
-    initialize () {
-
-        /************************ BEGIN API ************************/
-        // We extend the default converse.js API to add methods specific to MUC groupchats.
-        Object.assign(_converse.api, {
-            /**
-             * The "pubsub" namespace groups methods relevant to PubSub
-             *
-             * @namespace _converse.api.pubsub
-             * @memberOf _converse.api
-             */
-            pubsub: {
-                /**
-                 * 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 {Strophe.Builder} item The Strophe.Builder representation of the XML element being published
-                 * @param {object} options An object representing 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.
-                 */
-                async publish (jid, node, item, options, strict_options=true) {
-                    const bare_jid = _converse.session.get('bare_jid');
-                    const stanza = $iq({
-                        from: bare_jid,
-                        type: 'set',
-                        to: jid
-                    }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
-                        .c('publish', {'node': node})
-                            .cnode(item.tree()).up().up();
-
-                    if (options) {
-                        jid = jid || bare_jid;
-                        if (await api.disco.supports(Strophe.NS.PUBSUB + '#publish-options', jid)) {
-                            stanza.c('publish-options')
-                                .c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'})
-                                    .c('field', {'var': 'FORM_TYPE', 'type': 'hidden'})
-                                        .c('value').t(`${Strophe.NS.PUBSUB}#publish-options`).up().up()
-
-                            Object.keys(options).forEach(k => stanza.c('field', {'var': k}).c('value').t(options[k]).up().up());
-                        } else {
-                            log.warn(`_converse.api.publish: ${jid} does not support #publish-options, `+
-                                     `so we didn't set them even though they were provided.`)
-                        }
-                    }
-                    try {
-                        await api.sendIQ(stanza);
-                    } catch (iq) {
-                        if (iq instanceof Element &&
-                                strict_options &&
-                                iq.querySelector(`precondition-not-met[xmlns="${Strophe.NS.PUBSUB_ERROR}"]`)) {
-
-                            // 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(`PubSub: Republishing without publish options. ${el.outerHTML}`);
-                            await api.sendIQ(el);
-                        } else {
-                            throw iq;
-                        }
-                    }
-                }
-            }
-        });
-    }
-});

+ 92 - 0
src/headless/plugins/pubsub/api.js

@@ -0,0 +1,92 @@
+/**
+ * @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 '../../log.js';
+
+const { Strophe, stx } = converse.env;
+
+export default {
+    /**
+     * @typedef {import('strophe.js').Builder} Builder
+     * @typedef {import('strophe.js').Stanza} Stanza
+     *
+     * The "pubsub" namespace groups methods relevant to PubSub
+     * @namespace _converse.api.pubsub
+     * @memberOf _converse.api
+     */
+    pubsub: {
+        /**
+         * 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
+         *      (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) {
+            const bare_jid = _converse.session.get('bare_jid');
+            const entity_jid = jid || bare_jid;
+            const supports_publish_options = await api.disco.supports(
+                Strophe.NS.PUBSUB + '#publish-options',
+                entity_jid
+            );
+            if (!supports_publish_options) {
+                log.warn(
+                    `api.pubsub.publish: ${entity_jid} does not support #publish-options, ` +
+                        `so we didn't set them even though they were provided.`
+                );
+            }
+
+            const stanza = stx`
+                <iq xmlns="jabber:client"
+                        from="${bare_jid}"
+                        type="set"
+                        ${entity_jid !== bare_jid ? `to="${entity_jid}"` : ''}>
+                <pubsub xmlns="${Strophe.NS.PUBSUB}">
+                    <publish node="${node}">${item}</publish>
+                    ${
+                        options && supports_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>`;
+
+            try {
+                await api.sendIQ(stanza);
+            } catch (iq) {
+                if (
+                    iq instanceof Element &&
+                    !strict_options &&
+                    iq.querySelector(`precondition-not-met[xmlns="${Strophe.NS.PUBSUB_ERROR}"]`)
+                ) {
+                    // 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: Republishing without publish options. ${el.outerHTML}`);
+                    await api.sendIQ(el);
+                } else {
+                    throw iq;
+                }
+            }
+        },
+    },
+};

+ 21 - 0
src/headless/plugins/pubsub/index.js

@@ -0,0 +1,21 @@
+/**
+ * @module converse-pubsub
+ * @copyright The Converse.js contributors
+ * @license Mozilla Public License (MPLv2)
+ */
+import _converse from '../../shared/_converse.js';
+import converse from '../../shared/api/public.js';
+import pubsub_api from './api.js';
+import '../disco/index.js';
+
+const { Strophe } = converse.env;
+
+Strophe.addNamespace('PUBSUB_ERROR', Strophe.NS.PUBSUB + '#errors');
+
+converse.plugins.add('converse-pubsub', {
+    dependencies: ['converse-disco'],
+
+    initialize() {
+        Object.assign(_converse.api, pubsub_api);
+    },
+});

+ 42 - 0
src/headless/plugins/pubsub/types.ts

@@ -0,0 +1,42 @@
+export type PubSubConfigOptions = {
+    access_model: 'authorize' | 'open' | 'presence' | 'roster' | 'whitelist';
+    // Payload XSLT
+    dataform_xslt?: string;
+    deliver_notifications: boolean;
+    // Whether to deliver payloads with event notifications
+    deliver_payloads: boolean;
+    // Time after which to automatically purge items. `max` for no specific limit other than a server imposed maximum.
+    item_expire: string;
+    // Max # of items to persist. `max` for no specific limit other than a server imposed maximum.
+    max_items: string;
+    // Max Payload size in bytes
+    max_payload_size: string;
+    notification_type: 'normal' | 'headline';
+    // Notify subscribers when the node configuration changes
+    notify_config: boolean;
+    // Notify subscribers when the node is deleted
+    notify_delete: boolean;
+    // Notify subscribers when items are removed from the node
+    notify_retract: boolean;
+    // <field var='notify_sub' type='boolean'
+    notify_sub: boolean;
+    // <field var='persist_items' type='boolean'
+    persist_items: boolean;
+    // Deliver event notifications only to available users
+    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[];
+    // 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';
+    // Whether to allow subscriptions
+    subscribe: boolean;
+    // A friendly name for the node'/>
+    title: string;
+    // Specify the semantic type of payload data to be provided at this node.
+    type: string;
+};

+ 7 - 3
src/headless/types/plugins/bookmarks/collection.d.ts

@@ -16,10 +16,14 @@ declare class Bookmarks extends Collection {
      */
      */
     createBookmark(attrs: import("./types").BookmarkAttrs): void;
     createBookmark(attrs: import("./types").BookmarkAttrs): void;
     /**
     /**
-     * @returns {Promise<Stanza>}
+     * @param {'urn:xmpp:bookmarks:1'|'storage:bookmarks'} node
+     * @returns {Stanza|Stanza[]}
      */
      */
-    createPublishNode(): Promise<Stanza>;
-    sendBookmarkStanza(): Promise<any>;
+    getPublishedItems(node: "urn:xmpp:bookmarks:1" | "storage:bookmarks"): Stanza | Stanza[];
+    /**
+     * @returns {Promise<void|Element>}
+     */
+    sendBookmarkStanza(): Promise<void | Element>;
     /**
     /**
      * @param {Element} iq
      * @param {Element} iq
      * @param {import('./types').BookmarkAttrs} attrs
      * @param {import('./types').BookmarkAttrs} attrs

+ 22 - 0
src/headless/types/plugins/pubsub/api.d.ts

@@ -0,0 +1,22 @@
+declare namespace _default {
+    namespace pubsub {
+        /**
+         * 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
+         *      (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>}
+         */
+        function publish(jid: string, node: string, item: import("strophe.js").Builder | import("strophe.js").Stanza | (import("strophe.js").Builder | import("strophe.js").Stanza)[], options: import("./types").PubSubConfigOptions, strict_options?: boolean): Promise<void | Element>;
+    }
+}
+export default _default;
+//# sourceMappingURL=api.d.ts.map

+ 2 - 0
src/headless/types/plugins/pubsub/index.d.ts

@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=index.d.ts.map

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

@@ -0,0 +1,24 @@
+export type PubSubConfigOptions = {
+    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;
+};
+//# sourceMappingURL=types.d.ts.map

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

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

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

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

+ 1 - 1
src/plugins/omemo/devicelist.js

@@ -117,7 +117,7 @@ class DeviceList extends Model {
     publishDevices () {
     publishDevices () {
         const item = $build('item', { 'id': 'current' }).c('list', { 'xmlns': Strophe.NS.OMEMO });
         const item = $build('item', { 'id': 'current' }).c('list', { 'xmlns': Strophe.NS.OMEMO });
         this.devices.filter(d => d.get('active')).forEach(d => item.c('device', { 'id': d.get('id') }).up());
         this.devices.filter(d => d.get('active')).forEach(d => item.c('device', { 'id': d.get('id') }).up());
-        const options = { 'pubsub#access_model': 'open' };
+        const options = { access_model: 'open' };
         return api.pubsub.publish(null, Strophe.NS.OMEMO_DEVICELIST, item, options, false);
         return api.pubsub.publish(null, Strophe.NS.OMEMO_DEVICELIST, item, options, false);
     }
     }
 
 

+ 1 - 1
src/plugins/omemo/store.js

@@ -182,7 +182,7 @@ class OMEMOStore extends Model {
                 .t(prekey.pubKey)
                 .t(prekey.pubKey)
                 .up()
                 .up()
         );
         );
-        const options = { 'pubsub#access_model': 'open' };
+        const options = { access_model: 'open' };
         return api.pubsub.publish(null, node, item, options, false);
         return api.pubsub.publish(null, node, item, options, false);
     }
     }
 
 

+ 361 - 310
src/plugins/omemo/tests/omemo.js

@@ -1,10 +1,10 @@
 /*global mock, converse */
 /*global mock, converse */
-
-const { $iq, $msg, omemo, Strophe } = converse.env;
-const u = converse.env.utils;
+const { $iq, $msg, omemo, Strophe, stx, u } = converse.env;
 
 
 describe("The OMEMO module", function() {
 describe("The OMEMO module", function() {
 
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     it("adds methods for encrypting and decrypting messages via AES GCM",
     it("adds methods for encrypting and decrypting messages via AES GCM",
             mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
             mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
 
@@ -24,16 +24,21 @@ describe("The OMEMO module", function() {
         await mock.initializedOMEMO(_converse);
         await mock.initializedOMEMO(_converse);
         await mock.openChatBoxFor(_converse, contact_jid);
         await mock.openChatBoxFor(_converse, contact_jid);
         let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
         let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
-        let stanza = $iq({
-                'from': contact_jid,
-                'id': iq_stanza.getAttribute('id'),
-                'to': _converse.api.connection.get().jid,
-                'type': 'result',
-            }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
-                .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
-                    .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
-                        .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
-                            .c('device', {'id': '555'});
+        let stanza = stx`<iq from="${contact_jid}"
+                            xmlns="jabber:server"
+                            id="${iq_stanza.getAttribute('id')}"
+                            to="${_converse.api.connection.get().jid}"
+                            type="result">
+            <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                <items node="eu.siacs.conversations.axolotl.devicelist">
+                    <item xmlns="http://jabber.org/protocol/pubsub">
+                        <list xmlns="eu.siacs.conversations.axolotl">
+                            <device id="555"/>
+                        </list>
+                    </item>
+                </items>
+            </pubsub>
+        </iq>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         await u.waitUntil(() => _converse.state.omemo_store);
         await u.waitUntil(() => _converse.state.omemo_store);
         const devicelist = _converse.state.devicelists.get({'jid': contact_jid});
         const devicelist = _converse.state.devicelists.get({'jid': contact_jid});
@@ -51,82 +56,96 @@ describe("The OMEMO module", function() {
             keyCode: 13 // Enter
             keyCode: 13 // Enter
         });
         });
         iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '555'));
         iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '555'));
-        stanza = $iq({
-            'from': contact_jid,
-            'id': iq_stanza.getAttribute('id'),
-            'to': _converse.bare_jid,
-            'type': 'result',
-        }).c('pubsub', {
-            'xmlns': 'http://jabber.org/protocol/pubsub'
-            }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"})
-                .c('item')
-                    .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
-                        .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
-                        .c('signedPreKeySignature').t(btoa('2222')).up()
-                        .c('identityKey').t(btoa('3333')).up()
-                        .c('prekeys')
-                            .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
-                            .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
-                            .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
+        stanza = stx`<iq from="${contact_jid}"
+                id="${iq_stanza.getAttribute('id')}"
+                to="${_converse.bare_jid}"
+                xmlns="jabber:server"
+                type="result">
+            <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                <items node="eu.siacs.conversations.axolotl.bundles:555">
+                    <item>
+                        <bundle xmlns="eu.siacs.conversations.axolotl">
+                            <signedPreKeyPublic signedPreKeyId="4223">${btoa('1111')}</signedPreKeyPublic>
+                            <signedPreKeySignature>${btoa('2222')}</signedPreKeySignature>
+                            <identityKey>${btoa('3333')}</identityKey>
+                            <prekeys>
+                                <preKeyPublic preKeyId="1">${btoa('1001')}</preKeyPublic>
+                                <preKeyPublic preKeyId="2">${btoa('1002')}</preKeyPublic>
+                                <preKeyPublic preKeyId="3">${btoa('1003')}</preKeyPublic>
+                            </prekeys>
+                        </bundle>
+                    </item>
+                </items>
+            </pubsub>
+        </iq>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'));
         iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'));
-        stanza = $iq({
-            'from': _converse.bare_jid,
-            'id': iq_stanza.getAttribute('id'),
-            'to': _converse.bare_jid,
-            'type': 'result',
-        }).c('pubsub', {
-            'xmlns': 'http://jabber.org/protocol/pubsub'
-            }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"})
-                .c('item')
-                    .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
-                        .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up()
-                        .c('signedPreKeySignature').t(btoa('200000')).up()
-                        .c('identityKey').t(btoa('300000')).up()
-                        .c('prekeys')
-                            .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up()
-                            .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up()
-                            .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993'));
+        stanza = stx`<iq from="${_converse.bare_jid}"
+                xmlns="jabber:server"
+                id="${iq_stanza.getAttribute('id')}"
+                to="${_converse.bare_jid}"
+                type="result">
+            <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                <items node="eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe">
+                    <item>
+                        <bundle xmlns="eu.siacs.conversations.axolotl">
+                            <signedPreKeyPublic signedPreKeyId="4223">${btoa('100000')}</signedPreKeyPublic>
+                            <signedPreKeySignature>${btoa('200000')}</signedPreKeySignature>
+                            <identityKey>${btoa('300000')}</identityKey>
+                            <prekeys>
+                                <preKeyPublic preKeyId="1">${btoa('1991')}</preKeyPublic>
+                                <preKeyPublic preKeyId="2">${btoa('1992')}</preKeyPublic>
+                                <preKeyPublic preKeyId="3">${btoa('1993')}</preKeyPublic>
+                            </prekeys>
+                        </bundle>
+                    </item>
+                </items>
+            </pubsub>
+        </iq>`;
 
 
         spyOn(_converse.api.connection.get(), 'send').and.callFake(stanza => { sent_stanza = stanza });
         spyOn(_converse.api.connection.get(), 'send').and.callFake(stanza => { sent_stanza = stanza });
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         await u.waitUntil(() => sent_stanza);
         await u.waitUntil(() => sent_stanza);
-        expect(Strophe.serialize(sent_stanza)).toBe(
-            `<message from="romeo@montague.lit/orchard" id="${sent_stanza.getAttribute("id")}" `+
-                        `to="mercutio@montague.lit" `+
-                        `type="chat" xmlns="jabber:client">`+
-                `<body>This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo</body>`+
-                `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
-                `<request xmlns="urn:xmpp:receipts"/>`+
-                `<origin-id id="${sent_stanza.getAttribute('id')}" xmlns="urn:xmpp:sid:0"/>`+
-                `<encrypted xmlns="eu.siacs.conversations.axolotl">`+
-                    `<header sid="123456789">`+
-                        `<key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key>`+
-                        `<key rid="555">YzFwaDNSNzNYNw==</key>`+
-                        `<iv>${sent_stanza.querySelector("iv").textContent}</iv>`+
-                    `</header>`+
-                    `<payload>${sent_stanza.querySelector("payload").textContent}</payload>`+
-                `</encrypted>`+
-                `<store xmlns="urn:xmpp:hints"/>`+
-                `<encryption namespace="eu.siacs.conversations.axolotl" xmlns="urn:xmpp:eme:0"/>`+
-            `</message>`);
+        expect(sent_stanza).toEqualStanza(
+            stx`<message from="romeo@montague.lit/orchard"
+                        id="${sent_stanza.getAttribute("id")}"
+                        to="mercutio@montague.lit"
+                        type="chat"
+                        xmlns="jabber:client">
+                <body>This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo</body>
+                <active xmlns="http://jabber.org/protocol/chatstates"/>
+                <request xmlns="urn:xmpp:receipts"/>
+                <origin-id id="${sent_stanza.getAttribute('id')}" xmlns="urn:xmpp:sid:0"/>
+                <encrypted xmlns="eu.siacs.conversations.axolotl">
+                    <header sid="123456789">
+                        <key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key>
+                        <key rid="555">YzFwaDNSNzNYNw==</key>
+                        <iv>${sent_stanza.querySelector("iv").textContent}</iv>
+                    </header>
+                    <payload>${sent_stanza.querySelector("payload").textContent}</payload>
+                </encrypted>
+                <store xmlns="urn:xmpp:hints"/>
+                <encryption namespace="eu.siacs.conversations.axolotl" xmlns="urn:xmpp:eme:0"/>
+            </message>`);
 
 
         // Test reception of an encrypted message
         // Test reception of an encrypted message
         let obj = await omemo.encryptMessage('This is an encrypted message from the contact')
         let obj = await omemo.encryptMessage('This is an encrypted message from the contact')
         // XXX: Normally the key will be encrypted via libsignal.
         // XXX: Normally the key will be encrypted via libsignal.
         // However, we're mocking libsignal in the tests, so we include it as plaintext in the message.
         // However, we're mocking libsignal in the tests, so we include it as plaintext in the message.
-        stanza = $msg({
-                'from': contact_jid,
-                'to': _converse.api.connection.get().jid,
-                'type': 'chat',
-                'id': _converse.api.connection.get().getUniqueId()
-            }).c('body').t('This is a fallback message').up()
-                .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
-                    .c('header', {'sid':  '555'})
-                        .c('key', {'rid':  _converse.state.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
-                        .c('iv').t(obj.iv)
-                        .up().up()
-                    .c('payload').t(obj.payload);
+        stanza = stx`<message from="${contact_jid}"
+                        to="${_converse.api.connection.get().jid}"
+                        type="chat"
+                        id="${_converse.api.connection.get().getUniqueId()}"
+                        xmlns="jabber:client">
+                    <body>This is a fallback message</body>
+                    <encrypted xmlns="${Strophe.NS.OMEMO}">
+                        <header sid="555">
+                            <key rid="${_converse.state.omemo_store.get('device_id')}">${u.arrayBufferToBase64(obj.key_and_tag)}</key>
+                            <iv>${obj.iv}</iv>
+                        </header>
+                        <payload>${obj.payload}</payload>
+                    </encrypted>
+                </message>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         expect(view.model.messages.length).toBe(2);
         expect(view.model.messages.length).toBe(2);
@@ -135,17 +154,19 @@ describe("The OMEMO module", function() {
 
 
         // #1193 Check for a received message without <body> tag
         // #1193 Check for a received message without <body> tag
         obj = await omemo.encryptMessage('Another received encrypted message without fallback')
         obj = await omemo.encryptMessage('Another received encrypted message without fallback')
-        stanza = $msg({
-                'from': contact_jid,
-                'to': _converse.api.connection.get().jid,
-                'type': 'chat',
-                'id': _converse.api.connection.get().getUniqueId()
-            }).c('encrypted', {'xmlns': Strophe.NS.OMEMO})
-                .c('header', {'sid':  '555'})
-                    .c('key', {'rid':  _converse.state.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
-                    .c('iv').t(obj.iv)
-                    .up().up()
-                .c('payload').t(obj.payload);
+        stanza = stx`<message from="${contact_jid}"
+                        to="${_converse.api.connection.get().jid}"
+                        type="chat"
+                        xmlns="jabber:client"
+                        id="${_converse.api.connection.get().getUniqueId()}">
+                    <encrypted xmlns="${Strophe.NS.OMEMO}">
+                        <header sid="555">
+                            <key rid="${_converse.state.omemo_store.get('device_id')}">${u.arrayBufferToBase64(obj.key_and_tag)}</key>
+                            <iv>${obj.iv}</iv>
+                        </header>
+                        <payload>${obj.payload}</payload>
+                    </encrypted>
+                </message>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         await u.waitUntil(() => view.model.messages.length > 1);
         await u.waitUntil(() => view.model.messages.length > 1);
@@ -239,16 +260,21 @@ describe("The OMEMO module", function() {
         const my_devicelist = _converse.state.devicelists.get({'jid': _converse.bare_jid});
         const my_devicelist = _converse.state.devicelists.get({'jid': _converse.bare_jid});
         expect(my_devicelist.devices.length).toBe(2);
         expect(my_devicelist.devices.length).toBe(2);
 
 
-        const stanza = $iq({
-                'from': contact_jid,
-                'id': iq_stanza.getAttribute('id'),
-                'to': _converse.api.connection.get().jid,
-                'type': 'result',
-            }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
-                .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
-                    .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
-                        .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
-                            .c('device', {'id': '555'});
+        const stanza = stx`<iq from="${contact_jid}"
+                id="${iq_stanza.getAttribute('id')}"
+                to="${_converse.api.connection.get().jid}"
+                xmlns="jabber:server"
+                type="result">
+            <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                <items node="eu.siacs.conversations.axolotl.devicelist">
+                    <item xmlns="http://jabber.org/protocol/pubsub">
+                        <list xmlns="eu.siacs.conversations.axolotl">
+                            <device id="555"/>
+                        </list>
+                    </item>
+                </items>
+            </pubsub>
+        </iq>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         const omemo_store = await u.waitUntil(() => _converse.state.omemo_store);
         const omemo_store = await u.waitUntil(() => _converse.state.omemo_store);
 
 
@@ -260,8 +286,7 @@ describe("The OMEMO module", function() {
 
 
         // Test reception of an encrypted carbon message
         // Test reception of an encrypted carbon message
         const obj = await omemo.encryptMessage('This is an encrypted carbon message from another device of mine')
         const obj = await omemo.encryptMessage('This is an encrypted carbon message from another device of mine')
-        const carbon = u.toStanza(`
-            <message xmlns="jabber:client" to="romeo@montague.lit/orchard" from="romeo@montague.lit" type="chat">
+        const carbon = stx`<message xmlns="jabber:client" to="romeo@montague.lit/orchard" from="romeo@montague.lit" type="chat">
                 <sent xmlns="urn:xmpp:carbons:2">
                 <sent xmlns="urn:xmpp:carbons:2">
                     <forwarded xmlns="urn:xmpp:forward:0">
                     <forwarded xmlns="urn:xmpp:forward:0">
                     <message xmlns="jabber:client"
                     <message xmlns="jabber:client"
@@ -288,8 +313,7 @@ describe("The OMEMO module", function() {
                     </message>
                     </message>
                     </forwarded>
                     </forwarded>
                 </sent>
                 </sent>
-            </message>
-        `);
+            </message>`;
         _converse.api.connection.get().IQ_stanzas = [];
         _converse.api.connection.get().IQ_stanzas = [];
         _converse.api.connection.get()._dataRecv(mock.createRequest(carbon));
         _converse.api.connection.get()._dataRecv(mock.createRequest(carbon));
 
 
@@ -335,12 +359,12 @@ describe("The OMEMO module", function() {
             keyCode: 13 // Enter
             keyCode: 13 // Enter
         });
         });
         iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '988349631'));
         iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '988349631'));
-        expect(Strophe.serialize(iq_stanza)).toBe(
-            `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${_converse.bare_jid}" type="get" xmlns="jabber:client">`+
-                `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
-                    `<items node="eu.siacs.conversations.axolotl.bundles:988349631"/>`+
-                `</pubsub>`+
-            `</iq>`);
+        expect(iq_stanza).toEqualStanza(
+            stx`<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${_converse.bare_jid}" type="get" xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <items node="eu.siacs.conversations.axolotl.bundles:988349631"/>
+                </pubsub>
+            </iq>`);
 
 
         prekey_ids = Object.keys(omemo_store.getPreKeys());
         prekey_ids = Object.keys(omemo_store.getPreKeys());
         expect(prekey_ids.length).toBe(100);
         expect(prekey_ids.length).toBe(100);
@@ -359,21 +383,22 @@ describe("The OMEMO module", function() {
         // XXX: Normally the key will be encrypted via libsignal.
         // XXX: Normally the key will be encrypted via libsignal.
         // However, we're mocking libsignal in the tests, so we include
         // However, we're mocking libsignal in the tests, so we include
         // it as plaintext in the message.
         // it as plaintext in the message.
-        let stanza = $msg({
-                'from': contact_jid,
-                'to': _converse.api.connection.get().jid,
-                'type': 'chat',
-                'id': 'qwerty'
-            }).c('body').t('This is a fallback message').up()
-                .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
-                    .c('header', {'sid':  '555'})
-                        .c('key', {
-                            'prekey': 'true',
-                            'rid':  _converse.state.omemo_store.get('device_id')
-                        }).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
-                        .c('iv').t(obj.iv)
-                        .up().up()
-                    .c('payload').t(obj.payload);
+        let stanza = stx`<message from="${contact_jid}"
+                to="${_converse.api.connection.get().jid}"
+                xmlns="jabber:client"
+                type="chat"
+                id="qwerty">
+            <body>This is a fallback message</body>
+            <encrypted xmlns="${Strophe.NS.OMEMO}">
+                <header sid="555">
+                    <key prekey="true" rid="${_converse.state.omemo_store.get('device_id')}">
+                        ${u.arrayBufferToBase64(obj.key_and_tag)}
+                    </key>
+                    <iv>${obj.iv}</iv>
+                </header>
+                <payload>${obj.payload}</payload>
+            </encrypted>
+        </message>`;
 
 
         const generateMissingPreKeys = _converse.state.omemo_store.generateMissingPreKeys;
         const generateMissingPreKeys = _converse.state.omemo_store.generateMissingPreKeys;
         spyOn(_converse.state.omemo_store, 'generateMissingPreKeys').and.callFake(() => {
         spyOn(_converse.state.omemo_store, 'generateMissingPreKeys').and.callFake(() => {
@@ -387,16 +412,21 @@ describe("The OMEMO module", function() {
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
 
         let iq_stanza = await mock.deviceListFetched(_converse, contact_jid);
         let iq_stanza = await mock.deviceListFetched(_converse, contact_jid);
-        stanza = $iq({
-            'from': contact_jid,
-            'id': iq_stanza.getAttribute('id'),
-            'to': _converse.api.connection.get().jid,
-            'type': 'result',
-        }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
-            .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
-                .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
-                    .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
-                        .c('device', {'id': '555'});
+        stanza = stx`<iq from="${contact_jid}"
+                id="${iq_stanza.getAttribute('id')}"
+                to="${_converse.api.connection.get().jid}"
+                xmlns="jabber:server"
+                type="result">
+            <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                <items node="eu.siacs.conversations.axolotl.devicelist">
+                    <item xmlns="http://jabber.org/protocol/pubsub">
+                        <list xmlns="eu.siacs.conversations.axolotl">
+                            <device id="555"/>
+                        </list>
+                    </item>
+                </items>
+            </pubsub>
+        </iq>`;
 
 
         // XXX: the bundle gets published twice, we want to make sure
         // XXX: the bundle gets published twice, we want to make sure
         // that we wait for the 2nd, so we clear all the already sent
         // that we wait for the 2nd, so we clear all the already sent
@@ -405,37 +435,37 @@ describe("The OMEMO module", function() {
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         await u.waitUntil(() => _converse.state.omemo_store);
         await u.waitUntil(() => _converse.state.omemo_store);
         iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse), 1000);
         iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse), 1000);
-        expect(Strophe.serialize(iq_stanza)).toBe(
-            `<iq from="romeo@montague.lit" 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>`+
-                            `<bundle xmlns="eu.siacs.conversations.axolotl">`+
-                                `<signedPreKeyPublic signedPreKeyId="0">${btoa("1234")}</signedPreKeyPublic>`+
-                                    `<signedPreKeySignature>${btoa("11112222333344445555")}</signedPreKeySignature>`+
-                                    `<identityKey>${btoa("1234")}</identityKey>`+
-                                `<prekeys>`+
-                                    `<preKeyPublic preKeyId="0">${btoa("1234")}</preKeyPublic>`+
-                                    `<preKeyPublic preKeyId="1">${btoa("1234")}</preKeyPublic>`+
-                                    `<preKeyPublic preKeyId="2">${btoa("1234")}</preKeyPublic>`+
-                                    `<preKeyPublic preKeyId="3">${btoa("1234")}</preKeyPublic>`+
-                                    `<preKeyPublic preKeyId="4">${btoa("1234")}</preKeyPublic>`+
-                                `</prekeys>`+
-                            `</bundle>`+
-                        `</item>`+
-                    `</publish>`+
-                    `<publish-options>`+
-                        `<x type="submit" xmlns="jabber:x:data">`+
-                            `<field type="hidden" var="FORM_TYPE">`+
-                                `<value>http://jabber.org/protocol/pubsub#publish-options</value>`+
-                            `</field>`+
-                            `<field var="pubsub#access_model">`+
-                                `<value>open</value>`+
-                            `</field>`+
-                        `</x>`+
-                    `</publish-options>`+
-                `</pubsub>`+
-            `</iq>`)
+        expect(iq_stanza).toEqualStanza(
+            stx`<iq from="romeo@montague.lit" 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>
+                            <bundle xmlns="eu.siacs.conversations.axolotl">
+                                <signedPreKeyPublic signedPreKeyId="0">${btoa("1234")}</signedPreKeyPublic>
+                                    <signedPreKeySignature>${btoa("11112222333344445555")}</signedPreKeySignature>
+                                    <identityKey>${btoa("1234")}</identityKey>
+                                <prekeys>
+                                    <preKeyPublic preKeyId="0">${btoa("1234")}</preKeyPublic>
+                                    <preKeyPublic preKeyId="1">${btoa("1234")}</preKeyPublic>
+                                    <preKeyPublic preKeyId="2">${btoa("1234")}</preKeyPublic>
+                                    <preKeyPublic preKeyId="3">${btoa("1234")}</preKeyPublic>
+                                    <preKeyPublic preKeyId="4">${btoa("1234")}</preKeyPublic>
+                                </prekeys>
+                            </bundle>
+                        </item>
+                    </publish>
+                    <publish-options>
+                        <x type="submit" xmlns="jabber:x:data">
+                            <field type="hidden" var="FORM_TYPE">
+                                <value>http://jabber.org/protocol/pubsub#publish-options</value>
+                            </field>
+                            <field var="pubsub#access_model">
+                                <value>open</value>
+                            </field>
+                        </x>
+                    </publish-options>
+                </pubsub>
+            </iq>`)
         const own_device = _converse.state.devicelists.get(_converse.bare_jid).devices.get(_converse.state.omemo_store.get('device_id'));
         const own_device = _converse.state.devicelists.get(_converse.bare_jid).devices.get(_converse.state.omemo_store.get('device_id'));
         expect(own_device.get('bundle').prekeys.length).toBe(5);
         expect(own_device.get('bundle').prekeys.length).toBe(5);
         expect(_converse.state.omemo_store.generateMissingPreKeys).toHaveBeenCalled();
         expect(_converse.state.omemo_store.generateMissingPreKeys).toHaveBeenCalled();
@@ -457,12 +487,12 @@ describe("The OMEMO module", function() {
 
 
         // Wait until own devices are fetched
         // Wait until own devices are fetched
         let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid));
         let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid));
-        expect(Strophe.serialize(iq_stanza)).toBe(
-            `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
-                `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
-                    `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
-                `</pubsub>`+
-            `</iq>`);
+        expect(iq_stanza).toEqualStanza(
+            stx`<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="romeo@montague.lit" type="get" xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <items node="eu.siacs.conversations.axolotl.devicelist"/>
+                </pubsub>
+            </iq>`);
 
 
         let stanza = $iq({
         let stanza = $iq({
             'from': _converse.bare_jid,
             'from': _converse.bare_jid,
@@ -606,29 +636,30 @@ describe("The OMEMO module", function() {
         iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse));
         iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse));
         // Check that our own device is added again, but that removed
         // Check that our own device is added again, but that removed
         // devices are not added.
         // devices are not added.
-        expect(Strophe.serialize(iq_stanza)).toBe(
-            `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute(`id`)}" type="set" xmlns="jabber:client">`+
-                `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
-                    `<publish node="eu.siacs.conversations.axolotl.devicelist">`+
-                        `<item id="current">`+
-                            `<list xmlns="eu.siacs.conversations.axolotl">`+
-                                `<device id="123456789"/>`+
-                                `<device id="444"/>`+
-                            `</list>`+
-                        `</item>`+
-                    `</publish>`+
-                    `<publish-options>`+
-                        `<x type="submit" xmlns="jabber:x:data">`+
-                            `<field type="hidden" var="FORM_TYPE">`+
-                                `<value>http://jabber.org/protocol/pubsub#publish-options</value>`+
-                            `</field>`+
-                            `<field var="pubsub#access_model">`+
-                                `<value>open</value>`+
-                            `</field>`+
-                        `</x>`+
-                    `</publish-options>`+
-                `</pubsub>`+
-            `</iq>`);
+        expect(iq_stanza).toEqualStanza(
+            stx`<iq from="romeo@montague.lit"
+                    id="${iq_stanza.getAttribute(`id`)}" type="set" xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <publish node="eu.siacs.conversations.axolotl.devicelist">
+                        <item id="current">
+                            <list xmlns="eu.siacs.conversations.axolotl">
+                                <device id="123456789"/>
+                                <device id="444"/>
+                            </list>
+                        </item>
+                    </publish>
+                    <publish-options>
+                        <x type="submit" xmlns="jabber:x:data">
+                            <field type="hidden" var="FORM_TYPE">
+                                <value>http://jabber.org/protocol/pubsub#publish-options</value>
+                            </field>
+                            <field var="pubsub#access_model">
+                                <value>open</value>
+                            </field>
+                        </x>
+                    </publish-options>
+                </pubsub>
+            </iq>`);
         expect(_converse.state.devicelists.length).toBe(2);
         expect(_converse.state.devicelists.length).toBe(2);
         devices = _converse.state.devicelists.get(_converse.bare_jid).devices;
         devices = _converse.state.devicelists.get(_converse.bare_jid).devices;
         // The device id for this device (123456789) was also generated and added to the list,
         // The device id for this device (123456789) was also generated and added to the list,
@@ -823,56 +854,60 @@ describe("The OMEMO module", function() {
         await mock.waitForRoster(_converse, 'current', 1);
         await mock.waitForRoster(_converse, 'current', 1);
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid));
         let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid));
-        let stanza = $iq({
-            'from': contact_jid,
-            'id': iq_stanza.getAttribute('id'),
-            'to': _converse.bare_jid,
-            'type': 'result',
-        }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
-            .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
-                .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
-                    .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
-                        .c('device', {'id': '482886413b977930064a5888b92134fe'});
+        let stanza = stx`<iq from="${contact_jid}"
+                xmlns="jabber:server"
+                id="${iq_stanza.getAttribute('id')}"
+                to="${_converse.bare_jid}" type="result">
+            <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                <items node="eu.siacs.conversations.axolotl.devicelist">
+                    <item xmlns="http://jabber.org/protocol/pubsub">
+                        <list xmlns="eu.siacs.conversations.axolotl">
+                            <device id="482886413b977930064a5888b92134fe"/>
+                        </list>
+                    </item>
+                </items>
+            </pubsub>
+        </iq>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         expect(_converse.state.devicelists.length).toBe(1);
         expect(_converse.state.devicelists.length).toBe(1);
         await mock.openChatBoxFor(_converse, contact_jid);
         await mock.openChatBoxFor(_converse, contact_jid);
         iq_stanza = await mock.ownDeviceHasBeenPublished(_converse);
         iq_stanza = await mock.ownDeviceHasBeenPublished(_converse);
-        stanza = $iq({
-            'from': _converse.bare_jid,
-            'id': iq_stanza.getAttribute('id'),
-            'to': _converse.bare_jid,
-            'type': 'result'});
+        stanza = stx`<iq from="${_converse.bare_jid}"
+                        xmlns="jabber:server"
+                        id="${iq_stanza.getAttribute('id')}"
+                        to="${_converse.bare_jid}" type="result"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
 
         iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse));
         iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse));
-        expect(Strophe.serialize(iq_stanza)).toBe(
-            `<iq from="romeo@montague.lit" 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>`+
-                            `<bundle xmlns="eu.siacs.conversations.axolotl">`+
-                                `<signedPreKeyPublic signedPreKeyId="0">${btoa("1234")}</signedPreKeyPublic>`+
-                                    `<signedPreKeySignature>${btoa("11112222333344445555")}</signedPreKeySignature>`+
-                                    `<identityKey>${btoa("1234")}</identityKey>`+
-                                `<prekeys>`+
-                                    `<preKeyPublic preKeyId="0">${btoa("1234")}</preKeyPublic>`+
-                                    `<preKeyPublic preKeyId="1">${btoa("1234")}</preKeyPublic>`+
-                                `</prekeys>`+
-                            `</bundle>`+
-                        `</item>`+
-                    `</publish>`+
-                    `<publish-options>`+
-                        `<x type="submit" xmlns="jabber:x:data">`+
-                            `<field type="hidden" var="FORM_TYPE">`+
-                                `<value>http://jabber.org/protocol/pubsub#publish-options</value>`+
-                            `</field>`+
-                            `<field var="pubsub#access_model">`+
-                                `<value>open</value>`+
-                            `</field>`+
-                        `</x>`+
-                    `</publish-options>`+
-                `</pubsub>`+
-            `</iq>`)
+        expect(iq_stanza).toEqualStanza(
+            stx`<iq from="romeo@montague.lit"
+                    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>
+                            <bundle xmlns="eu.siacs.conversations.axolotl">
+                                <signedPreKeyPublic signedPreKeyId="0">${btoa("1234")}</signedPreKeyPublic>
+                                    <signedPreKeySignature>${btoa("11112222333344445555")}</signedPreKeySignature>
+                                    <identityKey>${btoa("1234")}</identityKey>
+                                <prekeys>
+                                    <preKeyPublic preKeyId="0">${btoa("1234")}</preKeyPublic>
+                                    <preKeyPublic preKeyId="1">${btoa("1234")}</preKeyPublic>
+                                </prekeys>
+                            </bundle>
+                        </item>
+                    </publish>
+                    <publish-options>
+                        <x type="submit" xmlns="jabber:x:data">
+                            <field type="hidden" var="FORM_TYPE">
+                                <value>http://jabber.org/protocol/pubsub#publish-options</value>
+                            </field>
+                            <field var="pubsub#access_model">
+                                <value>open</value>
+                            </field>
+                        </x>
+                    </publish-options>
+                </pubsub>
+            </iq>`)
 
 
         stanza = $iq({
         stanza = $iq({
             'from': _converse.bare_jid,
             'from': _converse.bare_jid,
@@ -899,23 +934,31 @@ describe("The OMEMO module", function() {
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
 
         let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid));
         let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid));
-        expect(Strophe.serialize(iq_stanza)).toBe(
-            `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
-                `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
-                    `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
-                `</pubsub>`+
-            `</iq>`);
-
-        let stanza = $iq({
-            'from': _converse.bare_jid,
-            'id': iq_stanza.getAttribute('id'),
-            'to': _converse.bare_jid,
-            'type': 'result',
-        }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
-            .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
-                .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
-                    .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
-                        .c('device', {'id': '482886413b977930064a5888b92134fe'});
+        expect(iq_stanza).toEqualStanza(
+            stx`<iq from="romeo@montague.lit"
+                    id="${iq_stanza.getAttribute("id")}"
+                    to="romeo@montague.lit"
+                    type="get"
+                    xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <items node="eu.siacs.conversations.axolotl.devicelist"/>
+                </pubsub>
+            </iq>`);
+
+        let stanza = stx`<iq from="${_converse.bare_jid}"
+                            xmlns="jabber:client"
+                            id="${iq_stanza.getAttribute('id')}"
+                            to="${_converse.bare_jid}" type="result">
+            <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                <items node="eu.siacs.conversations.axolotl.devicelist">
+                    <item xmlns="http://jabber.org/protocol/pubsub">
+                        <list xmlns="eu.siacs.conversations.axolotl">
+                            <device id="482886413b977930064a5888b92134fe"/>
+                        </list>
+                    </item>
+                </items>
+            </pubsub>
+        </iq>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         await u.waitUntil(() => _converse.state.omemo_store);
         await u.waitUntil(() => _converse.state.omemo_store);
         expect(_converse.state.devicelists.length).toBe(1);
         expect(_converse.state.devicelists.length).toBe(1);
@@ -925,39 +968,40 @@ describe("The OMEMO module", function() {
         expect(devicelist.devices.at(1).get('id')).toBe('123456789');
         expect(devicelist.devices.at(1).get('id')).toBe('123456789');
         // Check that own device was published
         // Check that own device was published
         iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse));
         iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse));
-        expect(Strophe.serialize(iq_stanza)).toBe(
-            `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute(`id`)}" type="set" xmlns="jabber:client">`+
-                `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
-                    `<publish node="eu.siacs.conversations.axolotl.devicelist">`+
-                        `<item id="current">`+
-                            `<list xmlns="eu.siacs.conversations.axolotl">`+
-                                `<device id="482886413b977930064a5888b92134fe"/>`+
-                                `<device id="123456789"/>`+
-                            `</list>`+
-                        `</item>`+
-                    `</publish>`+
-                    `<publish-options>`+
-                        `<x type="submit" xmlns="jabber:x:data">`+
-                            `<field type="hidden" var="FORM_TYPE">`+
-                                `<value>http://jabber.org/protocol/pubsub#publish-options</value>`+
-                            `</field>`+
-                            `<field var="pubsub#access_model">`+
-                                `<value>open</value>`+
-                            `</field>`+
-                        `</x>`+
-                    `</publish-options>`+
-                `</pubsub>`+
-            `</iq>`);
-
-        stanza = $iq({
-            'from': _converse.bare_jid,
-            'id': iq_stanza.getAttribute('id'),
-            'to': _converse.bare_jid,
-            'type': 'result'});
+        expect(iq_stanza).toEqualStanza(
+            stx`<iq from="romeo@montague.lit"
+                    id="${iq_stanza.getAttribute(`id`)}" type="set" xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <publish node="eu.siacs.conversations.axolotl.devicelist">
+                        <item id="current">
+                            <list xmlns="eu.siacs.conversations.axolotl">
+                                <device id="482886413b977930064a5888b92134fe"/>
+                                <device id="123456789"/>
+                            </list>
+                        </item>
+                    </publish>
+                    <publish-options>
+                        <x type="submit" xmlns="jabber:x:data">
+                            <field type="hidden" var="FORM_TYPE">
+                                <value>http://jabber.org/protocol/pubsub#publish-options</value>
+                            </field>
+                            <field var="pubsub#access_model">
+                                <value>open</value>
+                            </field>
+                        </x>
+                    </publish-options>
+                </pubsub>
+            </iq>`);
+
+        stanza = stx`<iq from="${_converse.bare_jid}"
+                         id="${iq_stanza.getAttribute('id')}"
+                         to="${_converse.bare_jid}"
+                         type="result"
+                         xmlns="jabber:server"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
 
         const iq_el = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse));
         const iq_el = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse));
-        expect(iq_el.getAttributeNames().sort().join()).toBe(["from", "type", "xmlns", "id"].sort().join());
+        expect(iq_el.getAttributeNames().sort().join()).toBe(["from", "id", "type", "xmlns"].sort().join());
         expect(iq_el.querySelector('prekeys').childNodes.length).toBe(100);
         expect(iq_el.querySelector('prekeys').childNodes.length).toBe(100);
 
 
         const signed_prekeys = iq_el.querySelectorAll('signedPreKeyPublic');
         const signed_prekeys = iq_el.querySelectorAll('signedPreKeyPublic');
@@ -967,37 +1011,44 @@ describe("The OMEMO module", function() {
         expect(iq_el.querySelectorAll('signedPreKeySignature').length).toBe(1);
         expect(iq_el.querySelectorAll('signedPreKeySignature').length).toBe(1);
         expect(iq_el.querySelectorAll('identityKey').length).toBe(1);
         expect(iq_el.querySelectorAll('identityKey').length).toBe(1);
 
 
-        stanza = $iq({
-            'from': _converse.bare_jid,
-            'id': iq_el.getAttribute('id'),
-            'to': _converse.bare_jid,
-            'type': 'result'});
+        stanza = stx`<iq xmlns="jabber:server"
+                 from="${_converse.bare_jid}"
+                 id="${iq_el.getAttribute('id')}"
+                 to="${_converse.bare_jid}"
+                 type="result"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         await _converse.api.waitUntil('OMEMOInitialized', 1000);
         await _converse.api.waitUntil('OMEMOInitialized', 1000);
         await mock.openChatBoxFor(_converse, contact_jid);
         await mock.openChatBoxFor(_converse, contact_jid);
 
 
         iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
         iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
-        expect(Strophe.serialize(iq_stanza)).toBe(
-            `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
-                `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
-                    `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
-                `</pubsub>`+
-            `</iq>`);
-
-        _converse.api.connection.get()._dataRecv(mock.createRequest($iq({
-            'from': contact_jid,
-            'id': iq_stanza.getAttribute('id'),
-            'to': _converse.bare_jid,
-            'type': 'result',
-        }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
-            .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
-                .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
-                    .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
-                        .c('device', {'id': '368866411b877c30064a5f62b917cffe'}).up()
-                        .c('device', {'id': '3300659945416e274474e469a1f0154c'}).up()
-                        .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
-                        .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'})
-        ));
+        expect(iq_stanza).toEqualStanza(
+            stx`<iq from="romeo@montague.lit"
+                    id="${iq_stanza.getAttribute("id")}"
+                    to="${contact_jid}" type="get" xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <items node="eu.siacs.conversations.axolotl.devicelist"/>
+                </pubsub>
+            </iq>`);
+
+        _converse.api.connection.get()._dataRecv(mock.createRequest(
+            stx`<iq from="${contact_jid}"
+                    id="${iq_stanza.getAttribute('id')}"
+                    to="${_converse.bare_jid}"
+                    type="result"
+                    xmlns="jabber:client">
+                <pubsub xmlns="http://jabber.org/protocol/pubsub">
+                    <items node="eu.siacs.conversations.axolotl.devicelist">
+                        <item xmlns="http://jabber.org/protocol/pubsub">
+                            <list xmlns="eu.siacs.conversations.axolotl">
+                                <device id="368866411b877c30064a5f62b917cffe"/>
+                                <device id="3300659945416e274474e469a1f0154c"/>
+                                <device id="4e30f35051b7b8b42abe083742187228"/>
+                                <device id="ae890ac52d0df67ed7cfdf51b644e901"/>
+                            </list>
+                        </item>
+                    </items>
+                </pubsub>
+            </iq>`));
 
 
         devicelist = _converse.state.devicelists.get(contact_jid);
         devicelist = _converse.state.devicelists.get(contact_jid);
         await u.waitUntil(() => devicelist.devices.length);
         await u.waitUntil(() => devicelist.devices.length);