浏览代码

Use `stx` to create message stanza

JC Brand 6 月之前
父节点
当前提交
985243c7e5

+ 3 - 3
package-lock.json

@@ -9868,8 +9868,8 @@
     },
     "node_modules/strophe.js": {
       "version": "3.1.0",
-      "resolved": "git+ssh://git@github.com/strophe/strophejs.git#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5",
-      "integrity": "sha512-M/T9Pio3eG7GUzVmQUSNg+XzjFwQ6qhzI+Z3uSwUIItxxpRIB8lQB2Afb0L7lbQiRYB7/9tS03GxksdqjfrS5g==",
+      "resolved": "git+ssh://git@github.com/strophe/strophejs.git#f54d83199ec62272ee8b4987b2e63f188e1b2e29",
+      "integrity": "sha512-ivy/25C19VudvLDMPhW4oZ4gIpicc0+AnnBzzV/YUikTbaS/ujy4Y/vO416alCFovqEmcc3AEXoQ4O8KqYEKug==",
       "license": "MIT",
       "optionalDependencies": {
         "@types/jsdom": "^21.1.7",
@@ -11121,7 +11121,7 @@
         "pluggable.js": "3.0.1",
         "sizzle": "^2.3.5",
         "sprintf-js": "^1.1.2",
-        "strophe.js": "strophe/strophejs#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5",
+        "strophe.js": "strophe/strophejs#f54d83199ec62272ee8b4987b2e63f188e1b2e29",
         "urijs": "^1.19.10"
       },
       "devDependencies": {}

+ 1 - 1
src/headless/package.json

@@ -42,7 +42,7 @@
     "pluggable.js": "3.0.1",
     "sizzle": "^2.3.5",
     "sprintf-js": "^1.1.2",
-    "strophe.js": "strophe/strophejs#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5",
+    "strophe.js": "strophe/strophejs#f54d83199ec62272ee8b4987b2e63f188e1b2e29",
     "urijs": "^1.19.10"
   },
   "devDependencies": {}

+ 6 - 0
src/headless/plugins/chat/message.js

