Skip to content

Commit

Permalink
Add support for XEP-0191 Blocking Command
Browse files Browse the repository at this point in the history
  • Loading branch information
jcbrand committed Jan 5, 2025
1 parent 5586d49 commit 76b2ad5
Show file tree
Hide file tree
Showing 39 changed files with 624 additions and 130 deletions.
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,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
1 change: 1 addition & 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
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;
88 changes: 88 additions & 0 deletions src/headless/plugins/blocklist/collection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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);

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

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;
73 changes: 73 additions & 0 deletions src/headless/plugins/blocklist/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @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-blocking', {
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('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

0 comments on commit 76b2ad5

Please sign in to comment.