Commit b5eea12d authored by JC Brand's avatar JC Brand

Refactor so that message attributes are parsed early

It's better to parse an incoming message stanza early, than to have
all kinds of methods throughout the codebase that does querySelector
etc.

Firstly, it allows us to catch and report errors and malicious stanzas early on.
It also simplifies programming because you don't need to try and
remember how to properly parse a stanza, all the work is done upfront
for you.
parent 27008aff
...@@ -37,7 +37,7 @@ describe("The nickname autocomplete feature", function () { ...@@ -37,7 +37,7 @@ describe("The nickname autocomplete feature", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t('Hello world').tree(); }).c('body').t('Hello world').tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
// Test that pressing @ brings up all options // Test that pressing @ brings up all options
const textarea = view.el.querySelector('textarea.chat-textarea'); const textarea = view.el.querySelector('textarea.chat-textarea');
......
...@@ -6,7 +6,7 @@ describe("A headlines box", function () { ...@@ -6,7 +6,7 @@ describe("A headlines box", function () {
mock.initConverse( mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) { ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
const { u, $msg} = converse.env; const { $msg } = converse.env;
/* XMPP spam message: /* XMPP spam message:
* *
* <message xmlns="jabber:client" * <message xmlns="jabber:client"
...@@ -17,7 +17,6 @@ describe("A headlines box", function () { ...@@ -17,7 +17,6 @@ describe("A headlines box", function () {
* <body>SORRY FOR THIS ADVERT</body * <body>SORRY FOR THIS ADVERT</body
* </message * </message
*/ */
sinon.spy(u, 'isHeadlineMessage');
const stanza = $msg({ const stanza = $msg({
'xmlns': 'jabber:client', 'xmlns': 'jabber:client',
'to': 'romeo@montague.lit', 'to': 'romeo@montague.lit',
...@@ -27,10 +26,7 @@ describe("A headlines box", function () { ...@@ -27,10 +26,7 @@ describe("A headlines box", function () {
.c('nick', {'xmlns': "http://jabber.org/protocol/nick"}).t("-wwdmz").up() .c('nick', {'xmlns': "http://jabber.org/protocol/nick"}).t("-wwdmz").up()
.c('body').t('SORRY FOR THIS ADVERT'); .c('body').t('SORRY FOR THIS ADVERT');
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(false)).toBeTruthy();
expect(_converse.api.headlines.get().length === 0); expect(_converse.api.headlines.get().length === 0);
u.isHeadlineMessage.restore();
done(); done();
})); }));
...@@ -51,7 +47,6 @@ describe("A headlines box", function () { ...@@ -51,7 +47,6 @@ describe("A headlines box", function () {
* </x> * </x>
* </message> * </message>
*/ */
sinon.spy(u, 'isHeadlineMessage');
const stanza = $msg({ const stanza = $msg({
'type': 'headline', 'type': 'headline',
'from': 'notify.example.com', 'from': 'notify.example.com',
...@@ -65,9 +60,6 @@ describe("A headlines box", function () { ...@@ -65,9 +60,6 @@ describe("A headlines box", function () {
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => _converse.chatboxviews.keys().includes('notify.example.com')); await u.waitUntil(() => _converse.chatboxviews.keys().includes('notify.example.com'));
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(true)).toBeTruthy();
u.isHeadlineMessage.restore(); // unwraps
const view = _converse.chatboxviews.get('notify.example.com'); const view = _converse.chatboxviews.get('notify.example.com');
expect(view.model.get('show_avatar')).toBeFalsy(); expect(view.model.get('show_avatar')).toBeFalsy();
expect(view.el.querySelector('img.avatar')).toBe(null); expect(view.el.querySelector('img.avatar')).toBe(null);
...@@ -155,9 +147,8 @@ describe("A headlines box", function () { ...@@ -155,9 +147,8 @@ describe("A headlines box", function () {
mock.initConverse( mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) { ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
const { u, $msg, _ } = converse.env; const { $msg, _ } = converse.env;
_converse.allow_non_roster_messaging = false; _converse.allow_non_roster_messaging = false;
sinon.spy(u, 'isHeadlineMessage');
const stanza = $msg({ const stanza = $msg({
'type': 'headline', 'type': 'headline',
'from': 'andre5114@jabber.snc.ru/Spark', 'from': 'andre5114@jabber.snc.ru/Spark',
...@@ -168,9 +159,6 @@ describe("A headlines box", function () { ...@@ -168,9 +159,6 @@ describe("A headlines box", function () {
.c('body').t('Здравствуйте друзья'); .c('body').t('Здравствуйте друзья');
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
expect(_.without('controlbox', _converse.chatboxviews.keys()).length).toBe(0); expect(_.without('controlbox', _converse.chatboxviews.keys()).length).toBe(0);
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(true)).toBeTruthy();
u.isHeadlineMessage.restore(); // unwraps
done(); done();
})); }));
}); });
...@@ -294,7 +294,7 @@ describe("Message Archive Management", function () { ...@@ -294,7 +294,7 @@ describe("Message Archive Management", function () {
</message>`); </message>`);
spyOn(view.model, 'getDuplicateMessage').and.callThrough(); spyOn(view.model, 'getDuplicateMessage').and.callThrough();
spyOn(view.model, 'updateMessage').and.callThrough(); spyOn(view.model, 'updateMessage').and.callThrough();
view.model.queueMessage(stanza); view.model.handleMAMResult({ 'messages': [stanza] });
await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
expect(view.model.getDuplicateMessage.calls.count()).toBe(1); expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
const result = view.model.getDuplicateMessage.calls.all()[0].returnValue const result = view.model.getDuplicateMessage.calls.all()[0].returnValue
...@@ -338,7 +338,7 @@ describe("Message Archive Management", function () { ...@@ -338,7 +338,7 @@ describe("Message Archive Management", function () {
</result> </result>
</message>`); </message>`);
spyOn(view.model, 'getDuplicateMessage').and.callThrough(); spyOn(view.model, 'getDuplicateMessage').and.callThrough();
view.model.queueMessage(stanza); view.model.handleMAMResult({ 'messages': [stanza] });
await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
expect(view.model.getDuplicateMessage.calls.count()).toBe(1); expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
...@@ -368,7 +368,7 @@ describe("Message Archive Management", function () { ...@@ -368,7 +368,7 @@ describe("Message Archive Management", function () {
</forwarded> </forwarded>
</result> </result>
</message>`); </message>`);
view.model.queueMessage(stanza); view.model.handleMAMResult({ 'messages': [stanza] });
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
...@@ -388,7 +388,7 @@ describe("Message Archive Management", function () { ...@@ -388,7 +388,7 @@ describe("Message Archive Management", function () {
</message>`); </message>`);
spyOn(view.model, 'getDuplicateMessage').and.callThrough(); spyOn(view.model, 'getDuplicateMessage').and.callThrough();
view.model.queueMessage(stanza); view.model.handleMAMResult({ 'messages': [stanza] });
await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
expect(view.model.getDuplicateMessage.calls.count()).toBe(1); expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
......
...@@ -512,9 +512,8 @@ describe("A Chat Message", function () { ...@@ -512,9 +512,8 @@ describe("A Chat Message", function () {
// Ideally we wouldn't have to filter out headline // Ideally we wouldn't have to filter out headline
// messages, but Prosody gives them the wrong 'type' :( // messages, but Prosody gives them the wrong 'type' :(
sinon.spy(converse.env.log, 'info'); spyOn(converse.env.log, 'info');
sinon.spy(_converse.api.chatboxes, 'get'); sinon.spy(_converse.api.chatboxes, 'get');
sinon.spy(u, 'isHeadlineMessage');
const msg = $msg({ const msg = $msg({
from: 'montague.lit', from: 'montague.lit',
to: _converse.bare_jid, to: _converse.bare_jid,
...@@ -522,16 +521,12 @@ describe("A Chat Message", function () { ...@@ -522,16 +521,12 @@ describe("A Chat Message", function () {
id: u.getUniqueId() id: u.getUniqueId()
}).c('body').t("This headline message will not be shown").tree(); }).c('body').t("This headline message will not be shown").tree();
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
expect(converse.env.log.info.calledWith( expect(converse.env.log.info).toHaveBeenCalledWith(
"handleMessageStanza: Ignoring incoming headline message from JID: montague.lit" "handleMessageStanza: Ignoring incoming server message from JID: montague.lit"
)).toBeTruthy(); );
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(true)).toBeTruthy();
expect(_converse.api.chatboxes.get.called).toBeFalsy(); expect(_converse.api.chatboxes.get.called).toBeFalsy();
// Remove sinon spies // Remove sinon spies
converse.env.log.info.restore();
_converse.api.chatboxes.get.restore(); _converse.api.chatboxes.get.restore();
u.isHeadlineMessage.restore();
done(); done();
})); }));
...@@ -1561,7 +1556,7 @@ describe("A Chat Message", function () { ...@@ -1561,7 +1556,7 @@ describe("A Chat Message", function () {
* </message> * </message>
*/ */
const error_txt = 'Server-to-server connection failed: Connecting failed: connection timeout'; const error_txt = 'Server-to-server connection failed: Connecting failed: connection timeout';
const sender_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
let fullname = _converse.xmppstatus.get('fullname'); // eslint-disable-line no-unused-vars let fullname = _converse.xmppstatus.get('fullname'); // eslint-disable-line no-unused-vars
fullname = _.isEmpty(fullname) ? _converse.bare_jid: fullname; fullname = _.isEmpty(fullname) ? _converse.bare_jid: fullname;
await _converse.api.chats.open(sender_jid) await _converse.api.chats.open(sender_jid)
...@@ -1757,7 +1752,7 @@ describe("A Chat Message", function () { ...@@ -1757,7 +1752,7 @@ describe("A Chat Message", function () {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length) await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
// Send a message from a different resource // Send a message from a different resource
spyOn(converse.env.log, 'info'); spyOn(converse.env.log, 'error');
spyOn(_converse.api.chatboxes, 'create').and.callThrough(); spyOn(_converse.api.chatboxes, 'create').and.callThrough();
_converse.filter_by_resource = true; _converse.filter_by_resource = true;
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
...@@ -1770,8 +1765,8 @@ describe("A Chat Message", function () { ...@@ -1770,8 +1765,8 @@ describe("A Chat Message", function () {
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
expect(converse.env.log.info).toHaveBeenCalledWith( expect(converse.env.log.error.calls.all().pop().args[0]).toBe(
"handleMessageStanza: Ignoring incoming message intended for a different resource: romeo@montague.lit/some-other-resource", "Ignoring incoming message intended for a different resource: romeo@montague.lit/some-other-resource",
); );
expect(_converse.api.chatboxes.create).not.toHaveBeenCalled(); expect(_converse.api.chatboxes.create).not.toHaveBeenCalled();
_converse.filter_by_resource = false; _converse.filter_by_resource = false;
......
...@@ -156,7 +156,7 @@ describe("The Minimized Chats Widget", function () { ...@@ -156,7 +156,7 @@ describe("The Minimized Chats Widget", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t(message).tree(); }).c('body').t(message).tree();
view.model.queueMessage(msg); view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length); await u.waitUntil(() => view.model.messages.length);
expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy(); expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy();
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe('1'); expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe('1');
......
...@@ -518,8 +518,7 @@ describe("Groupchats", function () { ...@@ -518,8 +518,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); const view = _converse.chatboxviews.get('jdev@conference.jabber.org');
await new Promise(resolve => view.model.once('change:subject', resolve)); await new Promise(resolve => view.model.once('change:subject', resolve));
const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc'), 1000);
const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc'));
expect(head_desc?.textContent.trim()).toBe(text); expect(head_desc?.textContent.trim()).toBe(text);
stanza = u.toStanza( stanza = u.toStanza(
...@@ -701,7 +700,7 @@ describe("Groupchats", function () { ...@@ -701,7 +700,7 @@ describe("Groupchats", function () {
'type': 'groupchat' 'type': 'groupchat'
}).c('body').t(message).tree(); }).c('body').t(message).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
spyOn(view.model, 'clearMessages').and.callThrough(); spyOn(view.model, 'clearMessages').and.callThrough();
await view.model.close(); await view.model.close();
...@@ -732,7 +731,7 @@ describe("Groupchats", function () { ...@@ -732,7 +731,7 @@ describe("Groupchats", function () {
'type': 'groupchat' 'type': 'groupchat'
}).c('body').t(message).tree(); }).c('body').t(message).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelector('.chat-msg__text a')); await u.waitUntil(() => view.el.querySelector('.chat-msg__text a'));
view.el.querySelector('.chat-msg__text a').click(); view.el.querySelector('.chat-msg__text a').click();
await u.waitUntil(() => _converse.chatboxes.length === 3) await u.waitUntil(() => _converse.chatboxes.length === 3)
...@@ -1269,7 +1268,7 @@ describe("Groupchats", function () { ...@@ -1269,7 +1268,7 @@ describe("Groupchats", function () {
'type': 'groupchat' 'type': 'groupchat'
}).c('body').t('Some message').tree(); }).c('body').t('Some message').tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop()); await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop());
let stanza = u.toStanza( let stanza = u.toStanza(
...@@ -1318,7 +1317,7 @@ describe("Groupchats", function () { ...@@ -1318,7 +1317,7 @@ describe("Groupchats", function () {
'to': 'romeo@montague.lit', 'to': 'romeo@montague.lit',
'type': 'groupchat' 'type': 'groupchat'
}).c('body').t(message).tree(); }).c('body').t(message).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop()); await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop());
expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Dyon van de Wege')).toBeTruthy(); expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Dyon van de Wege')).toBeTruthy();
expect(view.el.querySelector('.chat-msg__text').textContent.trim()).toBe('is tired'); expect(view.el.querySelector('.chat-msg__text').textContent.trim()).toBe('is tired');
...@@ -1330,7 +1329,7 @@ describe("Groupchats", function () { ...@@ -1330,7 +1329,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t(message).tree(); }).c('body').t(message).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
expect(sizzle('.chat-msg__author:last', view.el).pop().textContent.includes('**Romeo Montague')).toBeTruthy(); expect(sizzle('.chat-msg__author:last', view.el).pop().textContent.includes('**Romeo Montague')).toBeTruthy();
expect(sizzle('.chat-msg__text:last', view.el).pop().textContent.trim()).toBe('is as well'); expect(sizzle('.chat-msg__text:last', view.el).pop().textContent.trim()).toBe('is as well');
...@@ -2016,7 +2015,7 @@ describe("Groupchats", function () { ...@@ -2016,7 +2015,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t(text); }).c('body').t(text);
await view.model.queueMessage(message.nodeTree); await view.model.handleMessageStanza(message.nodeTree);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.content.querySelector('.chat-msg__text').textContent.trim()).toBe(text); expect(view.content.querySelector('.chat-msg__text').textContent.trim()).toBe(text);
...@@ -2061,7 +2060,7 @@ describe("Groupchats", function () { ...@@ -2061,7 +2060,7 @@ describe("Groupchats", function () {
by="lounge@montague.lit"/> by="lounge@montague.lit"/>
<origin-id xmlns="urn:xmpp:sid:0" id="${view.model.messages.at(0).get('origin_id')}"/> <origin-id xmlns="urn:xmpp:sid:0" id="${view.model.messages.at(0).get('origin_id')}"/>
</message>`); </message>`);
await view.model.queueMessage(stanza); await view.model.handleMessageStanza(stanza);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
expect(sizzle('.chat-msg__text:last').pop().textContent.trim()).toBe(text); expect(sizzle('.chat-msg__text:last').pop().textContent.trim()).toBe(text);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
...@@ -2083,7 +2082,7 @@ describe("Groupchats", function () { ...@@ -2083,7 +2082,7 @@ describe("Groupchats", function () {
const promises = []; const promises = [];
for (let i=0; i<20; i++) { for (let i=0; i<20; i++) {
promises.push( promises.push(
view.model.queueMessage( view.model.handleMessageStanza(
$msg({ $msg({
from: 'lounge@montague.lit/someone', from: 'lounge@montague.lit/someone',
to: 'romeo@montague.lit.com', to: 'romeo@montague.lit.com',
...@@ -2096,7 +2095,7 @@ describe("Groupchats", function () { ...@@ -2096,7 +2095,7 @@ describe("Groupchats", function () {
// Give enough time for `markScrolled` to have been called // Give enough time for `markScrolled` to have been called
setTimeout(async () => { setTimeout(async () => {
view.content.scrollTop = 0; view.content.scrollTop = 0;
await view.model.queueMessage( await view.model.handleMessageStanza(
$msg({ $msg({
from: 'lounge@montague.lit/someone', from: 'lounge@montague.lit/someone',
to: 'romeo@montague.lit.com', to: 'romeo@montague.lit.com',
...@@ -4863,8 +4862,7 @@ describe("Groupchats", function () { ...@@ -4863,8 +4862,7 @@ describe("Groupchats", function () {
view.model.set({'minimized': true}); view.model.set({'minimized': true});
const nick = mock.chatroom_names[0]; const nick = mock.chatroom_names[0];
await view.model.handleMessageStanza($msg({
await view.model.queueMessage($msg({
from: muc_jid+'/'+nick, from: muc_jid+'/'+nick,
id: u.getUniqueId(), id: u.getUniqueId(),
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
...@@ -4875,7 +4873,7 @@ describe("Groupchats", function () { ...@@ -4875,7 +4873,7 @@ describe("Groupchats", function () {
expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(1); expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(1);
expect(roomspanel.el.querySelector('.msgs-indicator').textContent.trim()).toBe('1'); expect(roomspanel.el.querySelector('.msgs-indicator').textContent.trim()).toBe('1');
await view.model.queueMessage($msg({ await view.model.handleMessageStanza($msg({
'from': muc_jid+'/'+nick, 'from': muc_jid+'/'+nick,
'id': u.getUniqueId(), 'id': u.getUniqueId(),
'to': 'romeo@montague.lit', 'to': 'romeo@montague.lit',
...@@ -5027,7 +5025,7 @@ describe("Groupchats", function () { ...@@ -5027,7 +5025,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy and nomorenicks are typing'); await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy and nomorenicks are typing');
// <composing> state for a different occupant // <composing> state for a different occupant
...@@ -5037,7 +5035,7 @@ describe("Groupchats", function () { ...@@ -5037,7 +5035,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy, nomorenicks and majortom are typing'); await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy, nomorenicks and majortom are typing');
// <composing> state for a different occupant // <composing> state for a different occupant
...@@ -5047,7 +5045,7 @@ describe("Groupchats", function () { ...@@ -5047,7 +5045,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy, nomorenicks and others are typing'); await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy, nomorenicks and others are typing');
// Check that new messages appear under the chat state notifications // Check that new messages appear under the chat state notifications
...@@ -5057,7 +5055,7 @@ describe("Groupchats", function () { ...@@ -5057,7 +5055,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t('hello world').tree(); }).c('body').t('hello world').tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve)); await new Promise(resolve => view.once('messageInserted', resolve));
const messages = view.el.querySelectorAll('.message'); const messages = view.el.querySelectorAll('.message');
...@@ -5146,7 +5144,7 @@ describe("Groupchats", function () { ...@@ -5146,7 +5144,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toBe('newguy is typing'); expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toBe('newguy is typing');
...@@ -5157,7 +5155,7 @@ describe("Groupchats", function () { ...@@ -5157,7 +5155,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() == 'newguy and nomorenicks are typing'); await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() == 'newguy and nomorenicks are typing');
...@@ -5168,7 +5166,7 @@ describe("Groupchats", function () { ...@@ -5168,7 +5166,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree(); }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() == 'nomorenicks is typing\nnewguy has stopped typing'); await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() == 'nomorenicks is typing\nnewguy has stopped typing');
done(); done();
})); }));
......
...@@ -104,14 +104,11 @@ describe("A Groupchat Message", function () { ...@@ -104,14 +104,11 @@ describe("A Groupchat Message", function () {
`); `);
const view = _converse.api.chatviews.get(muc_jid); const view = _converse.api.chatviews.get(muc_jid);
spyOn(view.model, 'onMessage').and.callThrough(); spyOn(view.model, 'onMessage').and.callThrough();
spyOn(converse.env.log, 'error');
await view.model.queueMessage(received_stanza);
spyOn(converse.env.log, 'warn');
_converse.connection._dataRecv(mock.createRequest(received_stanza)); _converse.connection._dataRecv(mock.createRequest(received_stanza));
await u.waitUntil(() => view.model.onMessage.calls.count()); await u.waitUntil(() => view.model.onMessage.calls.count() === 1);
expect(converse.env.log.warn).toHaveBeenCalledWith( expect(converse.env.log.error).toHaveBeenCalledWith(
'onMessage: Ignoring unencapsulated forwarded groupchat message' `Ignoring unencapsulated forwarded message from ${muc_jid}/mallory`
); );
expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
expect(view.model.messages.length).toBe(0); expect(view.model.messages.length).toBe(0);
...@@ -137,7 +134,7 @@ describe("A Groupchat Message", function () { ...@@ -137,7 +134,7 @@ describe("A Groupchat Message", function () {
}).c('body').t(message) }).c('body').t(message)
.c('active', {'xmlns': "http://jabber.org/protocol/chatstates"}) .c('active', {'xmlns': "http://jabber.org/protocol/chatstates"})
.tree(); .tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve)); await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.el.querySelector('.chat-msg')).not.toBe(null); expect(view.el.querySelector('.chat-msg')).not.toBe(null);
done(); done();
...@@ -160,7 +157,7 @@ describe("A Groupchat Message", function () { ...@@ -160,7 +157,7 @@ describe("A Groupchat Message", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t(message).tree(); }).c('body').t(message).tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve)); await new Promise(resolve => view.once('messageInserted', resolve));
expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy(); expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy();
done(); done();
...@@ -182,7 +179,7 @@ describe("A Groupchat Message", function () { ...@@ -182,7 +179,7 @@ describe("A Groupchat Message", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t('First message').tree(); }).c('body').t('First message').tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
msg = $msg({ msg = $msg({
...@@ -191,7 +188,7 @@ describe("A Groupchat Message", function () { ...@@ -191,7 +188,7 @@ describe("A Groupchat Message", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t('Another message').tree(); }).c('body').t('Another message').tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
expect(view.model.messages.length).toBe(2); expect(view.model.messages.length).toBe(2);
done(); done();
...@@ -237,7 +234,7 @@ describe("A Groupchat Message", function () { ...@@ -237,7 +234,7 @@ describe("A Groupchat Message", function () {
</message>`); </message>`);
spyOn(view.model, 'updateMessage'); spyOn(view.model, 'updateMessage');
await view.model.queueMessage(stanza); view.model.handleMAMResult({ 'messages': [stanza] });
await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 2); await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 2);
result = await view.model.getDuplicateMessage.calls.all()[1].returnValue; result = await view.model.getDuplicateMessage.calls.all()[1].returnValue;
expect(result instanceof _converse.Message).toBe(true); expect(result instanceof _converse.Message).toBe(true);
...@@ -342,11 +339,10 @@ describe("A Groupchat Message", function () { ...@@ -342,11 +339,10 @@ describe("A Groupchat Message", function () {
'type': 'groupchat' 'type': 'groupchat'
}).c('body').t('I am groot').tree(); }).c('body').t('I am groot').tree();
const view = _converse.api.chatviews.get(muc_jid); const view = _converse.api.chatviews.get(muc_jid);
spyOn(converse.env.log, 'warn'); spyOn(converse.env.log, 'error');
await view.model.queueMessage(msg); await view.model.handleMAMResult({ 'messages': [msg] });
expect(converse.env.log.warn).toHaveBeenCalledWith( expect(converse.env.log.error).toHaveBeenCalledWith(
'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+ 'Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied'
'according to the XEP groupchat messages SHOULD NOT be carbon copied'
); );
expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
expect(view.model.messages.length).toBe(0); expect(view.model.messages.length).toBe(0);
...@@ -367,7 +363,7 @@ describe("A Groupchat Message", function () { ...@@ -367,7 +363,7 @@ describe("A Groupchat Message", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t('I wrote this message!').tree(); }).c('body').t('I wrote this message!').tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner'); expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner');
expect(view.model.messages.last().occupant.get('role')).toBe('moderator'); expect(view.model.messages.last().occupant.get('role')).toBe('moderator');
...@@ -393,7 +389,7 @@ describe("A Groupchat Message", function () { ...@@ -393,7 +389,7 @@ describe("A Groupchat Message", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t('Another message!').tree(); }).c('body').t('Another message!').tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve)); await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.model.messages.last().occupant.get('affiliation')).toBe('member'); expect(view.model.messages.last().occupant.get('affiliation')).toBe('member');
expect(view.model.messages.last().occupant.get('role')).toBe('participant'); expect(view.model.messages.last().occupant.get('role')).toBe('participant');
...@@ -430,7 +426,7 @@ describe("A Groupchat Message", function () { ...@@ -430,7 +426,7 @@ describe("A Groupchat Message", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t('Message from someone not in the MUC right now').tree(); }).c('body').t('Message from someone not in the MUC right now').tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve)); await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.model.messages.last().occupant).toBeUndefined(); expect(view.model.messages.last().occupant).toBeUndefined();
// Check that there's a new "add" event handler, for when the occupant appears. // Check that there's a new "add" event handler, for when the occupant appears.
...@@ -495,7 +491,7 @@ describe("A Groupchat Message", function () { ...@@ -495,7 +491,7 @@ describe("A Groupchat Message", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t('I wrote this message!').tree(); }).c('body').t('I wrote this message!').tree();
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
expect(view.model.messages.last().get('sender')).toBe('me'); expect(view.model.messages.last().get('sender')).toBe('me');
done(); done();
})); }));
...@@ -520,7 +516,7 @@ describe("A Groupchat Message", function () { ...@@ -520,7 +516,7 @@ describe("A Groupchat Message", function () {
}).tree(); }).tree();
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
const msg_id = u.getUniqueId(); const msg_id = u.getUniqueId();
await view.model.queueMessage($msg({ await view.model.handleMessageStanza($msg({
'from': 'lounge@montague.lit/newguy', 'from': 'lounge@montague.lit/newguy',
'to': _converse.connection.jid, 'to': _converse.connection.jid,
'type': 'groupchat', 'type': 'groupchat',
...@@ -532,7 +528,7 @@ describe("A Groupchat Message", function () { ...@@ -532,7 +528,7 @@ describe("A Groupchat Message", function () {
expect(view.el.querySelector('.chat-msg__text').textContent) expect(view.el.querySelector('.chat-msg__text').textContent)
.toBe('But soft, what light through yonder airlock breaks?'); .toBe('But soft, what light through yonder airlock breaks?');
await view.model.queueMessage($msg({ await view.model.handleMessageStanza($msg({
'from': 'lounge@montague.lit/newguy', 'from': 'lounge@montague.lit/newguy',
'to': _converse.connection.jid, 'to': _converse.connection.jid,
'type': 'groupchat', 'type': 'groupchat',
...@@ -544,7 +540,7 @@ describe("A Groupchat Message", function () { ...@@ -544,7 +540,7 @@ describe("A Groupchat Message", function () {
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
await view.model.queueMessage($msg({ await view.model.handleMessageStanza($msg({
'from': 'lounge@montague.lit/newguy', 'from': 'lounge@montague.lit/newguy',
'to': _converse.connection.jid, 'to': _converse.connection.jid,
'type': 'groupchat', 'type': 'groupchat',
...@@ -641,7 +637,7 @@ describe("A Groupchat Message", function () { ...@@ -641,7 +637,7 @@ describe("A Groupchat Message", function () {
expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false); expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false);
// Check that messages from other users are skipped // Check that messages from other users are skipped
await view.model.queueMessage($msg({ await view.model.handleMessageStanza($msg({
'from': muc_jid+'/someone-else', 'from': muc_jid+'/someone-else',
'id': u.getUniqueId(), 'id': u.getUniqueId(),
'to': 'romeo@montague.lit', 'to': 'romeo@montague.lit',
...@@ -703,7 +699,7 @@ describe("A Groupchat Message", function () { ...@@ -703,7 +699,7 @@ describe("A Groupchat Message", function () {
by="lounge@montague.lit"/> by="lounge@montague.lit"/>
<origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/> <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
</message>`); </message>`);
await view.model.queueMessage(stanza); await view.model.handleMessageStanza(stanza);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1);
...@@ -776,13 +772,11 @@ describe("A Groupchat Message", function () { ...@@ -776,13 +772,11 @@ describe("A Groupchat Message", function () {
<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(_converse.api, "trigger").and.callThrough(); spyOn(stanza_utils, "parseMUCMessage").and.callThrough();
spyOn(stanza_utils, "isReceipt").and.callThrough();
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => stanza_utils.isReceipt.calls.count() === 1); 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); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
done(); done();
})); }));
...@@ -814,9 +808,9 @@ describe("A Groupchat Message", function () { ...@@ -814,9 +808,9 @@ describe("A Groupchat Message", function () {
<received xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/> <received xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
</message>`); </message>`);
const stanza_utils = converse.env.stanza_utils; const stanza_utils = converse.env.stanza_utils;
spyOn(stanza_utils, "isChatMarker").and.callThrough(); spyOn(stanza_utils, "getChatMarker").and.callThrough();
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 1); await u.waitUntil(() => stanza_utils.getChatMarker.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); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
...@@ -826,7 +820,7 @@ describe("A Groupchat Message", function () { ...@@ -826,7 +820,7 @@ describe("A Groupchat Message", function () {
<displayed xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/> <displayed xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
</message>`); </message>`);
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 2); await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 2);
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); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
...@@ -837,7 +831,7 @@ describe("A Groupchat Message", function () { ...@@ -837,7 +831,7 @@ describe("A Groupchat Message", function () {
</message>`); </message>`);
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 3); await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 3);
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
...@@ -848,8 +842,8 @@ describe("A Groupchat Message", function () { ...@@ -848,8 +842,8 @@ describe("A Groupchat Message", function () {
<markable xmlns="urn:xmpp:chat-markers:0"/> <markable xmlns="urn:xmpp:chat-markers:0"/>
</message>`); </message>`);
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 4); await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 4);
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
done(); done();
})); }));
...@@ -887,7 +881,7 @@ describe("A Groupchat Message", function () { ...@@ -887,7 +881,7 @@ describe("A Groupchat Message", function () {
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'6', 'end':'10', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up() .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'6', 'end':'10', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up()
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up() .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up()
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree; .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree;
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text')); const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
expect(message.classList.length).toEqual(1); expect(message.classList.length).toEqual(1);
expect(message.innerHTML).toBe( expect(message.innerHTML).toBe(
...@@ -928,7 +922,7 @@ describe("A Groupchat Message", function () { ...@@ -928,7 +922,7 @@ describe("A Groupchat Message", function () {
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'7', 'end':'11', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up() .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'7', 'end':'11', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up()
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'12', 'end':'15', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up() .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'12', 'end':'15', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up()
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'16', 'end':'24', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree; .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'16', 'end':'24', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree;
await view.model.queueMessage(msg); await view.model.handleMessageStanza(msg);
const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text')); const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
expect(message.classList.length).toEqual(1); expect(message.classList.length).toEqual(1);
expect(message.innerHTML).toBe( expect(message.innerHTML).toBe(
...@@ -972,7 +966,7 @@ describe("A Groupchat Message", function () { ...@@ -972,7 +966,7 @@ describe("A Groupchat Message", function () {
type="groupchat"> type="groupchat">
<body>Boo!</body> <body>Boo!</body>
</message>`); </message>`);
await view.model.queueMessage(stanza); await view.model.handleMessageStanza(stanza);
// Run a few unit tests for the parseTextForReferences method // Run a few unit tests for the parseTextForReferences method
let [text, references] = view.model.parseTextForReferences('hello z3r0') let [text, references] = view.model.parseTextForReferences('hello z3r0')
......
...@@ -95,10 +95,7 @@ describe("Notifications", function () { ...@@ -95,10 +95,7 @@ describe("Notifications", function () {
await u.waitUntil(() => _converse.chatboxviews.keys().length); await u.waitUntil(() => _converse.chatboxviews.keys().length);
const view = _converse.chatboxviews.get('notify.example.com'); const view = _converse.chatboxviews.get('notify.example.com');
await new Promise(resolve => view.once('messageInserted', resolve)); await new Promise(resolve => view.once('messageInserted', resolve));
expect( expect(_converse.chatboxviews.keys().includes('notify.example.com')).toBeTruthy();
_.includes(_converse.chatboxviews.keys(),
'notify.example.com')
).toBeTruthy();
expect(_converse.showMessageNotification).toHaveBeenCalled(); expect(_converse.showMessageNotification).toHaveBeenCalled();
done(); done();
})); }));
...@@ -175,7 +172,7 @@ describe("Notifications", function () { ...@@ -175,7 +172,7 @@ describe("Notifications", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t(text); }).c('body').t(text);
await view.model.queueMessage(message.nodeTree); await view.model.handleMessageStanza(message.nodeTree);
await u.waitUntil(() => _converse.playSoundNotification.calls.count()); await u.waitUntil(() => _converse.playSoundNotification.calls.count());
expect(_converse.playSoundNotification).toHaveBeenCalled(); expect(_converse.playSoundNotification).toHaveBeenCalled();
...@@ -186,7 +183,7 @@ describe("Notifications", function () { ...@@ -186,7 +183,7 @@ describe("Notifications", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t(text); }).c('body').t(text);
await view.model.queueMessage(message.nodeTree); await view.model.handleMessageStanza(message.nodeTree);
expect(_converse.playSoundNotification, 1); expect(_converse.playSoundNotification, 1);
_converse.play_sounds = false; _converse.play_sounds = false;
...@@ -197,7 +194,7 @@ describe("Notifications", function () { ...@@ -197,7 +194,7 @@ describe("Notifications", function () {
to: 'romeo@montague.lit', to: 'romeo@montague.lit',
type: 'groupchat' type: 'groupchat'
}).c('body').t(text); }).c('body').t(text);
await view.model.queueMessage(message.nodeTree); await view.model.handleMessageStanza(message.nodeTree);
expect(_converse.playSoundNotification, 1); expect(_converse.playSoundNotification, 1);
_converse.play_sounds = false; _converse.play_sounds = false;
done(); done();
......
...@@ -19,7 +19,7 @@ async function sendAndThenRetractMessage (_converse, view) { ...@@ -19,7 +19,7 @@ async function sendAndThenRetractMessage (_converse, view) {
by="lounge@montague.lit"/> by="lounge@montague.lit"/>
<origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/> <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
</message>`); </message>`);
await view.model.queueMessage(reflection_stanza); await view.model.handleMessageStanza(reflection_stanza);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract')); const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
...@@ -52,7 +52,7 @@ describe("Message Retractions", function () { ...@@ -52,7 +52,7 @@ describe("Message Retractions", function () {
</message> </message>
`); `);
const view = _converse.api.chatviews.get(muc_jid); const view = _converse.api.chatviews.get(muc_jid);
await view.model.queueMessage(received_stanza); await view.model.handleMessageStanza(received_stanza);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy(); expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
...@@ -394,7 +394,7 @@ describe("Message Retractions", function () { ...@@ -394,7 +394,7 @@ describe("Message Retractions", function () {
</message> </message>
`); `);
const view = _converse.api.chatviews.get(muc_jid); const view = _converse.api.chatviews.get(muc_jid);
await view.model.queueMessage(received_stanza); await view.model.handleMessageStanza(received_stanza);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy(); expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
...@@ -440,7 +440,7 @@ describe("Message Retractions", function () { ...@@ -440,7 +440,7 @@ describe("Message Retractions", function () {
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/> <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
</message> </message>
`); `);
await view.model.queueMessage(received_stanza); await view.model.handleMessageStanza(received_stanza);
await u.waitUntil(() => view.model.messages.length === 1); await u.waitUntil(() => view.model.messages.length === 1);
expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
...@@ -498,7 +498,7 @@ describe("Message Retractions", function () { ...@@ -498,7 +498,7 @@ describe("Message Retractions", function () {
</moderated> </moderated>
</apply-to> </apply-to>
</message>`); </message>`);
await view.model.queueMessage(retraction); await view.model.handleMessageStanza(retraction);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
...@@ -524,7 +524,7 @@ describe("Message Retractions", function () { ...@@ -524,7 +524,7 @@ describe("Message Retractions", function () {
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/> <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
</message> </message>
`); `);
await view.model.queueMessage(received_stanza); await view.model.handleMessageStanza(received_stanza);
await u.waitUntil(() => view.el.querySelector('.chat-msg__content')); await u.waitUntil(() => view.el.querySelector('.chat-msg__content'));
expect(view.el.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null); expect(view.el.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null);
const result = await view.model.canModerateMessages(); const result = await view.model.canModerateMessages();
...@@ -551,7 +551,7 @@ describe("Message Retractions", function () { ...@@ -551,7 +551,7 @@ describe("Message Retractions", function () {
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/> <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
</message> </message>
`); `);
await view.model.queueMessage(received_stanza); await view.model.handleMessageStanza(received_stanza);
await u.waitUntil(() => view.model.messages.length === 1); await u.waitUntil(() => view.model.messages.length === 1);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
...@@ -579,7 +579,7 @@ describe("Message Retractions", function () { ...@@ -579,7 +579,7 @@ describe("Message Retractions", function () {
</moderated> </moderated>
</apply-to> </apply-to>
</message>`); </message>`);
await view.model.queueMessage(retraction); await view.model.handleMessageStanza(retraction);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
...@@ -778,7 +778,7 @@ describe("Message Retractions", function () { ...@@ -778,7 +778,7 @@ describe("Message Retractions", function () {
by="lounge@montague.lit"/> by="lounge@montague.lit"/>
<origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/> <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
</message>`); </message>`);
await view.model.queueMessage(reflection_stanza); await view.model.handleMessageStanza(reflection_stanza);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('editable')).toBe(true); expect(view.model.messages.at(0).get('editable')).toBe(true);
...@@ -794,7 +794,7 @@ describe("Message Retractions", function () { ...@@ -794,7 +794,7 @@ describe("Message Retractions", function () {
</moderated> </moderated>
</apply-to> </apply-to>
</message>`); </message>`);
await view.model.queueMessage(retraction); await view.model.handleMessageStanza(retraction);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
...@@ -830,7 +830,7 @@ describe("Message Retractions", function () { ...@@ -830,7 +830,7 @@ describe("Message Retractions", function () {
by="lounge@montague.lit"/> by="lounge@montague.lit"/>
<origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/> <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
</message>`); </message>`);
await view.model.queueMessage(reflection_stanza); await view.model.handleMessageStanza(reflection_stanza);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('editable')).toBe(true); expect(view.model.messages.at(0).get('editable')).toBe(true);
...@@ -879,7 +879,7 @@ describe("Message Retractions", function () { ...@@ -879,7 +879,7 @@ describe("Message Retractions", function () {
</moderated> </moderated>
</apply-to> </apply-to>
</message>`); </message>`);
await view.model.queueMessage(retraction); await view.model.handleMessageStanza(retraction);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
expect(view.model.messages.at(0).get('moderation_reason')).toBe(undefined); expect(view.model.messages.at(0).get('moderation_reason')).toBe(undefined);
......
...@@ -289,7 +289,7 @@ describe("A groupchat shown in the groupchats list", function () { ...@@ -289,7 +289,7 @@ describe("A groupchat shown in the groupchats list", function () {
const view = _converse.chatboxviews.get(room_jid); const view = _converse.chatboxviews.get(room_jid);
view.model.set({'minimized': true}); view.model.set({'minimized': true});
const nick = mock.chatroom_names[0]; const nick = mock.chatroom_names[0];
await view.model.queueMessage( await view.model.handleMessageStanza(
$msg({ $msg({
from: room_jid+'/'+nick, from: room_jid+'/'+nick,
id: u.getUniqueId(), id: u.getUniqueId(),
...@@ -303,7 +303,7 @@ describe("A groupchat shown in the groupchats list", function () { ...@@ -303,7 +303,7 @@ describe("A groupchat shown in the groupchats list", function () {
expect(Array.from(room_el.classList).includes('unread-msgs')).toBeTruthy(); expect(Array.from(room_el.classList).includes('unread-msgs')).toBeTruthy();
// If the user is mentioned, the counter also gets updated // If the user is mentioned, the counter also gets updated
await view.model.queueMessage( await view.model.handleMessageStanza(
$msg({ $msg({
from: room_jid+'/'+nick, from: room_jid+'/'+nick,
id: u.getUniqueId(), id: u.getUniqueId(),
...@@ -316,7 +316,7 @@ describe("A groupchat shown in the groupchats list", function () { ...@@ -316,7 +316,7 @@ describe("A groupchat shown in the groupchats list", function () {
expect(indicator_el.textContent).toBe('1'); expect(indicator_el.textContent).toBe('1');
spyOn(view.model, 'incrementUnreadMsgCounter').and.callThrough(); spyOn(view.model, 'incrementUnreadMsgCounter').and.callThrough();
await view.model.queueMessage( await view.model.handleMessageStanza(
$msg({ $msg({
from: room_jid+'/'+nick, from: room_jid+'/'+nick,
id: u.getUniqueId(), id: u.getUniqueId(),
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
*/ */
import { converse } from "@converse/headless/converse-core"; import { converse } from "@converse/headless/converse-core";
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import st from "@converse/headless/utils/stanza";
const { Strophe, sizzle } = converse.env; const { Strophe, sizzle } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
...@@ -79,7 +80,7 @@ converse.plugins.add('converse-notification', { ...@@ -79,7 +80,7 @@ converse.plugins.add('converse-notification', {
return false; return false;
} else if (message.getAttribute('type') === 'groupchat') { } else if (message.getAttribute('type') === 'groupchat') {
return _converse.shouldNotifyOfGroupMessage(message); return _converse.shouldNotifyOfGroupMessage(message);
} else if (u.isHeadlineMessage(_converse, message)) { } else if (st.isHeadline(message)) {
// We want to show notifications for headline messages. // We want to show notifications for headline messages.
return _converse.isMessageToHiddenChat(message); return _converse.isMessageToHiddenChat(message);
} }
......
...@@ -397,6 +397,12 @@ converse.plugins.add('converse-chat', { ...@@ -397,6 +397,12 @@ converse.plugins.add('converse-chat', {
return this.messages.fetched; return this.messages.fetched;
}, },
async handleErrormessageStanza (stanza) {
if (await this.shouldShowErrorMessage(stanza)) {
this.createMessage(await st.parseMessage(stanza, _converse));
}
},
/** /**
* Queue an incoming `chat` message stanza for processing. * Queue an incoming `chat` message stanza for processing.
* @async * @async
...@@ -404,25 +410,29 @@ converse.plugins.add('converse-chat', { ...@@ -404,25 +410,29 @@ converse.plugins.add('converse-chat', {
* @method _converse.ChatRoom#queueMessage * @method _converse.ChatRoom#queueMessage
* @param { XMLElement } stanza - The message stanza. * @param { XMLElement } stanza - The message stanza.
*/ */
queueMessage (stanza, original_stanza, from_jid) { queueMessage (attrs) {
this.msg_chain = (this.msg_chain || this.messages.fetched); this.msg_chain = (this.msg_chain || this.messages.fetched);
this.msg_chain = this.msg_chain.then(() => this.onMessage(stanza, original_stanza, from_jid)); this.msg_chain = this.msg_chain.then(() => this.onMessage(attrs));
return this.msg_chain; return this.msg_chain;
}, },
async onMessage (stanza, original_stanza, from_jid) { async onMessage (attrs) {
const attrs = await st.parseMessage(stanza, original_stanza, this, _converse); attrs = await attrs;
if (u.isErrorObject(attrs)) {
attrs.stanza && log.error(attrs.stanza);
return log.error(attrs.message);
}
// TODO: move to OMEMO
attrs = attrs.encrypted ? await this.decrypt(attrs) : attrs;
const message = this.getDuplicateMessage(attrs); const message = this.getDuplicateMessage(attrs);
if (message) { if (message) {
this.updateMessage(message, original_stanza); this.updateMessage(message, attrs);
} else if ( } else if (
!this.handleReceipt (stanza, original_stanza, from_jid) && !this.handleReceipt(attrs) &&
!this.handleChatMarker(stanza, from_jid) !this.handleChatMarker(attrs) &&
!(await this.handleRetraction(attrs))
) { ) {
if (await this.handleRetraction(attrs)) { this.setEditable(attrs, attrs.time);
return;
}
this.setEditable(attrs, attrs.time, stanza);
if (attrs['chat_state'] && attrs.sender === 'them') { if (attrs['chat_state'] && attrs.sender === 'them') {
this.notifications.set('chat_state', attrs.chat_state); this.notifications.set('chat_state', attrs.chat_state);
...@@ -525,17 +535,16 @@ converse.plugins.add('converse-chat', { ...@@ -525,17 +535,16 @@ converse.plugins.add('converse-chat', {
} }
}, },
getUpdatedMessageAttributes (message, stanza) { // eslint-disable-line no-unused-vars getUpdatedMessageAttributes (message, attrs) {
return { // Filter the attrs object, restricting it to only the `is_archived` key.
'is_archived': st.isArchived(stanza), return (({ is_archived }) => ({ is_archived }))(attrs)
}
}, },
updateMessage (message, stanza) { updateMessage (message, attrs) {
// Overridden in converse-muc and converse-mam // Overridden in converse-muc and converse-mam
const attrs = this.getUpdatedMessageAttributes(message, stanza); const new_attrs = this.getUpdatedMessageAttributes(message, attrs);
if (attrs) { if (attrs) {
message.save(attrs); message.save(new_attrs);
} }
}, },
...@@ -682,10 +691,10 @@ converse.plugins.add('converse-chat', { ...@@ -682,10 +691,10 @@ converse.plugins.add('converse-chat', {
* message or `undefined` if not applicable. * message or `undefined` if not applicable.
*/ */
handleCorrection (attrs) { handleCorrection (attrs) {
if (!attrs.replaced_id || !attrs.from) { if (!attrs.replace_id || !attrs.from) {
return; return;
} }
const message = this.messages.findWhere({'msgid': attrs.replaced_id, 'from': attrs.from}); const message = this.messages.findWhere({'msgid': attrs.replace_id, 'from': attrs.from});
if (!message) { if (!message) {
return; return;
} }
...@@ -797,35 +806,23 @@ converse.plugins.add('converse-chat', { ...@@ -797,35 +806,23 @@ converse.plugins.add('converse-chat', {
api.send(stanza); api.send(stanza);
}, },
handleChatMarker (stanza, from_jid) { handleChatMarker (attrs) {
const to_bare_jid = Strophe.getBareJidFromJid(stanza.getAttribute('to')); const to_bare_jid = Strophe.getBareJidFromJid(attrs.to);
if (to_bare_jid !== _converse.bare_jid) { if (to_bare_jid !== _converse.bare_jid) {
return false; return false;
} }
const markers = sizzle(`[xmlns="${Strophe.NS.MARKERS}"]`, stanza); if (attrs.is_markable) {
if (markers.length === 0) { if (this.contact && !attrs.is_mam && !attrs.is_carbon) {
return false; this.sendMarker(attrs.from, attrs.msgid, 'received');
} else if (markers.length > 1) { }
log.error('handleChatMarker: Ignoring incoming stanza with multiple message markers');
log.error(stanza);
return false; return false;
} else { } else if (attrs.marker_id) {
const marker = markers.pop(); const message = this.messages.findWhere({'msgid': attrs.marker_id});
if (marker.nodeName === 'markable') { const field_name = `marker_${attrs.marker}`;
if (this.contact && !u.isMAMMessage(stanza) && !u.isCarbonMessage(stanza)) { if (message && !message.get(field_name)) {
this.sendMarker(from_jid, stanza.getAttribute('id'), 'received'); message.save({field_name: (new Date()).toISOString()});
}
return false;
} else {
const msgid = marker && marker.getAttribute('id'),
message = msgid && this.messages.findWhere({msgid}),
field_name = `marker_${marker.nodeName}`;
if (message && !message.get(field_name)) {
message.save({field_name: (new Date()).toISOString()});
}
return true;
} }
return true;
} }
}, },
...@@ -840,18 +837,12 @@ converse.plugins.add('converse-chat', { ...@@ -840,18 +837,12 @@ converse.plugins.add('converse-chat', {
api.send(receipt_stanza); api.send(receipt_stanza);
}, },
handleReceipt (stanza, original_stanza, from_jid) { handleReceipt (attrs) {
const is_me = Strophe.getBareJidFromJid(from_jid) === _converse.bare_jid; if (attrs.sender === 'them') {
const requests_receipt = sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop() !== undefined; if (attrs.is_receipt_request) {
if (requests_receipt && !is_me && !u.isCarbonMessage(stanza) && !u.isMAMMessage(original_stanza)) { this.sendReceiptStanza(attrs.from, attrs.msgid);
this.sendReceiptStanza(from_jid, stanza.getAttribute('id')); } else if (attrs.receipt_id) {
} const message = this.messages.findWhere({'msgid': attrs.receipt_id});
const to_bare_jid = Strophe.getBareJidFromJid(stanza.getAttribute('to'));
if (to_bare_jid === _converse.bare_jid) {
const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop();
if (receipt) {
const msgid = receipt && receipt.getAttribute('id'),
message = msgid && this.messages.findWhere({msgid});
if (message && !message.get('received')) { if (message && !message.get('received')) {
message.save({'received': (new Date()).toISOString()}); message.save({'received': (new Date()).toISOString()});
} }
...@@ -945,8 +936,8 @@ converse.plugins.add('converse-chat', { ...@@ -945,8 +936,8 @@ converse.plugins.add('converse-chat', {
* @param { Object } attrs An object containing message attributes. * @param { Object } attrs An object containing message attributes.
* @param { String } send_time - time when the message was sent * @param { String } send_time - time when the message was sent
*/ */
setEditable (attrs, send_time, stanza) { setEditable (attrs, send_time) {
if (stanza && u.isHeadlineMessage(_converse, stanza)) { if (attrs.is_headline) {
return; return;
} }
if (u.isEmptyMessage(attrs) || attrs.sender !== 'me') { if (u.isEmptyMessage(attrs) || attrs.sender !== 'me') {
...@@ -1124,37 +1115,13 @@ converse.plugins.add('converse-chat', { ...@@ -1124,37 +1115,13 @@ converse.plugins.add('converse-chat', {
}); });
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);
}
async function handleErrorMessage (stanza) { async function handleErrorMessage (stanza) {
const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from')); const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
if (utils.isSameBareJID(from_jid, _converse.bare_jid)) { if (utils.isSameBareJID(from_jid, _converse.bare_jid)) {
return; return;
} }
const chatbox = await api.chatboxes.get(from_jid); const chatbox = await api.chatboxes.get(from_jid);
if (!chatbox) { chatbox?.handleErrormessageStanza(stanza);
return;
}
const should_show = await chatbox.shouldShowErrorMessage(stanza);
if (!should_show) {
return;
}
const attrs = await st.parseMessage(stanza, stanza, chatbox, _converse);
await chatbox.createMessage(attrs);
} }
...@@ -1162,77 +1129,30 @@ converse.plugins.add('converse-chat', { ...@@ -1162,77 +1129,30 @@ converse.plugins.add('converse-chat', {
* Handler method for all incoming single-user chat "message" stanzas. * Handler method for all incoming single-user chat "message" stanzas.
* @private * @private
* @method _converse#handleMessageStanza * @method _converse#handleMessageStanza
* @param { XMLElement } stanza - The incoming message stanza * @param { MessageAttributes } attrs - The message attributes
*/ */
_converse.handleMessageStanza = async function (stanza) { _converse.handleMessageStanza = async function (stanza) {
const original_stanza = stanza; if (st.isServerMessage(stanza)) {
let to_jid = stanza.getAttribute('to'); // Prosody sends headline messages with type `chat`, so we need to filter them out here.
const to_resource = Strophe.getResourceFromJid(to_jid); const from = stanza.getAttribute('from');
return log.info(`handleMessageStanza: Ignoring incoming server message from JID: ${from}`);
if (api.settings.get('filter_by_resource') && (to_resource && to_resource !== _converse.resource)) {
return log.info(`handleMessageStanza: Ignoring incoming message intended for a different resource: ${to_jid}`);
} else if (utils.isHeadlineMessage(_converse, stanza)) {
// XXX: Prosody sends headline messages with the
// wrong type ('chat'), so we need to filter them out here.
return log.info(`handleMessageStanza: Ignoring incoming headline message from JID: ${stanza.getAttribute('from')}`);
}
const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
if (bare_forward) {
return rejectMessage(
stanza,
'Forwarded messages not part of an encapsulating protocol are not supported'
);
}
let from_jid = stanza.getAttribute('from') || _converse.bare_jid;
if (u.isCarbonMessage(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
return rejectMessage(stanza, 'Rejecting carbon from invalid JID');
}
}
if (u.isMAMMessage(stanza)) {
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 log.warn(`handleMessageStanza: Ignoring alleged MAM message from ${stanza.getAttribute('from')}`);
}
}
const from_bare_jid = Strophe.getBareJidFromJid(from_jid);
const is_me = from_bare_jid === _converse.bare_jid;
if (is_me && to_jid === null) {
return log.error(`Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`);
} }
const contact_jid = is_me ? Strophe.getBareJidFromJid(to_jid) : from_bare_jid; const attrs = await st.parseMessage(stanza, _converse);
const contact = await api.contacts.get(contact_jid); if (u.isErrorObject(attrs)) {
if (contact === undefined && !api.settings.get("allow_non_roster_messaging")) { attrs.stanza && log.error(attrs.stanza);
log.error(`Blocking messaging with a JID not in our roster because allow_non_roster_messaging is false.`); return log.error(attrs.message);
return log.error(stanza);
} }
// Get chat box, but only create when the message has something to show to the user const has_body = !!sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length;
const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0; const chatbox = await api.chats.get(attrs.contact_jid, {'nickname': attrs.nick }, has_body);
const roster_nick = contact?.attributes?.nickname; chatbox && await chatbox.queueMessage(attrs);
const chatbox = await api.chats.get(contact_jid, {'nickname': roster_nick}, has_body);
chatbox && await chatbox.queueMessage(stanza, original_stanza, from_jid);
/** /**
* Triggered when a message stanza is been received and processed. * Triggered when a message stanza is been received and processed.
* @event _converse#message * @event _converse#message
* @type { object } * @type { object }
* @property { _converse.ChatBox | _converse.ChatRoom } chatbox
* @property { XMLElement } stanza * @property { XMLElement } stanza
* @example _converse.api.listen.on('message', obj => { ... }); * @example _converse.api.listen.on('message', obj => { ... });
*/ */
api.trigger('message', {'stanza': original_stanza, 'chatbox': chatbox}); api.trigger('message', {'stanza': stanza});
} }
......
...@@ -7,8 +7,6 @@ import { isString } from "lodash"; ...@@ -7,8 +7,6 @@ import { isString } from "lodash";
import { converse } from "@converse/headless/converse-core"; import { converse } from "@converse/headless/converse-core";
import st from "./utils/stanza"; import st from "./utils/stanza";
const u = converse.env.utils;
converse.plugins.add('converse-headlines', { converse.plugins.add('converse-headlines', {
/* Plugin dependencies are other plugins which might be /* Plugin dependencies are other plugins which might be
...@@ -83,7 +81,7 @@ converse.plugins.add('converse-headlines', { ...@@ -83,7 +81,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 (u.isHeadlineMessage(_converse, stanza)) { if (st.isHeadline(stanza) || st.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) &&
...@@ -100,7 +98,7 @@ converse.plugins.add('converse-headlines', { ...@@ -100,7 +98,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, stanza, chatbox, _converse); const attrs = await st.parseMessage(stanza, _converse);
await chatbox.createMessage(attrs); await chatbox.createMessage(attrs);
api.trigger('message', {'chatbox': chatbox, 'stanza': stanza}); api.trigger('message', {'chatbox': chatbox, 'stanza': stanza});
} }
...@@ -109,10 +107,7 @@ converse.plugins.add('converse-headlines', { ...@@ -109,10 +107,7 @@ converse.plugins.add('converse-headlines', {
/************************ BEGIN Event Handlers ************************/ /************************ BEGIN Event Handlers ************************/
function registerHeadlineHandler () { function registerHeadlineHandler () {
_converse.connection.addHandler(message => { _converse.connection.addHandler(message => (onHeadlineMessage(message) || true), null, 'message');
onHeadlineMessage(message);
return true
}, null, 'message');
} }
api.listen.on('connected', registerHeadlineHandler); api.listen.on('connected', registerHeadlineHandler);
api.listen.on('reconnected', registerHeadlineHandler); api.listen.on('reconnected', registerHeadlineHandler);
......
...@@ -11,9 +11,11 @@ import { intersection, pick } from 'lodash' ...@@ -11,9 +11,11 @@ import { intersection, pick } from 'lodash'
import { converse } from "./converse-core"; import { converse } from "./converse-core";
import log from "./log"; import log from "./log";
import sizzle from "sizzle"; import sizzle from "sizzle";
import st from "./utils/stanza";
let _converse; let _converse;
const { Strophe, $iq, dayjs } = converse.env; const { Strophe, $iq, dayjs } = converse.env;
const { NS } = Strophe;
const u = converse.env.utils; const u = converse.env.utils;
// XEP-0313 Message Archive Management // XEP-0313 Message Archive Management
...@@ -47,6 +49,26 @@ const MAMEnabledChat = { ...@@ -47,6 +49,26 @@ const MAMEnabledChat = {
} }
}, },
async handleMAMResult (result, query, options, page_direction) {
const is_muc = this.get('type') === _converse.CHATROOMS_TYPE;
result.messages = result.messages.map(
s => (is_muc ? st.parseMUCMessage(s, this, _converse) : st.parseMessage(s, _converse))
);
/**
* Synchronous event which allows listeners to first do some
* work based on the MAM result before calling the handlers here.
* @event _converse#MAMResult
*/
await api.trigger('MAMResult', result, query, {'synchronous': true});
result.messages.forEach(m => this.queueMessage(m));
if (result.error) {
result.error.retry = () => this.fetchArchivedMessages(options, page_direction);
this.createMessageFromError(result.error);
}
},
/** /**
* Fetch XEP-0313 archived messages based on the passed in criteria. * Fetch XEP-0313 archived messages based on the passed in criteria.
* @private * @private
...@@ -64,55 +86,34 @@ const MAMEnabledChat = { ...@@ -64,55 +86,34 @@ const MAMEnabledChat = {
* @param { string } [options.with] - The JID of the entity with * @param { string } [options.with] - The JID of the entity with
* which messages were exchanged. * which messages were exchanged.
* @param { boolean } [options.groupchat] - True if archive in groupchat. * @param { boolean } [options.groupchat] - True if archive in groupchat.
* @param { boolean } [page] - Whether this function should recursively * @param { ('forwards'|'backwards')} [page_direction] - Determines whether this function should
* page through the entire result set if a limited number of results * recursively page through the entire result set if a limited number of results were returned.
* were returned.
*/ */
async fetchArchivedMessages (options={}, page) { async fetchArchivedMessages (options={}, page_direction) {
if (this.disable_mam) { if (this.disable_mam) {
return; return;
} }
const is_groupchat = this.get('type') === _converse.CHATROOMS_TYPE; const is_muc = this.get('type') === _converse.CHATROOMS_TYPE;
const mam_jid = is_groupchat ? this.get('jid') : _converse.bare_jid; const mam_jid = is_muc ? this.get('jid') : _converse.bare_jid;
if (!(await api.disco.supports(Strophe.NS.MAM, mam_jid))) { if (!(await api.disco.supports(NS.MAM, mam_jid))) {
return; return;
} }
const msg_handler = is_groupchat ? s => this.queueMessage(s) : s => _converse.handleMessageStanza(s);
const query = Object.assign({ const query = Object.assign({
'groupchat': is_groupchat, 'groupchat': is_muc,
'max': api.settings.get('archived_messages_page_size'), 'max': api.settings.get('archived_messages_page_size'),
'with': this.get('jid'), 'with': this.get('jid'),
}, options); }, options);
const result = await api.archive.query(query); const result = await api.archive.query(query);
/** await this.handleMAMResult(result, query, options, page_direction);
* *Hook* which allows plugins to inspect and potentially modify the result of a MAM query
* from {@link MAMEnabledChat.fetchArchivedMessages}.
* @event _converse#MAMResult
*/
api.hook('MAMResult', this, { result, query });
for (const message of result.messages) {
try {
await msg_handler(message);
} catch (e) {
log.error(e);
}
}
if (result.error) {
result.error.retry = () => this.fetchArchivedMessages(options, page);
this.createMessageFromError(result.error);
}
if (page && result.rsm) { if (page_direction && result.rsm) {
if (page === 'forwards') { if (page_direction === 'forwards') {
options = result.rsm.next(api.settings.get('archived_messages_page_size'), options.before); options = result.rsm.next(api.settings.get('archived_messages_page_size'), options.before);
} else if (page === 'backwards') { } else if (page_direction === 'backwards') {
options = result.rsm.previous(api.settings.get('archived_messages_page_size'), options.after); options = result.rsm.previous(api.settings.get('archived_messages_page_size'), options.after);
} }
return this.fetchArchivedMessages(options, page); return this.fetchArchivedMessages(options, page_direction);
} else { } else {
// TODO: Add a special kind of message which will // TODO: Add a special kind of message which will
// render as a link to fetch further messages, either // render as a link to fetch further messages, either
...@@ -162,12 +163,12 @@ converse.plugins.add('converse-mam', { ...@@ -162,12 +163,12 @@ converse.plugins.add('converse-mam', {
* Per JID preferences will be set in chat boxes, so it'll * Per JID preferences will be set in chat boxes, so it'll
* probbaly be handled elsewhere in any case. * probbaly be handled elsewhere in any case.
*/ */
const preference = sizzle(`prefs[xmlns="${Strophe.NS.MAM}"]`, iq).pop(); const preference = sizzle(`prefs[xmlns="${NS.MAM}"]`, iq).pop();
const default_pref = preference.getAttribute('default'); const default_pref = preference.getAttribute('default');
if (default_pref !== api.settings.get('message_archiving')) { if (default_pref !== api.settings.get('message_archiving')) {
const stanza = $iq({'type': 'set'}) const stanza = $iq({'type': 'set'})
.c('prefs', { .c('prefs', {
'xmlns':Strophe.NS.MAM, 'xmlns':NS.MAM,
'default':api.settings.get('message_archiving') 'default':api.settings.get('message_archiving')
}); });
Array.from(preference.children).forEach(child => stanza.cnode(child).up()); Array.from(preference.children).forEach(child => stanza.cnode(child).up());
...@@ -185,11 +186,11 @@ converse.plugins.add('converse-mam', { ...@@ -185,11 +186,11 @@ converse.plugins.add('converse-mam', {
function getMAMPrefsFromFeature (feature) { function getMAMPrefsFromFeature (feature) {
const prefs = feature.get('preferences') || {}; const prefs = feature.get('preferences') || {};
if (feature.get('var') !== Strophe.NS.MAM || api.settings.get('message_archiving') === undefined) { if (feature.get('var') !== NS.MAM || api.settings.get('message_archiving') === undefined) {
return; return;
} }
if (prefs['default'] !== api.settings.get('message_archiving')) { if (prefs['default'] !== api.settings.get('message_archiving')) {
api.sendIQ($iq({'type': 'get'}).c('prefs', {'xmlns': Strophe.NS.MAM})) api.sendIQ($iq({'type': 'get'}).c('prefs', {'xmlns': NS.MAM}))
.then(iq => _converse.onMAMPreferences(iq, feature)) .then(iq => _converse.onMAMPreferences(iq, feature))
.catch(_converse.onMAMError); .catch(_converse.onMAMError);
} }
...@@ -207,7 +208,7 @@ converse.plugins.add('converse-mam', { ...@@ -207,7 +208,7 @@ converse.plugins.add('converse-mam', {
} }
/************************ BEGIN Event Handlers ************************/ /************************ BEGIN Event Handlers ************************/
api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.MAM)); api.listen.on('addClientFeatures', () => api.disco.own.features.add(NS.MAM));
api.listen.on('serviceDiscovered', getMAMPrefsFromFeature); api.listen.on('serviceDiscovered', getMAMPrefsFromFeature);
api.listen.on('chatRoomViewInitialized', view => { api.listen.on('chatRoomViewInitialized', view => {
if (_converse.muc_show_logs_before_join) { if (_converse.muc_show_logs_before_join) {
...@@ -437,18 +438,18 @@ converse.plugins.add('converse-mam', { ...@@ -437,18 +438,18 @@ converse.plugins.add('converse-mam', {
} }
const jid = attrs.to || _converse.bare_jid; const jid = attrs.to || _converse.bare_jid;
const supported = await api.disco.supports(Strophe.NS.MAM, jid); const supported = await api.disco.supports(NS.MAM, jid);
if (!supported) { if (!supported) {
log.warn(`Did not fetch MAM archive for ${jid} because it doesn't support ${Strophe.NS.MAM}`); log.warn(`Did not fetch MAM archive for ${jid} because it doesn't support ${NS.MAM}`);
return {'messages': []}; return {'messages': []};
} }
const queryid = u.getUniqueId(); const queryid = u.getUniqueId();
const stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid}); const stanza = $iq(attrs).c('query', {'xmlns':NS.MAM, 'queryid':queryid});
if (options) { if (options) {
stanza.c('x', {'xmlns':Strophe.NS.XFORM, 'type': 'submit'}) stanza.c('x', {'xmlns':NS.XFORM, 'type': 'submit'})
.c('field', {'var':'FORM_TYPE', 'type': 'hidden'}) .c('field', {'var':'FORM_TYPE', 'type': 'hidden'})
.c('value').t(Strophe.NS.MAM).up().up(); .c('value').t(NS.MAM).up().up();
if (options['with'] && !options.groupchat) { if (options['with'] && !options.groupchat) {
stanza.c('field', {'var':'with'}).c('value') stanza.c('field', {'var':'with'}).c('value')
...@@ -474,7 +475,7 @@ converse.plugins.add('converse-mam', { ...@@ -474,7 +475,7 @@ converse.plugins.add('converse-mam', {
const messages = []; const messages = [];
const message_handler = _converse.connection.addHandler(stanza => { const message_handler = _converse.connection.addHandler(stanza => {
const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop(); const result = sizzle(`message > result[xmlns="${NS.MAM}"]`, stanza).pop();
if (result === undefined || result.getAttribute('queryid') !== queryid) { if (result === undefined || result.getAttribute('queryid') !== queryid) {
return true; return true;
} }
...@@ -490,7 +491,7 @@ converse.plugins.add('converse-mam', { ...@@ -490,7 +491,7 @@ converse.plugins.add('converse-mam', {
} }
messages.push(stanza); messages.push(stanza);
return true; return true;
}, Strophe.NS.MAM); }, NS.MAM);
let error; let error;
const iq_result = await api.sendIQ(stanza, api.settings.get('message_archiving_timeout'), false) const iq_result = await api.sendIQ(stanza, api.settings.get('message_archiving_timeout'), false)
...@@ -508,9 +509,9 @@ converse.plugins.add('converse-mam', { ...@@ -508,9 +509,9 @@ converse.plugins.add('converse-mam', {
_converse.connection.deleteHandler(message_handler); _converse.connection.deleteHandler(message_handler);
let rsm; let rsm;
const fin = iq_result && sizzle(`fin[xmlns="${Strophe.NS.MAM}"]`, iq_result).pop(); const fin = iq_result && sizzle(`fin[xmlns="${NS.MAM}"]`, iq_result).pop();
if (fin && [null, 'false'].includes(fin.getAttribute('complete'))) { if (fin && [null, 'false'].includes(fin.getAttribute('complete'))) {
const set = sizzle(`set[xmlns="${Strophe.NS.RSM}"]`, fin).pop(); const set = sizzle(`set[xmlns="${NS.RSM}"]`, fin).pop();
if (set) { if (set) {
rsm = new _converse.RSM({'xml': set}); rsm = new _converse.RSM({'xml': set});
Object.assign(rsm, Object.assign(pick(options, [...MAM_ATTRIBUTES, ..._converse.RSM_ATTRIBUTES]), rsm)); Object.assign(rsm, Object.assign(pick(options, [...MAM_ATTRIBUTES, ..._converse.RSM_ATTRIBUTES]), rsm));
......
...@@ -608,6 +608,26 @@ converse.plugins.add('converse-muc', { ...@@ -608,6 +608,26 @@ converse.plugins.add('converse-muc', {
} }
}, },
async handleErrormessageStanza (stanza) {
if (await this.shouldShowErrorMessage(stanza)) {
this.createMessage(await st.parseMUCMessage(stanza, this, _converse));
}
},
async handleMessageStanza (stanza) {
if (st.isArchived(stanza)) {
// MAM messages are handled in converse-mam.
// We shouldn't get MAM messages here because
// they shouldn't have a `type` attribute.
return log.warn(`Received a MAM message with type "groupchat"`);
}
api.trigger('message', {'stanza': stanza});
this.createInfoMessages(stanza);
this.fetchFeaturesIfConfigurationChanged(stanza);
const attrs = await st.parseMUCMessage(stanza, this, _converse);
return attrs && this.queueMessage(attrs);
},
registerHandlers () { registerHandlers () {
// Register presence and message handlers for this groupchat // Register presence and message handlers for this groupchat
const room_jid = this.get('jid'); const room_jid = this.get('jid');
...@@ -618,17 +638,9 @@ converse.plugins.add('converse-muc', { ...@@ -618,17 +638,9 @@ converse.plugins.add('converse-muc', {
{'ignoreNamespaceFragment': true, 'matchBareFromJid': true} {'ignoreNamespaceFragment': true, 'matchBareFromJid': true}
); );
this.message_handler = _converse.connection.addHandler(stanza => { this.message_handler = _converse.connection.addHandler(
if (sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop()) { stanza => (!!this.handleMessageStanza(stanza) || true),
// MAM messages are handled in converse-mam. null, 'message', 'groupchat', null, room_jid,
// We shouldn't get MAM messages here because
// they shouldn't have a `type` attribute.
log.warn(`received a mam message with type "chat".`);
return true;
}
this.queueMessage(stanza);
return true;
}, null, 'message', 'groupchat', null, room_jid,
{'matchBareFromJid': true} {'matchBareFromJid': true}
); );
...@@ -1650,7 +1662,7 @@ converse.plugins.add('converse-muc', { ...@@ -1650,7 +1662,7 @@ converse.plugins.add('converse-muc', {
* @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.parseMessage} * message, as returned by {@link st.parseMUCMessage}
*/ */
async handleSubjectChange (attrs) { async handleSubjectChange (attrs) {
if (isString(attrs.subject) && !attrs.thread && !attrs.message) { if (isString(attrs.subject) && !attrs.thread && !attrs.message) {
...@@ -1703,8 +1715,7 @@ converse.plugins.add('converse-muc', { ...@@ -1703,8 +1715,7 @@ converse.plugins.add('converse-muc', {
* @param { Object } attrs - The message attributes * @param { Object } attrs - The message attributes
*/ */
ignorableCSN (attrs) { ignorableCSN (attrs) {
const is_csn = u.isOnlyChatStateNotification(attrs); return attrs.chat_state && !attrs.body && (attrs.is_delayed || this.isOwnMessage(attrs));
return is_csn && (attrs.is_delayed || this.isOwnMessage(attrs));
}, },
...@@ -1729,21 +1740,17 @@ converse.plugins.add('converse-muc', { ...@@ -1729,21 +1740,17 @@ converse.plugins.add('converse-muc', {
}, },
getUpdatedMessageAttributes (message, stanza) { getUpdatedMessageAttributes (message, attrs) {
// Overridden in converse-muc and converse-mam // Overridden in converse-muc and converse-mam
const attrs = _converse.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, stanza); const new_attrs = _converse.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, attrs);
if (this.isOwnMessage(message)) { if (this.isOwnMessage(attrs)) {
const stanza_id = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop(); const stanza_id_keys = Object.keys(attrs).filter(k => k.startsWith('stanza_id'));
const by_jid = stanza_id ? stanza_id.getAttribute('by') : undefined; Object.assign(new_attrs, pick(attrs, stanza_id_keys));
if (by_jid) {
const key = `stanza_id ${by_jid}`;
attrs[key] = stanza_id.getAttribute('id');
}
if (!message.get('received')) { if (!message.get('received')) {
attrs.received = (new Date()).toISOString(); new_attrs.received = (new Date()).toISOString();
} }
} }
return attrs; return new_attrs;
}, },
/** /**
...@@ -1808,7 +1815,7 @@ converse.plugins.add('converse-muc', { ...@@ -1808,7 +1815,7 @@ converse.plugins.add('converse-muc', {
* @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.parseMessage} * message, as returned by {@link st.parseMUCMessage}
* @returns { _converse.ChatRoomMessage } * @returns { _converse.ChatRoomMessage }
*/ */
findDanglingModeration (attrs) { findDanglingModeration (attrs) {
...@@ -1839,7 +1846,7 @@ converse.plugins.add('converse-muc', { ...@@ -1839,7 +1846,7 @@ converse.plugins.add('converse-muc', {
* @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.parseMessage} * message, as returned by {@link st.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.
*/ */
...@@ -1954,47 +1961,25 @@ converse.plugins.add('converse-muc', { ...@@ -1954,47 +1961,25 @@ converse.plugins.add('converse-muc', {
* should be called. * should be called.
* @private * @private
* @method _converse.ChatRoom#onMessage * @method _converse.ChatRoom#onMessage
* @param { XMLElement } stanza - The message stanza. * @param { MessageAttributes } attrs - The message attributes
*/ */
async onMessage (stanza) { async onMessage (attrs) {
if (sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length) { if (u.isErrorObject(attrs)) {
return log.warn('onMessage: Ignoring unencapsulated forwarded groupchat message'); attrs.stanza && log.error(attrs.stanza);
} return log.error(attrs.message);
if (u.isCarbonMessage(stanza)) {
return log.warn(
'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+
'according to the XEP groupchat messages SHOULD NOT be carbon copied'
);
}
const original_stanza = stanza;
if (u.isMAMMessage(stanza)) {
if (original_stanza.getAttribute('from') === this.get('jid')) {
const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
stanza = sizzle(selector, stanza).pop();
} else {
return log.warn(`onMessage: Ignoring alleged MAM groupchat message from ${stanza.getAttribute('from')}`);
}
} }
await this.createInfoMessages(stanza); // TODO: move to OMEMO
this.fetchFeaturesIfConfigurationChanged(stanza); attrs = attrs.encrypted ? await this.decrypt(attrs) : attrs;
const attrs = await st.parseMessage(stanza, original_stanza, this, _converse);
const message = this.getDuplicateMessage(attrs); const message = this.getDuplicateMessage(attrs);
if (message) { if (message) {
this.updateMessage(message, original_stanza); return this.updateMessage(message, attrs);
} } else if (attrs.is_receipt_request || attrs.is_marker || this.ignorableCSN(attrs)) {
if (message || return;
st.isReceipt(stanza) ||
st.isChatMarker(stanza) ||
this.ignorableCSN(attrs)) {
return api.trigger('message', {'stanza': original_stanza});
} }
if (await this.handleRetraction(attrs) || if (await this.handleRetraction(attrs) ||
await this.handleModeration(attrs) || await this.handleModeration(attrs) ||
await this.handleSubjectChange(attrs)) { await this.handleSubjectChange(attrs)) {
this.removeNotification(attrs.nick, ['composing', 'paused']); return this.removeNotification(attrs.nick, ['composing', 'paused']);
return api.trigger('message', {'stanza': original_stanza});
} }
this.setEditable(attrs, attrs.time); this.setEditable(attrs, attrs.time);
...@@ -2006,7 +1991,6 @@ converse.plugins.add('converse-muc', { ...@@ -2006,7 +1991,6 @@ converse.plugins.add('converse-muc', {
this.removeNotification(attrs.nick, ['composing', 'paused']); this.removeNotification(attrs.nick, ['composing', 'paused']);
this.incrementUnreadMsgCounter(msg); this.incrementUnreadMsgCounter(msg);
} }
api.trigger('message', {'stanza': original_stanza, 'chatbox': this});
}, },
handleModifyError(pres) { handleModifyError(pres) {
......
...@@ -42,16 +42,6 @@ u.toStanza = function (string) { ...@@ -42,16 +42,6 @@ u.toStanza = function (string) {
return node.firstElementChild; return node.firstElementChild;
} }
u.isMAMMessage = function (stanza) {
return sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).length > 0;
}
u.isCarbonMessage = function (stanza) {
const xmlns = Strophe.NS.CARBONS;
return sizzle(`message > received[xmlns="${xmlns}"]`, stanza).length > 0 ||
sizzle(`message > sent[xmlns="${xmlns}"]`, stanza).length > 0;
}
u.getLongestSubstring = function (string, candidates) { u.getLongestSubstring = function (string, candidates) {
function reducer (accumulator, current_value) { function reducer (accumulator, current_value) {
if (string.startsWith(current_value)) { if (string.startsWith(current_value)) {
...@@ -142,6 +132,7 @@ u.isEmptyMessage = function (attrs) { ...@@ -142,6 +132,7 @@ u.isEmptyMessage = function (attrs) {
!attrs['message']; !attrs['message'];
}; };
//TODO: Remove
u.isOnlyChatStateNotification = function (msg) { u.isOnlyChatStateNotification = function (msg) {
if (msg instanceof Element) { if (msg instanceof Element) {
// See XEP-0085 Chat State Notification // See XEP-0085 Chat State Notification
...@@ -174,25 +165,6 @@ u.isChatRoom = function (model) { ...@@ -174,25 +165,6 @@ u.isChatRoom = function (model) {
return model && (model.get('type') === 'chatroom'); return model && (model.get('type') === 'chatroom');
} }
u.isHeadlineMessage = function (_converse, message) {
const from_jid = message.getAttribute('from');
if (message.getAttribute('type') === 'headline') {
return true;
}
const chatbox = _converse.chatboxes.get(Strophe.getBareJidFromJid(from_jid));
if (u.isChatRoom(chatbox)) {
return false;
}
if (message.getAttribute('type') !== 'error' && from_jid && !from_jid.includes('@')) {
// Some servers (I'm looking at you Prosody) don't set the message
// 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 message.
return true;
}
return false;
};
u.isErrorObject = function (o) { u.isErrorObject = function (o) {
return o instanceof Error; return o instanceof Error;
} }
......
import * as strophe from 'strophe.js/src/core'; import * as strophe from 'strophe.js/src/core';
import { propertyOf } from "lodash";
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import log from '@converse/headless/log';
import sizzle from 'sizzle'; import sizzle from 'sizzle';
import u from '@converse/headless/utils/core'; import u from '@converse/headless/utils/core';
import log from "../log";
import { __ } from '@converse/headless/i18n';
import { api } from "@converse/headless/converse-core";
const Strophe = strophe.default.Strophe; const Strophe = strophe.default.Strophe;
const $msg = strophe.default.$msg;
const { NS } = Strophe;
function getSenderAttributes (stanza, chatbox, _converse) {
if (u.isChatRoom(chatbox)) {
const from = stanza.getAttribute('from');
const nick = Strophe.unescapeNode(Strophe.getResourceFromJid(from));
return {
'from': from,
'from_muc': Strophe.getBareJidFromJid(from),
'nick': nick,
'sender': nick === chatbox.get('nick') ? 'me': 'them',
'received': (new Date()).toISOString(),
}
} else {
const from = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
if (from === _converse.bare_jid) {
return {
from,
'sender': 'me',
'fullname': _converse.xmppstatus.get('fullname')
}
} else {
return {
from,
'sender': 'them',
'fullname': chatbox.get('fullname')
}
}
}
}
function getSpoilerAttributes (stanza) { function getSpoilerAttributes (stanza) {
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop(); const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
return { return {
...@@ -59,14 +33,14 @@ function getOutOfBandAttributes (stanza) { ...@@ -59,14 +33,14 @@ function getOutOfBandAttributes (stanza) {
function getCorrectionAttributes (stanza, original_stanza) { function getCorrectionAttributes (stanza, original_stanza) {
const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop(); const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
if (el) { if (el) {
const replaced_id = el.getAttribute('id'); const replace_id = el.getAttribute('id');
const msgid = replaced_id; const msgid = replace_id;
if (replaced_id) { if (replace_id) {
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop(); const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
const time = delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString(); const time = delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString();
return { return {
msgid, msgid,
replaced_id, replace_id,
'edited': time 'edited': time
} }
} }
...@@ -74,64 +48,325 @@ function getCorrectionAttributes (stanza, original_stanza) { ...@@ -74,64 +48,325 @@ function getCorrectionAttributes (stanza, original_stanza) {
return {}; return {};
} }
function getEncryptionAttributes (stanza, original_stanza, attrs, chatbox, _converse) {
const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop(); function getEncryptionAttributes (stanza, _converse) {
const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop();
if (!encrypted || !_converse.config.get('trusted')) { if (!encrypted || !_converse.config.get('trusted')) {
return attrs; return {};
} }
const device_id = _converse.omemo_store?.get('device_id'); const device_id = _converse.omemo_store?.get('device_id');
const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted).pop(); const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted).pop();
if (key) { if (key) {
const header = encrypted.querySelector('header'); const header = encrypted.querySelector('header');
attrs['is_encrypted'] = true; return {
attrs['encrypted'] = { 'is_encrypted': true,
'device_id': header.getAttribute('sid'), 'encrypted': {
'iv': header.querySelector('iv').textContent, 'device_id': header.getAttribute('sid'),
'key': key.textContent, 'iv': header.querySelector('iv').textContent,
'payload': encrypted.querySelector('payload')?.textContent || null, 'key': key.textContent,
'prekey': ['true', '1'].includes(key.getAttribute('prekey')) 'payload': encrypted.querySelector('payload')?.textContent || null,
'prekey': ['true', '1'].includes(key.getAttribute('prekey'))
}
}
}
return {};
}
function isReceiptRequest (stanza, attrs) {
return (
attrs.sender !== 'me' &&
!attrs.is_carbon &&
!attrs.is_mam &&
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');
if (by_jid) {
attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
} else {
attrs[`stanza_id`] = 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
}
}
} }
// Returns a promise
return chatbox.decrypt(attrs);
} else { } else {
return attrs; 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')
};
});
}
/**
* Returns the human readable error message contained in an message stanza of type 'error'.
* @private
* @param { XMLElement } stanza - The message stanza
*/
function getErrorMessage (stanza) {
if (stanza.getAttribute('type') === 'error') {
const error = stanza.querySelector('error');
return error.querySelector('text')?.textContent ||
__('Sorry, an error occurred:') + ' ' + error.innerHTML;
} }
} }
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);
}
/** /**
* The stanza utils object. Contains utility functions related to stanza * Returns the human readable error message contained in a `groupchat` message stanza of type `error`.
* processing. * @private
* @namespace stanza_utils * @param { XMLElement } stanza - The message stanza
*/ */
const stanza_utils = { function getMUCErrorMessage (stanza) {
if (stanza.getAttribute('type') === 'error') {
const forbidden = sizzle(`error forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).pop();
if (forbidden) {
const msg = __("Your message was not delivered because you weren't allowed to send it.");
const text = sizzle(`error text[xmlns="${Strophe.NS.STANZAS}"]`, stanza).pop();
const server_msg = text ? __('The message from the server is: "%1$s"', text.textContent) : '';
return server_msg ? `${msg} ${server_msg}` : msg;
} else if (sizzle(`not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
return __("Your message was not delivered because you're not present in the groupchat.");
}
}
}
class StanzaParseError extends Error {
constructor (message, stanza) {
super(message, stanza);
this.name = 'StanzaParseError';
this.stanza = stanza;
}
}
isReceipt (stanza) { function rejectUnencapsulatedForward (stanza) {
return sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).length > 0; 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;
}, },
isChatMarker (stanza) { /**
return sizzle( * Determines whether the passed in stanza is a XEP-0333 Chat Marker
`received[xmlns="${Strophe.NS.MARKERS}"], * @private
displayed[xmlns="${Strophe.NS.MARKERS}"], * @method st#getChatMarker
acknowledged[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length > 0; * @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 represents a XEP-0313 MAM stanza * Determines whether the passed in stanza is a XEP-0313 MAM stanza
* @private * @private
* @method stanza_utils#isArchived * @method st#isArchived
* @param { XMLElement } stanza - The message stanza * @param { XMLElement } stanza - The message stanza
* @returns { Boolean } * @returns { Boolean }
*/ */
isArchived (original_stanza) { isArchived (original_stanza) {
return !!sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop(); return !!sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
}, },
/** /**
* Returns an object containing all attribute names and values for a particular element. * Returns an object containing all attribute names and values for a particular element.
* @private * @method st#getAttributes
* @method stanza_utils#getAttributes
* @param { XMLElement } stanza * @param { XMLElement } stanza
* @returns { Object } * @returns { Object }
*/ */
...@@ -142,235 +377,319 @@ const stanza_utils = { ...@@ -142,235 +377,319 @@ const stanza_utils = {
}, {}); }, {});
}, },
/** /**
* Extract the XEP-0359 stanza IDs from the passed in stanza * Parses a passed in message stanza and returns an object of attributes.
* and return a map containing them. * @method st#parseMessage
* @private
* @method stanza_utils#getStanzaIDs
* @param { XMLElement } stanza - The message stanza * @param { XMLElement } stanza - The message stanza
* @returns { Object } * @param { _converse } _converse
* @returns { (MessageAttributes|Error) }
*/ */
getStanzaIDs (stanza, original_stanza) { async parseMessage (stanza, _converse) {
const attrs = {}; const err = rejectUnencapsulatedForward(stanza);
// Store generic stanza ids if (err) {
const sids = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza); return err;
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');
attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
} }
// Store the origin id let to_jid = stanza.getAttribute('to');
const origin_id = sizzle(`origin-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop(); const to_resource = Strophe.getResourceFromJid(to_jid);
if (origin_id) { if (api.settings.get('filter_by_resource') && (to_resource && to_resource !== _converse.resource)) {
attrs['origin_id'] = origin_id.getAttribute('id'); return new StanzaParseError(`Ignoring incoming message intended for a different resource: ${to_jid}`, stanza);
} }
return attrs;
},
/** @method stanza_utils#getModerationAttributes let from_jid = stanza.getAttribute('from') || _converse.bare_jid;
* @param { XMLElement } stanza - The message stanza if (isCarbon(stanza)) {
* @param { XMLElement } original_stanza - The original stanza, that contains the if (from_jid === _converse.bare_jid) {
* message stanza, if it was contained, otherwise it's the message stanza itself. const selector = `[xmlns="${Strophe.NS.CARBONS}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
* @param { _converse.ChatRoom } room - The MUC in which the moderation stanza is received. stanza = sizzle(selector, stanza).pop();
* @returns { Object } to_jid = stanza.getAttribute('to');
*/ from_jid = stanza.getAttribute('from');
getModerationAttributes (stanza, original_stanza, room) { } else {
const fastening = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop(); // Prevent message forging via carbons: https://xmpp.org/extensions/xep-0280.html#security
if (fastening) { rejectMessage(stanza, 'Rejecting carbon from invalid JID');
const applies_to_id = fastening.getAttribute('id'); return new StanzaParseError(`Rejecting carbon from invalid JID ${to_jid}`, stanza);
const moderated = sizzle(`moderated[xmlns="${Strophe.NS.MODERATE}"]`, fastening).pop();
if (moderated) {
const retracted = sizzle(`retract[xmlns="${Strophe.NS.RETRACT}"]`, moderated).pop();
if (retracted) {
const from = stanza.getAttribute('from');
if (from !== room.get('jid')) {
log.warn("getModerationAttributes: ignore moderation stanza that's not from the MUC!");
log.error(original_stanza);
return {};
}
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
} if (st.isArchived(stanza)) {
} 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);
} }
} }
return {};
},
/** const from_bare_jid = Strophe.getBareJidFromJid(from_jid);
* @method stanza_utils#getRetractionAttributes const is_me = from_bare_jid === _converse.bare_jid;
* @param { XMLElement } stanza - The message stanza if (is_me && to_jid === null) {
* @param { XMLElement } original_stanza - The original stanza, that contains the return new StanzaParseError(
* message stanza, if it was contained, otherwise it's the message stanza itself. `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`,
* @returns { Object } stanza
*/ );
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 {};
},
getReferences (stanza) {
const text = propertyOf(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')
};
});
},
getErrorMessage (stanza, is_muc, _converse) { const is_headline = st.isHeadline(stanza);
const { __ } = _converse; const is_server_message = st.isServerMessage(stanza);
if (is_muc) { let contact, contact_jid;
const forbidden = sizzle(`error forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).pop(); if (!is_headline && !is_server_message) {
if (forbidden) { contact_jid = is_me ? Strophe.getBareJidFromJid(to_jid) : from_bare_jid;
const msg = __("Your message was not delivered because you weren't allowed to send it."); contact = await api.contacts.get(contact_jid);
const text = sizzle(`error text[xmlns="${Strophe.NS.STANZAS}"]`, stanza).pop(); if (contact === undefined && !api.settings.get("allow_non_roster_messaging")) {
const server_msg = text ? __('The message from the server is: "%1$s"', text.textContent) : ''; log.error(stanza);
return server_msg ? `${msg} ${server_msg}` : msg; return new StanzaParseError(
} else if (sizzle(`not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) { `Blocking messaging with a JID not in our roster because allow_non_roster_messaging is false.`,
return __("Your message was not delivered because you're not present in the groupchat."); stanza
);
} }
} }
const error = stanza.querySelector('error'); /**
return propertyOf(error.querySelector('text'))('textContent') || * @typedef { Object } MessageAttributes
__('Sorry, an error occurred:') + ' ' + error.innerHTML; * 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_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_receipt_request - Does this message request a XEP-0184 receipt?
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
* @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 } edit - An ISO8601 string recording the time that the message was edited per XEP-0308
* @property { String } error - The error message, in case it's an error stanza
* @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 original_stanza = stanza;
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_headline,
is_server_message,
'body': stanza.querySelector('body')?.textContent?.trim(),
'chat_state': getChatState(stanza),
'error': getErrorMessage(stanza),
'from': Strophe.getBareJidFromJid(stanza.getAttribute('from')),
'is_archived': st.isArchived(original_stanza),
'is_carbon': isCarbon(original_stanza),
'is_delayed': !!delay,
'is_markable': !!sizzle(`markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length,
'is_marker': !!marker,
'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')
},
getOutOfBandAttributes(stanza),
getSpoilerAttributes(stanza),
getCorrectionAttributes(stanza, original_stanza),
getStanzaIDs(stanza, original_stanza),
getRetractionAttributes(stanza, original_stanza),
getEncryptionAttributes(stanza, _converse)
);
/** if (attrs.is_archived) {
* Given a message stanza, return the text contained in its body. const from = original_stanza.getAttribute('from');
* @private if (from && contact_jid && from !== contact_jid) {
* @method stanza_utils#getMessageBody return new StanzaParseError(`Invalid Stanza: Forged MAM message from ${from}`, stanza);
* @param { XMLElement } stanza
* @param { Boolean } is_muc
* @param { _converse } _converse
*/
getMessageBody (stanza, is_muc, _converse) {
const type = stanza.getAttribute('type');
if (type === 'error') {
return stanza_utils.getErrorMessage(stanza, is_muc, _converse);
} else {
const body = stanza.querySelector('body');
if (body) {
return body.textContent.trim();
} }
} }
}, 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_receipt_request': isReceiptRequest(stanza, attrs)
}, attrs);
getChatState (stanza) { // We prefer to use one of the XEP-0359 unique and stable stanza IDs
return stanza.getElementsByTagName('composing').length && 'composing' || // as the Model id, to avoid duplicates.
stanza.getElementsByTagName('paused').length && 'paused' || attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${(attrs.from)}`] || u.getUniqueId();
stanza.getElementsByTagName('inactive').length && 'inactive' || return attrs;
stanza.getElementsByTagName('active').length && 'active' ||
stanza.getElementsByTagName('gone').length && 'gone';
}, },
/** /**
* Parses a passed in message stanza and returns an object of attributes. * Parses a passed in message stanza and returns an object of attributes.
* @private * @method st#parseMUCMessage
* @method stanza_utils#parseMessage
* @param { XMLElement } stanza - The message stanza * @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the * @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself. * message stanza, if it was contained, otherwise it's the message stanza itself.
* @param { _converse.ChatBox|_converse.ChatRoom } chatbox * @param { _converse.ChatRoom } chatbox
* @param { _converse } _converse * @param { _converse } _converse
* @returns { Object } * @returns { (MUCMessageAttributes|Error) }
*/ */
async parseMessage (stanza, original_stanza, chatbox, _converse) { parseMUCMessage (stanza, chatbox, _converse) {
const is_muc = u.isChatRoom(chatbox); const err = rejectUnencapsulatedForward(stanza);
let attrs = Object.assign( if (err) {
stanza_utils.getStanzaIDs(stanza, original_stanza), return err;
stanza_utils.getRetractionAttributes(stanza, original_stanza), }
is_muc ? stanza_utils.getModerationAttributes(stanza, original_stanza, chatbox) : {},
); const selector = `[xmlns="${NS.MAM}"] > forwarded[xmlns="${NS.FORWARD}"] > message`;
const text = stanza_utils.getMessageBody(stanza, is_muc, _converse) || undefined; 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 delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
attrs = Object.assign( const from = stanza.getAttribute('from');
{ const marker = st.getChatMarker(stanza);
'chat_state': stanza_utils.getChatState(stanza), const now = (new Date()).toISOString();
'is_archived': stanza_utils.isArchived(original_stanza), /**
* @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_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_receipt_request - Does this message request a XEP-0184 receipt?
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
* @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 } edit - An ISO8601 string recording the time that the message was edited per XEP-0308
* @property { String } error - The error message, in case it's an error stanza
* @property { String } from - The sender JID
* @property { String } from_muc - The JID of the MUC from which this message was sent
* @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,
'body': stanza.querySelector('body')?.textContent?.trim(),
'chat_state': getChatState(stanza),
'error': getMUCErrorMessage(stanza),
'from_muc': Strophe.getBareJidFromJid(from),
'is_archived': st.isArchived(original_stanza),
'is_carbon': isCarbon(original_stanza),
'is_delayed': !!delay, 'is_delayed': !!delay,
'is_only_emojis': text ? u.isOnlyEmojis(text) : false, 'is_headline': st.isHeadline(stanza),
'message': text, 'is_markable': !!sizzle(`markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length,
'is_marker': !!marker,
'marker_id': marker && marker.getAttribute('id'),
'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'), 'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
'references': stanza_utils.getReferences(stanza), 'nick': Strophe.unescapeNode(Strophe.getResourceFromJid(from)),
'subject': propertyOf(stanza.querySelector('subject'))('textContent'), 'receipt_id': getReceiptId(stanza),
'thread': propertyOf(stanza.querySelector('thread'))('textContent'), 'received': (new Date()).toISOString(),
'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString(), 'references': getReferences(stanza),
'type': stanza.getAttribute('type') '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'),
}, },
attrs,
getSenderAttributes(stanza, chatbox, _converse),
getOutOfBandAttributes(stanza), getOutOfBandAttributes(stanza),
getSpoilerAttributes(stanza), getSpoilerAttributes(stanza),
getCorrectionAttributes(stanza, original_stanza), getCorrectionAttributes(stanza, original_stanza),
) getStanzaIDs(stanza, original_stanza),
attrs = await getEncryptionAttributes(stanza, original_stanza, attrs, chatbox, _converse) getRetractionAttributes(stanza, original_stanza),
// We prefer to use one of the XEP-0359 unique and stable stanza IDs getModerationAttributes(stanza),
// as the Model id, to avoid duplicates. getEncryptionAttributes(stanza, _converse)
);
attrs = Object.assign({
'is_only_emojis': attrs.body ? u.isOnlyEmojis(attrs.body) : false,
'is_receipt_request': isReceiptRequest(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(); attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${(attrs.from_muc || attrs.from)}`] || u.getUniqueId();
return attrs; return attrs;
}, },
/** /**
* Parses a passed in MUC presence stanza and returns an object of attributes. * Parses a passed in MUC presence stanza and returns an object of attributes.
* @private * @method st#parseMUCPresence
* @method stanza_utils#parseMUCPresence
* @param { XMLElement } stanza - The presence stanza * @param { XMLElement } stanza - The presence stanza
* @returns { Object } * @returns { Object }
*/ */
...@@ -414,4 +733,4 @@ const stanza_utils = { ...@@ -414,4 +733,4 @@ const stanza_utils = {
} }
} }
export default stanza_utils; export default st;
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
websocket_url: 'ws://chat.example.org:5380/xmpp-websocket', websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
// bosh_service_url: 'http://chat.example.org:5280/http-bind', // bosh_service_url: 'http://chat.example.org:5280/http-bind',
muc_show_logs_before_join: true, muc_show_logs_before_join: true,
whitelisted_plugins: ['converse-debug'], whitelisted_plugins: ['converse-debug', 'converse-batched-probe'],
}); });
</script> </script>
</html> </html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment