Commit e80afbfe authored by JC Brand's avatar JC Brand

Move MUC and stanza utils into shared and plugin-specific files

parent e8eea632
...@@ -49,6 +49,7 @@ module.exports = function(config) { ...@@ -49,6 +49,7 @@ module.exports = function(config) {
{ pattern: "spec/corrections.js", type: 'module' }, { pattern: "spec/corrections.js", type: 'module' },
{ pattern: "spec/styling.js", type: 'module' }, { pattern: "spec/styling.js", type: 'module' },
{ pattern: "spec/receipts.js", type: 'module' }, { pattern: "spec/receipts.js", type: 'module' },
{ pattern: "spec/markers.js", type: 'module' },
{ pattern: "spec/muc_messages.js", type: 'module' }, { pattern: "spec/muc_messages.js", type: 'module' },
{ pattern: "spec/me-messages.js", type: 'module' }, { pattern: "spec/me-messages.js", type: 'module' },
{ pattern: "spec/mentions.js", type: 'module' }, { pattern: "spec/mentions.js", type: 'module' },
......
...@@ -1007,19 +1007,17 @@ describe("Chatboxes", function () { ...@@ -1007,19 +1007,17 @@ describe("Chatboxes", function () {
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); 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) 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); view.model.save('scrolled', true);
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length); await u.waitUntil(() => view.model.messages.length);
expect(view.model.get('num_unread')).toBe(1); expect(view.model.get('num_unread')).toBe(1);
const msgid = view.model.messages.last().get('id'); const msgid = view.model.messages.last().get('id');
expect(view.model.get('first_unread_id')).toBe(msgid); expect(view.model.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => view.model.sendMarker.calls.count() === 1); await u.waitUntil(() => sent_stanzas.length);
expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined(); expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done(); done();
})); }));
...@@ -1031,15 +1029,14 @@ describe("Chatboxes", function () { ...@@ -1031,15 +1029,14 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; 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 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); 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); const chatbox = _converse.chatboxes.get(sender_jid);
spyOn(chatbox, 'sendMarker').and.callThrough();
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
expect(chatbox.get('num_unread')).toBe(0); expect(chatbox.get('num_unread')).toBe(0);
await u.waitUntil(() => chatbox.sendMarker.calls.count() === 2); await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 2);
expect(sent_stanzas[1].nodeTree.querySelector('displayed')).toBeDefined(); expect(sent_stanzas[1].querySelector('displayed')).toBeDefined();
done(); done();
})); }));
...@@ -1053,12 +1050,10 @@ describe("Chatboxes", function () { ...@@ -1053,12 +1050,10 @@ describe("Chatboxes", function () {
const msgFactory = function () { const msgFactory = function () {
return mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); 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); 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); const chatbox = _converse.chatboxes.get(sender_jid);
spyOn(chatbox, 'sendMarker').and.callThrough();
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
const msg = msgFactory(); const msg = msgFactory();
_converse.handleMessageStanza(msg); _converse.handleMessageStanza(msg);
...@@ -1066,8 +1061,8 @@ describe("Chatboxes", function () { ...@@ -1066,8 +1061,8 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1); expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id'); const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid); expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1); await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length);
expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined(); expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done(); done();
})); }));
...@@ -1079,11 +1074,10 @@ describe("Chatboxes", function () { ...@@ -1079,11 +1074,10 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; 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 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); 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); const chatbox = _converse.chatboxes.get(sender_jid);
spyOn(chatbox, 'sendMarker').and.callThrough();
chatbox.save('scrolled', true); chatbox.save('scrolled', true);
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
const msg = msgFactory(); const msg = msgFactory();
...@@ -1092,8 +1086,8 @@ describe("Chatboxes", function () { ...@@ -1092,8 +1086,8 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1); expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id'); const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid); expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1); await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined(); expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done(); done();
})); }));
...@@ -1105,11 +1099,10 @@ describe("Chatboxes", function () { ...@@ -1105,11 +1099,10 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; 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 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); 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); const chatbox = _converse.chatboxes.get(sender_jid);
spyOn(chatbox, 'sendMarker').and.callThrough();
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
const msg = msgFactory(); const msg = msgFactory();
_converse.handleMessageStanza(msg); _converse.handleMessageStanza(msg);
...@@ -1117,12 +1110,12 @@ describe("Chatboxes", function () { ...@@ -1117,12 +1110,12 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1); expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id'); const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid); expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1); await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined(); expect(sent_stanzas[0].querySelector('received')).toBeDefined();
_converse.saveWindowState({'type': 'focus'}); _converse.saveWindowState({'type': 'focus'});
expect(chatbox.get('num_unread')).toBe(0); expect(chatbox.get('num_unread')).toBe(0);
await u.waitUntil(() => chatbox.sendMarker.calls.count() === 2); await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 2);
expect(sent_stanzas[1].nodeTree.querySelector('displayed')).toBeDefined(); expect(sent_stanzas[1].querySelector('displayed')).toBeDefined();
done(); done();
})); }));
...@@ -1134,11 +1127,10 @@ describe("Chatboxes", function () { ...@@ -1134,11 +1127,10 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; 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 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); 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); const chatbox = _converse.chatboxes.get(sender_jid);
spyOn(chatbox, 'sendMarker').and.callThrough();
chatbox.save('scrolled', true); chatbox.save('scrolled', true);
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
const msg = msgFactory(); const msg = msgFactory();
...@@ -1147,13 +1139,12 @@ describe("Chatboxes", function () { ...@@ -1147,13 +1139,12 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1); expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id'); const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid); expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1); await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined(); expect(sent_stanzas[0].querySelector('received')).toBeDefined();
_converse.saveWindowState({'type': 'focus'}); _converse.saveWindowState({'type': 'focus'});
await u.waitUntil(() => chatbox.get('num_unread') === 1); await u.waitUntil(() => chatbox.get('num_unread') === 1);
expect(chatbox.get('first_unread_id')).toBe(msgid); expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1); expect(sent_stanzas[0].querySelector('received')).toBeDefined();
expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
done(); done();
})); }));
}); });
......
/*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(`
<message from='${contact_jid}'
id='${msgid}'
type="chat"
to='${_converse.jid}'>
<body>My lord, dispatch; read o'er these articles.</body>
<markable xmlns='urn:xmpp:chat-markers:0'/>
</message>`);
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(
`<message from="romeo@montague.lit/orchard" `+
`id="${sent_stanzas[0].getAttribute('id')}" `+
`to="${contact_jid}" type="chat" xmlns="jabber:client">`+
`<received id="${msgid}" xmlns="urn:xmpp:chat-markers:0"/>`+
`</message>`);
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(`
<message from='${contact_jid}'
id='${msgid}'
type="chat"
to='${_converse.jid}'>
<body>My lord, dispatch; read o'er these articles.</body>
<markable xmlns='urn:xmpp:chat-markers:0'/>
</message>`);
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(
`<message id="${sent_messages[0].getAttribute('id')}" to="${contact_jid}" type="chat" xmlns="jabber:client">`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<no-store xmlns="urn:xmpp:hints"/>`+
`<no-permanent-store xmlns="urn:xmpp:hints"/>`+
`</message>`
);
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(`
<message xmlns="jabber:client"
to="${_converse.bare_jid}"
type="chat"
id="2e972ea0-0050-44b7-a830-f6638a2595b3"
from="${contact_jid}">
<body>😊</body>
<markable xmlns="urn:xmpp:chat-markers:0"/>
<origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
<stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
</message>`);
_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(
`<message xmlns="jabber:client" to="${_converse.bare_jid}" type="chat" from="${contact_jid}">
<sent xmlns="urn:xmpp:carbons:2">
<forwarded xmlns="urn:xmpp:forward:0">
<message xmlns="jabber:client" to="${contact_jid}" type="chat" from="${_converse.bare_jid}/other-resource">
<received xmlns="urn:xmpp:chat-markers:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
<store xmlns="urn:xmpp:hints"/>
<stanza-id xmlns="urn:xmpp:sid:0" id="F4TC6CvHwzqRbeHb" by="${_converse.bare_jid}"/>
</message>
</forwarded>
</sent>
</message>`);
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(`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<received xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
</message>`);
_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(`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<displayed xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
</message>`);
_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(`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<acknowledged xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
</message>`);
_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(`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<body>'tis I!</body>
<markable xmlns="urn:xmpp:chat-markers:0"/>
</message>`);
_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();
}));
});
...@@ -1548,122 +1548,3 @@ describe("A Chat Message", function () { ...@@ -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(`
<message from='${contact_jid}'
id='${msgid}'
type="chat"
to='${_converse.jid}'>
<body>My lord, dispatch; read o'er these articles.</body>
<markable xmlns='urn:xmpp:chat-markers:0'/>
</message>`);
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(
`<message from="romeo@montague.lit/orchard" `+
`id="${sent_stanzas[0].nodeTree.getAttribute('id')}" `+
`to="${contact_jid}" type="chat" xmlns="jabber:client">`+
`<received id="${msgid}" xmlns="urn:xmpp:chat-markers:0"/>`+
`</message>`);
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(`
<message from='${contact_jid}'
id='${msgid}'
type="chat"
to='${_converse.jid}'>
<body>My lord, dispatch; read o'er these articles.</body>
<markable xmlns='urn:xmpp:chat-markers:0'/>
</message>`);
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(
`<message id="${sent_messages[0].getAttribute('id')}" to="${contact_jid}" type="chat" xmlns="jabber:client">`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<no-store xmlns="urn:xmpp:hints"/>`+
`<no-permanent-store xmlns="urn:xmpp:hints"/>`+
`</message>`
);
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(`
<message xmlns="jabber:client"
to="${_converse.bare_jid}"
type="chat"
id="2e972ea0-0050-44b7-a830-f6638a2595b3"
from="${contact_jid}">
<body>😊</body>
<markable xmlns="urn:xmpp:chat-markers:0"/>
<origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
<stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
</message>`);
_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(
`<message xmlns="jabber:client" to="${_converse.bare_jid}" type="chat" from="${contact_jid}">
<sent xmlns="urn:xmpp:carbons:2">
<forwarded xmlns="urn:xmpp:forward:0">
<message xmlns="jabber:client" to="${contact_jid}" type="chat" from="${_converse.bare_jid}/other-resource">
<received xmlns="urn:xmpp:chat-markers:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
<store xmlns="urn:xmpp:hints"/>
<stanza-id xmlns="urn:xmpp:sid:0" id="F4TC6CvHwzqRbeHb" by="${_converse.bare_jid}"/>
</message>
</forwarded>
</sent>
</message>`);
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();
}));
});
/*global mock, converse */ /*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 u = converse.env.utils;
const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
...@@ -620,86 +620,29 @@ describe("A Groupchat Message", function () { ...@@ -620,86 +620,29 @@ describe("A Groupchat Message", function () {
await new Promise(resolve => view.model.messages.once('rendered', resolve)); await new Promise(resolve => view.model.messages.once('rendered', resolve));
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
const msg_obj = view.model.messages.at(0);
const stanza = u.toStanza(`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<received xmlns="urn:xmpp:receipts" id="${msg_obj.get('msgid')}"/>
<origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
</message>`);
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); const msg_obj = view.model.messages.at(0);
let stanza = u.toStanza(` let stanza = u.toStanza(`
<message xml:lang="en" to="romeo@montague.lit/orchard" <message xmlns="jabber:client"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client"> from="${msg_obj.get('from')}"
<received xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/> to="${_converse.connection.jid}"
</message>`); type="groupchat">
const stanza_utils = converse.env.stanza_utils; <body>${msg_obj.get('message')}</body>
spyOn(stanza_utils, "getChatMarker").and.callThrough(); <stanza-id xmlns="urn:xmpp:sid:0"
_converse.connection._dataRecv(mock.createRequest(stanza)); id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 1); by="lounge@montague.lit"/>
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
stanza = u.toStanza(`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<displayed xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
</message>`); </message>`);
_converse.connection._dataRecv(mock.createRequest(stanza)); await view.model.handleMessageStanza(stanza);
await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 2); await u.waitUntil(() => view.model.messages.last().get('received'));
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
stanza = u.toStanza(` stanza = u.toStanza(`
<message xml:lang="en" to="romeo@montague.lit/orchard" <message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client"> from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<acknowledged xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/> <received xmlns="urn:xmpp:receipts" id="${msg_obj.get('msgid')}"/>
<origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
</message>`); </message>`);
_converse.connection._dataRecv(mock.createRequest(stanza)); _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').length).toBe(1);
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
stanza = u.toStanza(`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<body>'tis I!</body>
<markable xmlns="urn:xmpp:chat-markers:0"/>
</message>`);
_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(); done();
})); }));
}); });
...@@ -12,7 +12,6 @@ import pluggable from 'pluggable.js/src/pluggable'; ...@@ -12,7 +12,6 @@ import pluggable from 'pluggable.js/src/pluggable';
import syncDriver from 'localforage-webextensionstorage-driver/sync'; import syncDriver from 'localforage-webextensionstorage-driver/sync';
import localDriver from 'localforage-webextensionstorage-driver/local'; import localDriver from 'localforage-webextensionstorage-driver/local';
import sizzle from 'sizzle'; import sizzle from 'sizzle';
import stanza_utils from "@converse/headless/utils/stanza";
import u from '@converse/headless/utils/core'; import u from '@converse/headless/utils/core';
import { Collection } from "@converse/skeletor/src/collection"; import { Collection } from "@converse/skeletor/src/collection";
import { Connection, MockConnection } from '@converse/headless/shared/connection.js'; import { Connection, MockConnection } from '@converse/headless/shared/connection.js';
...@@ -1654,7 +1653,6 @@ Object.assign(converse, { ...@@ -1654,7 +1653,6 @@ Object.assign(converse, {
log, log,
sizzle, sizzle,
sprintf, sprintf,
stanza_utils,
u, u,
} }
}); });
......
import { converse } from "../core.js"; import { converse } from "../core.js";
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import sizzle from 'sizzle'; import sizzle from 'sizzle';
import st from "../utils/stanza"; import { getAttributes } from '@converse/headless/shared/parsers';
const { Strophe } = converse.env; const { Strophe } = converse.env;
let _converse, api; let _converse, api;
...@@ -11,7 +11,7 @@ Strophe.addNamespace('ADHOC', 'http://jabber.org/protocol/commands'); ...@@ -11,7 +11,7 @@ Strophe.addNamespace('ADHOC', 'http://jabber.org/protocol/commands');
function parseForCommands (stanza) { function parseForCommands (stanza) {
const items = sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"][node="${Strophe.NS.ADHOC}"] item`, 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)
} }
......
...@@ -8,9 +8,10 @@ import MessageMixin from './message.js'; ...@@ -8,9 +8,10 @@ import MessageMixin from './message.js';
import ModelWithContact from './model-with-contact.js'; import ModelWithContact from './model-with-contact.js';
import chat_api from './api.js'; import chat_api from './api.js';
import log from '../../log.js'; import log from '../../log.js';
import st from '../../utils/stanza';
import { Collection } from "@converse/skeletor/src/collection"; import { Collection } from "@converse/skeletor/src/collection";
import { _converse, api, converse } from '../../core.js'; 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 { Strophe, sizzle, utils } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
...@@ -74,12 +75,12 @@ converse.plugins.add('converse-chat', { ...@@ -74,12 +75,12 @@ converse.plugins.add('converse-chat', {
* @param { MessageAttributes } attrs - The message attributes * @param { MessageAttributes } attrs - The message attributes
*/ */
_converse.handleMessageStanza = async function (stanza) { _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. // Prosody sends headline messages with type `chat`, so we need to filter them out here.
const from = stanza.getAttribute('from'); const from = stanza.getAttribute('from');
return log.info(`handleMessageStanza: Ignoring incoming server message from JID: ${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)) { if (u.isErrorObject(attrs)) {
attrs.stanza && log.error(attrs.stanza); attrs.stanza && log.error(attrs.stanza);
return log.error(attrs.message); return log.error(attrs.message);
......
import ModelWithContact from './model-with-contact.js'; import ModelWithContact from './model-with-contact.js';
import filesize from "filesize"; import filesize from "filesize";
import log from "../../log.js"; import log from '@converse/headless/log';
import st from "../../utils/stanza";
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
import { _converse, api, converse } from "../../core.js"; import { _converse, api, converse } from "../../core.js";
import { find, isMatch, isObject, pick } from "lodash-es"; 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; const { Strophe, $msg } = converse.env;
...@@ -130,7 +131,7 @@ const ChatBox = ModelWithContact.extend({ ...@@ -130,7 +131,7 @@ const ChatBox = ModelWithContact.extend({
async handleErrorMessageStanza (stanza) { async handleErrorMessageStanza (stanza) {
const { __ } = _converse; const { __ } = _converse;
const attrs = await st.parseMessage(stanza, _converse); const attrs = await parseMessage(stanza, _converse);
if (!await this.shouldShowErrorMessage(attrs)) { if (!await this.shouldShowErrorMessage(attrs)) {
return; return;
} }
...@@ -392,7 +393,7 @@ const ChatBox = ModelWithContact.extend({ ...@@ -392,7 +393,7 @@ const ChatBox = ModelWithContact.extend({
* @private * @private
* @method _converse.ChatBox#findDanglingRetraction * @method _converse.ChatBox#findDanglingRetraction
* @param { object } attrs - Attributes representing a received * @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage} * message, as returned by {@link parseMessage}
* @returns { _converse.Message } * @returns { _converse.Message }
*/ */
findDanglingRetraction (attrs) { findDanglingRetraction (attrs) {
...@@ -419,7 +420,7 @@ const ChatBox = ModelWithContact.extend({ ...@@ -419,7 +420,7 @@ const ChatBox = ModelWithContact.extend({
* @private * @private
* @method _converse.ChatBox#handleRetraction * @method _converse.ChatBox#handleRetraction
* @param { object } attrs - Attributes representing a received * @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 * @returns { Boolean } Returns `true` or `false` depending on
* whether a message was retracted or not. * whether a message was retracted or not.
*/ */
...@@ -459,7 +460,7 @@ const ChatBox = ModelWithContact.extend({ ...@@ -459,7 +460,7 @@ const ChatBox = ModelWithContact.extend({
* @private * @private
* @method _converse.ChatBox#handleCorrection * @method _converse.ChatBox#handleCorrection
* @param { object } attrs - Attributes representing a received * @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 * @returns { _converse.Message|undefined } Returns the corrected
* message or `undefined` if not applicable. * message or `undefined` if not applicable.
*/ */
...@@ -497,7 +498,7 @@ const ChatBox = ModelWithContact.extend({ ...@@ -497,7 +498,7 @@ const ChatBox = ModelWithContact.extend({
* @private * @private
* @method _converse.ChatBox#getDuplicateMessage * @method _converse.ChatBox#getDuplicateMessage
* @param { object } attrs - Attributes representing a received * @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage} * message, as returned by {@link parseMessage}
* @returns {Promise<_converse.Message>} * @returns {Promise<_converse.Message>}
*/ */
getDuplicateMessage (attrs) { getDuplicateMessage (attrs) {
...@@ -604,27 +605,10 @@ const ChatBox = ModelWithContact.extend({ ...@@ -604,27 +605,10 @@ const ChatBox = ModelWithContact.extend({
if (!msg) return; if (!msg) return;
if (msg?.get('is_markable') || force) { if (msg?.get('is_markable') || force) {
const from_jid = Strophe.getBareJidFromJid(msg.get('from')); 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) { handleChatMarker (attrs) {
const to_bare_jid = Strophe.getBareJidFromJid(attrs.to); const to_bare_jid = Strophe.getBareJidFromJid(attrs.to);
if (to_bare_jid !== _converse.bare_jid) { if (to_bare_jid !== _converse.bare_jid) {
...@@ -632,7 +616,7 @@ const ChatBox = ModelWithContact.extend({ ...@@ -632,7 +616,7 @@ const ChatBox = ModelWithContact.extend({
} }
if (attrs.is_markable) { if (attrs.is_markable) {
if (this.contact && !attrs.is_archived && !attrs.is_carbon) { if (this.contact && !attrs.is_archived && !attrs.is_carbon) {
this.sendMarker(attrs.from, attrs.msgid, 'received'); sendMarker(attrs.from, attrs.msgid, 'received');
} }
return false; return false;
} else if (attrs.marker_id) { } else if (attrs.marker_id) {
......
This diff is collapsed.
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
* @description XEP-0045 Multi-User Chat Views * @description XEP-0045 Multi-User Chat Views
*/ */
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
import st from "../utils/stanza"; import { isHeadline, isServerMessage } from '@converse/headless/shared/parsers';
import { parseMessage } from '@converse/headless/plugins/chat/parsers';
converse.plugins.add('converse-headlines', { converse.plugins.add('converse-headlines', {
...@@ -79,7 +80,7 @@ converse.plugins.add('converse-headlines', { ...@@ -79,7 +80,7 @@ converse.plugins.add('converse-headlines', {
async function onHeadlineMessage (stanza) { async function onHeadlineMessage (stanza) {
// Handler method for all incoming messages of type "headline". // Handler method for all incoming messages of type "headline".
if (st.isHeadline(stanza) || st.isServerMessage(stanza)) { if (isHeadline(stanza) || isServerMessage(stanza)) {
const from_jid = stanza.getAttribute('from'); const from_jid = stanza.getAttribute('from');
if (from_jid.includes('@') && if (from_jid.includes('@') &&
!_converse.roster.get(from_jid) && !_converse.roster.get(from_jid) &&
...@@ -96,7 +97,7 @@ converse.plugins.add('converse-headlines', { ...@@ -96,7 +97,7 @@ converse.plugins.add('converse-headlines', {
'type': _converse.HEADLINES_TYPE, 'type': _converse.HEADLINES_TYPE,
'from': from_jid 'from': from_jid
}); });
const attrs = await st.parseMessage(stanza, _converse); const attrs = await parseMessage(stanza, _converse);
await chatbox.createMessage(attrs); await chatbox.createMessage(attrs);
api.trigger('message', {chatbox, stanza, attrs}); api.trigger('message', {chatbox, stanza, attrs});
} }
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import "./disco"; import "./disco";
import { _converse, api, converse } from "@converse/headless/core"; import log from '@converse/headless/log';
import log from "../log.js";
import sizzle from "sizzle"; import sizzle from "sizzle";
import st from "../utils/stanza"; import { parseMessage } from '@converse/headless/plugins/chat/parsers';
import { parseMUCMessage } from '@converse/headless/plugins/muc/parsers';
import { RSM } from '@converse/headless/shared/rsm'; import { RSM } from '@converse/headless/shared/rsm';
import { _converse, api, converse } from "@converse/headless/core";
const { Strophe, $iq, dayjs } = converse.env; const { Strophe, $iq, dayjs } = converse.env;
const { NS } = Strophe; const { NS } = Strophe;
...@@ -49,7 +50,7 @@ const MAMEnabledChat = { ...@@ -49,7 +50,7 @@ const MAMEnabledChat = {
await api.emojis.initialize(); await api.emojis.initialize();
const is_muc = this.get('type') === _converse.CHATROOMS_TYPE; const is_muc = this.get('type') === _converse.CHATROOMS_TYPE;
result.messages = result.messages.map( result.messages = result.messages.map(
s => (is_muc ? st.parseMUCMessage(s, this, _converse) : st.parseMessage(s, _converse)) s => (is_muc ? parseMUCMessage(s, this, _converse) : parseMessage(s, _converse))
); );
/** /**
......
...@@ -13,7 +13,7 @@ import ChatRoomOccupant from './occupant.js'; ...@@ -13,7 +13,7 @@ import ChatRoomOccupant from './occupant.js';
import ChatRoomOccupants from './occupants.js'; import ChatRoomOccupants from './occupants.js';
import log from '../../log'; import log from '../../log';
import muc_api from './api.js'; import muc_api from './api.js';
import muc_utils from '../../utils/muc'; import muc_utils from './utils.js';
import u from '../../utils/form'; import u from '../../utils/form';
import { Collection } from '@converse/skeletor/src/collection'; import { Collection } from '@converse/skeletor/src/collection';
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
......
import log from '../../log'; import log from '../../log';
import { Model } from '@converse/skeletor/src/model.js'; import muc_utils from './utils.js';
import muc_utils from '../../utils/muc';
import p from '../../utils/parse-helpers'; import p from '../../utils/parse-helpers';
import sizzle from 'sizzle'; import sizzle from 'sizzle';
import st from '../../utils/stanza';
import u from '../../utils/form'; import u from '../../utils/form';
import { Model } from '@converse/skeletor/src/model.js';
import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe'; import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
import { _converse, api, converse } from '../../core.js'; import { _converse, api, converse } from '../../core.js';
import { debounce, intersection, invoke, isElement, pick, zipObject } from 'lodash-es'; import { debounce, intersection, invoke, isElement, pick, zipObject } from 'lodash-es';
import { isArchived } from '@converse/headless/shared/parsers';
import { parseMemberListIQ, parseMUCMessage, parseMUCPresence } from './parsers.js';
import { sendMarker } from '@converse/headless/shared/actions';
const ACTION_INFO_CODES = ['301', '303', '333', '307', '321', '322']; const ACTION_INFO_CODES = ['301', '303', '333', '307', '321', '322'];
...@@ -194,7 +196,7 @@ const ChatRoomMixin = { ...@@ -194,7 +196,7 @@ const ChatRoomMixin = {
return; return;
} }
const from_jid = Strophe.getBareJidFromJid(msg.get('from')); const from_jid = Strophe.getBareJidFromJid(msg.get('from'));
this.sendMarker(from_jid, id, type, msg.get('type')); sendMarker(from_jid, id, type, msg.get('type'));
} }
}, },
...@@ -365,7 +367,7 @@ const ChatRoomMixin = { ...@@ -365,7 +367,7 @@ const ChatRoomMixin = {
async handleErrorMessageStanza (stanza) { async handleErrorMessageStanza (stanza) {
const { __ } = _converse; const { __ } = _converse;
const attrs = await st.parseMUCMessage(stanza, this, _converse); const attrs = await parseMUCMessage(stanza, this, _converse);
if (!(await this.shouldShowErrorMessage(attrs))) { if (!(await this.shouldShowErrorMessage(attrs))) {
return; return;
} }
...@@ -414,7 +416,7 @@ const ChatRoomMixin = { ...@@ -414,7 +416,7 @@ const ChatRoomMixin = {
* @param { XMLElement } stanza * @param { XMLElement } stanza
*/ */
async handleMessageStanza (stanza) { async handleMessageStanza (stanza) {
if (st.isArchived(stanza)) { if (isArchived(stanza)) {
// MAM messages are handled in converse-mam. // MAM messages are handled in converse-mam.
// We shouldn't get MAM messages here because // We shouldn't get MAM messages here because
// they shouldn't have a `type` attribute. // they shouldn't have a `type` attribute.
...@@ -431,7 +433,7 @@ const ChatRoomMixin = { ...@@ -431,7 +433,7 @@ const ChatRoomMixin = {
* @property { MUCMessageAttributes } attrs * @property { MUCMessageAttributes } attrs
* @property { ChatRoom } chatbox * @property { ChatRoom } chatbox
*/ */
const attrs = await st.parseMUCMessage(stanza, this, _converse); const attrs = await parseMUCMessage(stanza, this, _converse);
const data = { stanza, attrs, 'chatbox': this }; const data = { stanza, attrs, 'chatbox': this };
/** /**
* Triggered when a groupchat message stanza has been received and parsed. * Triggered when a groupchat message stanza has been received and parsed.
...@@ -1305,8 +1307,7 @@ const ChatRoomMixin = { ...@@ -1305,8 +1307,7 @@ const ChatRoomMixin = {
log.warn(result); log.warn(result);
return err; return err;
} }
return muc_utils return parseMemberListIQ(result)
.parseMemberListIQ(result)
.filter(p => p) .filter(p => p)
.sort((a, b) => (a.nick < b.nick ? -1 : a.nick > b.nick ? 1 : 0)); .sort((a, b) => (a.nick < b.nick ? -1 : a.nick > b.nick ? 1 : 0));
}, },
...@@ -1438,7 +1439,7 @@ const ChatRoomMixin = { ...@@ -1438,7 +1439,7 @@ const ChatRoomMixin = {
* @param { XMLElement } pres - The presence stanza * @param { XMLElement } pres - The presence stanza
*/ */
updateOccupantsOnPresence (pres) { updateOccupantsOnPresence (pres) {
const data = st.parseMUCPresence(pres); const data = parseMUCPresence(pres);
if (data.type === 'error' || (!data.jid && !data.nick)) { if (data.type === 'error' || (!data.jid && !data.nick)) {
return true; return true;
} }
...@@ -1538,7 +1539,7 @@ const ChatRoomMixin = { ...@@ -1538,7 +1539,7 @@ const ChatRoomMixin = {
* @private * @private
* @method _converse.ChatRoom#handleSubjectChange * @method _converse.ChatRoom#handleSubjectChange
* @param { object } attrs - Attributes representing a received * @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMUCMessage} * message, as returned by {@link parseMUCMessage}
*/ */
async handleSubjectChange (attrs) { async handleSubjectChange (attrs) {
const __ = _converse.__; const __ = _converse.__;
...@@ -1692,7 +1693,7 @@ const ChatRoomMixin = { ...@@ -1692,7 +1693,7 @@ const ChatRoomMixin = {
* @private * @private
* @method _converse.ChatRoom#findDanglingModeration * @method _converse.ChatRoom#findDanglingModeration
* @param { object } attrs - Attributes representing a received * @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMUCMessage} * message, as returned by {@link parseMUCMessage}
* @returns { _converse.ChatRoomMessage } * @returns { _converse.ChatRoomMessage }
*/ */
findDanglingModeration (attrs) { findDanglingModeration (attrs) {
...@@ -1723,7 +1724,7 @@ const ChatRoomMixin = { ...@@ -1723,7 +1724,7 @@ const ChatRoomMixin = {
* @private * @private
* @method _converse.ChatRoom#handleModeration * @method _converse.ChatRoom#handleModeration
* @param { object } attrs - Attributes representing a received * @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMUCMessage} * message, as returned by {@link parseMUCMessage}
* @returns { Boolean } Returns `true` or `false` depending on * @returns { Boolean } Returns `true` or `false` depending on
* whether a message was moderated or not. * whether a message was moderated or not.
*/ */
......
This diff is collapsed.
...@@ -4,10 +4,6 @@ ...@@ -4,10 +4,6 @@
* @description This is the MUC utilities module. * @description This is the MUC utilities module.
*/ */
import { difference, indexOf } from "lodash-es"; import { difference, indexOf } from "lodash-es";
import { converse } from "@converse/headless/core";
import u from "./core";
const { Strophe, sizzle } = converse.env;
/** /**
* The MUC utils object. Contains utility functions related to multi-user chat. * The MUC utils object. Contains utility functions related to multi-user chat.
...@@ -58,49 +54,7 @@ const muc_utils = { ...@@ -58,49 +54,7 @@ const muc_utils = {
delta = delta.concat(difference(old_jids, new_jids).map(jid => ({'jid': jid, 'affiliation': 'none'}))); delta = delta.concat(difference(old_jids, new_jids).map(jid => ({'jid': jid, 'affiliation': 'none'})));
} }
return delta; return delta;
}, }
/**
* Given an IQ stanza with a member list, create an array of objects containing
* known member data (e.g. jid, nick, role, affiliation).
* @private
* @method muc_utils#parseMemberListIQ
* @returns { MemberListItem[] }
*/
parseMemberListIQ (iq) {
return sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq).map(
(item) => {
/**
* @typedef {Object} MemberListItem
* Either the JID or the nickname (or both) will be available.
* @property {string} affiliation
* @property {string} [role]
* @property {string} [jid]
* @property {string} [nick]
*/
const data = {
'affiliation': item.getAttribute('affiliation'),
}
const jid = item.getAttribute('jid');
if (u.isValidJID(jid)) {
data['jid'] = jid;
} else {
// XXX: Prosody sends nick for the jid attribute value
// Perhaps for anonymous room?
data['nick'] = jid;
}
const nick = item.getAttribute('nick');
if (nick) {
data['nick'] = nick;
}
const role = item.getAttribute('role');
if (role) {
data['role'] = nick;
}
return data;
}
);
},
} }
export default muc_utils; export default muc_utils;
import log from '../log';
import { Strophe, $msg } from 'strophe.js/src/strophe';
import { _converse, api, converse } from '@converse/headless/core';
const u = converse.env.utils;
export function rejectMessage (stanza, text) {
// Reject an incoming message by replying with an error message of type "cancel".
api.send(
$msg({
'to': stanza.getAttribute('from'),
'type': 'error',
'id': stanza.getAttribute('id')
})
.c('error', { 'type': 'cancel' })
.c('not-allowed', { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
.up()
.c('text', { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
.t(text)
);
log.warn(`Rejecting message stanza with the following reason: ${text}`);
log.warn(stanza);
}
/**
* 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
*/
export function 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);
}
import dayjs from 'dayjs';
import sizzle from 'sizzle';
import { Strophe } from 'strophe.js/src/strophe';
import { _converse, api } from '@converse/headless/core';
import { rejectMessage } from '@converse/headless/shared/actions';
const { NS } = Strophe;
export class StanzaParseError extends Error {
constructor (message, stanza) {
super(message, stanza);
this.name = 'StanzaParseError';
this.stanza = stanza;
}
}
/**
* Extract the XEP-0359 stanza IDs from the passed in stanza
* and return a map containing them.
* @private
* @param { XMLElement } stanza - The message stanza
* @returns { Object }
*/
export function getStanzaIDs (stanza, original_stanza) {
const attrs = {};
// Store generic stanza ids
const sids = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza);
const sid_attrs = sids.reduce((acc, s) => {
acc[`stanza_id ${s.getAttribute('by')}`] = s.getAttribute('id');
return acc;
}, {});
Object.assign(attrs, sid_attrs);
// Store the archive id
const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
if (result) {
const by_jid = original_stanza.getAttribute('from') || _converse.bare_jid;
attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
}
// Store the origin id
const origin_id = sizzle(`origin-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
if (origin_id) {
attrs['origin_id'] = origin_id.getAttribute('id');
}
return attrs;
}
export function getEncryptionAttributes (stanza, _converse) {
const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop();
const attrs = { 'is_encrypted': !!encrypted };
if (!encrypted || api.settings.get('clear_cache_on_logout')) {
return attrs;
}
const header = encrypted.querySelector('header');
attrs['encrypted'] = { 'device_id': header.getAttribute('sid') };
const device_id = _converse.omemo_store?.get('device_id');
const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted).pop();
if (key) {
Object.assign(attrs.encrypted, {
'iv': header.querySelector('iv').textContent,
'key': key.textContent,
'payload': encrypted.querySelector('payload')?.textContent || null,
'prekey': ['true', '1'].includes(key.getAttribute('prekey'))
});
}
return attrs;
}
/**
* @private
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
* @returns { Object }
*/
export function getRetractionAttributes (stanza, original_stanza) {
const fastening = sizzle(`> apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
if (fastening) {
const applies_to_id = fastening.getAttribute('id');
const retracted = sizzle(`> retract[xmlns="${Strophe.NS.RETRACT}"]`, fastening).pop();
if (retracted) {
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
const time = delay ? dayjs(delay.getAttribute('stamp')).toISOString() : new Date().toISOString();
return {
'editable': false,
'retracted': time,
'retracted_id': applies_to_id
};
}
} else {
const tombstone = sizzle(`> retracted[xmlns="${Strophe.NS.RETRACT}"]`, stanza).pop();
if (tombstone) {
return {
'editable': false,
'is_tombstone': true,
'retracted': tombstone.getAttribute('stamp')
};
}
}
return {};
}
export function getCorrectionAttributes (stanza, original_stanza) {
const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
if (el) {
const replace_id = el.getAttribute('id');
const msgid = replace_id;
if (replace_id) {
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
const time = delay ? dayjs(delay.getAttribute('stamp')).toISOString() : new Date().toISOString();
return {
msgid,
replace_id,
'edited': time
};
}
}
return {};
}
export function getSpoilerAttributes (stanza) {
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
return {
'is_spoiler': !!spoiler,
'spoiler_hint': spoiler?.textContent
};
}
export function getOutOfBandAttributes (stanza) {
const xform = sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, stanza).pop();
if (xform) {
return {
'oob_url': xform.querySelector('url')?.textContent,
'oob_desc': xform.querySelector('desc')?.textContent
};
}
return {};
}
/**
* Returns the human readable error message contained in a `groupchat` message stanza of type `error`.
* @private
* @param { XMLElement } stanza - The message stanza
*/
export function getErrorAttributes (stanza) {
if (stanza.getAttribute('type') === 'error') {
const error = stanza.querySelector('error');
const text = sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop();
return {
'is_error': true,
'error_text': text?.textContent,
'error_type': error.getAttribute('type'),
'error_condition': error.firstElementChild.nodeName
};
}
return {};
}
export function getReferences (stanza) {
const text = stanza.querySelector('body')?.textContent;
return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => {
const begin = ref.getAttribute('begin');
const end = ref.getAttribute('end');
return {
'begin': begin,
'end': end,
'type': ref.getAttribute('type'),
'value': text.slice(begin, end),
'uri': ref.getAttribute('uri')
};
});
}
export function getReceiptId (stanza) {
const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop();
return receipt?.getAttribute('id');
}
/**
* Determines whether the passed in stanza is a XEP-0280 Carbon
* @private
* @param { XMLElement } stanza - The message stanza
* @returns { Boolean }
*/
export function isCarbon (stanza) {
const xmlns = Strophe.NS.CARBONS;
return (
sizzle(`message > received[xmlns="${xmlns}"]`, stanza).length > 0 ||
sizzle(`message > sent[xmlns="${xmlns}"]`, stanza).length > 0
);
}
/**
* Returns the XEP-0085 chat state contained in a message stanza
* @private
* @param { XMLElement } stanza - The message stanza
*/
export function getChatState (stanza) {
return sizzle(
`
composing[xmlns="${NS.CHATSTATES}"],
paused[xmlns="${NS.CHATSTATES}"],
inactive[xmlns="${NS.CHATSTATES}"],
active[xmlns="${NS.CHATSTATES}"],
gone[xmlns="${NS.CHATSTATES}"]`,
stanza
).pop()?.nodeName;
}
export function isValidReceiptRequest (stanza, attrs) {
return (
attrs.sender !== 'me' &&
!attrs.is_carbon &&
!attrs.is_archived &&
sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).length
);
}
export function rejectUnencapsulatedForward (stanza) {
const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
if (bare_forward) {
rejectMessage(stanza, 'Forwarded messages not part of an encapsulating protocol are not supported');
const from_jid = stanza.getAttribute('from');
return new StanzaParseError(`Ignoring unencapsulated forwarded message from ${from_jid}`, stanza);
}
}
/**
* Determines whether the passed in stanza is a XEP-0333 Chat Marker
* @private
* @method getChatMarker
* @param { XMLElement } stanza - The message stanza
* @returns { Boolean }
*/
export function getChatMarker (stanza) {
// If we receive more than one marker (which shouldn't happen), we take
// the highest level of acknowledgement.
return sizzle(`
acknowledged[xmlns="${Strophe.NS.MARKERS}"],
displayed[xmlns="${Strophe.NS.MARKERS}"],
received[xmlns="${Strophe.NS.MARKERS}"]`,
stanza
).pop();
}
export function isHeadline (stanza) {
return stanza.getAttribute('type') === 'headline';
}
export function isServerMessage (stanza) {
const from_jid = stanza.getAttribute('from');
if (stanza.getAttribute('type') !== 'error' && from_jid && !from_jid.includes('@')) {
// Some servers (e.g. Prosody) don't set the stanza
// type to "headline" when sending server messages.
// For now we check if an @ signal is included, and if not,
// we assume it's a headline stanza.
return true;
}
return false;
}
/**
* Determines whether the passed in stanza is a XEP-0313 MAM stanza
* @private
* @method isArchived
* @param { XMLElement } stanza - The message stanza
* @returns { Boolean }
*/
export function isArchived (original_stanza) {
return !!sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
}
/**
* Returns an object containing all attribute names and values for a particular element.
* @method getAttributes
* @param { XMLElement } stanza
* @returns { Object }
*/
export function getAttributes (stanza) {
return stanza.getAttributeNames().reduce((acc, name) => {
acc[name] = Strophe.xmlunescape(stanza.getAttribute(name));
return acc;
}, {});
}
This diff is collapsed.
import BootstrapModal from "./base.js"; import BootstrapModal from "./base.js";
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import st from "@converse/headless/utils/stanza";
import tpl_list_chatrooms_modal from "./templates/muc-list.js"; import tpl_list_chatrooms_modal from "./templates/muc-list.js";
import tpl_room_description from "templates/room_description.html"; import tpl_room_description from "templates/room_description.html";
import tpl_spinner from "templates/spinner.js"; import tpl_spinner from "templates/spinner.js";
import { __ } from '../i18n'; import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
import { getAttributes } from '@converse/headless/shared/parsers';
import { head } from "lodash-es"; import { head } from "lodash-es";
const { Strophe, $iq, sizzle } = converse.env; const { Strophe, $iq, sizzle } = converse.env;
...@@ -144,7 +144,7 @@ export default BootstrapModal.extend({ ...@@ -144,7 +144,7 @@ export default BootstrapModal.extend({
const rooms = iq ? sizzle('query item', iq) : []; const rooms = iq ? sizzle('query item', iq) : [];
if (rooms.length) { if (rooms.length) {
this.model.set({'feedback_text': __('Groupchats found')}, {'silent': true}); this.model.set({'feedback_text': __('Groupchats found')}, {'silent': true});
this.items = rooms.map(st.getAttributes); this.items = rooms.map(getAttributes);
} else { } else {
this.items = []; this.items = [];
this.model.set({'feedback_text': __('No groupchats found')}, {'silent': true}); this.model.set({'feedback_text': __('No groupchats found')}, {'silent': true});
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
import '../../components/muc-sidebar'; import '../../components/muc-sidebar';
import '../chatview/index.js'; import '../chatview/index.js';
import '../modal.js'; import '../modal.js';
import '@converse/headless/utils/muc';
import ChatRoomViewMixin from './muc.js'; import ChatRoomViewMixin from './muc.js';
import MUCConfigForm from './config-form.js'; import MUCConfigForm from './config-form.js';
import MUCPasswordForm from './password-form.js'; import MUCPasswordForm from './password-form.js';
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment