diff --git a/karma.conf.js b/karma.conf.js
index 8e0939fd3..3babc9bc2 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -49,6 +49,7 @@ module.exports = function(config) {
{ pattern: "spec/corrections.js", type: 'module' },
{ pattern: "spec/styling.js", type: 'module' },
{ pattern: "spec/receipts.js", type: 'module' },
+ { pattern: "spec/markers.js", type: 'module' },
{ pattern: "spec/muc_messages.js", type: 'module' },
{ pattern: "spec/me-messages.js", type: 'module' },
{ pattern: "spec/mentions.js", type: 'module' },
diff --git a/spec/chatbox.js b/spec/chatbox.js
index 57ab92718..5754cdeb3 100644
--- a/spec/chatbox.js
+++ b/spec/chatbox.js
@@ -1007,19 +1007,17 @@ describe("Chatboxes", function () {
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
- const sent_stanzas = [];
- spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
-
const view = await mock.openChatBoxFor(_converse, sender_jid)
- spyOn(view.model, 'sendMarker').and.callThrough();
+ const sent_stanzas = [];
+ spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
view.model.save('scrolled', true);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length);
expect(view.model.get('num_unread')).toBe(1);
const msgid = view.model.messages.last().get('id');
expect(view.model.get('first_unread_id')).toBe(msgid);
- await u.waitUntil(() => view.model.sendMarker.calls.count() === 1);
- expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
+ await u.waitUntil(() => sent_stanzas.length);
+ expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done();
}));
@@ -1031,15 +1029,14 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msg = mock.createChatMessage(_converse, sender_jid, 'This message will be read');
- const sent_stanzas = [];
- spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
await mock.openChatBoxFor(_converse, sender_jid);
+ const sent_stanzas = [];
+ spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
- spyOn(chatbox, 'sendMarker').and.callThrough();
await _converse.handleMessageStanza(msg);
expect(chatbox.get('num_unread')).toBe(0);
- await u.waitUntil(() => chatbox.sendMarker.calls.count() === 2);
- expect(sent_stanzas[1].nodeTree.querySelector('displayed')).toBeDefined();
+ await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 2);
+ expect(sent_stanzas[1].querySelector('displayed')).toBeDefined();
done();
}));
@@ -1053,12 +1050,10 @@ describe("Chatboxes", function () {
const msgFactory = function () {
return mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
};
-
- const sent_stanzas = [];
- spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
await mock.openChatBoxFor(_converse, sender_jid);
+ const sent_stanzas = [];
+ spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
- spyOn(chatbox, 'sendMarker').and.callThrough();
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
@@ -1066,8 +1061,8 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
- await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1);
- expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
+ await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length);
+ expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done();
}));
@@ -1079,11 +1074,10 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
- const sent_stanzas = [];
- spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
await mock.openChatBoxFor(_converse, sender_jid);
+ const sent_stanzas = [];
+ spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
- spyOn(chatbox, 'sendMarker').and.callThrough();
chatbox.save('scrolled', true);
_converse.windowState = 'hidden';
const msg = msgFactory();
@@ -1092,8 +1086,8 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
- await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1);
- expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
+ await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
+ expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done();
}));
@@ -1105,11 +1099,10 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
- const sent_stanzas = [];
- spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
await mock.openChatBoxFor(_converse, sender_jid);
+ const sent_stanzas = [];
+ spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
- spyOn(chatbox, 'sendMarker').and.callThrough();
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
@@ -1117,12 +1110,12 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
- await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1);
- expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
+ await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
+ expect(sent_stanzas[0].querySelector('received')).toBeDefined();
_converse.saveWindowState({'type': 'focus'});
expect(chatbox.get('num_unread')).toBe(0);
- await u.waitUntil(() => chatbox.sendMarker.calls.count() === 2);
- expect(sent_stanzas[1].nodeTree.querySelector('displayed')).toBeDefined();
+ await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 2);
+ expect(sent_stanzas[1].querySelector('displayed')).toBeDefined();
done();
}));
@@ -1134,11 +1127,10 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
- const sent_stanzas = [];
- spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
await mock.openChatBoxFor(_converse, sender_jid);
+ const sent_stanzas = [];
+ spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
- spyOn(chatbox, 'sendMarker').and.callThrough();
chatbox.save('scrolled', true);
_converse.windowState = 'hidden';
const msg = msgFactory();
@@ -1147,13 +1139,12 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
- await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1);
- expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
+ await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
+ expect(sent_stanzas[0].querySelector('received')).toBeDefined();
_converse.saveWindowState({'type': 'focus'});
await u.waitUntil(() => chatbox.get('num_unread') === 1);
expect(chatbox.get('first_unread_id')).toBe(msgid);
- await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1);
- expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
+ expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done();
}));
});
diff --git a/spec/markers.js b/spec/markers.js
new file mode 100644
index 000000000..daa2c807a
--- /dev/null
+++ b/spec/markers.js
@@ -0,0 +1,186 @@
+/*global mock, converse */
+
+const Strophe = converse.env.Strophe;
+const u = converse.env.utils;
+// See: https://xmpp.org/rfcs/rfc3921.html
+
+
+describe("A XEP-0333 Chat Marker", function () {
+
+ it("is sent when a markable message is received from a roster contact",
+ mock.initConverse(
+ ['rosterGroupsFetched'], {},
+ async function (done, _converse) {
+
+ await mock.waitForRoster(_converse, 'current', 1);
+ const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+ await mock.openChatBoxFor(_converse, contact_jid);
+ const msgid = u.getUniqueId();
+ const stanza = u.toStanza(`
+
+ My lord, dispatch; read o'er these articles.
+
+ `);
+
+ const sent_stanzas = [];
+ spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
+ _converse.connection._dataRecv(mock.createRequest(stanza));
+ await u.waitUntil(() => sent_stanzas.length === 2);
+ expect(Strophe.serialize(sent_stanzas[0])).toBe(
+ ``+
+ ``+
+ ``);
+ done();
+ }));
+
+ it("is not sent when a markable message is received from someone not on the roster",
+ mock.initConverse(
+ ['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
+ async function (done, _converse) {
+
+ await mock.waitForRoster(_converse, 'current', 0);
+ const contact_jid = 'someone@montague.lit';
+ const msgid = u.getUniqueId();
+ const stanza = u.toStanza(`
+
+ My lord, dispatch; read o'er these articles.
+
+ `);
+
+ const sent_stanzas = [];
+ spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
+ await _converse.handleMessageStanza(stanza);
+ const sent_messages = sent_stanzas
+ .map(s => s?.nodeTree ?? s)
+ .filter(e => e.nodeName === 'message');
+
+ await u.waitUntil(() => sent_messages.length === 2);
+ expect(Strophe.serialize(sent_messages[0])).toBe(
+ ``+
+ ``+
+ ``+
+ ``+
+ ``
+ );
+ done();
+ }));
+
+ it("is ignored if it's a carbon copy of one that I sent from a different client",
+ mock.initConverse(
+ ['rosterGroupsFetched'], {},
+ async function (done, _converse) {
+
+ await mock.waitForRoster(_converse, 'current', 1);
+ await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
+
+ const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+ await mock.openChatBoxFor(_converse, contact_jid);
+ const view = _converse.api.chatviews.get(contact_jid);
+
+ let stanza = u.toStanza(`
+
+ 😊
+
+
+
+ `);
+ _converse.connection._dataRecv(mock.createRequest(stanza));
+ await new Promise(resolve => view.model.messages.once('rendered', resolve));
+ expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+ expect(view.model.messages.length).toBe(1);
+
+ stanza = u.toStanza(
+ `
+
+
+
+
+
+
+
+
+
+ `);
+ spyOn(_converse.api, "trigger").and.callThrough();
+ _converse.connection._dataRecv(mock.createRequest(stanza));
+ await u.waitUntil(() => _converse.api.trigger.calls.count(), 500);
+ expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+ expect(view.model.messages.length).toBe(1);
+ done();
+ }));
+
+
+ it("may be returned for a MUC message",
+ mock.initConverse(
+ ['rosterGroupsFetched'], {},
+ async function (done, _converse) {
+
+ await mock.waitForRoster(_converse, 'current');
+ const muc_jid = 'lounge@montague.lit';
+ await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+ const view = _converse.api.chatviews.get(muc_jid);
+ const textarea = view.el.querySelector('textarea.chat-textarea');
+ textarea.value = 'But soft, what light through yonder airlock breaks?';
+ view.onKeyDown({
+ target: textarea,
+ preventDefault: function preventDefault () {},
+ keyCode: 13 // Enter
+ });
+ await new Promise(resolve => view.model.messages.once('rendered', resolve));
+ expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+ expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim())
+ .toBe("But soft, what light through yonder airlock breaks?");
+
+ const msg_obj = view.model.messages.at(0);
+ let stanza = u.toStanza(`
+
+
+ `);
+ _converse.connection._dataRecv(mock.createRequest(stanza));
+ await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
+ expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
+
+ stanza = u.toStanza(`
+
+
+ `);
+ _converse.connection._dataRecv(mock.createRequest(stanza));
+ expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+ expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
+
+ stanza = u.toStanza(`
+
+
+ `);
+ _converse.connection._dataRecv(mock.createRequest(stanza));
+
+ expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+ expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
+
+ stanza = u.toStanza(`
+
+ 'tis I!
+
+ `);
+ _converse.connection._dataRecv(mock.createRequest(stanza));
+ await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
+ expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
+ done();
+ }));
+});
diff --git a/spec/messages.js b/spec/messages.js
index a781c1e3a..ccae65bbc 100644
--- a/spec/messages.js
+++ b/spec/messages.js
@@ -1548,122 +1548,3 @@ describe("A Chat Message", function () {
}));
});
});
-
-describe("A XEP-0333 Chat Marker", function () {
-
- it("is sent when a markable message is received from a roster contact",
- mock.initConverse(
- ['rosterGroupsFetched'], {},
- async function (done, _converse) {
-
- await mock.waitForRoster(_converse, 'current', 1);
- const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
- await mock.openChatBoxFor(_converse, contact_jid);
- const view = _converse.api.chatviews.get(contact_jid);
- const msgid = u.getUniqueId();
- const stanza = u.toStanza(`
-
- My lord, dispatch; read o'er these articles.
-
- `);
-
- const sent_stanzas = [];
- spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
- spyOn(view.model, 'sendMarker').and.callThrough();
- _converse.connection._dataRecv(mock.createRequest(stanza));
- await u.waitUntil(() => view.model.sendMarker.calls.count() === 2);
- expect(Strophe.serialize(sent_stanzas[0])).toBe(
- ``+
- ``+
- ``);
- done();
- }));
-
- it("is not sent when a markable message is received from someone not on the roster",
- mock.initConverse(
- ['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
- async function (done, _converse) {
-
- await mock.waitForRoster(_converse, 'current', 0);
- const contact_jid = 'someone@montague.lit';
- const msgid = u.getUniqueId();
- const stanza = u.toStanza(`
-
- My lord, dispatch; read o'er these articles.
-
- `);
-
- const sent_stanzas = [];
- spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
- await _converse.handleMessageStanza(stanza);
- const sent_messages = sent_stanzas
- .map(s => _.isElement(s) ? s : s.nodeTree)
- .filter(e => e.nodeName === 'message');
-
- await u.waitUntil(() => sent_messages.length === 2);
- expect(Strophe.serialize(sent_messages[0])).toBe(
- ``+
- ``+
- ``+
- ``+
- ``
- );
- done();
- }));
-
- it("is ignored if it's a carbon copy of one that I sent from a different client",
- mock.initConverse(
- ['rosterGroupsFetched'], {},
- async function (done, _converse) {
-
- await mock.waitForRoster(_converse, 'current', 1);
- await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
-
- const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
- await mock.openChatBoxFor(_converse, contact_jid);
- const view = _converse.api.chatviews.get(contact_jid);
-
- let stanza = u.toStanza(`
-
- 😊
-
-
-
- `);
- _converse.connection._dataRecv(mock.createRequest(stanza));
- await new Promise(resolve => view.model.messages.once('rendered', resolve));
- expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
- expect(view.model.messages.length).toBe(1);
-
- stanza = u.toStanza(
- `
-
-
-
-
-
-
-
-
-
- `);
- spyOn(_converse.api, "trigger").and.callThrough();
- _converse.connection._dataRecv(mock.createRequest(stanza));
- await u.waitUntil(() => _converse.api.trigger.calls.count(), 500);
- expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
- expect(view.model.messages.length).toBe(1);
- done();
- }));
-});
diff --git a/spec/muc_messages.js b/spec/muc_messages.js
index 00251fbc3..f547bb38b 100644
--- a/spec/muc_messages.js
+++ b/spec/muc_messages.js
@@ -1,6 +1,6 @@
/*global mock, converse */
-const { Promise, Strophe, $msg, $pres, sizzle, stanza_utils } = converse.env;
+const { Promise, Strophe, $msg, $pres, sizzle } = converse.env;
const u = converse.env.utils;
const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
@@ -621,85 +621,28 @@ describe("A Groupchat Message", function () {
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
const msg_obj = view.model.messages.at(0);
- const stanza = u.toStanza(`
+ let stanza = u.toStanza(`
+
+ ${msg_obj.get('message')}
+
+
+ `);
+ await view.model.handleMessageStanza(stanza);
+ await u.waitUntil(() => view.model.messages.last().get('received'));
+
+ stanza = u.toStanza(`
`);
- spyOn(stanza_utils, "parseMUCMessage").and.callThrough();
_converse.connection._dataRecv(mock.createRequest(stanza));
- await u.waitUntil(() => stanza_utils.parseMUCMessage.calls.count() === 1);
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
- expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
- done();
- }));
-
- it("can cause a chat marker to be returned",
- mock.initConverse(
- ['rosterGroupsFetched'], {},
- async function (done, _converse) {
-
- await mock.waitForRoster(_converse, 'current');
- const muc_jid = 'lounge@montague.lit';
- await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
- const view = _converse.api.chatviews.get(muc_jid);
- const textarea = view.el.querySelector('textarea.chat-textarea');
- textarea.value = 'But soft, what light through yonder airlock breaks?';
- view.onKeyDown({
- target: textarea,
- preventDefault: function preventDefault () {},
- keyCode: 13 // Enter
- });
- await new Promise(resolve => view.model.messages.once('rendered', resolve));
- expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
- expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim())
- .toBe("But soft, what light through yonder airlock breaks?");
-
- const msg_obj = view.model.messages.at(0);
- let stanza = u.toStanza(`
-
-
- `);
- const stanza_utils = converse.env.stanza_utils;
- spyOn(stanza_utils, "getChatMarker").and.callThrough();
- _converse.connection._dataRecv(mock.createRequest(stanza));
- await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 1);
- expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
- expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
-
- stanza = u.toStanza(`
-
-
- `);
- _converse.connection._dataRecv(mock.createRequest(stanza));
- await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 2);
- expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
- expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
-
- stanza = u.toStanza(`
-
-
- `);
- _converse.connection._dataRecv(mock.createRequest(stanza));
-
- await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 3);
- expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
- expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
-
- stanza = u.toStanza(`
-
- 'tis I!
-
- `);
- _converse.connection._dataRecv(mock.createRequest(stanza));
- await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 4);
- await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
- expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
done();
}));
});
diff --git a/src/headless/core.js b/src/headless/core.js
index c2b9537fc..8e900a6ec 100644
--- a/src/headless/core.js
+++ b/src/headless/core.js
@@ -12,7 +12,6 @@ import pluggable from 'pluggable.js/src/pluggable';
import syncDriver from 'localforage-webextensionstorage-driver/sync';
import localDriver from 'localforage-webextensionstorage-driver/local';
import sizzle from 'sizzle';
-import stanza_utils from "@converse/headless/utils/stanza";
import u from '@converse/headless/utils/core';
import { Collection } from "@converse/skeletor/src/collection";
import { Connection, MockConnection } from '@converse/headless/shared/connection.js';
@@ -1654,7 +1653,6 @@ Object.assign(converse, {
log,
sizzle,
sprintf,
- stanza_utils,
u,
}
});
diff --git a/src/headless/plugins/adhoc.js b/src/headless/plugins/adhoc.js
index 9e167e5a6..39e2d0ddf 100644
--- a/src/headless/plugins/adhoc.js
+++ b/src/headless/plugins/adhoc.js
@@ -1,7 +1,7 @@
import { converse } from "../core.js";
import log from "@converse/headless/log";
import sizzle from 'sizzle';
-import st from "../utils/stanza";
+import { getAttributes } from '@converse/headless/shared/parsers';
const { Strophe } = converse.env;
let _converse, api;
@@ -11,7 +11,7 @@ Strophe.addNamespace('ADHOC', 'http://jabber.org/protocol/commands');
function parseForCommands (stanza) {
const items = sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"][node="${Strophe.NS.ADHOC}"] item`, stanza);
- return items.map(st.getAttributes)
+ return items.map(getAttributes)
}
diff --git a/src/headless/plugins/chat/index.js b/src/headless/plugins/chat/index.js
index 3d5adf0d2..6e21def46 100644
--- a/src/headless/plugins/chat/index.js
+++ b/src/headless/plugins/chat/index.js
@@ -8,9 +8,10 @@ import MessageMixin from './message.js';
import ModelWithContact from './model-with-contact.js';
import chat_api from './api.js';
import log from '../../log.js';
-import st from '../../utils/stanza';
import { Collection } from "@converse/skeletor/src/collection";
import { _converse, api, converse } from '../../core.js';
+import { isServerMessage, } from '@converse/headless/shared/parsers';
+import { parseMessage } from './parsers.js';
const { Strophe, sizzle, utils } = converse.env;
const u = converse.env.utils;
@@ -74,12 +75,12 @@ converse.plugins.add('converse-chat', {
* @param { MessageAttributes } attrs - The message attributes
*/
_converse.handleMessageStanza = async function (stanza) {
- if (st.isServerMessage(stanza)) {
+ if (isServerMessage(stanza)) {
// Prosody sends headline messages with type `chat`, so we need to filter them out here.
const from = stanza.getAttribute('from');
return log.info(`handleMessageStanza: Ignoring incoming server message from JID: ${from}`);
}
- const attrs = await st.parseMessage(stanza, _converse);
+ const attrs = await parseMessage(stanza, _converse);
if (u.isErrorObject(attrs)) {
attrs.stanza && log.error(attrs.stanza);
return log.error(attrs.message);
diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js
index d6d5de769..862ceac8f 100644
--- a/src/headless/plugins/chat/model.js
+++ b/src/headless/plugins/chat/model.js
@@ -1,10 +1,11 @@
import ModelWithContact from './model-with-contact.js';
import filesize from "filesize";
-import log from "../../log.js";
-import st from "../../utils/stanza";
+import log from '@converse/headless/log';
import { Model } from '@converse/skeletor/src/model.js';
import { _converse, api, converse } from "../../core.js";
import { find, isMatch, isObject, pick } from "lodash-es";
+import { parseMessage } from './parsers.js';
+import { sendMarker } from '@converse/headless/shared/actions';
const { Strophe, $msg } = converse.env;
@@ -130,7 +131,7 @@ const ChatBox = ModelWithContact.extend({
async handleErrorMessageStanza (stanza) {
const { __ } = _converse;
- const attrs = await st.parseMessage(stanza, _converse);
+ const attrs = await parseMessage(stanza, _converse);
if (!await this.shouldShowErrorMessage(attrs)) {
return;
}
@@ -392,7 +393,7 @@ const ChatBox = ModelWithContact.extend({
* @private
* @method _converse.ChatBox#findDanglingRetraction
* @param { object } attrs - Attributes representing a received
- * message, as returned by {@link st.parseMessage}
+ * message, as returned by {@link parseMessage}
* @returns { _converse.Message }
*/
findDanglingRetraction (attrs) {
@@ -419,7 +420,7 @@ const ChatBox = ModelWithContact.extend({
* @private
* @method _converse.ChatBox#handleRetraction
* @param { object } attrs - Attributes representing a received
- * message, as returned by {@link st.parseMessage}
+ * message, as returned by {@link parseMessage}
* @returns { Boolean } Returns `true` or `false` depending on
* whether a message was retracted or not.
*/
@@ -459,7 +460,7 @@ const ChatBox = ModelWithContact.extend({
* @private
* @method _converse.ChatBox#handleCorrection
* @param { object } attrs - Attributes representing a received
- * message, as returned by {@link st.parseMessage}
+ * message, as returned by {@link parseMessage}
* @returns { _converse.Message|undefined } Returns the corrected
* message or `undefined` if not applicable.
*/
@@ -497,7 +498,7 @@ const ChatBox = ModelWithContact.extend({
* @private
* @method _converse.ChatBox#getDuplicateMessage
* @param { object } attrs - Attributes representing a received
- * message, as returned by {@link st.parseMessage}
+ * message, as returned by {@link parseMessage}
* @returns {Promise<_converse.Message>}
*/
getDuplicateMessage (attrs) {
@@ -604,27 +605,10 @@ const ChatBox = ModelWithContact.extend({
if (!msg) return;
if (msg?.get('is_markable') || force) {
const from_jid = Strophe.getBareJidFromJid(msg.get('from'));
- this.sendMarker(from_jid, msg.get('msgid'), type, msg.get('type'));
+ sendMarker(from_jid, msg.get('msgid'), type, msg.get('type'));
}
},
- /**
- * Send out a XEP-0333 chat marker
- * @param { String } to_jid
- * @param { String } id - The id of the message being marked
- * @param { String } type - The marker type
- * @param { String } msg_type
- */
- sendMarker (to_jid, id, type, msg_type) {
- const stanza = $msg({
- 'from': _converse.connection.jid,
- 'id': u.getUniqueId(),
- 'to': to_jid,
- 'type': msg_type ? msg_type : 'chat'
- }).c(type, {'xmlns': Strophe.NS.MARKERS, 'id': id});
- api.send(stanza);
- },
-
handleChatMarker (attrs) {
const to_bare_jid = Strophe.getBareJidFromJid(attrs.to);
if (to_bare_jid !== _converse.bare_jid) {
@@ -632,7 +616,7 @@ const ChatBox = ModelWithContact.extend({
}
if (attrs.is_markable) {
if (this.contact && !attrs.is_archived && !attrs.is_carbon) {
- this.sendMarker(attrs.from, attrs.msgid, 'received');
+ sendMarker(attrs.from, attrs.msgid, 'received');
}
return false;
} else if (attrs.marker_id) {
diff --git a/src/headless/plugins/chat/parsers.js b/src/headless/plugins/chat/parsers.js
new file mode 100644
index 000000000..c4238c858
--- /dev/null
+++ b/src/headless/plugins/chat/parsers.js
@@ -0,0 +1,219 @@
+import dayjs from 'dayjs';
+import log from '@converse/headless/log';
+import u from '@converse/headless/utils/core';
+import { api, converse } from '@converse/headless/core';
+import { rejectMessage } from '@converse/headless/shared/actions';
+
+import {
+ StanzaParseError,
+ getChatMarker,
+ getChatState,
+ getCorrectionAttributes,
+ getEncryptionAttributes,
+ getErrorAttributes,
+ getOutOfBandAttributes,
+ getReceiptId,
+ getReferences,
+ getRetractionAttributes,
+ getSpoilerAttributes,
+ getStanzaIDs,
+ isArchived,
+ isCarbon,
+ isHeadline,
+ isServerMessage,
+ isValidReceiptRequest,
+ rejectUnencapsulatedForward,
+} from '@converse/headless/shared/parsers';
+
+const { Strophe, sizzle } = converse.env;
+
+
+/**
+ * Parses a passed in message stanza and returns an object of attributes.
+ * @method st#parseMessage
+ * @param { XMLElement } stanza - The message stanza
+ * @param { _converse } _converse
+ * @returns { (MessageAttributes|Error) }
+ */
+export async function parseMessage (stanza, _converse) {
+ const err = rejectUnencapsulatedForward(stanza);
+ if (err) {
+ return err;
+ }
+
+ let to_jid = stanza.getAttribute('to');
+ const to_resource = Strophe.getResourceFromJid(to_jid);
+ if (api.settings.get('filter_by_resource') && to_resource && to_resource !== _converse.resource) {
+ return new StanzaParseError(
+ `Ignoring incoming message intended for a different resource: ${to_jid}`,
+ stanza
+ );
+ }
+
+ const original_stanza = stanza;
+ let from_jid = stanza.getAttribute('from') || _converse.bare_jid;
+ if (isCarbon(stanza)) {
+ if (from_jid === _converse.bare_jid) {
+ const selector = `[xmlns="${Strophe.NS.CARBONS}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
+ stanza = sizzle(selector, stanza).pop();
+ to_jid = stanza.getAttribute('to');
+ from_jid = stanza.getAttribute('from');
+ } else {
+ // Prevent message forging via carbons: https://xmpp.org/extensions/xep-0280.html#security
+ rejectMessage(stanza, 'Rejecting carbon from invalid JID');
+ return new StanzaParseError(`Rejecting carbon from invalid JID ${to_jid}`, stanza);
+ }
+ }
+
+ const is_archived = isArchived(stanza);
+ if (is_archived) {
+ if (from_jid === _converse.bare_jid) {
+ const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
+ stanza = sizzle(selector, stanza).pop();
+ to_jid = stanza.getAttribute('to');
+ from_jid = stanza.getAttribute('from');
+ } else {
+ return new StanzaParseError(
+ `Invalid Stanza: alleged MAM message from ${stanza.getAttribute('from')}`,
+ stanza
+ );
+ }
+ }
+
+ const from_bare_jid = Strophe.getBareJidFromJid(from_jid);
+ const is_me = from_bare_jid === _converse.bare_jid;
+ if (is_me && to_jid === null) {
+ return new StanzaParseError(
+ `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`,
+ stanza
+ );
+ }
+
+ const is_headline = isHeadline(stanza);
+ const is_server_message = isServerMessage(stanza);
+ let contact, contact_jid;
+ if (!is_headline && !is_server_message) {
+ contact_jid = is_me ? Strophe.getBareJidFromJid(to_jid) : from_bare_jid;
+ contact = await api.contacts.get(contact_jid);
+ if (contact === undefined && !api.settings.get('allow_non_roster_messaging')) {
+ log.error(stanza);
+ return new StanzaParseError(
+ `Blocking messaging with a JID not in our roster because allow_non_roster_messaging is false.`,
+ stanza
+ );
+ }
+ }
+ /**
+ * @typedef { Object } MessageAttributes
+ * The object which {@link parseMessage} returns
+ * @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else
+ * @property { Array