Skip to content

Commit

Permalink
step forward to an usable version
Browse files Browse the repository at this point in the history
* get/set blocklist with common code
* add promises for blocklistFetched and blocklistUpdated events
* disallow sending messages to blocked contacts
  • Loading branch information
udanieli committed May 30, 2023
1 parent 81e8868 commit de9f83d
Show file tree
Hide file tree
Showing 14 changed files with 164 additions and 122 deletions.
187 changes: 87 additions & 100 deletions src/headless/plugins/blocking/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,97 +4,102 @@ 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.warn("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 () {
const features = await api.disco.getFeatures(_converse.domain);
if (!features?.findWhere({ 'var': Strophe.NS.BLOCKING })) {
return false;
refreshBlocklist () {
log.debug("refreshing blocklist");
if (!this.isBlockingAvailable()) {
log.debug("XEP-0191 NOT available, not refreshing...");
api.trigger('blockListFetched', []);
return
}
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`;
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 = [] ) {
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 +113,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();
},
};

/**
* 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});
} 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

0 comments on commit de9f83d

Please sign in to comment.