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

Support for XEP-0191 Blocking Command #3574

Merged
merged 20 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .aiderignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Add files and directories to ignore by Aider
node_modules/
dist/
build/
*.log
*.tmp
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ node_modules
# Builds
.sv?
/vendor/
.aider*
8 changes: 7 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@
- #1057: Removed the `mobile` view mode. Instead of setting `view_mode` to `mobile`, set it to `fullscreen`.
- #1174: Show MUC avatars in the rooms list
- #1195: Add actions to quote and copy messages
- #1303: Display non-contacts who sent us a message somehow in fullscreen
- #1349: XEP-0392 Consistent Color Generation
- #2383: Add modal to start chats with JIDs not in the roster
- #2586: Add support for XEP-0402 Bookmarks
- #2623: Merge MUC join and bookmark, leave and unset autojoin
- #2716: Fix issue with chat display when opening via URL
- #2980: Allow setting an avatar for MUCs
- #3033: Add the `muc_grouped_by_domain` option to display MUCs on the same domain in collapsible groups
- #3038: Message to self from other client is ignored
- #3038: Support showing yourself in the MUC sidebar. Adds new config option `muc_show_self`.
- #3100: fixed width `.box-flyout` breaks responsive design in embedded, mobile viewport mode.
- #3038: Support showing yourself in the MUC sidebar. Adds new config option `muc_show_self`.
- #3155: Some ad-hoc commands not working
- #3155: Some adhoc commands aren't working
- #3299: Registration fails when a password contains an &
Expand All @@ -43,7 +48,8 @@
- Fix: trying to use emojis with an uppercase letter breaks the message field.
- Fix: renaming getEmojisByAtrribute to getEmojisByAttribute.

### Changes
### Changes and features
- Add support for XEP-0191 Blocking Command
- Upgrade to Bootstrap 5
- Add an occupants filter to the MUC sidebar
- Change contacts filter to rename the anachronistic `Online` state to `Available`.
Expand Down
6 changes: 6 additions & 0 deletions conversejs.doap
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html"/>
<xmpp:since>11.0.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/>
Expand Down
2 changes: 2 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module.exports = function(config) {
},
{ pattern: "src/shared/tests/mock.js", type: 'module' },

{ pattern: "src/headless/plugins/blocklist/tests/blocklist.js", type: 'module' },
{ pattern: "src/headless/plugins/bookmarks/tests/bookmarks.js", type: 'module' },
{ pattern: "src/headless/plugins/bookmarks/tests/deprecated.js", type: 'module' },
{ pattern: "src/headless/plugins/caps/tests/caps.js", type: 'module' },
Expand Down Expand Up @@ -125,6 +126,7 @@ module.exports = function(config) {
{ pattern: "src/plugins/roomslist/tests/grouplists.js", type: 'module' },
{ pattern: "src/plugins/rootview/tests/root.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/add-contact-modal.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/new-chat-modal.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/presence.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/protocol.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/roster.js", type: 'module' },
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/headless/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import log from './log.js';

export { EmojiPicker } from './plugins/emoji/index.js';
export { Bookmark, Bookmarks } from './plugins/bookmarks/index.js'; // XEP-0199 XMPP Ping
import './plugins/blocklist/index.js';
import './plugins/bosh/index.js'; // XEP-0206 BOSH
import './plugins/caps/index.js'; // XEP-0115 Entity Capabilities
export { ChatBox, Message, Messages } from './plugins/chat/index.js'; // RFC-6121 Instant messaging
Expand Down
2 changes: 1 addition & 1 deletion src/headless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"pluggable.js": "3.0.1",
"sizzle": "^2.3.5",
"sprintf-js": "^1.1.2",
"strophe.js": "strophe/strophejs#9b643a43b5cd79f0d8a2e0015e9d2c0d0fec8578",
"strophe.js": "strophe/strophejs#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5",
"urijs": "^1.19.10"
},
"devDependencies": {}
Expand Down
51 changes: 51 additions & 0 deletions src/headless/plugins/blocklist/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import promise_api from '../../shared/api/promise.js';
import { sendBlockStanza, sendUnblockStanza } from './utils.js';

const { waitUntil } = promise_api;

/**
* Groups methods relevant to XEP-0191 Blocking Command
* @namespace api.blocklist
* @memberOf api
*/
const blocklist = {
/**
* Retrieves the current user's blocklist
* @returns {Promise<import('./collection').default>}
*/
async get() {
return await waitUntil('blocklistInitialized');
},

/**
* Adds a new entity to the blocklist
* @param {string|string[]} jid
* @param {boolean} [send_stanza=true]
* @returns {Promise<import('./collection').default>}
*/
async add(jid, send_stanza = true) {
const blocklist = await waitUntil('blocklistInitialized');
const jids = Array.isArray(jid) ? jid : [jid];
if (send_stanza) await sendBlockStanza(jids);
jids.forEach((jid) => blocklist.create({ jid }));
return blocklist;
},

/**
* Removes an entity from the blocklist
* @param {string|string[]} jid
* @param {boolean} [send_stanza=true]
* @returns {Promise<import('./collection').default>}
*/
async remove(jid, send_stanza = true) {
const blocklist = await waitUntil('blocklistInitialized');
const jids = Array.isArray(jid) ? jid : [jid];
if (send_stanza) await sendUnblockStanza(jids);
blocklist.remove(jids);
return blocklist;
},
};

const blocklist_api = { blocklist };

export default blocklist_api;
103 changes: 103 additions & 0 deletions src/headless/plugins/blocklist/collection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { getOpenPromise } from '@converse/openpromise';
import { Collection } from '@converse/skeletor';
import log from '../../log.js';
import _converse from '../../shared/_converse.js';
import { initStorage } from '../../utils/storage.js';
import api from '../../shared/api/index.js';
import converse from '../../shared/api/public.js';
import BlockedEntity from './model.js';

