Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added more moderation UI complete with user blocking/ignoring #3119

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/headless/headless.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import "./plugins/roster/index.js"; // RFC-6121 Contacts Roster
import "./plugins/smacks/index.js"; // XEP-0198 Stream Management
import "./plugins/status/index.js";
import "./plugins/vcard/index.js"; // XEP-0054 VCard-temp
import "./plugins/blocking/index.js"; // XEP-0191 Blocking Command
/* END: Removable components */

import { converse } from "./core.js";
Expand Down
149 changes: 149 additions & 0 deletions src/headless/plugins/blocking/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import log from '@converse/headless/log.js';
import { _converse, api, converse } from "@converse/headless/core.js";
import { setLastStanzaDate } from './utils.js';

const { Strophe, $iq, sizzle, u } = converse.env;

export default {
/**
* Retrieves the blocklist held by the logged in user at a JID by sending an IQ stanza.
* Saves the model variable _converse.blocked.set
* @private
* @method api.refreshBlocklist
*/
async refreshBlocklist () {
const features = await api.disco.getFeatures(_converse.domain);
if (!features?.findWhere({'var': Strophe.NS.BLOCKING})) {
return false;
}
if (!_converse.connection) {
return false;
}

const iq = $iq({
'type': 'get',
'id': u.getUniqueId('blocklist')
}).c('blocklist', {'xmlns': Strophe.NS.BLOCKING});

const result = await api.sendIQ(iq).catch(e => { log.fatal(e); return null });
if (result === null) {
const err_msg = `An error occured while fetching the blocklist`;
api.alert('error', __('Error'), err_msg);
log(err_msg, Strophe.LogLevel.WARN);
return false;
} else if (u.isErrorStanza(result)) {
log.error(`Error while fetching blocklist from ${jid}`);
log.error(result);
return false;
}

const blocklist = sizzle('item', result).map(item => item.getAttribute('jid'));
_converse.blocked.set({'set': new Set(blocklist)});
return true;
},

/**
* Handle incoming iq stanzas in the BLOCKING namespace. Adjusts the global blocked_set.
* @private
* @method api.handleBlockingStanza
* @param { Object } [stanza] - The incoming stanza to handle
*/
async handleBlockingStanza ( stanza ) {
if (stanza.firstElementChild.tagName === 'block') {
const users_to_block = sizzle('item', stanza).map(item => item.getAttribute('jid'));
users_to_block.forEach(_converse.blocked.get('set').add, _converse.blocked.get('set'));
} else if (stanza.firstElementChild.tagName === 'unblock') {
const users_to_unblock = sizzle('item', stanza).map(item => item.getAttribute('jid'));
users_to_unblock.forEach(_converse.blocked.get('set').delete, _converse.blocked.get('set'));
} else {
log.error("Received blocklist push update but could not interpret it.");
}
// TODO: Fix this to not use the length as an update key, and
// use a more accurate update method, like a length-extendable hash
_converse.blocked.set({ 'len': _converse.blocked.get('set').size });
},

/**
* Blocks JIDs by sending an IQ stanza
* @method api.blockUser
*
* @param { Array } [jid_list] - The list of JIDs to block
*/
async blockUser ( jid_list ) {
if (!_converse.disco_entities.get(_converse.domain)?.features?.findWhere({'var': Strophe.NS.BLOCKING})) {
return false;
}
if (!_converse.connection) {
return false;
}

const block_items = jid_list.map(jid => Strophe.xmlElement('item', { 'jid': jid }));
const block_element = Strophe.xmlElement('block', {'xmlns': Strophe.NS.BLOCKING });

block_items.forEach(block_element.appendChild, block_element);

const iq = $iq({
'type': 'set',
'id': u.getUniqueId('block')
}).cnode(block_element);

const result = await api.sendIQ(iq).catch(e => { log.fatal(e); return false });
const err_msg = `An error occured while trying to block user(s) ${jid_list}`;
if (result === null) {
api.alert('error', __('Error'), err_msg);
log(err_msg, Strophe.LogLevel.WARN);
return false;
} else if (u.isErrorStanza(result)) {
log.error(err_msg);
log.error(result);
return false;
}
return true;
},

/**
* Unblocks JIDs by sending an IQ stanza to the server JID specified
* @method api.unblockUser
* @param { Array } [jid_list] - The list of JIDs to unblock
*/
async unblockUser ( jid_list ) {
if (!_converse.disco_entities.get(_converse.domain)?.features?.findWhere({'var': Strophe.NS.BLOCKING})) {
return false;
}
if (!_converse.connection) {
return false;
}

const unblock_items = jid_list.map(jid => Strophe.xmlElement('item', { 'jid': jid }));
const unblock_element = Strophe.xmlElement('unblock', {'xmlns': Strophe.NS.BLOCKING});

unblock_items.forEach(unblock_element.append, unblock_element);

const iq = $iq({
'type': 'set',
'id': u.getUniqueId('block')
}).cnode(unblock_element);

const result = await api.sendIQ(iq).catch(e => { log.fatal(e); return false });
const err_msg = `An error occured while trying to unblock user(s) ${jid_list}`;
if (result === null) {
api.alert('error', __('Error'), err_msg);
log(err_msg, Strophe.LogLevel.WARN);
return false;
} else if (u.isErrorStanza(result)) {
log.error(err_msg);
log.error(result);
return false;
}
return true;
},

/**
* Retrieved the blocked set
* @method api.blockedUsers
*/
blockedUsers () {
return _converse.blocked.get('set');
}

}
39 changes: 39 additions & 0 deletions src/headless/plugins/blocking/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @description
* Converse.js plugin which adds support for XEP-0191: Blocking
* Allows users to block other users, which hides their messages
*/
import blocking_api from './api.js';
import { _converse, api, converse } from "@converse/headless/core.js";
import { onConnected } from './utils.js';
import { Model } from '@converse/skeletor/src/model.js';

