From de9f83dd24ae6bd091f653007f49f7251ac044f3 Mon Sep 17 00:00:00 2001 From: danieli Date: Sat, 20 May 2023 19:13:36 +0200 Subject: [PATCH 1/5] step forward to an usable version * get/set blocklist with common code * add promises for blocklistFetched and blocklistUpdated events * disallow sending messages to blocked contacts --- src/headless/plugins/blocking/api.js | 187 ++++++++---------- src/headless/plugins/blocking/index.js | 7 +- src/headless/plugins/blocking/utils.js | 4 +- src/headless/plugins/chat/model.js | 15 +- src/plugins/chatview/bottom-panel.js | 3 +- src/plugins/chatview/message-form.js | 1 + .../chatview/templates/bottom-panel.js | 2 +- .../chatview/templates/message-form.js | 5 +- src/plugins/profile/blocked.js | 4 +- .../profile/templates/profile_modal.js | 8 +- src/shared/chat/message-actions.js | 6 + src/shared/chat/message.js | 12 +- src/shared/modals/templates/user-details.js | 16 +- src/shared/modals/user-details.js | 16 ++ 14 files changed, 164 insertions(+), 122 deletions(-) diff --git a/src/headless/plugins/blocking/api.js b/src/headless/plugins/blocking/api.js index 7141629949..fd9a308916 100644 --- a/src/headless/plugins/blocking/api.js +++ b/src/headless/plugins/blocking/api.js @@ -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); @@ -108,43 +113,13 @@ 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 ); }, /** @@ -152,6 +127,18 @@ export default { * @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 ); + } +} diff --git a/src/headless/plugins/blocking/index.js b/src/headless/plugins/blocking/index.js index e8f13fae2c..99c26a90f9 100644 --- a/src/headless/plugins/blocking/index.js +++ b/src/headless/plugins/blocking/index.js @@ -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'; @@ -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); diff --git a/src/headless/plugins/blocking/utils.js b/src/headless/plugins/blocking/utils.js index f8faedb98c..56bb8e308e 100644 --- a/src/headless/plugins/blocking/utils.js +++ b/src/headless/plugins/blocking/utils.js @@ -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); } diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js index 0e9eac7dd3..c31351b9b3 100644 --- a/src/headless/plugins/chat/model.js +++ b/src/headless/plugins/chat/model.js @@ -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 } }, @@ -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(); }, diff --git a/src/plugins/chatview/bottom-panel.js b/src/plugins/chatview/bottom-panel.js index 114cb69bd2..a934f499aa 100644 --- a/src/plugins/chatview/bottom-panel.js +++ b/src/plugins/chatview/bottom-panel.js @@ -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)); diff --git a/src/plugins/chatview/message-form.js b/src/plugins/chatview/message-form.js index 3ce9e80d6e..656f1321b0 100644 --- a/src/plugins/chatview/message-form.js +++ b/src/plugins/chatview/message-form.js @@ -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) { diff --git a/src/plugins/chatview/templates/bottom-panel.js b/src/plugins/chatview/templates/bottom-panel.js index f60df5ffff..0a09124bcd 100644 --- a/src/plugins/chatview/templates/bottom-panel.js +++ b/src/plugins/chatview/templates/bottom-panel.js @@ -14,7 +14,7 @@ export default (o) => { return html` ${ o.model.ui.get('scrolled') && o.model.get('num_unread') ? html`
o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼
` : '' } - ${api.settings.get('show_toolbar') ? html` + ${api.settings.get('show_toolbar') && !o.model.get('contact_blocked') ? html` { - 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`
+
{ ${ show_send_button ? 'chat-textarea-send-button' : '' } ${ o.composing_spoiler ? 'spoiler' : '' }" placeholder="${label_message}">${ o.message_value || '' } +
`; } diff --git a/src/plugins/profile/blocked.js b/src/plugins/profile/blocked.js index 532b2956b5..a30861ce87 100644 --- a/src/plugins/profile/blocked.js +++ b/src/plugins/profile/blocked.js @@ -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 { @@ -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`` } diff --git a/src/plugins/profile/templates/profile_modal.js b/src/plugins/profile/templates/profile_modal.js index ca71e30112..012133f61b 100644 --- a/src/plugins/profile/templates/profile_modal.js +++ b/src/plugins/profile/templates/profile_modal.js @@ -1,6 +1,6 @@ import "shared/components/image-picker.js"; import { __ } from 'i18n'; -import { _converse } from "@converse/headless/core"; +import { _converse, api } from "@converse/headless/core"; import { html } from "lit"; @@ -57,7 +57,9 @@ export default (el) => { ` ); - if (_converse.pluggable.plugins['converse-blocking']?.enabled(_converse)) { + const blocking_available = (_converse.pluggable.plugins['converse-blocking']?.enabled(_converse) && api.isBlockingAvailable()); + + if (blocking_available) { navigation_tabs.push(html`` ); - const blocking_available = (_converse.pluggable.plugins['converse-blocking']?.enabled(_converse) && api.isBlockingAvailable()); - - if (blocking_available) { + if (el.isBlockingAvailable()) { navigation_tabs.push(html`` ); - if (el.isBlockingAvailable()) { + if (el.blocked_jids.length > 0) { navigation_tabs.push(html`