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

WIP: Block users #3164

Open
wants to merge 5 commits into
base: jcbrand/block-users
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
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
186 changes: 87 additions & 99 deletions src/headless/plugins/blocking/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,97 +4,103 @@ import { _converse, api, converse } from '@converse/headless/core.js';
const { Strophe, $iq, sizzle, u } = converse.env;

export default {

/**
* Checks if XEP-0191 is supported
*/
async isBlockingAvailable () {
const has_feature = await api.disco.supports(Strophe.NS.BLOCKING, _converse.domain);
if (!has_feature) {
log.info("XEP-0191 not supported, no blocklist available");
return false;
}
log.debug("XEP-0191 available");
return true;
},
/**
* 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 () {
udanieli marked this conversation as resolved.
Show resolved Hide resolved
const features = await api.disco.getFeatures(_converse.domain);
if (!features?.findWhere({ 'var': Strophe.NS.BLOCKING })) {
return false;
log.debug("refreshing blocklist");
const available = await this.isBlockingAvailable();
if (!available) {
log.debug("XEP-0191 NOT available, not refreshing...");
api.trigger('blockListFetched', []);
udanieli marked this conversation as resolved.
Show resolved Hide resolved
return
}
if (!_converse.connection) {
udanieli marked this conversation as resolved.
Show resolved Hide resolved
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`;
const { __ } = converse.env;
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`);
log.error(result);
return false;
}

const blocklist = sizzle('item', result).map((item) => item.getAttribute('jid'));
_converse.blocked.set({ 'set': new Set(blocklist) });
return true;
log.debug("getting blocklist...");
return this.sendBlockingStanza( 'blocklist', 'get' );
},

/**
* Handle incoming iq stanzas in the BLOCKING namespace. Adjusts the global blocked_set.
* 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
*/
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'));
handleBlockingStanza ( stanza ) {
const action = stanza.firstElementChild.tagName;
const items = sizzle('item', stanza).map(item => item.getAttribute('jid'));
const msg_type = stanza.getAttribute('type');

log.debug(`handle blocking stanza Type ${msg_type} action ${action}`);
if (msg_type == 'result' && action == 'blocklist' ) {
log.debug(`resetting blocklist: ${items}`);
_converse.blocked.set({'set': new Set()});
items.forEach((item) => { _converse.blocked.get('set').add(item)});

/**
* Triggered once the _converse.blocked list has been fetched
* @event _converse#blockListFetched
* @example _converse.api.listen.on('blockListFetched', () => { ... });
*/
api.trigger('blockListFetched', _converse.blocked.get('set'));
log.debug("triggered blockListFetched");

} else if (msg_type == 'set' && action == 'block') {
log.debug(`adding people to blocklist: ${items}`);
items.forEach((item) => { _converse.blocked.get('set').add(item)});
api.trigger('blockListUpdated', _converse.blocked.get('set'));
} else if (msg_type == 'set' && action == 'unblock') {
log.debug(`removing people from blocklist: ${items}`);
items.forEach((item) => { _converse.blocked.get('set').delete(item)});
api.trigger('blockListUpdated', _converse.blocked.get('set'));
} else {
log.error('Received blocklist push update but could not interpret it.');
log.error("Received a 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 });
return true;
},

/**
* Blocks JIDs by sending an IQ stanza
* @method api.blockUser
*
* @param { Array } [jid_list] - The list of JIDs to block
* Send block/unblock IQ stanzas to the server for the JID specified
* @method api.sendBlockingStanza
* @param { String } action - "block", "unblock" or "blocklist"
* @param { String } iq_type - "get" or "set"
* @param { Array } [jid_list] - (optional) The list of JIDs to block or unblock
*/
async blockUser (jid_list) {
if (!_converse.disco_entities.get(_converse.domain)?.features?.findWhere({ 'var': Strophe.NS.BLOCKING })) {
return false;
}
async sendBlockingStanza ( action, iq_type = 'set', jid_list = [] ) {
udanieli marked this conversation as resolved.
Show resolved Hide resolved
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 element = Strophe.xmlElement(action, {'xmlns': Strophe.NS.BLOCKING});
jid_list.forEach((jid) => {
const item = Strophe.xmlElement('item', { 'jid': jid });
element.append(item);
});

const iq = $iq({
'type': 'set',
'id': u.getUniqueId('block'),
}).cnode(block_element);
'type': iq_type,
'id': u.getUniqueId(action)
}).cnode(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}`;
const result = await api.sendIQ(iq).catch(e => { log.fatal(e); return false });
const err_msg = `An error occured while trying to ${action} user(s) ${jid_list}`;
if (result === null) {
api.alert('error', __('Error'), err_msg);
log(err_msg, Strophe.LogLevel.WARN);
Expand All @@ -108,50 +114,32 @@ export default {
},

/**
* 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
* Blocks JIDs by sending an IQ stanza
* @method api.blockUser
*
* @param { Array } [jid_list] - The list of JIDs to block
*/
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;
blockUser ( jid_list ) {
return this.sendBlockingStanza( 'block', 'set', jid_list );
},

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

return new Set();
udanieli marked this conversation as resolved.
Show resolved Hide resolved
},
};

/**
* 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
*/
unblockUser ( jid_list ) {
return this.sendBlockingStanza( 'unblock', 'set', jid_list );
}
}
7 changes: 5 additions & 2 deletions src/headless/plugins/blocking/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* @description
* Converse.js plugin which adds support for XEP-0191: Blocking
* Allows users to block other users, which hides their messages.
* Converse.js plugin which adds support for XEP-0191: Blocking.
* Allows users to block communications with other users on the server side,
* so a user cannot receive messages from a blocked contact.
*/
import blocking_api from './api.js';
import { _converse, api, converse } from '@converse/headless/core.js';
Expand All @@ -28,6 +29,8 @@ converse.plugins.add('converse-blocking', {

initialize () {
_converse.blocked = new SetModel();
api.promises.add(["blockListFetched"]);

Object.assign(api, blocking_api);

api.listen.on('discoInitialized', onConnected);
Expand Down
4 changes: 3 additions & 1 deletion src/headless/plugins/blocking/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { _converse, api, converse } from "@converse/headless/core.js";
const { Strophe } = converse.env;

export function onConnected () {
_converse.connection.addHandler(
api.handleBlockingStanza, Strophe.NS.BLOCKING, 'iq', ['set', 'result']
);
api.refreshBlocklist();
_converse.connection.addHandler(api.handleBlockingStanza, Strophe.NS.BLOCKING, 'iq', 'set', null, null);
}
15 changes: 14 additions & 1 deletion src/headless/plugins/chat/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ const ChatBox = ModelWithContact.extend({
'time_opened': this.get('time_opened') || (new Date()).getTime(),
'time_sent': (new Date(0)).toISOString(),
'type': _converse.PRIVATE_CHAT_TYPE,
'url': ''
'url': '',
'contact_blocked': false
}
},

Expand Down Expand Up @@ -78,9 +79,21 @@ const ChatBox = ModelWithContact.extend({
* @example _converse.api.listen.on('chatBoxInitialized', model => { ... });
*/
await api.trigger('chatBoxInitialized', this, {'Synchronous': true});
await api.waitUntil('blockListFetched');
if (api.blockedUsers) this.checkIfContactBlocked(api.blockedUsers());
api.listen.on('blockListUpdated', this.checkIfContactBlocked, this);
this.initialized.resolve();
},

checkIfContactBlocked (jid_set) {
const contact_blocked = this.get('contact_blocked');
if (jid_set.has(this.get('jid')) && !contact_blocked) {
return this.set({'contact_blocked': true});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general rule, we always use curly brackets for if statements, unless it's a single if (no else) one a single line.

} else if (!jid_set.has(this.get('jid')) && contact_blocked) {
return this.set({'contact_blocked': false});
}
},

getMessagesCollection () {
return new _converse.Messages();
},
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/chatview/bottom-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ export default class ChatBottomPanel extends ElementView {
async initialize () {
this.model = await api.chatboxes.get(this.getAttribute('jid'));
await this.model.initialized;
this.listenTo(this.model, 'change:num_unread', this.debouncedRender)
this.listenTo(this.model, 'change:num_unread', this.debouncedRender);
this.listenTo(this.model, 'emoji-picker-autocomplete', this.autocompleteInPicker);
this.listenTo(this.model, 'change:contact_blocked', () => this.render());

this.addEventListener('focusin', ev => this.emitFocused(ev));
this.addEventListener('focusout', ev => this.emitBlurred(ev));
Expand Down
1 change: 1 addition & 0 deletions src/plugins/chatview/message-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default class MessageForm extends ElementView {
await this.model.initialized;
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
this.listenTo(this.model, 'change:composing_spoiler', () => this.render());
this.listenTo(this.model, 'change:contact_blocked', () => this.render());

this.handleEmojiSelection = ({ detail }) => {
if (this.model.get('jid') === detail.jid) {
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/chatview/templates/bottom-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default (o) => {
return html`
${ o.model.ui.get('scrolled') && o.model.get('num_unread') ?
html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
${api.settings.get('show_toolbar') ? html`
${api.settings.get('show_toolbar') && !o.model.get('contact_blocked') ? html`
<converse-chat-toolbar
class="chat-toolbar no-text-select"
.model=${o.model}
Expand Down
5 changes: 4 additions & 1 deletion src/plugins/chatview/templates/message-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { resetElementHeight } from '../utils.js';


export default (o) => {
const label_message = o.composing_spoiler ? __('Hidden message') : __('Message');
var label_message = o.composing_spoiler ? __('Hidden message') : __('Message');
if (o.contact_blocked) label_message = __('You blocked this contact.');
const label_spoiler_hint = __('Optional hint');
const show_send_button = api.settings.get('show_send_button');

return html`
<form class="sendXMPPMessage">
<fieldset ?disabled=${o.contact_blocked}>
<input type="text"
enterkeyhint="send"
placeholder="${label_spoiler_hint || ''}"i
Expand All @@ -30,5 +32,6 @@ export default (o) => {
${ show_send_button ? 'chat-textarea-send-button' : '' }
${ o.composing_spoiler ? 'spoiler' : '' }"
placeholder="${label_message}">${ o.message_value || '' }</textarea>
</fieldset>
</form>`;
}
4 changes: 3 additions & 1 deletion src/plugins/profile/blocked.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CustomElement } from 'shared/components/element.js';
import { api, _converse } from '@converse/headless/core';
import { html } from 'lit';
import { __ } from 'i18n';


class BlockedUsersProfile extends CustomElement {
Expand All @@ -10,13 +11,14 @@ class BlockedUsersProfile extends CustomElement {
}

render () { // eslint-disable-line class-methods-use-this
const i18n_unblock = __('Unblock');
// TODO: Displaying the JID bare like this is probably wrong. It should probably be escaped
// sanitized, or canonicalized or something before display. The same goes for all such
// displays in this commit.
const { blocked } = _converse;
return html`<ul>
${Array.from(blocked.get('set')).map(
jid => html`<li><p>${jid}</p><button @click=${() => api.unblockUser(jid)}>Unblock</button></li>`
jid => html`<li><p>${jid}</p><button type="button" class="btn btn-success" @click=${() => api.unblockUser(jid)}>${ i18n_unblock }</button></li>`
)}
</ul>`
}
Expand Down
Loading