const { Strophe } = converse.env;

const SetModel = Model.extend({
defaults: {
'set': new Set(),
'len': 0,
}
});

Strophe.addNamespace('BLOCKING', "urn:xmpp:blocking");

converse.plugins.add('converse-blocking', {
enabled (_converse) {
return (
!_converse.api.settings.get('blacklisted_plugins').includes('converse-blocking')
);
},


dependencies: ["converse-disco"],

initialize () {
_converse.blocked = new SetModel();
Object.assign(api, blocking_api);

api.listen.on('discoInitialized', onConnected);
api.listen.on('reconnected', onConnected);
}
});
10 changes: 10 additions & 0 deletions src/headless/plugins/blocking/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { _converse, api, converse } from "@converse/headless/core.js";

const { Strophe, $iq } = converse.env;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class

Unused variable $iq.

export async function onConnected () {
api.refreshBlocklist();
_converse.connection.addHandler(api.handleBlockingStanza, Strophe.NS.BLOCKING, 'iq', 'set', null, null);
}


5 changes: 4 additions & 1 deletion src/headless/plugins/chat/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,10 @@ const ChatBox = ModelWithContact.extend({
// when the user writes a message as opposed to when a
// message is received.
this.ui.set('scrolled', false);
} else if (this.isHidden()) {
} else if ( this.isHidden() ||
( _converse.pluggable.plugins['converse.blocking'] &&
api.blockedUsers()?.has(message?.get('from_real_jid'))
) {
this.incrementUnreadMsgsCounter(message);
} else {
this.sendMarkerForMessage(message);
Expand Down
6 changes: 6 additions & 0 deletions src/headless/plugins/muc/muc.js
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,12 @@ const ChatRoomMixin = {

getAllowedCommands () {
let allowed_commands = ['clear', 'help', 'me', 'nick', 'register'];
// Only allow blocking commands when server supports it and we also support it
if ( _converse.disco_entities.get(_converse.domain, true)?.features?.findWhere({'var': Strophe.NS.BLOCKING}) &&
( _converse.pluggable.plugins['converse-blocking']?.enabled(_converse) )
) {
allowed_commands = [...allowed_commands, ...['block', 'unblock']];
}
if (this.config.get('changesubject') || ['owner', 'admin'].includes(this.getOwnAffiliation())) {
allowed_commands = [...allowed_commands, ...['subject', 'topic']];
}
Expand Down
3 changes: 2 additions & 1 deletion src/headless/shared/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export const CORE_PLUGINS = [
'converse-roster',
'converse-smacks',
'converse-status',
'converse-vcard'
'converse-vcard',
'converse-blocking'
];

export const URL_PARSE_OPTIONS = { 'start': /(\b|_)(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi };
Expand Down
13 changes: 12 additions & 1 deletion src/plugins/muc-views/modals/occupant.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import BaseModal from "plugins/modal/modal.js";
import tpl_occupant_modal from "./templates/occupant.js";
import { tpl_occupant_modal, tpl_footer } from "./templates/occupant.js";
import { _converse, api } from "@converse/headless/core";
import { Model } from '@converse/skeletor/src/model.js';

Expand All @@ -9,6 +9,9 @@ export default class OccupantModal extends BaseModal {
super.initialize()
const model = this.model ?? this.message;
this.listenTo(model, 'change', () => this.render());
if ( _converse.pluggable.plugins['converse-blocking']?.enabled(_converse) ) {
this.listenTo(_converse.blocked, 'change', this.render);
}
/**
* Triggered once the OccupantModal has been initialized
* @event _converse#occupantModalInitialized
Expand All @@ -28,9 +31,17 @@ export default class OccupantModal extends BaseModal {
}

renderModal () {
const model = this.model ?? this.message;
if (model?.collection?.chatroom) {
this.listenToOnce(model.collection.chatroom, 'change', () => this.render());
}
return tpl_occupant_modal(this);
}

renderModalFooter () {
return tpl_footer(this);
}

getModalTitle () {
const model = this.model ?? this.message;
return model?.getDisplayName();
Expand Down
Loading