From 5f8616d5d36d821e1bd397deeed58bac3b12c1c0 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 7 Feb 2025 11:05:38 +0200 Subject: [PATCH] Save the occupant id of the mod that retracted a message --- src/headless/plugins/muc/muc.js | 11 ++- src/headless/plugins/muc/occupants.js | 2 +- src/headless/plugins/muc/parsers.js | 2 + .../plugins/muc/tests/affiliations.js | 44 +++++----- src/headless/types/plugins/muc/occupants.d.ts | 2 +- src/plugins/muc-views/tests/retractions.js | 80 ++++++++++++++++++- 6 files changed, 112 insertions(+), 29 deletions(-) diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js index 0e87631ede..4472da3507 100644 --- a/src/headless/plugins/muc/muc.js +++ b/src/headless/plugins/muc/muc.js @@ -2078,7 +2078,14 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) { * whether a message was moderated or not. */ async handleModeration (attrs) { - const MODERATION_ATTRIBUTES = ['editable', 'moderated', 'moderated_by', 'moderated_id', 'moderation_reason']; + const MODERATION_ATTRIBUTES = [ + 'editable', + 'moderated', + 'moderated_by', + 'moderated_by_id', + 'moderated_id', + 'moderation_reason' + ]; if (attrs.moderated === 'retracted') { const query = {}; const key = `stanza_id ${this.get('jid')}`; @@ -2096,7 +2103,7 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) { const message = this.findDanglingModeration(attrs); if (message) { const moderation_attrs = pick(message.attributes, MODERATION_ATTRIBUTES); - const new_attrs = Object.assign({ 'dangling_moderation': false }, attrs, moderation_attrs); + const new_attrs = Object.assign({ dangling_moderation: false }, attrs, moderation_attrs); delete new_attrs['id']; // Delete id, otherwise a new cache entry gets created message.save(new_attrs); return true; diff --git a/src/headless/plugins/muc/occupants.js b/src/headless/plugins/muc/occupants.js index cd26f54f55..c3c868d832 100644 --- a/src/headless/plugins/muc/occupants.js +++ b/src/headless/plugins/muc/occupants.js @@ -143,7 +143,7 @@ class MUCOccupants extends Collection { * Lookup by occupant_id is done first, then jid, and then nick. * * @method _converse.MUCOccupants#findOccupant - * @param { OccupantData } data + * @param {OccupantData} data */ findOccupant (data) { if (data.occupant_id) { diff --git a/src/headless/plugins/muc/parsers.js b/src/headless/plugins/muc/parsers.js index 3c243f587d..1fdd029e93 100644 --- a/src/headless/plugins/muc/parsers.js +++ b/src/headless/plugins/muc/parsers.js @@ -130,6 +130,7 @@ function getModerationAttributes(stanza) { editable: false, moderated: 'retracted', moderated_by: moderated.getAttribute('by'), + moderated_by_id: moderated.querySelector('occupant-id')?.getAttribute('id'), moderated_id: retract.getAttribute('id'), moderation_reason: retract.querySelector('reason')?.textContent, }; @@ -141,6 +142,7 @@ function getModerationAttributes(stanza) { editable: false, is_tombstone: true, moderated_by: tombstone.getAttribute('by'), + moderated_by_id: tombstone.querySelector('occupant-id')?.getAttribute('id'), retracted: tombstone.getAttribute('stamp'), moderation_reason: tombstone.querySelector('reason')?.textContent, }; diff --git a/src/headless/plugins/muc/tests/affiliations.js b/src/headless/plugins/muc/tests/affiliations.js index 6dc84c146c..d905ce30ff 100644 --- a/src/headless/plugins/muc/tests/affiliations.js +++ b/src/headless/plugins/muc/tests/affiliations.js @@ -1,41 +1,37 @@ /*global mock, converse */ - -const $pres = converse.env.$pres; -const Strophe = converse.env.Strophe; +const { stx, Strophe } = converse.env; describe('The MUC Affiliations API', function () { + beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); + it('can be used to set affiliations in MUCs without having to join them first', mock.initConverse([], {}, async function (_converse) { const { api } = _converse; const user_jid = 'annoyingguy@montague.lit'; const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'id': '27C55F89-1C6A-459A-9EB5-77690145D624', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user' }) - .c('item', { - 'jid': user_jid, - 'affiliation': 'member', - 'role': 'participant' - }); + const presence = stx` + + + + `; _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); - api.rooms.affiliations.set(muc_jid, { 'jid': user_jid, 'affiliation': 'outcast', 'reason': 'Ban hammer!' }); + api.rooms.affiliations.set(muc_jid, { jid: user_jid, affiliation: 'outcast', reason: 'Ban hammer!' }); const iq = _converse.api.connection.get().IQ_stanzas.pop(); - expect(Strophe.serialize(iq)).toBe( - `` + - `` + - `` + - `Ban hammer!` + - `` + - `` + - ``); - + expect(iq).toEqualStanza(stx` + + + + Ban hammer! + + + `); }) ); }); diff --git a/src/headless/types/plugins/muc/occupants.d.ts b/src/headless/types/plugins/muc/occupants.d.ts index 88001a49ba..71a847a1ac 100644 --- a/src/headless/types/plugins/muc/occupants.d.ts +++ b/src/headless/types/plugins/muc/occupants.d.ts @@ -32,7 +32,7 @@ declare class MUCOccupants extends Collection { * Lookup by occupant_id is done first, then jid, and then nick. * * @method _converse.MUCOccupants#findOccupant - * @param { OccupantData } data + * @param {OccupantData} data */ findOccupant(data: { jid?: string; diff --git a/src/plugins/muc-views/tests/retractions.js b/src/plugins/muc-views/tests/retractions.js index cdfc2164a1..6fb0aad8b8 100644 --- a/src/plugins/muc-views/tests/retractions.js +++ b/src/plugins/muc-views/tests/retractions.js @@ -365,7 +365,6 @@ describe("Message Retractions", function () { expect(result).toBe(false); })); - it("can be retracted by a moderator, with the retraction message received before the IQ response", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { @@ -439,6 +438,85 @@ describe("Message Retractions", function () { expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); expect(view.model.messages.at(0).get('editable')).toBe(false); })); + + it("can be followed up by a retraction from a different moderator", + mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + + const nick = 'romeo'; + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE, Strophe.NS.OCCUPANTID]; + const model = await mock.openAndEnterChatRoom(_converse, muc_jid, nick, features); + + // The other moderator enters + const name = mock.chatroom_names[0]; + const user_jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const mod_jid = `${muc_jid}/${name}`; + const mod_occ_id = u.getUniqueId(); + _converse.api.connection.get()._dataRecv(mock.createRequest( + stx` + + + + + `)); + + await u.waitUntil(() => model.occupants.length === 2); + const mod = model.occupants.findOccupant({ occupant_id: mod_occ_id }); + expect(mod.get('affiliation')).toBe('moderator'); + expect(mod.get('occupant_id')).toBe(mod_occ_id); + + const stanza_id = 'stanza-id-1'; + const received_stanza = stx` + + Visit this site to get free Bitcoin! + + `; + await model.handleMessageStanza(received_stanza); + await u.waitUntil(() => model.messages.length === 1); + expect(model.messages.length).toBe(1); + + const view = _converse.chatboxviews.get(muc_jid); + const reason = "This content is inappropriate for this forum!" + + const retraction = stx` + + + + + + ${reason} + + `; + await view.model.handleMessageStanza(retraction); + + await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); + expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1); + const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message .retraction'); + expect(msg_el.firstElementChild.textContent.trim()).toBe('Dyon van de Wege has removed a message'); + const qel = view.querySelector('.chat-msg--retracted .chat-msg__message q'); + expect(qel.textContent).toBe('This content is inappropriate for this forum!'); + + expect(view.model.messages.length).toBe(1); + const message = view.model.messages.at(0); + expect(message.get('moderated')).toBe('retracted'); + expect(message.get('moderated_by')).toBe(mod_jid); + expect(message.get('moderated_by_id')).toBe(mod_occ_id); + expect(message.get('moderation_reason')).toBe(reason); + expect(message.get('editable')).toBe(false); + })); });