@@ -35,6 +35,9 @@ class Message extends ModelWithContact(ColorAwareModel(Model)) {
     constructor (models, options) {
         super(models, options);
         this.file = null;
+
+        /** @type {import('./types').MessageAttributes} */
+        this.attributes;
     }
 
     async initialize () {
@@ -209,6 +212,9 @@ class Message extends ModelWithContact(ColorAwareModel(Model)) {
         return api.sendIQ(iq);
     }
 
+    /**
+     * @param {Element} stanza
+     */
     getUploadRequestMetadata (stanza) { // eslint-disable-line class-methods-use-this
         const headers = sizzle(`slot[xmlns="${Strophe.NS.HTTPUPLOAD}"] put header`, stanza);
         // https://xmpp.org/extensions/xep-0363.html#request

+ 9 - 1
src/headless/plugins/chat/types.ts

@@ -1,5 +1,13 @@
 import {EncryptionAttrs} from "../../shared/types";
 
+// Represents a XEP-0372 reference
+export type Reference = {
+    begin: number;
+    end: number;
+    type: string;
+    uri: string;
+}
+
 export type MessageErrorAttributes = {
     is_error: boolean; // Whether an error was received for this message
     error: string; // The error name
@@ -41,7 +49,7 @@ export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & {
     plaintext: string; // The decrypted text of this message, in case it was encrypted.
     receipt_id: string; // The `id` attribute of a XEP-0184 <receipt> element
     received: string; // An ISO8601 string recording the time that the message was received
-    references: Array<Object>; // A list of objects representing XEP-0372 references
+    references: Array<Reference>; // A list of objects representing XEP-0372 references
     replace_id: string; // The `id` attribute of a XEP-0308 <replace> element
     retracted: string; // An ISO8601 string recording the time that the message was retracted
     retracted_id: string; // The `id` attribute of a XEP-424 <retracted> element

+ 1 - 0
src/headless/plugins/muc/occupant.js

@@ -39,6 +39,7 @@ class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) {
             states: [],
             hidden: true,
             num_unread: 0,
+            message_type: 'chat',
         };
     }
 

+ 3 - 3
src/headless/plugins/muc/tests/messages.js

@@ -128,8 +128,8 @@ describe("A MUC message", function () {
 
         const muc_jid = 'lounge@montague.lit';
         const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
-        const received_stanza = u.toStanza(`
-            <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.api.connection.get().getUniqueId()}' >
+        const received_stanza = stx`
+            <message xmlns="jabber:client" to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.api.connection.get().getUniqueId()}' >
                 <reply xmlns='urn:xmpp:reply:0' id='${_converse.api.connection.get().getUniqueId()}' to='${_converse.jid}'/>
                 <fallback xmlns='urn:xmpp:feature-fallback:0' for='urn:xmpp:reply:0'>
                     <body start='0' end='10'/>
@@ -139,7 +139,7 @@ describe("A MUC message", function () {
     pong</body>
                 <request xmlns='urn:xmpp:receipts'/>
             </message>
-        `);
+        `;
         await model.handleMessageStanza(received_stanza);
         await u.waitUntil(() => model.messages.last());
         expect(model.messages.last().get('body')).toBe('> ping\n    pong');

+ 41 - 57
src/headless/shared/model-with-messages.js

@@ -15,7 +15,7 @@ import { MethodNotImplementedError } from './errors.js';
 import { sendMarker, sendReceiptStanza, sendRetractionMessage } from './actions.js';
 import { parseMessage } from '../plugins/chat/parsers';
 
-const { Strophe, $msg, u } = converse.env;
+const { Strophe, stx, u } = converse.env;
 
 /**
  * Adds a messages collection to a model and various methods related to sending
@@ -155,9 +155,9 @@ export default function ModelWithMessages(BaseModel) {
         }
 
         /**
-         * @param {MessageAttributes|Error} attrs_or_error
+         * @param {MessageAttributes|Error} _attrs_or_error
          */
-        async onMessage(attrs_or_error) {
+        async onMessage(_attrs_or_error) {
             throw new MethodNotImplementedError('onMessage is not implemented');
         }
 
@@ -889,64 +889,48 @@ export default function ModelWithMessages(BaseModel) {
         /**
          * Given a {@link Message} return the XML stanza that represents it.
          * @method ChatBox#createMessageStanza
-         * @param { Message } message - The message object
+         * @param {Message} message - The message object
          */
         async createMessageStanza(message) {
-            const stanza = $msg({
-                'from': message.get('from') || api.connection.get().jid,
-                'to': message.get('to') || this.get('jid'),
-                'type': this.get('message_type'),
-                'id': (message.get('edited') && u.getUniqueId()) || message.get('msgid'),
-            })
-                .c('body')
-                .t(message.get('body'))
-                .up()
-                .c(constants.ACTIVE, { 'xmlns': Strophe.NS.CHATSTATES })
-                .root();
-
-            if (message.get('type') === 'chat') {
-                stanza.c('request', { 'xmlns': Strophe.NS.RECEIPTS }).root();
-            }
-
-            if (!message.get('is_encrypted')) {
-                if (message.get('is_spoiler')) {
-                    if (message.get('spoiler_hint')) {
-                        stanza.c('spoiler', { 'xmlns': Strophe.NS.SPOILER }, message.get('spoiler_hint')).root();
-                    } else {
-                        stanza.c('spoiler', { 'xmlns': Strophe.NS.SPOILER }).root();
-                    }
-                }
-                (message.get('references') || []).forEach((reference) => {
-                    const attrs = {
-                        'xmlns': Strophe.NS.REFERENCE,
-                        'begin': reference.begin,
-                        'end': reference.end,
-                        'type': reference.type,
-                    };
-                    if (reference.uri) {
-                        attrs.uri = reference.uri;
+            const {
+                body,
+                edited,
+                is_encrypted,
+                is_spoiler,
+                msgid,
+                oob_url,
+                origin_id,
+                references,
+                spoiler_hint,
+                type,
+            } = message.attributes;
+
+            const stanza = stx`
+                <message xmlns="jabber:client"
+                        from="${message.get('from') || api.connection.get().jid}"
+                        to="${message.get('to') || this.get('jid')}"
+                        type="${this.get('message_type')}"
+                        id="${(edited && u.getUniqueId()) || msgid}">
+                    ${body ? stx`<body>${body}</body>` : ''}
+                    <active xmlns="${Strophe.NS.CHATSTATES}"/>
+                    ${type === 'chat' ? stx`<request xmlns="${Strophe.NS.RECEIPTS}"></request>` : ''}
+                    ${!is_encrypted && oob_url ? stx`<x xmlns="${Strophe.NS.OUTOFBAND}"><url>${oob_url}</url></x>` : ''}
+                    ${!is_encrypted && is_spoiler ? stx`<spoiler xmlns="${Strophe.NS.SPOILER}">${spoiler_hint ?? ''}</spoiler>` : ''}
+                    ${
+                        !is_encrypted
+                            ? references?.map(
+                                  (ref) => stx`<reference xmlns="${Strophe.NS.REFERENCE}"
+                                                begin="${ref.begin}"
+                                                end="${ref.end}"
+                                                type="${ref.type}"
+                                                uri="${ref.uri}"></reference>`
+                              )
+                            : ''
                     }
-                    stanza.c('reference', attrs).root();
-                });
-
-                if (message.get('oob_url')) {
-                    stanza.c('x', { 'xmlns': Strophe.NS.OUTOFBAND }).c('url').t(message.get('oob_url')).root();
-                }
-            }
+                    ${edited ? stx`<replace xmlns="${Strophe.NS.MESSAGE_CORRECT}" id="${msgid}"></replace>` : ''}
+                    ${origin_id ? stx`<origin-id xmlns="${Strophe.NS.SID}" id="${origin_id}"></origin-id>` : ''}
+                </message>`;
 
-            if (message.get('edited')) {
-                stanza
-                    .c('replace', {
-                        'xmlns': Strophe.NS.MESSAGE_CORRECT,
-                        'id': message.get('msgid'),
-                    })
-                    .root();
-            }
-
-            if (message.get('origin_id')) {
-                stanza.c('origin-id', { 'xmlns': Strophe.NS.SID, 'id': message.get('origin_id') }).root();
-            }
-            stanza.root();
             /**
              * *Hook* which allows plugins to update an outgoing message stanza
              * @event _converse#createMessageStanza

+ 6 - 1
src/headless/types/plugins/chat/message.d.ts

@@ -157,6 +157,8 @@ declare class Message extends Message_base {
         is_ephemeral: boolean;
     };
     file: any;
+    /** @type {import('./types').MessageAttributes} */
+    attributes: import("./types").MessageAttributes;
     initialize(): Promise<void>;
     chatbox: any;
     initialized: any;
@@ -204,7 +206,10 @@ declare class Message extends Message_base {
      * @method _converse.Message#sendSlotRequestStanza
      */
     private sendSlotRequestStanza;
-    getUploadRequestMetadata(stanza: any): {
+    /**
+     * @param {Element} stanza
+     */
+    getUploadRequestMetadata(stanza: Element): {
         headers: {
             name: string;
             value: string;

+ 1 - 1
src/headless/types/plugins/chat/model.d.ts

@@ -17,7 +17,7 @@ declare const ChatBox_base: {
         messages: any;
         fetchMessages(): any;
         afterMessagesFetched(): void;
-        onMessage(attrs_or_error: import("./types").MessageAttributes | Error): Promise<void>;
+        onMessage(_attrs_or_error: import("./types").MessageAttributes | Error): Promise<void>;
         getUpdatedMessageAttributes(message: import("./message.js").default, attrs: import("./types").MessageAttributes): object;
         updateMessage(message: import("./message.js").default, attrs: import("./types").MessageAttributes): void;
         handleCorrection(attrs: import("./types").MessageAttributes | import("../muc/types.js").MUCMessageAttributes): Promise<import("./message.js").default | void>;

+ 7 - 1
src/headless/types/plugins/chat/types.d.ts

@@ -1,4 +1,10 @@
 import { EncryptionAttrs } from "../../shared/types";
+export type Reference = {
+    begin: number;
+    end: number;
+    type: string;
+    uri: string;
+};
 export type MessageErrorAttributes = {
     is_error: boolean;
     error: string;
@@ -42,7 +48,7 @@ export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & {
     plaintext: string;
     receipt_id: string;
     received: string;
-    references: Array<Object>;
+    references: Array<Reference>;
     replace_id: string;
     retracted: string;
     retracted_id: string;

+ 1 - 1
src/headless/types/plugins/muc/muc.d.ts

@@ -17,7 +17,7 @@ declare const MUC_base: {
         messages: any;
         fetchMessages(): any;
         afterMessagesFetched(): void;
-        onMessage(attrs_or_error: import("../chat/types").MessageAttributes | Error): Promise<void>;
+        onMessage(_attrs_or_error: import("../chat/types").MessageAttributes | Error): Promise<void>;
         getUpdatedMessageAttributes(message: import("../chat/message.js").default, attrs: import("../chat/types").MessageAttributes): object;
         updateMessage(message: import("../chat/message.js").default, attrs: import("../chat/types").MessageAttributes): void;
         handleCorrection(attrs: import("../chat/types").MessageAttributes | import("./types").MUCMessageAttributes): Promise<import("../chat/message.js").default | void>;

+ 1 - 1
src/headless/types/plugins/muc/occupant.d.ts

@@ -17,7 +17,7 @@ declare const MUCOccupant_base: {
         messages: any;
         fetchMessages(): any;
         afterMessagesFetched(): void;
-        onMessage(attrs_or_error: import("../chat/types").MessageAttributes | Error): Promise<void>;
+        onMessage(_attrs_or_error: import("../chat/types").MessageAttributes | Error): Promise<void>;
         getUpdatedMessageAttributes(message: import("../chat").Message, attrs: import("../chat/types").MessageAttributes): object;
         updateMessage(message: import("../chat").Message, attrs: import("../chat/types").MessageAttributes): void;
         handleCorrection(attrs: import("../chat/types").MessageAttributes | import("./types").MUCMessageAttributes): Promise<import("../chat").Message | void>;

+ 1 - 1
src/headless/types/shared/chatbox.d.ts

@@ -16,7 +16,7 @@ declare const ChatBoxBase_base: {
         messages: any;
         fetchMessages(): any;
         afterMessagesFetched(): void;
-        onMessage(attrs_or_error: import("../plugins/chat/types.js").MessageAttributes | Error): Promise<void>;
+        onMessage(_attrs_or_error: import("../plugins/chat/types.js").MessageAttributes | Error): Promise<void>;
         getUpdatedMessageAttributes(message: import("../index.js").Message, attrs: import("../plugins/chat/types.js").MessageAttributes): object;
         updateMessage(message: import("../index.js").Message, attrs: import("../plugins/chat/types.js").MessageAttributes): void;
         handleCorrection(attrs: import("../plugins/chat/types.js").MessageAttributes | import("../plugins/muc/types.js").MUCMessageAttributes): Promise<import("../index.js").Message | void>;

+ 3 - 3
src/headless/types/shared/model-with-messages.d.ts

@@ -38,9 +38,9 @@ export default function ModelWithMessages<T extends import("./types").ModelExten
         fetchMessages(): any;
         afterMessagesFetched(): void;
         /**
-         * @param {MessageAttributes|Error} attrs_or_error
+         * @param {MessageAttributes|Error} _attrs_or_error
          */
-        onMessage(attrs_or_error: import("../plugins/chat/types.ts").MessageAttributes | Error): Promise<void>;
+        onMessage(_attrs_or_error: import("../plugins/chat/types.ts").MessageAttributes | Error): Promise<void>;
         /**
          * @param {Message} message
          * @param {MessageAttributes} attrs
@@ -216,7 +216,7 @@ export default function ModelWithMessages<T extends import("./types").ModelExten
         /**
          * Given a {@link Message} return the XML stanza that represents it.
          * @method ChatBox#createMessageStanza
-         * @param { Message } message - The message object
+         * @param {Message} message - The message object
          */
         createMessageStanza(message: import("../plugins/chat/message").default): Promise<any>;
         /**

+ 2 - 0
src/plugins/adhoc-views/tests/adhoc.js

@@ -4,6 +4,8 @@ const { Strophe, sizzle, u, stx } = converse.env;
 
 describe("Ad-hoc commands", function () {
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     it("can be queried for via a modal", mock.initConverse([], {}, async (_converse) => {
         const { api } = _converse;
         const entity_jid = 'muc.montague.lit';

+ 12 - 10
src/plugins/chatview/tests/corrections.js

@@ -4,6 +4,8 @@ const { Promise, $msg, Strophe, sizzle, u } = converse.env;
 
 describe("A Chat Message", function () {
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     it("can be sent as a correction by using the up arrow",
             mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
@@ -56,16 +58,16 @@ describe("A Chat Message", function () {
 
         expect(api.connection.get().send).toHaveBeenCalled();
         const msg = api.connection.get().send.calls.all()[0].args[0];
-        expect(Strophe.serialize(msg))
-        .toBe(`<message from="romeo@montague.lit/orchard" id="${msg.getAttribute("id")}" `+
-                `to="mercutio@montague.lit" type="chat" `+
-                `xmlns="jabber:client">`+
-                    `<body>But soft, what light through yonder window breaks?</body>`+
-                    `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
-                    `<request xmlns="urn:xmpp:receipts"/>`+
-                    `<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+
-                    `<origin-id id="${msg.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
-            `</message>`);
+        expect(msg).toEqualStanza(
+            stx`<message from="romeo@montague.lit/orchard" id="${msg.getAttribute("id")}"
+                        to="mercutio@montague.lit" type="chat"
+                        xmlns="jabber:client">
+                    <body>But soft, what light through yonder window breaks?</body>
+                    <active xmlns="http://jabber.org/protocol/chatstates"/>
+                    <request xmlns="urn:xmpp:receipts"/>
+                    <replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>
+                    <origin-id id="${msg.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>
+            </message>`);
         expect(view.model.messages.models.length).toBe(1);
         const corrected_message = view.model.messages.at(0);
         expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid'));

+ 30 - 75
src/plugins/chatview/tests/spoilers.js

@@ -1,5 +1,6 @@
 /* global mock, converse */
 
+const { Strophe, sizzle, $msg, u, stx } = converse.env;
 const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
 
 const settings = {
@@ -11,6 +12,7 @@ const settings = {
 
 describe("A spoiler message", function () {
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
     beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
     afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
 
@@ -20,26 +22,13 @@ describe("A spoiler message", function () {
         const { api } = _converse;
         await mock.waitForRoster(_converse, 'current');
         const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-
-        /* <message to='romeo@montague.net/orchard' from='juliet@capulet.net/balcony' id='spoiler2'>
-         *      <body>And at the end of the story, both of them die! It is so tragic!</body>
-         *      <spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>
-         *  </message>
-         */
         const spoiler_hint = "Love story end"
         const spoiler = "And at the end of the story, both of them die! It is so tragic!";
-        const $msg = converse.env.$msg;
-        const u = converse.env.utils;
-        const msg = $msg({
-                'xmlns': 'jabber:client',
-                'to': _converse.bare_jid,
-                'from': sender_jid,
-                'type': 'chat'
-            }).c('body').t(spoiler).up()
-                .c('spoiler', {
-                    'xmlns': 'urn:xmpp:spoiler:0',
-                }).t(spoiler_hint)
-            .tree();
+        const msg = stx`
+            <message xmlns="jabber:client" to="${_converse.bare_jid}" from="${sender_jid}" type="chat">
+                <body>${spoiler}</body>
+                <spoiler xmlns="urn:xmpp:spoiler:0">${spoiler_hint}</spoiler>
+            </message>`;
         api.connection.get()._dataRecv(mock.createRequest(msg));
         await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve));
         const view = _converse.chatboxviews.get(sender_jid);
@@ -58,23 +47,12 @@ describe("A spoiler message", function () {
         const { api } = _converse;
         await mock.waitForRoster(_converse, 'current');
         const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-        /* <message to='romeo@montague.net/orchard' from='juliet@capulet.net/balcony' id='spoiler2'>
-         *      <body>And at the end of the story, both of them die! It is so tragic!</body>
-         *      <spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>
-         *  </message>
-         */
-        const $msg = converse.env.$msg;
-        const u = converse.env.utils;
         const spoiler = "And at the end of the story, both of them die! It is so tragic!";
-        const msg = $msg({
-                'xmlns': 'jabber:client',
-                'to': _converse.bare_jid,
-                'from': sender_jid,
-                'type': 'chat'
-            }).c('body').t(spoiler).up()
-                .c('spoiler', {
-                    'xmlns': 'urn:xmpp:spoiler:0',
-                }).tree();
+        const msg = stx`
+            <message xmlns="jabber:client" to="${_converse.bare_jid}" from="${sender_jid}" type="chat">
+                <body>${spoiler}</body>
+                <spoiler xmlns="urn:xmpp:spoiler:0"></spoiler>
+            </message>`;
         api.connection.get()._dataRecv(mock.createRequest(msg));
         await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve));
         const view = _converse.chatboxviews.get(sender_jid);
@@ -97,17 +75,11 @@ describe("A spoiler message", function () {
         mock.openControlBox(_converse);
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
-        const { $pres, Strophe} = converse.env;
-        const u = converse.env.utils;
-
         // XXX: We need to send a presence from the contact, so that we
         // have a resource, that resource is then queried to see
         // whether Strophe.NS.SPOILER is supported, in which case
         // the spoiler button will appear.
-        const presence = $pres({
-            'from': contact_jid+'/phone',
-            'to': 'romeo@montague.lit'
-        });
+        const presence = stx`<presence xmlns="jabber:client" from="${contact_jid}/phone" to="romeo@montague.lit"/>`;
         api.connection.get()._dataRecv(mock.createRequest(presence));
         await mock.openChatBoxFor(_converse, contact_jid);
         await mock.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]);
@@ -128,21 +100,9 @@ describe("A spoiler message", function () {
         });
         await new Promise(resolve => api.listen.on('sendMessage', resolve));
 
-        /* Test the XML stanza
-         *
-         * <message from="romeo@montague.lit/orchard"
-         *          to="max.frankfurter@montague.lit"
-         *          type="chat"
-         *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
-         *          xmlns="jabber:client">
-         *    <body>This is the spoiler</body>
-         *    <active xmlns="http://jabber.org/protocol/chatstates"/>
-         *    <spoiler xmlns="urn:xmpp:spoiler:0"/>
-         * </message>"
-         */
         const stanza = api.connection.get().send.calls.argsFor(0)[0];
-        const spoiler_el = await u.waitUntil(() => stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]'));
-        expect(spoiler_el.textContent).toBe('');
+        const spoiler_el = sizzle('spoiler[xmlns="urn:xmpp:spoiler:0"]', stanza).pop();
+        expect(spoiler_el.textContent).toBe('')
 
         const spoiler = 'This is the spoiler';
         const body_el = stanza.querySelector('body');
@@ -174,17 +134,11 @@ describe("A spoiler message", function () {
         mock.openControlBox(_converse);
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
 
-        const { $pres, Strophe} = converse.env;
-        const u = converse.env.utils;
-
         // XXX: We need to send a presence from the contact, so that we
         // have a resource, that resource is then queried to see
         // whether Strophe.NS.SPOILER is supported, in which case
         // the spoiler button will appear.
-        const presence = $pres({
-            'from': contact_jid+'/phone',
-            'to': 'romeo@montague.lit'
-        });
+        const presence = stx`<presence xmlns="jabber:client" from="${contact_jid}/phone" to="romeo@montague.lit"/>`;
         api.connection.get()._dataRecv(mock.createRequest(presence));
         await mock.openChatBoxFor(_converse, contact_jid);
         await mock.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]);
@@ -210,21 +164,22 @@ describe("A spoiler message", function () {
         await new Promise(resolve => api.listen.on('sendMessage', resolve));
 
         const stanza = api.connection.get().send.calls.argsFor(0)[0];
-        expect(Strophe.serialize(stanza)).toBe(
-            `<message from="romeo@montague.lit/orchard" ` +
-                    `id="${stanza.getAttribute('id')}" `+
-                    `to="mercutio@montague.lit" `+
-                    `type="chat" `+
-                    `xmlns="jabber:client">`+
-                `<body>This is the spoiler</body>`+
-                `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
-                `<request xmlns="urn:xmpp:receipts"/>`+
-                `<spoiler xmlns="urn:xmpp:spoiler:0">This is the hint</spoiler>`+
-                `<origin-id id="${stanza.querySelector('origin-id').getAttribute('id')}" xmlns="urn:xmpp:sid:0"/>`+
-            `</message>`
+        expect(stanza).toEqualStanza(
+            stx`<message from="romeo@montague.lit/orchard"
+                    id="${stanza.getAttribute('id')}"
+                    to="mercutio@montague.lit"
+                    type="chat"
+                    xmlns="jabber:client">
+                <body>This is the spoiler</body>
+                <active xmlns="http://jabber.org/protocol/chatstates"/>
+                <request xmlns="urn:xmpp:receipts"/>
+                <spoiler xmlns="urn:xmpp:spoiler:0">This is the hint</spoiler>
+                <origin-id id="${stanza.querySelector('origin-id').getAttribute('id')}" xmlns="urn:xmpp:sid:0"/>
+            </message>`
         );
 
-        await u.waitUntil(() => stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]')?.textContent === 'This is the hint');
+        const spoiler_el = sizzle('spoiler[xmlns="urn:xmpp:spoiler:0"]', stanza).pop();
+        expect(spoiler_el?.textContent).toBe('This is the hint');
 
         const spoiler = 'This is the spoiler'
         const body_el = stanza.querySelector('body');

+ 10 - 9
src/plugins/muc-views/tests/muc-private-messages.js

@@ -130,15 +130,16 @@ describe('MUC Private Messages', () => {
 
                 const sent_stanza = api.connection.get().sent_stanzas.pop();
                 expect(sent_stanza).toEqualStanza(stx`
-                        <message from="${muc_jid}/${nick}"
-                                to="${muc_jid}/firstwitch"
-                                id="${sent_stanza.getAttribute('id')}"
-                                xmlns="jabber:client">
-                            <body>hello</body>
-                            <active xmlns="http://jabber.org/protocol/chatstates"/>
-                            <request xmlns="urn:xmpp:receipts"/>
-                            <origin-id xmlns="urn:xmpp:sid:0" id="${sent_stanza.querySelector('origin-id')?.getAttribute('id')}"/>
-                        </message>`);
+                    <message from="${muc_jid}/${nick}"
+                            type="chat"
+                            to="${muc_jid}/firstwitch"
+                            id="${sent_stanza.getAttribute('id')}"
+                            xmlns="jabber:client">
+                        <body>hello</body>
+                        <active xmlns="http://jabber.org/protocol/chatstates"/>
+                        <request xmlns="urn:xmpp:receipts"/>
+                        <origin-id xmlns="urn:xmpp:sid:0" id="${sent_stanza.querySelector('origin-id')?.getAttribute('id')}"/>
+                    </message>`);
             })
         );
 

+ 27 - 35
src/plugins/omemo/utils.js

@@ -17,7 +17,7 @@ import { MIMETYPES_MAP } from 'utils/file.js';
 import { IQError, UserFacingError } from 'shared/errors.js';
 import DeviceLists from './devicelists.js';
 
-const { Strophe, URI, sizzle } = converse.env;
+const { Strophe, URI, sizzle, stx } = converse.env;
 const { CHATROOMS_TYPE, PRIVATE_CHAT_TYPE } = constants;
 const {
     appendArrayBuffer,
@@ -467,24 +467,6 @@ async function decryptWhisperMessage (attrs) {
     }
 }
 
-export function addKeysToMessageStanza (stanza, dicts, iv) {
-    for (const i in dicts) {
-        if (Object.prototype.hasOwnProperty.call(dicts, i)) {
-            const payload = dicts[i].payload;
-            const device = dicts[i].device;
-            const prekey = 3 == parseInt(payload.type, 10);
-
-            stanza.c('key', { 'rid': device.get('id') }).t(btoa(payload.body));
-            if (prekey) {
-                stanza.attrs({ 'prekey': prekey });
-            }
-            stanza.up();
-        }
-    }
-    stanza.c('iv').t(iv).up().up();
-    return Promise.resolve(stanza);
-}
-
 /**
  * Given an XML element representing a user's OMEMO bundle, parse it
  * and return a map.
@@ -873,16 +855,6 @@ export async function createOMEMOMessageStanza (chat, data) {
         throw new Error('No message body to encrypt!');
     }
     const devices = await getBundlesAndBuildSessions(chat);
-
-    // An encrypted header is added to the message for
-    // each device that is supposed to receive it.
-    // These headers simply contain the key that the
-    // payload message is encrypted with,
-    // and they are separately encrypted using the
-    // session corresponding to the counterpart device.
-    stanza.c('encrypted', { 'xmlns': Strophe.NS.OMEMO })
-        .c('header', { 'sid': _converse.state.omemo_store.get('device_id') });
-
     const { key_and_tag, iv, payload } = await omemo.encryptMessage(message.get('plaintext'));
 
     // The 16 bytes key and the GCM authentication tag (The tag
@@ -892,13 +864,33 @@ export async function createOMEMOMessageStanza (chat, data) {
     // concatenation is encrypted using the corresponding
     // long-standing SignalProtocol session.
     const dicts = await Promise.all(devices
-        .filter(device => device.get('trusted') != UNTRUSTED && device.get('active'))
-        .map(device => encryptKey(key_and_tag, device)));
+        .filter((device) => device.get('trusted') != UNTRUSTED && device.get('active'))
+        .map((device) => encryptKey(key_and_tag, device)));
 
-    stanza = await addKeysToMessageStanza(stanza, dicts, iv);
-    stanza.c('payload').t(payload).up().up();
-    stanza.c('store', { 'xmlns': Strophe.NS.HINTS }).up();
-    stanza.c('encryption', { 'xmlns': Strophe.NS.EME,  namespace: Strophe.NS.OMEMO });
+    // An encrypted header is added to the message for
+    // each device that is supposed to receive it.
+    // These headers simply contain the key that the
+    // payload message is encrypted with,
+    // and they are separately encrypted using the
+    // session corresponding to the counterpart device.
+    stanza.cnode(
+        stx`<encrypted xmlns="${Strophe.NS.OMEMO}">
+            <header sid="${_converse.state.omemo_store.get('device_id')}">
+                ${dicts.map(({ payload, device }) => {
+                    const prekey = 3 == parseInt(payload.type, 10);
+                    if (prekey) {
+                        return stx`<key rid="${device.get('id')}" prekey="true">${btoa(payload.body)}</key>`;
+                    }
+                    return stx`<key rid="${device.get('id')}">${btoa(payload.body)}</key>`;
+                })}
+                <iv>${iv}</iv>
+            </header>
+            <payload>${payload}</payload>
+        </encrypted>`
+    ).root();
+
+    stanza.cnode(stx`<store xmlns="${Strophe.NS.HINTS}"/>`).root();
+    stanza.cnode(stx`<encryption xmlns="${Strophe.NS.EME}" namespace="${Strophe.NS.OMEMO}"/>`).root();
     return { message, stanza };
 }