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 () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t('Hello world').tree();
await view.model.queueMessage(msg);
await view.model.handleMessageStanza(msg);
// Test that pressing @ brings up all options
const textarea = view.el.querySelector('textarea.chat-textarea');
......
......@@ -6,7 +6,7 @@ describe("A headlines box", function () {
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
const { u, $msg} = converse.env;
const { $msg } = converse.env;
/* XMPP spam message:
*
* <message xmlns="jabber:client"
......@@ -17,7 +17,6 @@ describe("A headlines box", function () {
* <body>SORRY FOR THIS ADVERT</body
* </message
*/
sinon.spy(u, 'isHeadlineMessage');
const stanza = $msg({
'xmlns': 'jabber:client',
'to': 'romeo@montague.lit',
......@@ -27,10 +26,7 @@ describe("A headlines box", function () {
.c('nick', {'xmlns': "http://jabber.org/protocol/nick"}).t("-wwdmz").up()
.c('body').t('SORRY FOR THIS ADVERT');
_converse.connection._dataRecv(mock.createRequest(stanza));
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(false)).toBeTruthy();
expect(_converse.api.headlines.get().length === 0);
u.isHeadlineMessage.restore();
done();
}));
......@@ -51,7 +47,6 @@ describe("A headlines box", function () {
* </x>
* </message>
*/
sinon.spy(u, 'isHeadlineMessage');
const stanza = $msg({
'type': 'headline',
'from': 'notify.example.com',
......@@ -65,9 +60,6 @@ describe("A headlines box", function () {
_converse.connection._dataRecv(mock.createRequest(stanza));
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');
expect(view.model.get('show_avatar')).toBeFalsy();
expect(view.el.querySelector('img.avatar')).toBe(null);
......@@ -155,9 +147,8 @@ describe("A headlines box", function () {
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
const { u, $msg, _ } = converse.env;
const { $msg, _ } = converse.env;
_converse.allow_non_roster_messaging = false;
sinon.spy(u, 'isHeadlineMessage');
const stanza = $msg({
'type': 'headline',
'from': 'andre5114@jabber.snc.ru/Spark',
......@@ -168,9 +159,6 @@ describe("A headlines box", function () {
.c('body').t('Здравствуйте друзья');
_converse.connection._dataRecv(mock.createRequest(stanza));
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();
}));
});
......@@ -294,7 +294,7 @@ describe("Message Archive Management", function () {
</message>`);
spyOn(view.model, 'getDuplicateMessage').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());
expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
const result = view.model.getDuplicateMessage.calls.all()[0].returnValue
......@@ -338,7 +338,7 @@ describe("Message Archive Management", function () {
</result>
</message>`);
spyOn(view.model, 'getDuplicateMessage').and.callThrough();
view.model.queueMessage(stanza);
view.model.handleMAMResult({ 'messages': [stanza] });
await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
......@@ -368,7 +368,7 @@ describe("Message Archive Management", function () {
</forwarded>
</result>
</message>`);
view.model.queueMessage(stanza);
view.model.handleMAMResult({ 'messages': [stanza] });
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
......@@ -388,7 +388,7 @@ describe("Message Archive Management", function () {
</message>`);
spyOn(view.model, 'getDuplicateMessage').and.callThrough();
view.model.queueMessage(stanza);
view.model.handleMAMResult({ 'messages': [stanza] });
await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
......
......@@ -512,9 +512,8 @@ describe("A Chat Message", function () {
// Ideally we wouldn't have to filter out headline
// 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(u, 'isHeadlineMessage');
const msg = $msg({
from: 'montague.lit',
to: _converse.bare_jid,
......@@ -522,16 +521,12 @@ describe("A Chat Message", function () {
id: u.getUniqueId()
}).c('body').t("This headline message will not be shown").tree();
await _converse.handleMessageStanza(msg);
expect(converse.env.log.info.calledWith(
"handleMessageStanza: Ignoring incoming headline message from JID: montague.lit"
)).toBeTruthy();
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(true)).toBeTruthy();
expect(converse.env.log.info).toHaveBeenCalledWith(
"handleMessageStanza: Ignoring incoming server message from JID: montague.lit"
);
expect(_converse.api.chatboxes.get.called).toBeFalsy();
// Remove sinon spies
converse.env.log.info.restore();
_converse.api.chatboxes.get.restore();
u.isHeadlineMessage.restore();
done();
}));
......@@ -1561,7 +1556,7 @@ describe("A Chat Message", function () {
* </message>
*/
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
fullname = _.isEmpty(fullname) ? _converse.bare_jid: fullname;
await _converse.api.chats.open(sender_jid)
......@@ -1757,7 +1752,7 @@ describe("A Chat Message", function () {
await mock.waitForRoster(_converse, 'current');
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
// Send a message from a different resource
spyOn(converse.env.log, 'info');
spyOn(converse.env.log, 'error');
spyOn(_converse.api.chatboxes, 'create').and.callThrough();
_converse.filter_by_resource = true;
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
......@@ -1770,8 +1765,8 @@ describe("A Chat Message", function () {
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.handleMessageStanza(msg);
expect(converse.env.log.info).toHaveBeenCalledWith(
"handleMessageStanza: Ignoring incoming message intended for a different resource: romeo@montague.lit/some-other-resource",
expect(converse.env.log.error.calls.all().pop().args[0]).toBe(
"Ignoring incoming message intended for a different resource: romeo@montague.lit/some-other-resource",
);
expect(_converse.api.chatboxes.create).not.toHaveBeenCalled();
_converse.filter_by_resource = false;
......
......@@ -156,7 +156,7 @@ describe("The Minimized Chats Widget", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t(message).tree();
view.model.queueMessage(msg);
view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length);
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');
......
......@@ -518,8 +518,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(stanza));
const view = _converse.chatboxviews.get('jdev@conference.jabber.org');
await new Promise(resolve => view.model.once('change:subject', resolve));
const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc'));
const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc'), 1000);
expect(head_desc?.textContent.trim()).toBe(text);
stanza = u.toStanza(
......@@ -701,7 +700,7 @@ describe("Groupchats", function () {
'type': 'groupchat'
}).c('body').t(message).tree();
await view.model.queueMessage(msg);
await view.model.handleMessageStanza(msg);
spyOn(view.model, 'clearMessages').and.callThrough();
await view.model.close();
......@@ -732,7 +731,7 @@ describe("Groupchats", function () {
'type': 'groupchat'
}).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'));
view.el.querySelector('.chat-msg__text a').click();
await u.waitUntil(() => _converse.chatboxes.length === 3)
......@@ -1269,7 +1268,7 @@ describe("Groupchats", function () {
'type': 'groupchat'
}).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());
let stanza = u.toStanza(
......@@ -1318,7 +1317,7 @@ describe("Groupchats", function () {
'to': 'romeo@montague.lit',
'type': 'groupchat'
}).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());
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');
......@@ -1330,7 +1329,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).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);
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');
......@@ -2016,7 +2015,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).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);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.content.querySelector('.chat-msg__text').textContent.trim()).toBe(text);
......@@ -2061,7 +2060,7 @@ describe("Groupchats", function () {
by="lounge@montague.lit"/>
<origin-id xmlns="urn:xmpp:sid:0" id="${view.model.messages.at(0).get('origin_id')}"/>
</message>`);
await view.model.queueMessage(stanza);
await view.model.handleMessageStanza(stanza);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
expect(sizzle('.chat-msg__text:last').pop().textContent.trim()).toBe(text);
expect(view.model.messages.length).toBe(1);
......@@ -2083,7 +2082,7 @@ describe("Groupchats", function () {
const promises = [];
for (let i=0; i<20; i++) {
promises.push(
view.model.queueMessage(
view.model.handleMessageStanza(
$msg({
from: 'lounge@montague.lit/someone',
to: 'romeo@montague.lit.com',
......@@ -2096,7 +2095,7 @@ describe("Groupchats", function () {
// Give enough time for `markScrolled` to have been called
setTimeout(async () => {
view.content.scrollTop = 0;
await view.model.queueMessage(
await view.model.handleMessageStanza(
$msg({
from: 'lounge@montague.lit/someone',
to: 'romeo@montague.lit.com',
......@@ -4863,8 +4862,7 @@ describe("Groupchats", function () {
view.model.set({'minimized': true});
const nick = mock.chatroom_names[0];
await view.model.queueMessage($msg({
await view.model.handleMessageStanza($msg({
from: muc_jid+'/'+nick,
id: u.getUniqueId(),
to: 'romeo@montague.lit',
......@@ -4875,7 +4873,7 @@ describe("Groupchats", function () {
expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).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,
'id': u.getUniqueId(),
'to': 'romeo@montague.lit',
......@@ -5027,7 +5025,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).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');
// <composing> state for a different occupant
......@@ -5037,7 +5035,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).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');
// <composing> state for a different occupant
......@@ -5047,7 +5045,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).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');
// Check that new messages appear under the chat state notifications
......@@ -5057,7 +5055,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t('hello world').tree();
await view.model.queueMessage(msg);
await view.model.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
const messages = view.el.querySelectorAll('.message');
......@@ -5146,7 +5144,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).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);
expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toBe('newguy is typing');
......@@ -5157,7 +5155,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).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');
......@@ -5168,7 +5166,7 @@ describe("Groupchats", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).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');
done();
}));
......
This diff is collapsed.
......@@ -95,10 +95,7 @@ describe("Notifications", function () {
await u.waitUntil(() => _converse.chatboxviews.keys().length);
const view = _converse.chatboxviews.get('notify.example.com');
await new Promise(resolve => view.once('messageInserted', resolve));
expect(
_.includes(_converse.chatboxviews.keys(),
'notify.example.com')
).toBeTruthy();
expect(_converse.chatboxviews.keys().includes('notify.example.com')).toBeTruthy();
expect(_converse.showMessageNotification).toHaveBeenCalled();
done();
}));
......@@ -175,7 +172,7 @@ describe("Notifications", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t(text);
await view.model.queueMessage(message.nodeTree);
await view.model.handleMessageStanza(message.nodeTree);
await u.waitUntil(() => _converse.playSoundNotification.calls.count());
expect(_converse.playSoundNotification).toHaveBeenCalled();
......@@ -186,7 +183,7 @@ describe("Notifications", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t(text);
await view.model.queueMessage(message.nodeTree);
await view.model.handleMessageStanza(message.nodeTree);
expect(_converse.playSoundNotification, 1);
_converse.play_sounds = false;
......@@ -197,7 +194,7 @@ describe("Notifications", function () {
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t(text);
await view.model.queueMessage(message.nodeTree);
await view.model.handleMessageStanza(message.nodeTree);
expect(_converse.playSoundNotification, 1);
_converse.play_sounds = false;
done();
......
......@@ -19,7 +19,7 @@ async function sendAndThenRetractMessage (_converse, view) {
by="lounge@montague.lit"/>
<origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
</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);
const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
......@@ -52,7 +52,7 @@ describe("Message Retractions", function () {
</message>
`);
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);
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
......@@ -394,7 +394,7 @@ describe("Message Retractions", function () {
</message>
`);
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);
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
......@@ -440,7 +440,7 @@ describe("Message Retractions", function () {
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
</message>
`);
await view.model.queueMessage(received_stanza);
await view.model.handleMessageStanza(received_stanza);
await u.waitUntil(() => view.model.messages.length === 1);
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
......@@ -498,7 +498,7 @@ describe("Message Retractions", function () {
</moderated>
</apply-to>
</message>`);
await view.model.queueMessage(retraction);
await view.model.handleMessageStanza(retraction);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
......@@ -524,7 +524,7 @@ describe("Message Retractions", function () {
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
</message>
`);
await view.model.queueMessage(received_stanza);
await view.model.handleMessageStanza(received_stanza);
await u.waitUntil(() => view.el.querySelector('.chat-msg__content'));
expect(view.el.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null);
const result = await view.model.canModerateMessages();
......@@ -551,7 +551,7 @@ describe("Message Retractions", function () {
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
</message>
`);
await view.model.queueMessage(received_stanza);
await view.model.handleMessageStanza(received_stanza);
await u.waitUntil(() => view.model.messages.length === 1);
expect(view.model.messages.length).toBe(1);
......@@ -579,7 +579,7 @@ describe("Message Retractions", function () {
</moderated>
</apply-to>
</message>`);
await view.model.queueMessage(retraction);
await view.model.handleMessageStanza(retraction);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
expect(view.model.messages.length).toBe(1);
......@@ -778,7 +778,7 @@ describe("Message Retractions", function () {
by="lounge@montague.lit"/>
<origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
</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);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('editable')).toBe(true);
......@@ -794,7 +794,7 @@ describe("Message Retractions", function () {
</moderated>
</apply-to>
</message>`);
await view.model.queueMessage(retraction);
await view.model.handleMessageStanza(retraction);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
......@@ -830,7 +830,7 @@ describe("Message Retractions", function () {
by="lounge@montague.lit"/>
<origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
</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);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('editable')).toBe(true);
......@@ -879,7 +879,7 @@ describe("Message Retractions", function () {
</moderated>
</apply-to>
</message>`);
await view.model.queueMessage(retraction);
await view.model.handleMessageStanza(retraction);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
expect(view.model.messages.at(0).get('moderation_reason')).toBe(undefined);
......
......@@ -289,7 +289,7 @@ describe("A groupchat shown in the groupchats list", function () {
const view = _converse.chatboxviews.get(room_jid);
view.model.set({'minimized': true});
const nick = mock.chatroom_names[0];
await view.model.queueMessage(
await view.model.handleMessageStanza(
$msg({
from: room_jid+'/'+nick,
id: u.getUniqueId(),
......@@ -303,7 +303,7 @@ describe("A groupchat shown in the groupchats list", function () {
expect(Array.from(room_el.classList).includes('unread-msgs')).toBeTruthy();
// If the user is mentioned, the counter also gets updated
await view.model.queueMessage(
await view.model.handleMessageStanza(
$msg({
from: room_jid+'/'+nick,
id: u.getUniqueId(),
......@@ -316,7 +316,7 @@ describe("A groupchat shown in the groupchats list", function () {
expect(indicator_el.textContent).toBe('1');
spyOn(view.model, 'incrementUnreadMsgCounter').and.callThrough();
await view.model.queueMessage(
await view.model.handleMessageStanza(
$msg({
from: room_jid+'/'+nick,
id: u.getUniqueId(),
......
......@@ -5,6 +5,7 @@
*/
import { converse } from "@converse/headless/converse-core";
import log from "@converse/headless/log";
import st from "@converse/headless/utils/stanza";
const { Strophe, sizzle } = converse.env;
const u = converse.env.utils;
......@@ -79,7 +80,7 @@ converse.plugins.add('converse-notification', {
return false;
} else if (message.getAttribute('type') === 'groupchat') {
return _converse.shouldNotifyOfGroupMessage(message);
} else if (u.isHeadlineMessage(_converse, message)) {
} else if (st.isHeadline(message)) {
// We want to show notifications for headline messages.
return _converse.isMessageToHiddenChat(message);
}
......
This diff is collapsed.
......@@ -7,8 +7,6 @@ import { isString } from "lodash";
import { converse } from "@converse/headless/converse-core";
import st from "./utils/stanza";
const u = converse.env.utils;
converse.plugins.add('converse-headlines', {
/* Plugin dependencies are other plugins which might be
......@@ -83,7 +81,7 @@ converse.plugins.add('converse-headlines', {
async function onHeadlineMessage (stanza) {
// 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');
if (from_jid.includes('@') &&
!_converse.roster.get(from_jid) &&
......@@ -100,7 +98,7 @@ converse.plugins.add('converse-headlines', {
'type': _converse.HEADLINES_TYPE,
'from': from_jid
});
const attrs = await st.parseMessage(stanza, stanza, chatbox, _converse);
const attrs = await st.parseMessage(stanza, _converse);
await chatbox.createMessage(attrs);
api.trigger('message', {'chatbox': chatbox, 'stanza': stanza});
}
......@@ -109,10 +107,7 @@ converse.plugins.add('converse-headlines', {
/************************ BEGIN Event Handlers ************************/
function registerHeadlineHandler () {
_converse.connection.addHandler(message => {
onHeadlineMessage(message);
return true
}, null, 'message');
_converse.connection.addHandler(message => (onHeadlineMessage(message) || true), null, 'message');
}
api.listen.on('connected', registerHeadlineHandler);
api.listen.on('reconnected', registerHeadlineHandler);
......
......@@ -11,9 +11,11 @@ import { intersection, pick } from 'lodash'
import { converse } from "./converse-core";
import log from "./log";
import sizzle from "sizzle";
import st from "./utils/stanza";
let _converse;
const { Strophe, $iq, dayjs } = converse.env;
const { NS } = Strophe;
const u = converse.env.utils;
// XEP-0313 Message Archive Management
......@@ -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.
* @private
......@@ -64,55 +86,34 @@ const MAMEnabledChat = {
* @param { string } [options.with] - The JID of the entity with
* which messages were exchanged.
* @param { boolean } [options.groupchat] - True if archive in groupchat.
* @param { boolean } [page] - Whether this function should recursively
* page through the entire result set if a limited number of results
* were returned.
* @param { ('forwards'|'backwards')} [page_direction] - Determines whether this function should
* recursively page through the entire result set if a limited number of results were returned.
*/
async fetchArchivedMessages (options={}, page) {
async fetchArchivedMessages (options={}, page_direction) {
if (this.disable_mam) {
return;
}
const is_groupchat = this.get('type') === _converse.CHATROOMS_TYPE;
const mam_jid = is_groupchat ? this.get('jid') : _converse.bare_jid;
if (!(await api.disco.supports(Strophe.NS.MAM, mam_jid))) {
const is_muc = this.get('type') === _converse.CHATROOMS_TYPE;
const mam_jid = is_muc ? this.get('jid') : _converse.bare_jid;
if (!(await api.disco.supports(NS.MAM, mam_jid))) {
return;
}
const msg_handler = is_groupchat ? s => this.queueMessage(s) : s => _converse.handleMessageStanza(s);
const query = Object.assign({
'groupchat': is_groupchat,
'groupchat': is_muc,
'max': api.settings.get('archived_messages_page_size'),
'with': this.get('jid'),
}, options);
const result = await api.archive.query(query);
/**
* *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);
}
await this.handleMAMResult(result, query, options, page_direction);
if (page && result.rsm) {
if (page === 'forwards') {
if (page_direction && result.rsm) {
if (page_direction === 'forwards') {
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);
}
return this.fetchArchivedMessages(options, page);
return this.fetchArchivedMessages(options, page_direction);
} else {
// TODO: Add a special kind of message which will
// render as a link to fetch further messages, either
......@@ -162,12 +163,12 @@ converse.plugins.add('converse-mam', {
* Per JID preferences will be set in chat boxes, so it'll
* 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');
if (default_pref !== api.settings.get('message_archiving')) {
const stanza = $iq({'type': 'set'})
.c('prefs', {
'xmlns':Strophe.NS.MAM,
'xmlns':NS.MAM,
'default':api.settings.get('message_archiving')
});
Array.from(preference.children).forEach(child => stanza.cnode(child).up());
......@@ -185,11 +186,11 @@ converse.plugins.add('converse-mam', {
function getMAMPrefsFromFeature (feature) {
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;
}
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))
.catch(_converse.onMAMError);
}
......@@ -207,7 +208,7 @@ converse.plugins.add('converse-mam', {
}
/************************ 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('chatRoomViewInitialized', view => {
if (_converse.muc_show_logs_before_join) {
......@@ -437,18 +438,18 @@ converse.plugins.add('converse-mam', {
}
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) {
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': []};
}
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) {
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('value').t(Strophe.NS.MAM).up().up();
.c('value').t(NS.MAM).up().up();
if (options['with'] && !options.groupchat) {
stanza.c('field', {'var':'with'}).c('value')
......@@ -474,7 +475,7 @@ converse.plugins.add('converse-mam', {
const messages = [];
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) {
return true;
}
......@@ -490,7 +491,7 @@ converse.plugins.add('converse-mam', {
}
messages.push(stanza);
return true;
}, Strophe.NS.MAM);
}, NS.MAM);
let error;
const iq_result = await api.sendIQ(stanza, api.settings.get('message_archiving_timeout'), false)
......@@ -508,9 +509,9 @@ converse.plugins.add('converse-mam', {
_converse.connection.deleteHandler(message_handler);
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'))) {
const set = sizzle(`set[xmlns="${Strophe.NS.RSM}"]`, fin).pop();
const set = sizzle(`set[xmlns="${NS.RSM}"]`, fin).pop();
if (set) {
rsm = new _converse.RSM({'xml': set});
Object.assign(rsm, Object.assign(pick(options, [...MAM_ATTRIBUTES, ..._converse.RSM_ATTRIBUTES]), rsm));
......
......@@ -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 () {
// Register presence and message handlers for this groupchat
const room_jid = this.get('jid');
......@@ -618,17 +638,9 @@ converse.plugins.add('converse-muc', {
{'ignoreNamespaceFragment': true, 'matchBareFromJid': true}
);
this.message_handler = _converse.connection.addHandler(stanza => {
if (sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop()) {
// MAM messages are handled in converse-mam.
// 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,
this.message_handler = _converse.connection.addHandler(
stanza => (!!this.handleMessageStanza(stanza) || true),
null, 'message', 'groupchat', null, room_jid,
{'matchBareFromJid': true}
);
......@@ -1650,7 +1662,7 @@ converse.plugins.add('converse-muc', {
* @private
* @method _converse.ChatRoom#handleSubjectChange
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage}
* message, as returned by {@link st.parseMUCMessage}
*/
async handleSubjectChange (attrs) {
if (isString(attrs.subject) && !attrs.thread && !attrs.message) {
......@@ -1703,8 +1715,7 @@ converse.plugins.add('converse-muc', {
* @param { Object } attrs - The message attributes
*/
ignorableCSN (attrs) {
const is_csn = u.isOnlyChatStateNotification(attrs);
return is_csn && (attrs.is_delayed || this.isOwnMessage(attrs));
return attrs.chat_state && !attrs.body && (attrs.is_delayed || this.isOwnMessage(attrs));
},
......@@ -1729,21 +1740,17 @@ converse.plugins.add('converse-muc', {
},
getUpdatedMessageAttributes (message, stanza) {
getUpdatedMessageAttributes (message, attrs) {
// Overridden in converse-muc and converse-mam
const attrs = _converse.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, stanza);
if (this.isOwnMessage(message)) {
const stanza_id = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
const by_jid = stanza_id ? stanza_id.getAttribute('by') : undefined;
if (by_jid) {
const key = `stanza_id ${by_jid}`;
attrs[key] = stanza_id.getAttribute('id');
}
const new_attrs = _converse.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, attrs);
if (this.isOwnMessage(attrs)) {
const stanza_id_keys = Object.keys(attrs).filter(k => k.startsWith('stanza_id'));
Object.assign(new_attrs, pick(attrs, stanza_id_keys));
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', {
* @private
* @method _converse.ChatRoom#findDanglingModeration
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage}
* message, as returned by {@link st.parseMUCMessage}
* @returns { _converse.ChatRoomMessage }
*/
findDanglingModeration (attrs) {
......@@ -1839,7 +1846,7 @@ converse.plugins.add('converse-muc', {
* @private
* @method _converse.ChatRoom#handleModeration
* @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
* whether a message was moderated or not.
*/
......@@ -1954,47 +1961,25 @@ converse.plugins.add('converse-muc', {
* should be called.
* @private
* @method _converse.ChatRoom#onMessage
* @param { XMLElement } stanza - The message stanza.
* @param { MessageAttributes } attrs - The message attributes
*/
async onMessage (stanza) {
if (sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length) {
return log.warn('onMessage: Ignoring unencapsulated forwarded groupchat 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')}`);
async onMessage (attrs) {
if (u.isErrorObject(attrs)) {
attrs.stanza && log.error(attrs.stanza);
return log.error(attrs.message);
}
}
await this.createInfoMessages(stanza);
this.fetchFeaturesIfConfigurationChanged(stanza);
const attrs = await st.parseMessage(stanza, original_stanza, this, _converse);
// TODO: move to OMEMO
attrs = attrs.encrypted ? await this.decrypt(attrs) : attrs;
const message = this.getDuplicateMessage(attrs);
if (message) {
this.updateMessage(message, original_stanza);
}
if (message ||
st.isReceipt(stanza) ||
st.isChatMarker(stanza) ||
this.ignorableCSN(attrs)) {
return api.trigger('message', {'stanza': original_stanza});
return this.updateMessage(message, attrs);
} else if (attrs.is_receipt_request || attrs.is_marker || this.ignorableCSN(attrs)) {
return;
}
if (await this.handleRetraction(attrs) ||
await this.handleModeration(attrs) ||
await this.handleSubjectChange(attrs)) {
this.removeNotification(attrs.nick, ['composing', 'paused']);
return api.trigger('message', {'stanza': original_stanza});
return this.removeNotification(attrs.nick, ['composing', 'paused']);
}
this.setEditable(attrs, attrs.time);
......@@ -2006,7 +1991,6 @@ converse.plugins.add('converse-muc', {
this.removeNotification(attrs.nick, ['composing', 'paused']);
this.incrementUnreadMsgCounter(msg);
}
api.trigger('message', {'stanza': original_stanza, 'chatbox': this});
},
handleModifyError(pres) {
......
......@@ -42,16 +42,6 @@ u.toStanza = function (string) {
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) {
function reducer (accumulator, current_value) {
if (string.startsWith(current_value)) {
......@@ -142,6 +132,7 @@ u.isEmptyMessage = function (attrs) {
!attrs['message'];
};
//TODO: Remove
u.isOnlyChatStateNotification = function (msg) {
if (msg instanceof Element) {
// See XEP-0085 Chat State Notification
......@@ -174,25 +165,6 @@ u.isChatRoom = function (model) {
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) {
return o instanceof Error;
}
......
This diff is collapsed.
......@@ -34,7 +34,7 @@
websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
// bosh_service_url: 'http://chat.example.org:5280/http-bind',
muc_show_logs_before_join: true,
whitelisted_plugins: ['converse-debug'],
whitelisted_plugins: ['converse-debug', 'converse-batched-probe'],
});
</script>
</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