Skip to content

Commit

Permalink
Save the occupant id of the mod that retracted a message
Browse files Browse the repository at this point in the history
  • Loading branch information
jcbrand committed Feb 7, 2025
1 parent 5bc7963 commit 6086c2b
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 29 deletions.
11 changes: 9 additions & 2 deletions src/headless/plugins/muc/muc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')}`;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/headless/plugins/muc/occupants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions src/headless/plugins/muc/parsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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,
};
Expand Down
44 changes: 20 additions & 24 deletions src/headless/plugins/muc/tests/affiliations.js
Original file line number Diff line number Diff line change
@@ -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 = '[email protected]';
const muc_jid = '[email protected]';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const presence = $pres({
'from': '[email protected]/annoyingGuy',
'id': '27C55F89-1C6A-459A-9EB5-77690145D624',
'to': '[email protected]/desktop'
})
.c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user' })
.c('item', {
'jid': user_jid,
'affiliation': 'member',
'role': 'participant'
});
const presence = stx`
<presence from="[email protected]/annoyingGuy"
id="27C55F89-1C6A-459A-9EB5-77690145D624"
to="[email protected]/desktop"
xmlns="jabber:client">
<x xmlns="http://jabber.org/protocol/muc#user"/>
<item jid="${user_jid}" affiliation="member" role="participant"/>
</presence>`;
_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(
`<iq id="${iq.getAttribute('id')}" to="[email protected]" type="set" xmlns="jabber:client">` +
`<query xmlns="http://jabber.org/protocol/muc#admin">` +
`<item affiliation="outcast" jid="${user_jid}">` +
`<reason>Ban hammer!</reason>` +
`</item>` +
`</query>` +
`</iq>`);

expect(iq).toEqualStanza(stx`
<iq id="${iq.getAttribute('id')}" to="[email protected]" type="set" xmlns="jabber:client">
<query xmlns="http://jabber.org/protocol/muc#admin">
<item affiliation="outcast" jid="${user_jid}">
<reason>Ban hammer!</reason>
</item>
</query>
</iq>`);
})
);
});
2 changes: 1 addition & 1 deletion src/headless/types/plugins/muc/occupants.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
80 changes: 79 additions & 1 deletion src/plugins/muc-views/tests/retractions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down Expand Up @@ -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 = '[email protected]';
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`<presence from="${mod_jid}"
id="${u.getUniqueId()}"
to="${_converse.bare_jid}"
xmlns="jabber:client">
<x xmlns="http://jabber.org/protocol/muc#user">
<item jid="${user_jid}" affiliation="moderator" role="participant"/>
</x>
<occupant-id xmlns="urn:xmpp:occupant-id:0" id="${mod_occ_id}" />
</presence>`));

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`
<message to="${_converse.jid}"
from="${muc_jid}/mallory"
type="groupchat"
id="${_converse.api.connection.get().getUniqueId()}"
xmlns="jabber:client">
<body>Visit this site to get free Bitcoin!</body>
<stanza-id xmlns="urn:xmpp:sid:0" id="${stanza_id}" by="${muc_jid}"/>
</message>`;
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`
<message type="groupchat"
id='retraction-id-1'
from="${muc_jid}"
to="${muc_jid}/romeo"
xmlns="jabber:client">
<retract id="${stanza_id}" xmlns='urn:xmpp:message-retract:1'>
<moderated by="${mod_jid}" xmlns='urn:xmpp:message-moderate:1'>
<occupant-id xmlns="urn:xmpp:occupant-id:0" id="${mod_occ_id}" />
</moderated>
<reason>${reason}</reason>
</retract>
</message>`;
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);
}));
});


Expand Down

0 comments on commit 6086c2b

Please sign in to comment.