const { stx, u } = converse.env;

class Blocklist extends Collection {
get idAttribute() {
return 'jid';
}

constructor() {
super();
this.model = BlockedEntity;
}

async initialize() {
const { session } = _converse;
const cache_key = `converse.blocklist-${session.get('bare_jid')}`;
this.fetched_flag = `${cache_key}-fetched`;
initStorage(this, cache_key);

this.on('add', this.rejectContactRequest);

await this.fetchBlocklist();

/**
* Triggered once the {@link Blocklist} collection
* has been created and cached blocklist have been fetched.
* @event _converse#blocklistInitialized
* @type {Blocklist}
* @example _converse.api.listen.on('blocklistInitialized', (blocklist) => { ... });
*/
api.trigger('blocklistInitialized', this);
}

/**
* @param {BlockedEntity} item
*/
async rejectContactRequest(item) {
const roster = await api.waitUntil('rosterContactsFetched');
const contact = roster.get(item.get('jid'));
if (contact?.get('requesting')) {
const chat = await api.chats.get(contact.get('jid'));
chat?.close();
contact.unauthorize().destroy();
}
}

fetchBlocklist() {
const deferred = getOpenPromise();
if (window.sessionStorage.getItem(this.fetched_flag)) {
this.fetch({
success: () => deferred.resolve(),
error: () => deferred.resolve(),
});
} else {
this.fetchBlocklistFromServer(deferred);
}
return deferred;
}

/**
* @param {Object} deferred
*/
async fetchBlocklistFromServer(deferred) {
const stanza = stx`<iq xmlns="jabber:client"
type="get"
id="${u.getUniqueId()}"><blocklist xmlns="urn:xmpp:blocking"/></iq>`;

try {
this.onBlocklistReceived(deferred, await api.sendIQ(stanza));
} catch (e) {
log.error(e);
deferred.resolve();
return;
}
}

/**
* @param {Object} deferred
* @param {Element} iq
*/
async onBlocklistReceived(deferred, iq) {
Array.from(iq.querySelectorAll('blocklist item')).forEach((item) => {
const jid = item.getAttribute('jid');
const blocked = this.get(jid);
blocked ? blocked.save({ jid }) : this.create({ jid });
});

window.sessionStorage.setItem(this.fetched_flag, 'true');
if (deferred !== undefined) {
return deferred.resolve();
}
}
}

export default Blocklist;
1 change: 1 addition & 0 deletions src/headless/plugins/blocklist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './plugin.js';
16 changes: 16 additions & 0 deletions src/headless/plugins/blocklist/model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Model } from '@converse/skeletor';
import converse from '../../shared/api/public.js';

const { Strophe } = converse.env;

class BlockedEntity extends Model {
get idAttribute () {
return 'jid';
}

getDisplayName () {
return Strophe.xmlunescape(this.get('name'));
}
}

export default BlockedEntity;
88 changes: 88 additions & 0 deletions src/headless/plugins/blocklist/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
* @description Adds support for XEP-0191 Blocking Command
*/
import _converse from '../../shared/_converse.js';
import api from '../../shared/api/index.js';
import converse from '../../shared/api/public.js';
import log from '../../log.js';
import Blocklist from './collection.js';
import BlockedEntity from './model.js';
import blocklist_api from './api.js';

const { Strophe, sizzle } = converse.env;

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

converse.plugins.add('converse-blocklist', {
dependencies: ['converse-disco'],

initialize() {
const exports = { Blocklist, BlockedEntity };
Object.assign(_converse.exports, exports);
Object.assign(api, blocklist_api);

api.promises.add(['blocklistInitialized']);

api.listen.on(
'getErrorAttributesForMessage',
/**
* @param {import('plugins/chat/types').MessageAttributes} attrs
* @param {import('plugins/chat/types').MessageErrorAttributes} new_attrs
*/
(attrs, new_attrs) => {
if (attrs.errors.find((e) => e.name === 'blocked' && e.xmlns === `${Strophe.NS.BLOCKING}:errors`)) {
const { __ } = _converse;
new_attrs.error = __('You are blocked from sending messages.');
}
return new_attrs;
}
);

api.listen.on('connected', () => {
const connection = api.connection.get();
connection.addHandler(
/** @param {Element} stanza */ (stanza) => {
const bare_jid = _converse.session.get('bare_jid');
const from = stanza.getAttribute('from');
if (Strophe.getBareJidFromJid(from ?? bare_jid) != bare_jid) {
log.warn(`Received a blocklist push stanza from a suspicious JID ${from}`);
return true;
}

const add_jids = sizzle(`block[xmlns="${Strophe.NS.BLOCKING}"] item`, stanza).map(
/** @param {Element} item */ (item) => item.getAttribute('jid')
);
if (add_jids.length) api.blocklist.add(add_jids, false);

const remove_jids = sizzle(`unblock[xmlns="${Strophe.NS.BLOCKING}"] item`, stanza).map(
/** @param {Element} item */ (item) => item.getAttribute('jid')
);
if (remove_jids.length) api.blocklist.remove(remove_jids, false);

return true;
},
Strophe.NS.BLOCKING,
'iq',
'set'
);
});

api.listen.on('clearSession', () => {
const { state } = _converse;
if (state.blocklist) {
state.blocklist.clearStore({ 'silent': true });
window.sessionStorage.removeItem(state.blocklist.fetched_flag);
delete state.blocklist;
}
});

api.listen.on('discoInitialized', async () => {
const domain = _converse.session.get('domain');
if (await api.disco.supports(Strophe.NS.BLOCKING, domain)) {
_converse.state.blocklist = new _converse.exports.Blocklist();
}
});
},
});
Loading
Loading