Move MUC and stanza utils into shared and plugin-specific files
This commit is contained in:
parent
e8eea6324e
commit
e80afbfe39
@ -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 () {
|
|||||||
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 () {
|
|||||||
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 () {
|
|||||||
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 () {
|
|||||||
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 () {
|
|||||||
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 () {
|
|||||||
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 () {
|
|||||||
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 () {
|
|||||||
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 () {
|
|||||||
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 () {
|
|||||||
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();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
186
spec/markers.js
Normal file
186
spec/markers.js
Normal file
@ -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(`
|
||||||
|
<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();
|
||||||
|
}));
|
||||||
|
});
|
119
spec/messages.js
119
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(`
|
|
||||||
<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();
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*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;
|
||||||
|
|
||||||
@ -621,85 +621,28 @@ describe("A Groupchat Message", function () {
|
|||||||
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 msg_obj = view.model.messages.at(0);
|
||||||
const stanza = u.toStanza(`
|
let stanza = u.toStanza(`
|
||||||
|
<message xmlns="jabber:client"
|
||||||
|
from="${msg_obj.get('from')}"
|
||||||
|
to="${_converse.connection.jid}"
|
||||||
|
type="groupchat">
|
||||||
|
<body>${msg_obj.get('message')}</body>
|
||||||
|
<stanza-id xmlns="urn:xmpp:sid:0"
|
||||||
|
id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
|
||||||
|
by="lounge@montague.lit"/>
|
||||||
|
<origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
|
||||||
|
</message>`);
|
||||||
|
await view.model.handleMessageStanza(stanza);
|
||||||
|
await u.waitUntil(() => view.model.messages.last().get('received'));
|
||||||
|
|
||||||
|
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">
|
||||||
<received xmlns="urn:xmpp:receipts" 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"/>
|
<origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
|
||||||
</message>`);
|
</message>`);
|
||||||
spyOn(stanza_utils, "parseMUCMessage").and.callThrough();
|
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_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').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(`
|
|
||||||
<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>`);
|
|
||||||
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(`
|
|
||||||
<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));
|
|
||||||
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(`
|
|
||||||
<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));
|
|
||||||
|
|
||||||
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(`
|
|
||||||
<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';
|
|||||||
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, {
|
|||||||
log,
|
log,
|
||||||
sizzle,
|
sizzle,
|
||||||
sprintf,
|
sprintf,
|
||||||
stanza_utils,
|
|
||||||
u,
|
u,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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');
|
|||||||
|
|
||||||
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';
|
|||||||
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', {
|
|||||||
* @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);
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
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({
|
|||||||
|
|
||||||
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({
|
|||||||
* @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({
|
|||||||
* @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({
|
|||||||
* @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({
|
|||||||
* @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({
|
|||||||
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({
|
|||||||
}
|
}
|
||||||
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) {
|
||||||
|
219
src/headless/plugins/chat/parsers.js
Normal file
219
src/headless/plugins/chat/parsers.js
Normal file
@ -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<Object> } references - A list of objects representing XEP-0372 references
|
||||||
|
* @property { Boolean } editable - Is this message editable via XEP-0308?
|
||||||
|
* @property { Boolean } is_archived - Is this message from a XEP-0313 MAM archive?
|
||||||
|
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
|
||||||
|
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
|
||||||
|
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
|
||||||
|
* @property { Boolean } is_error - Whether an error was received for this message
|
||||||
|
* @property { Boolean } is_headline - Is this a "headline" message?
|
||||||
|
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
|
||||||
|
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
|
||||||
|
* @property { Boolean } is_only_emojis - Does the message body contain only emojis?
|
||||||
|
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
|
||||||
|
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
|
||||||
|
* @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored
|
||||||
|
* @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
|
||||||
|
* @property { Object } encrypted - XEP-0384 encryption payload attributes
|
||||||
|
* @property { String } body - The contents of the <body> tag of the message stanza
|
||||||
|
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
|
||||||
|
* @property { String } contact_jid - The JID of the other person or entity
|
||||||
|
* @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308
|
||||||
|
* @property { String } error_condition - The defined error condition
|
||||||
|
* @property { String } error_text - The error text received from the server
|
||||||
|
* @property { String } error_type - The type of error received from the server
|
||||||
|
* @property { String } from - The sender JID
|
||||||
|
* @property { String } fullname - The full name of the sender
|
||||||
|
* @property { String } marker - The XEP-0333 Chat Marker value
|
||||||
|
* @property { String } marker_id - The `id` attribute of a XEP-0333 chat marker
|
||||||
|
* @property { String } msgid - The root `id` attribute of the stanza
|
||||||
|
* @property { String } nick - The roster nickname of the sender
|
||||||
|
* @property { String } oob_desc - The description of the XEP-0066 out of band data
|
||||||
|
* @property { String } oob_url - The URL of the XEP-0066 out of band data
|
||||||
|
* @property { String } origin_id - The XEP-0359 Origin ID
|
||||||
|
* @property { String } receipt_id - The `id` attribute of a XEP-0184 <receipt> element
|
||||||
|
* @property { String } received - An ISO8601 string recording the time that the message was received
|
||||||
|
* @property { String } replace_id - The `id` attribute of a XEP-0308 <replace> element
|
||||||
|
* @property { String } retracted - An ISO8601 string recording the time that the message was retracted
|
||||||
|
* @property { String } retracted_id - The `id` attribute of a XEP-424 <retracted> element
|
||||||
|
* @property { String } spoiler_hint The XEP-0382 spoiler hint
|
||||||
|
* @property { String } stanza_id - The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple.
|
||||||
|
* @property { String } subject - The <subject> element value
|
||||||
|
* @property { String } thread - The <thread> element value
|
||||||
|
* @property { String } time - The time (in ISO8601 format), either given by the XEP-0203 <delay> element, or of receipt.
|
||||||
|
* @property { String } to - The recipient JID
|
||||||
|
* @property { String } type - The type of message
|
||||||
|
*/
|
||||||
|
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
|
||||||
|
const marker = getChatMarker(stanza);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
let attrs = Object.assign(
|
||||||
|
{
|
||||||
|
contact_jid,
|
||||||
|
is_archived,
|
||||||
|
is_headline,
|
||||||
|
is_server_message,
|
||||||
|
'body': stanza.querySelector('body')?.textContent?.trim(),
|
||||||
|
'chat_state': getChatState(stanza),
|
||||||
|
'from': Strophe.getBareJidFromJid(stanza.getAttribute('from')),
|
||||||
|
'is_carbon': isCarbon(original_stanza),
|
||||||
|
'is_delayed': !!delay,
|
||||||
|
'is_markable': !!sizzle(`markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length,
|
||||||
|
'is_marker': !!marker,
|
||||||
|
'is_unstyled': !!sizzle(`unstyled[xmlns="${Strophe.NS.STYLING}"]`, stanza).length,
|
||||||
|
'marker_id': marker && marker.getAttribute('id'),
|
||||||
|
'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
|
||||||
|
'nick': contact?.attributes?.nickname,
|
||||||
|
'receipt_id': getReceiptId(stanza),
|
||||||
|
'received': new Date().toISOString(),
|
||||||
|
'references': getReferences(stanza),
|
||||||
|
'sender': is_me ? 'me' : 'them',
|
||||||
|
'subject': stanza.querySelector('subject')?.textContent,
|
||||||
|
'thread': stanza.querySelector('thread')?.textContent,
|
||||||
|
'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : now,
|
||||||
|
'to': stanza.getAttribute('to'),
|
||||||
|
'type': stanza.getAttribute('type')
|
||||||
|
},
|
||||||
|
getErrorAttributes(stanza),
|
||||||
|
getOutOfBandAttributes(stanza),
|
||||||
|
getSpoilerAttributes(stanza),
|
||||||
|
getCorrectionAttributes(stanza, original_stanza),
|
||||||
|
getStanzaIDs(stanza, original_stanza),
|
||||||
|
getRetractionAttributes(stanza, original_stanza),
|
||||||
|
getEncryptionAttributes(stanza, _converse)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (attrs.is_archived) {
|
||||||
|
const from = original_stanza.getAttribute('from');
|
||||||
|
if (from && from !== _converse.bare_jid) {
|
||||||
|
return new StanzaParseError(`Invalid Stanza: Forged MAM message from ${from}`, stanza);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await api.emojis.initialize();
|
||||||
|
attrs = Object.assign(
|
||||||
|
{
|
||||||
|
'message': attrs.body || attrs.error, // TODO: Remove and use body and error attributes instead
|
||||||
|
'is_only_emojis': attrs.body ? u.isOnlyEmojis(attrs.body) : false,
|
||||||
|
'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs)
|
||||||
|
},
|
||||||
|
attrs
|
||||||
|
);
|
||||||
|
|
||||||
|
// We prefer to use one of the XEP-0359 unique and stable stanza IDs
|
||||||
|
// as the Model id, to avoid duplicates.
|
||||||
|
attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${attrs.from}`] || u.getUniqueId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* *Hook* which allows plugins to add additional parsing
|
||||||
|
* @event _converse#parseMessage
|
||||||
|
*/
|
||||||
|
return api.hook('parseMessage', stanza, attrs);
|
||||||
|
}
|
@ -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', {
|
|||||||
|
|
||||||
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', {
|
|||||||
'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 @@
|
|||||||
* @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 = {
|
|||||||
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';
|
|||||||
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';
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
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 = {
|
|||||||
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 = {
|
|||||||
|
|
||||||
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 = {
|
|||||||
* @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 = {
|
|||||||
* @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 = {
|
|||||||
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 = {
|
|||||||
* @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 = {
|
|||||||
* @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 = {
|
|||||||
* @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 = {
|
|||||||
* @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.
|
||||||
*/
|
*/
|
||||||
|
307
src/headless/plugins/muc/parsers.js
Normal file
307
src/headless/plugins/muc/parsers.js
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import {
|
||||||
|
StanzaParseError,
|
||||||
|
getChatMarker,
|
||||||
|
getChatState,
|
||||||
|
getCorrectionAttributes,
|
||||||
|
getEncryptionAttributes,
|
||||||
|
getErrorAttributes,
|
||||||
|
getOutOfBandAttributes,
|
||||||
|
getReceiptId,
|
||||||
|
getReferences,
|
||||||
|
getRetractionAttributes,
|
||||||
|
getSpoilerAttributes,
|
||||||
|
getStanzaIDs,
|
||||||
|
isArchived,
|
||||||
|
isCarbon,
|
||||||
|
isHeadline,
|
||||||
|
isValidReceiptRequest,
|
||||||
|
rejectUnencapsulatedForward,
|
||||||
|
} from '@converse/headless/shared/parsers';
|
||||||
|
import { api, converse } from '@converse/headless/core';
|
||||||
|
|
||||||
|
const { Strophe, sizzle, u } = converse.env;
|
||||||
|
const { NS } = Strophe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 }
|
||||||
|
*/
|
||||||
|
function getModerationAttributes (stanza) {
|
||||||
|
const fastening = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
|
||||||
|
if (fastening) {
|
||||||
|
const applies_to_id = fastening.getAttribute('id');
|
||||||
|
const moderated = sizzle(`moderated[xmlns="${Strophe.NS.MODERATE}"]`, fastening).pop();
|
||||||
|
if (moderated) {
|
||||||
|
const retracted = sizzle(`retract[xmlns="${Strophe.NS.RETRACT}"]`, moderated).pop();
|
||||||
|
if (retracted) {
|
||||||
|
return {
|
||||||
|
'editable': false,
|
||||||
|
'moderated': 'retracted',
|
||||||
|
'moderated_by': moderated.getAttribute('by'),
|
||||||
|
'moderated_id': applies_to_id,
|
||||||
|
'moderation_reason': moderated.querySelector('reason')?.textContent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const tombstone = sizzle(`> moderated[xmlns="${Strophe.NS.MODERATE}"]`, stanza).pop();
|
||||||
|
if (tombstone) {
|
||||||
|
const retracted = sizzle(`retracted[xmlns="${Strophe.NS.RETRACT}"]`, tombstone).pop();
|
||||||
|
if (retracted) {
|
||||||
|
return {
|
||||||
|
'editable': false,
|
||||||
|
'is_tombstone': true,
|
||||||
|
'moderated_by': tombstone.getAttribute('by'),
|
||||||
|
'retracted': tombstone.getAttribute('stamp'),
|
||||||
|
'moderation_reason': tombstone.querySelector('reason')?.textContent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a passed in message stanza and returns an object of attributes.
|
||||||
|
* @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.
|
||||||
|
* @param { _converse.ChatRoom } chatbox
|
||||||
|
* @param { _converse } _converse
|
||||||
|
* @returns { Promise<MUCMessageAttributes|Error> }
|
||||||
|
*/
|
||||||
|
export async function parseMUCMessage (stanza, chatbox, _converse) {
|
||||||
|
const err = rejectUnencapsulatedForward(stanza);
|
||||||
|
if (err) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selector = `[xmlns="${NS.MAM}"] > forwarded[xmlns="${NS.FORWARD}"] > message`;
|
||||||
|
const original_stanza = stanza;
|
||||||
|
stanza = sizzle(selector, stanza).pop() || stanza;
|
||||||
|
|
||||||
|
if (sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length) {
|
||||||
|
return new StanzaParseError(
|
||||||
|
`Invalid Stanza: Forged MAM groupchat message from ${stanza.getAttribute('from')}`,
|
||||||
|
stanza
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
|
||||||
|
const from = stanza.getAttribute('from');
|
||||||
|
const nick = Strophe.unescapeNode(Strophe.getResourceFromJid(from));
|
||||||
|
const marker = getChatMarker(stanza);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
/**
|
||||||
|
* @typedef { Object } MUCMessageAttributes
|
||||||
|
* The object which {@link parseMUCMessage} returns
|
||||||
|
* @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else
|
||||||
|
* @property { Array<Object> } references - A list of objects representing XEP-0372 references
|
||||||
|
* @property { Boolean } editable - Is this message editable via XEP-0308?
|
||||||
|
* @property { Boolean } is_archived - Is this message from a XEP-0313 MAM archive?
|
||||||
|
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
|
||||||
|
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
|
||||||
|
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
|
||||||
|
* @property { Boolean } is_error - Whether an error was received for this message
|
||||||
|
* @property { Boolean } is_headline - Is this a "headline" message?
|
||||||
|
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
|
||||||
|
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
|
||||||
|
* @property { Boolean } is_only_emojis - Does the message body contain only emojis?
|
||||||
|
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
|
||||||
|
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
|
||||||
|
* @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored
|
||||||
|
* @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
|
||||||
|
* @property { Object } encrypted - XEP-0384 encryption payload attributes
|
||||||
|
* @property { String } body - The contents of the <body> tag of the message stanza
|
||||||
|
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
|
||||||
|
* @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308
|
||||||
|
* @property { String } error_condition - The defined error condition
|
||||||
|
* @property { String } error_text - The error text received from the server
|
||||||
|
* @property { String } error_type - The type of error received from the server
|
||||||
|
* @property { String } from - The sender JID (${muc_jid}/${nick})
|
||||||
|
* @property { String } from_muc - The JID of the MUC from which this message was sent
|
||||||
|
* @property { String } from_real_jid - The real JID of the sender, if available
|
||||||
|
* @property { String } fullname - The full name of the sender
|
||||||
|
* @property { String } marker - The XEP-0333 Chat Marker value
|
||||||
|
* @property { String } marker_id - The `id` attribute of a XEP-0333 chat marker
|
||||||
|
* @property { String } moderated - The type of XEP-0425 moderation (if any) that was applied
|
||||||
|
* @property { String } moderated_by - The JID of the user that moderated this message
|
||||||
|
* @property { String } moderated_id - The XEP-0359 Stanza ID of the message that this one moderates
|
||||||
|
* @property { String } moderation_reason - The reason provided why this message moderates another
|
||||||
|
* @property { String } msgid - The root `id` attribute of the stanza
|
||||||
|
* @property { String } nick - The MUC nickname of the sender
|
||||||
|
* @property { String } oob_desc - The description of the XEP-0066 out of band data
|
||||||
|
* @property { String } oob_url - The URL of the XEP-0066 out of band data
|
||||||
|
* @property { String } origin_id - The XEP-0359 Origin ID
|
||||||
|
* @property { String } receipt_id - The `id` attribute of a XEP-0184 <receipt> element
|
||||||
|
* @property { String } received - An ISO8601 string recording the time that the message was received
|
||||||
|
* @property { String } replace_id - The `id` attribute of a XEP-0308 <replace> element
|
||||||
|
* @property { String } retracted - An ISO8601 string recording the time that the message was retracted
|
||||||
|
* @property { String } retracted_id - The `id` attribute of a XEP-424 <retracted> element
|
||||||
|
* @property { String } spoiler_hint The XEP-0382 spoiler hint
|
||||||
|
* @property { String } stanza_id - The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple.
|
||||||
|
* @property { String } subject - The <subject> element value
|
||||||
|
* @property { String } thread - The <thread> element value
|
||||||
|
* @property { String } time - The time (in ISO8601 format), either given by the XEP-0203 <delay> element, or of receipt.
|
||||||
|
* @property { String } to - The recipient JID
|
||||||
|
* @property { String } type - The type of message
|
||||||
|
*/
|
||||||
|
let attrs = Object.assign(
|
||||||
|
{
|
||||||
|
from,
|
||||||
|
nick,
|
||||||
|
'body': stanza.querySelector('body')?.textContent?.trim(),
|
||||||
|
'chat_state': getChatState(stanza),
|
||||||
|
'from_muc': Strophe.getBareJidFromJid(from),
|
||||||
|
'from_real_jid': chatbox.occupants.findOccupant({ nick })?.get('jid'),
|
||||||
|
'is_archived': isArchived(original_stanza),
|
||||||
|
'is_carbon': isCarbon(original_stanza),
|
||||||
|
'is_delayed': !!delay,
|
||||||
|
'is_headline': isHeadline(stanza),
|
||||||
|
'is_markable': !!sizzle(`markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length,
|
||||||
|
'is_marker': !!marker,
|
||||||
|
'is_unstyled': !!sizzle(`unstyled[xmlns="${Strophe.NS.STYLING}"]`, stanza).length,
|
||||||
|
'marker_id': marker && marker.getAttribute('id'),
|
||||||
|
'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
|
||||||
|
'receipt_id': getReceiptId(stanza),
|
||||||
|
'received': new Date().toISOString(),
|
||||||
|
'references': getReferences(stanza),
|
||||||
|
'subject': stanza.querySelector('subject')?.textContent,
|
||||||
|
'thread': stanza.querySelector('thread')?.textContent,
|
||||||
|
'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : now,
|
||||||
|
'to': stanza.getAttribute('to'),
|
||||||
|
'type': stanza.getAttribute('type')
|
||||||
|
},
|
||||||
|
getErrorAttributes(stanza),
|
||||||
|
getOutOfBandAttributes(stanza),
|
||||||
|
getSpoilerAttributes(stanza),
|
||||||
|
getCorrectionAttributes(stanza, original_stanza),
|
||||||
|
getStanzaIDs(stanza, original_stanza),
|
||||||
|
getRetractionAttributes(stanza, original_stanza),
|
||||||
|
getModerationAttributes(stanza),
|
||||||
|
getEncryptionAttributes(stanza, _converse)
|
||||||
|
);
|
||||||
|
|
||||||
|
await api.emojis.initialize();
|
||||||
|
attrs = Object.assign(
|
||||||
|
{
|
||||||
|
'is_only_emojis': attrs.body ? u.isOnlyEmojis(attrs.body) : false,
|
||||||
|
'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs),
|
||||||
|
'message': attrs.body || attrs.error, // TODO: Remove and use body and error attributes instead
|
||||||
|
'sender': attrs.nick === chatbox.get('nick') ? 'me' : 'them'
|
||||||
|
},
|
||||||
|
attrs
|
||||||
|
);
|
||||||
|
|
||||||
|
if (attrs.is_archived && original_stanza.getAttribute('from') !== attrs.from_muc) {
|
||||||
|
return new StanzaParseError(
|
||||||
|
`Invalid Stanza: Forged MAM message from ${original_stanza.getAttribute('from')}`,
|
||||||
|
stanza
|
||||||
|
);
|
||||||
|
} else if (attrs.is_archived && original_stanza.getAttribute('from') !== chatbox.get('jid')) {
|
||||||
|
return new StanzaParseError(
|
||||||
|
`Invalid Stanza: Forged MAM groupchat message from ${stanza.getAttribute('from')}`,
|
||||||
|
stanza
|
||||||
|
);
|
||||||
|
} else if (attrs.is_carbon) {
|
||||||
|
return new StanzaParseError('Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied', stanza);
|
||||||
|
}
|
||||||
|
// We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
|
||||||
|
attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${attrs.from_muc || attrs.from}`] || u.getUniqueId();
|
||||||
|
/**
|
||||||
|
* *Hook* which allows plugins to add additional parsing
|
||||||
|
* @event _converse#parseMUCMessage
|
||||||
|
*/
|
||||||
|
return api.hook('parseMUCMessage', stanza, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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[] }
|
||||||
|
*/
|
||||||
|
export function 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a passed in MUC presence stanza and returns an object of attributes.
|
||||||
|
* @method parseMUCPresence
|
||||||
|
* @param { XMLElement } stanza - The presence stanza
|
||||||
|
* @returns { Object }
|
||||||
|
*/
|
||||||
|
export function parseMUCPresence (stanza) {
|
||||||
|
const from = stanza.getAttribute('from');
|
||||||
|
const type = stanza.getAttribute('type');
|
||||||
|
const data = {
|
||||||
|
'from': from,
|
||||||
|
'nick': Strophe.getResourceFromJid(from),
|
||||||
|
'type': type,
|
||||||
|
'states': [],
|
||||||
|
'hats': [],
|
||||||
|
'show': type !== 'unavailable' ? 'online' : 'offline'
|
||||||
|
};
|
||||||
|
Array.from(stanza.children).forEach(child => {
|
||||||
|
if (child.matches('status')) {
|
||||||
|
data.status = child.textContent || null;
|
||||||
|
} else if (child.matches('show')) {
|
||||||
|
data.show = child.textContent || 'online';
|
||||||
|
} else if (child.matches('x') && child.getAttribute('xmlns') === Strophe.NS.MUC_USER) {
|
||||||
|
Array.from(child.children).forEach(item => {
|
||||||
|
if (item.nodeName === 'item') {
|
||||||
|
data.affiliation = item.getAttribute('affiliation');
|
||||||
|
data.role = item.getAttribute('role');
|
||||||
|
data.jid = item.getAttribute('jid');
|
||||||
|
data.nick = item.getAttribute('nick') || data.nick;
|
||||||
|
} else if (item.nodeName == 'status' && item.getAttribute('code')) {
|
||||||
|
data.states.push(item.getAttribute('code'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (child.matches('x') && child.getAttribute('xmlns') === Strophe.NS.VCARDUPDATE) {
|
||||||
|
data.image_hash = child.querySelector('photo')?.textContent;
|
||||||
|
} else if (child.matches('hats') && child.getAttribute('xmlns') === Strophe.NS.MUC_HATS) {
|
||||||
|
data['hats'] = Array.from(child.children).map(
|
||||||
|
c =>
|
||||||
|
c.matches('hat') && {
|
||||||
|
'title': c.getAttribute('title'),
|
||||||
|
'uri': c.getAttribute('uri')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
@ -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 = {
|
|||||||
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;
|
41
src/headless/shared/actions.js
Normal file
41
src/headless/shared/actions.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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);
|
||||||
|
}
|
287
src/headless/shared/parsers.js
Normal file
287
src/headless/shared/parsers.js
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
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;
|
||||||
|
}, {});
|
||||||
|
}
|
@ -1,738 +0,0 @@
|
|||||||
import { Strophe, $msg } from 'strophe.js/src/strophe';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import sizzle from 'sizzle';
|
|
||||||
import u from '@converse/headless/utils/core';
|
|
||||||
import log from "../log";
|
|
||||||
import { _converse, api } from "@converse/headless/core";
|
|
||||||
|
|
||||||
const { NS } = Strophe;
|
|
||||||
|
|
||||||
|
|
||||||
function getSpoilerAttributes (stanza) {
|
|
||||||
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
|
|
||||||
return {
|
|
||||||
'is_spoiler': !!spoiler,
|
|
||||||
'spoiler_hint': spoiler?.textContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function isValidReceiptRequest (stanza, attrs) {
|
|
||||||
return (
|
|
||||||
attrs.sender !== 'me' &&
|
|
||||||
!attrs.is_carbon &&
|
|
||||||
!attrs.is_archived &&
|
|
||||||
sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getReceiptId (stanza) {
|
|
||||||
const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop();
|
|
||||||
return receipt?.getAttribute('id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the XEP-0085 chat state contained in a message stanza
|
|
||||||
* @private
|
|
||||||
* @param { XMLElement } stanza - The message stanza
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether the passed in stanza is a XEP-0280 Carbon
|
|
||||||
* @private
|
|
||||||
* @param { XMLElement } stanza - The message stanza
|
|
||||||
* @returns { Boolean }
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 }
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 }
|
|
||||||
*/
|
|
||||||
function getModerationAttributes (stanza) {
|
|
||||||
const fastening = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
|
|
||||||
if (fastening) {
|
|
||||||
const applies_to_id = fastening.getAttribute('id');
|
|
||||||
const moderated = sizzle(`moderated[xmlns="${Strophe.NS.MODERATE}"]`, fastening).pop();
|
|
||||||
if (moderated) {
|
|
||||||
const retracted = sizzle(`retract[xmlns="${Strophe.NS.RETRACT}"]`, moderated).pop();
|
|
||||||
if (retracted) {
|
|
||||||
return {
|
|
||||||
'editable': false,
|
|
||||||
'moderated': 'retracted',
|
|
||||||
'moderated_by': moderated.getAttribute('by'),
|
|
||||||
'moderated_id': applies_to_id,
|
|
||||||
'moderation_reason': moderated.querySelector('reason')?.textContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const tombstone = sizzle(`> moderated[xmlns="${Strophe.NS.MODERATE}"]`, stanza).pop();
|
|
||||||
if (tombstone) {
|
|
||||||
const retracted = sizzle(`retracted[xmlns="${Strophe.NS.RETRACT}"]`, tombstone).pop();
|
|
||||||
if (retracted) {
|
|
||||||
return {
|
|
||||||
'editable': false,
|
|
||||||
'is_tombstone': true,
|
|
||||||
'moderated_by': tombstone.getAttribute('by'),
|
|
||||||
'retracted': tombstone.getAttribute('stamp'),
|
|
||||||
'moderation_reason': tombstone.querySelector('reason')?.textContent
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 }
|
|
||||||
*/
|
|
||||||
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 {};
|
|
||||||
}
|
|
||||||
|
|
||||||
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')
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the human readable error message contained in a `groupchat` message stanza of type `error`.
|
|
||||||
* @private
|
|
||||||
* @param { XMLElement } stanza - The message stanza
|
|
||||||
*/
|
|
||||||
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 {};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class StanzaParseError extends Error {
|
|
||||||
constructor (message, stanza) {
|
|
||||||
super(message, stanza);
|
|
||||||
this.name = 'StanzaParseError';
|
|
||||||
this.stanza = stanza;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The stanza utils object. Contains utility functions related to stanza processing.
|
|
||||||
* @namespace st
|
|
||||||
*/
|
|
||||||
const st = {
|
|
||||||
|
|
||||||
isHeadline (stanza) {
|
|
||||||
return stanza.getAttribute('type') === 'headline';
|
|
||||||
},
|
|
||||||
|
|
||||||
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-0333 Chat Marker
|
|
||||||
* @private
|
|
||||||
* @method st#getChatMarker
|
|
||||||
* @param { XMLElement } stanza - The message stanza
|
|
||||||
* @returns { Boolean }
|
|
||||||
*/
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether the passed in stanza is a XEP-0313 MAM stanza
|
|
||||||
* @private
|
|
||||||
* @method st#isArchived
|
|
||||||
* @param { XMLElement } stanza - The message stanza
|
|
||||||
* @returns { Boolean }
|
|
||||||
*/
|
|
||||||
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 st#getAttributes
|
|
||||||
* @param { XMLElement } stanza
|
|
||||||
* @returns { Object }
|
|
||||||
*/
|
|
||||||
getAttributes (stanza) {
|
|
||||||
return stanza.getAttributeNames().reduce((acc, name) => {
|
|
||||||
acc[name] = Strophe.xmlunescape(stanza.getAttribute(name))
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) }
|
|
||||||
*/
|
|
||||||
async 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 = st.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 = st.isHeadline(stanza);
|
|
||||||
const is_server_message = st.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 st.parseMessage} returns
|
|
||||||
* @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else
|
|
||||||
* @property { Array<Object> } references - A list of objects representing XEP-0372 references
|
|
||||||
* @property { Boolean } editable - Is this message editable via XEP-0308?
|
|
||||||
* @property { Boolean } is_archived - Is this message from a XEP-0313 MAM archive?
|
|
||||||
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
|
|
||||||
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
|
|
||||||
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
|
|
||||||
* @property { Boolean } is_error - Whether an error was received for this message
|
|
||||||
* @property { Boolean } is_headline - Is this a "headline" message?
|
|
||||||
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
|
|
||||||
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
|
|
||||||
* @property { Boolean } is_only_emojis - Does the message body contain only emojis?
|
|
||||||
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
|
|
||||||
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
|
|
||||||
* @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored
|
|
||||||
* @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
|
|
||||||
* @property { Object } encrypted - XEP-0384 encryption payload attributes
|
|
||||||
* @property { String } body - The contents of the <body> tag of the message stanza
|
|
||||||
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
|
|
||||||
* @property { String } contact_jid - The JID of the other person or entity
|
|
||||||
* @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308
|
|
||||||
* @property { String } error_condition - The defined error condition
|
|
||||||
* @property { String } error_text - The error text received from the server
|
|
||||||
* @property { String } error_type - The type of error received from the server
|
|
||||||
* @property { String } from - The sender JID
|
|
||||||
* @property { String } fullname - The full name of the sender
|
|
||||||
* @property { String } marker - The XEP-0333 Chat Marker value
|
|
||||||
* @property { String } marker_id - The `id` attribute of a XEP-0333 chat marker
|
|
||||||
* @property { String } msgid - The root `id` attribute of the stanza
|
|
||||||
* @property { String } nick - The roster nickname of the sender
|
|
||||||
* @property { String } oob_desc - The description of the XEP-0066 out of band data
|
|
||||||
* @property { String } oob_url - The URL of the XEP-0066 out of band data
|
|
||||||
* @property { String } origin_id - The XEP-0359 Origin ID
|
|
||||||
* @property { String } receipt_id - The `id` attribute of a XEP-0184 <receipt> element
|
|
||||||
* @property { String } received - An ISO8601 string recording the time that the message was received
|
|
||||||
* @property { String } replace_id - The `id` attribute of a XEP-0308 <replace> element
|
|
||||||
* @property { String } retracted - An ISO8601 string recording the time that the message was retracted
|
|
||||||
* @property { String } retracted_id - The `id` attribute of a XEP-424 <retracted> element
|
|
||||||
* @property { String } spoiler_hint The XEP-0382 spoiler hint
|
|
||||||
* @property { String } stanza_id - The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple.
|
|
||||||
* @property { String } subject - The <subject> element value
|
|
||||||
* @property { String } thread - The <thread> element value
|
|
||||||
* @property { String } time - The time (in ISO8601 format), either given by the XEP-0203 <delay> element, or of receipt.
|
|
||||||
* @property { String } to - The recipient JID
|
|
||||||
* @property { String } type - The type of message
|
|
||||||
*/
|
|
||||||
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
|
|
||||||
const marker = st.getChatMarker(stanza);
|
|
||||||
const now = (new Date()).toISOString();
|
|
||||||
let attrs = Object.assign({
|
|
||||||
contact_jid,
|
|
||||||
is_archived,
|
|
||||||
is_headline,
|
|
||||||
is_server_message,
|
|
||||||
'body': stanza.querySelector('body')?.textContent?.trim(),
|
|
||||||
'chat_state': getChatState(stanza),
|
|
||||||
'from': Strophe.getBareJidFromJid(stanza.getAttribute('from')),
|
|
||||||
'is_carbon': isCarbon(original_stanza),
|
|
||||||
'is_delayed': !!delay,
|
|
||||||
'is_markable': !!sizzle(`markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length,
|
|
||||||
'is_marker': !!marker,
|
|
||||||
'is_unstyled': !!sizzle(`unstyled[xmlns="${Strophe.NS.STYLING}"]`, stanza).length,
|
|
||||||
'marker_id': marker && marker.getAttribute('id'),
|
|
||||||
'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
|
|
||||||
'nick': contact?.attributes?.nickname,
|
|
||||||
'receipt_id': getReceiptId(stanza),
|
|
||||||
'received': (new Date()).toISOString(),
|
|
||||||
'references': getReferences(stanza),
|
|
||||||
'sender': is_me ? 'me' : 'them',
|
|
||||||
'subject': stanza.querySelector('subject')?.textContent,
|
|
||||||
'thread': stanza.querySelector('thread')?.textContent,
|
|
||||||
'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : now,
|
|
||||||
'to': stanza.getAttribute('to'),
|
|
||||||
'type': stanza.getAttribute('type')
|
|
||||||
},
|
|
||||||
getErrorAttributes(stanza),
|
|
||||||
getOutOfBandAttributes(stanza),
|
|
||||||
getSpoilerAttributes(stanza),
|
|
||||||
getCorrectionAttributes(stanza, original_stanza),
|
|
||||||
getStanzaIDs(stanza, original_stanza),
|
|
||||||
getRetractionAttributes(stanza, original_stanza),
|
|
||||||
getEncryptionAttributes(stanza, _converse)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (attrs.is_archived) {
|
|
||||||
const from = original_stanza.getAttribute('from');
|
|
||||||
if (from && from !== _converse.bare_jid) {
|
|
||||||
return new StanzaParseError(`Invalid Stanza: Forged MAM message from ${from}`, stanza);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await api.emojis.initialize();
|
|
||||||
attrs = Object.assign({
|
|
||||||
'message': attrs.body || attrs.error, // TODO: Remove and use body and error attributes instead
|
|
||||||
'is_only_emojis': attrs.body ? u.isOnlyEmojis(attrs.body) : false,
|
|
||||||
'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs)
|
|
||||||
}, attrs);
|
|
||||||
|
|
||||||
// We prefer to use one of the XEP-0359 unique and stable stanza IDs
|
|
||||||
// as the Model id, to avoid duplicates.
|
|
||||||
attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${(attrs.from)}`] || u.getUniqueId();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* *Hook* which allows plugins to add additional parsing
|
|
||||||
* @event _converse#parseMessage
|
|
||||||
*/
|
|
||||||
return api.hook('parseMessage', stanza, attrs);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a passed in message stanza and returns an object of attributes.
|
|
||||||
* @method st#parseMUCMessage
|
|
||||||
* @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.
|
|
||||||
* @param { _converse.ChatRoom } chatbox
|
|
||||||
* @param { _converse } _converse
|
|
||||||
* @returns { Promise<MUCMessageAttributes|Error> }
|
|
||||||
*/
|
|
||||||
async parseMUCMessage (stanza, chatbox, _converse) {
|
|
||||||
const err = rejectUnencapsulatedForward(stanza);
|
|
||||||
if (err) {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selector = `[xmlns="${NS.MAM}"] > forwarded[xmlns="${NS.FORWARD}"] > message`;
|
|
||||||
const original_stanza = stanza;
|
|
||||||
stanza = sizzle(selector, stanza).pop() || stanza;
|
|
||||||
|
|
||||||
if (sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length) {
|
|
||||||
return new StanzaParseError(
|
|
||||||
`Invalid Stanza: Forged MAM groupchat message from ${stanza.getAttribute('from')}`,
|
|
||||||
stanza
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
|
|
||||||
const from = stanza.getAttribute('from');
|
|
||||||
const nick = Strophe.unescapeNode(Strophe.getResourceFromJid(from));
|
|
||||||
const marker = st.getChatMarker(stanza);
|
|
||||||
const now = (new Date()).toISOString();
|
|
||||||
/**
|
|
||||||
* @typedef { Object } MUCMessageAttributes
|
|
||||||
* The object which {@link st.parseMUCMessage} returns
|
|
||||||
* @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else
|
|
||||||
* @property { Array<Object> } references - A list of objects representing XEP-0372 references
|
|
||||||
* @property { Boolean } editable - Is this message editable via XEP-0308?
|
|
||||||
* @property { Boolean } is_archived - Is this message from a XEP-0313 MAM archive?
|
|
||||||
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
|
|
||||||
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
|
|
||||||
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
|
|
||||||
* @property { Boolean } is_error - Whether an error was received for this message
|
|
||||||
* @property { Boolean } is_headline - Is this a "headline" message?
|
|
||||||
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
|
|
||||||
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
|
|
||||||
* @property { Boolean } is_only_emojis - Does the message body contain only emojis?
|
|
||||||
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
|
|
||||||
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
|
|
||||||
* @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored
|
|
||||||
* @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
|
|
||||||
* @property { Object } encrypted - XEP-0384 encryption payload attributes
|
|
||||||
* @property { String } body - The contents of the <body> tag of the message stanza
|
|
||||||
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
|
|
||||||
* @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308
|
|
||||||
* @property { String } error_condition - The defined error condition
|
|
||||||
* @property { String } error_text - The error text received from the server
|
|
||||||
* @property { String } error_type - The type of error received from the server
|
|
||||||
* @property { String } from - The sender JID (${muc_jid}/${nick})
|
|
||||||
* @property { String } from_muc - The JID of the MUC from which this message was sent
|
|
||||||
* @property { String } from_real_jid - The real JID of the sender, if available
|
|
||||||
* @property { String } fullname - The full name of the sender
|
|
||||||
* @property { String } marker - The XEP-0333 Chat Marker value
|
|
||||||
* @property { String } marker_id - The `id` attribute of a XEP-0333 chat marker
|
|
||||||
* @property { String } moderated - The type of XEP-0425 moderation (if any) that was applied
|
|
||||||
* @property { String } moderated_by - The JID of the user that moderated this message
|
|
||||||
* @property { String } moderated_id - The XEP-0359 Stanza ID of the message that this one moderates
|
|
||||||
* @property { String } moderation_reason - The reason provided why this message moderates another
|
|
||||||
* @property { String } msgid - The root `id` attribute of the stanza
|
|
||||||
* @property { String } nick - The MUC nickname of the sender
|
|
||||||
* @property { String } oob_desc - The description of the XEP-0066 out of band data
|
|
||||||
* @property { String } oob_url - The URL of the XEP-0066 out of band data
|
|
||||||
* @property { String } origin_id - The XEP-0359 Origin ID
|
|
||||||
* @property { String } receipt_id - The `id` attribute of a XEP-0184 <receipt> element
|
|
||||||
* @property { String } received - An ISO8601 string recording the time that the message was received
|
|
||||||
* @property { String } replace_id - The `id` attribute of a XEP-0308 <replace> element
|
|
||||||
* @property { String } retracted - An ISO8601 string recording the time that the message was retracted
|
|
||||||
* @property { String } retracted_id - The `id` attribute of a XEP-424 <retracted> element
|
|
||||||
* @property { String } spoiler_hint The XEP-0382 spoiler hint
|
|
||||||
* @property { String } stanza_id - The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple.
|
|
||||||
* @property { String } subject - The <subject> element value
|
|
||||||
* @property { String } thread - The <thread> element value
|
|
||||||
* @property { String } time - The time (in ISO8601 format), either given by the XEP-0203 <delay> element, or of receipt.
|
|
||||||
* @property { String } to - The recipient JID
|
|
||||||
* @property { String } type - The type of message
|
|
||||||
*/
|
|
||||||
let attrs = Object.assign({
|
|
||||||
from,
|
|
||||||
nick,
|
|
||||||
'body': stanza.querySelector('body')?.textContent?.trim(),
|
|
||||||
'chat_state': getChatState(stanza),
|
|
||||||
'from_muc': Strophe.getBareJidFromJid(from),
|
|
||||||
'from_real_jid': chatbox.occupants.findOccupant({nick})?.get('jid'),
|
|
||||||
'is_archived': st.isArchived(original_stanza),
|
|
||||||
'is_carbon': isCarbon(original_stanza),
|
|
||||||
'is_delayed': !!delay,
|
|
||||||
'is_headline': st.isHeadline(stanza),
|
|
||||||
'is_markable': !!sizzle(`markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length,
|
|
||||||
'is_marker': !!marker,
|
|
||||||
'is_unstyled': !!sizzle(`unstyled[xmlns="${Strophe.NS.STYLING}"]`, stanza).length,
|
|
||||||
'marker_id': marker && marker.getAttribute('id'),
|
|
||||||
'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
|
|
||||||
'receipt_id': getReceiptId(stanza),
|
|
||||||
'received': (new Date()).toISOString(),
|
|
||||||
'references': getReferences(stanza),
|
|
||||||
'subject': stanza.querySelector('subject')?.textContent,
|
|
||||||
'thread': stanza.querySelector('thread')?.textContent,
|
|
||||||
'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : now,
|
|
||||||
'to': stanza.getAttribute('to'),
|
|
||||||
'type': stanza.getAttribute('type'),
|
|
||||||
},
|
|
||||||
getErrorAttributes(stanza),
|
|
||||||
getOutOfBandAttributes(stanza),
|
|
||||||
getSpoilerAttributes(stanza),
|
|
||||||
getCorrectionAttributes(stanza, original_stanza),
|
|
||||||
getStanzaIDs(stanza, original_stanza),
|
|
||||||
getRetractionAttributes(stanza, original_stanza),
|
|
||||||
getModerationAttributes(stanza),
|
|
||||||
getEncryptionAttributes(stanza, _converse)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
await api.emojis.initialize();
|
|
||||||
attrs = Object.assign({
|
|
||||||
'is_only_emojis': attrs.body ? u.isOnlyEmojis(attrs.body) : false,
|
|
||||||
'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs),
|
|
||||||
'message': attrs.body || attrs.error, // TODO: Remove and use body and error attributes instead
|
|
||||||
'sender': attrs.nick === chatbox.get('nick') ? 'me': 'them',
|
|
||||||
}, attrs);
|
|
||||||
|
|
||||||
if (attrs.is_archived && original_stanza.getAttribute('from') !== attrs.from_muc) {
|
|
||||||
return new StanzaParseError(
|
|
||||||
`Invalid Stanza: Forged MAM message from ${original_stanza.getAttribute('from')}`,
|
|
||||||
stanza
|
|
||||||
);
|
|
||||||
} else if (attrs.is_archived && original_stanza.getAttribute('from') !== chatbox.get('jid')) {
|
|
||||||
return new StanzaParseError(
|
|
||||||
`Invalid Stanza: Forged MAM groupchat message from ${stanza.getAttribute('from')}`,
|
|
||||||
stanza
|
|
||||||
);
|
|
||||||
} else if (attrs.is_carbon) {
|
|
||||||
return new StanzaParseError(
|
|
||||||
"Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied",
|
|
||||||
stanza
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
|
|
||||||
attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${(attrs.from_muc || attrs.from)}`] || u.getUniqueId();
|
|
||||||
/**
|
|
||||||
* *Hook* which allows plugins to add additional parsing
|
|
||||||
* @event _converse#parseMUCMessage
|
|
||||||
*/
|
|
||||||
return api.hook('parseMUCMessage', stanza, attrs);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a passed in MUC presence stanza and returns an object of attributes.
|
|
||||||
* @method st#parseMUCPresence
|
|
||||||
* @param { XMLElement } stanza - The presence stanza
|
|
||||||
* @returns { Object }
|
|
||||||
*/
|
|
||||||
parseMUCPresence (stanza) {
|
|
||||||
const from = stanza.getAttribute("from");
|
|
||||||
const type = stanza.getAttribute("type");
|
|
||||||
const data = {
|
|
||||||
'from': from,
|
|
||||||
'nick': Strophe.getResourceFromJid(from),
|
|
||||||
'type': type,
|
|
||||||
'states': [],
|
|
||||||
'hats': [],
|
|
||||||
'show': type !== 'unavailable' ? 'online' : 'offline'
|
|
||||||
};
|
|
||||||
Array.from(stanza.children).forEach(child => {
|
|
||||||
if (child.matches('status')) {
|
|
||||||
data.status = child.textContent || null;
|
|
||||||
} else if (child.matches('show')) {
|
|
||||||
data.show = child.textContent || 'online';
|
|
||||||
} else if (child.matches('x') && child.getAttribute('xmlns') === Strophe.NS.MUC_USER) {
|
|
||||||
Array.from(child.children).forEach(item => {
|
|
||||||
if (item.nodeName === "item") {
|
|
||||||
data.affiliation = item.getAttribute("affiliation");
|
|
||||||
data.role = item.getAttribute("role");
|
|
||||||
data.jid = item.getAttribute("jid");
|
|
||||||
data.nick = item.getAttribute("nick") || data.nick;
|
|
||||||
} else if (item.nodeName == 'status' && item.getAttribute("code")) {
|
|
||||||
data.states.push(item.getAttribute("code"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (child.matches('x') && child.getAttribute('xmlns') === Strophe.NS.VCARDUPDATE) {
|
|
||||||
data.image_hash = child.querySelector('photo')?.textContent;
|
|
||||||
} else if (child.matches('hats') && child.getAttribute('xmlns') === Strophe.NS.MUC_HATS) {
|
|
||||||
data['hats'] = Array.from(child.children).map(c => c.matches('hat') && {
|
|
||||||
'title': c.getAttribute('title'),
|
|
||||||
'uri': c.getAttribute('uri')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default st;
|
|
@ -1,11 +1,11 @@
|
|||||||
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({
|
|||||||
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 @@
|
|||||||
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';
|
||||||
|
Loading…
Reference in New Issue
Block a user