Skip to content

Commit

Permalink
Upon nickname change, set new nick on the MUC bookmark
Browse files Browse the repository at this point in the history
- Adds a new bookmarks API
  • Loading branch information
jcbrand committed Jan 3, 2025
1 parent 0639980 commit 5586d49
Show file tree
Hide file tree
Showing 25 changed files with 537 additions and 301 deletions.
40 changes: 40 additions & 0 deletions src/headless/plugins/bookmarks/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import _converse from '../../shared/_converse.js';
import promise_api from '../../shared/api/promise.js';

const { waitUntil } = promise_api;

/**
* Groups methods relevant to XEP-0402 MUC bookmarks.
*
* @namespace api.bookmarks
* @memberOf api
*/
const bookmarks = {
/**
* Calling this function will result in an IQ stanza being sent out to set
* the bookmark on the server.
*
* @method api.bookmarks.set
* @param {import('./types').BookmarkAttrs} attrs - The room attributes
* @param {boolean} create=true - Whether the bookmark should be created if it doesn't exist
* @returns {Promise<import('./model').default>}
*/
async set(attrs, create = true) {
const bookmarks = await waitUntil('bookmarksInitialized');
return bookmarks.setBookmark(attrs, create);
},

/**
* @method api.bookmarks.get
* @param {string} jid - The JID of the bookmark to return.
* @returns {Promise<import('./model').default>}
*/
async get(jid) {
const bookmarks = await waitUntil('bookmarksInitialized');
return bookmarks.get(jid);
},
};

const bookmarks_api = { bookmarks };

export default bookmarks_api;
93 changes: 59 additions & 34 deletions src/headless/plugins/bookmarks/plugin.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
/**
* @copyright 2022, the Converse.js contributors
* @copyright 2025, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import "../../plugins/muc/index.js";
import Bookmark from './model.js';
import Bookmarks from './collection.js';
import _converse from '../../shared/_converse.js';
import api from '../../shared/api/index.js';
import converse from '../../shared/api/public.js';
import { initBookmarks, getNicknameFromBookmark, handleBookmarksPush } from './utils.js';
import '../../plugins/muc/index.js';
import log from '../../log';
import bookmarks_api from './api.js';

const { Strophe } = converse.env;

Strophe.addNamespace('BOOKMARKS', 'storage:bookmarks');
Strophe.addNamespace('BOOKMARKS2', 'urn:xmpp:bookmarks:1');


converse.plugins.add('converse-bookmarks', {

dependencies: ["converse-chatboxes", "converse-muc"],
dependencies: ['converse-chatboxes', 'converse-muc'],

overrides: {
// Overrides mentioned here will be picked up by converse.js's
Expand All @@ -27,7 +27,7 @@ converse.plugins.add('converse-bookmarks', {
// New functions which don't exist yet can also be added.

ChatRoom: {
getDisplayName () {
getDisplayName() {
const { _converse, getDisplayName } = this.__super__;
const { bookmarks } = _converse.state;
const bookmark = this.get('bookmarked') ? bookmarks?.get(this.get('jid')) : null;
Expand All @@ -37,40 +37,66 @@ converse.plugins.add('converse-bookmarks', {
/**
* @param {string} nick
*/
getAndPersistNickname (nick) {
getAndPersistNickname(nick) {
nick = nick || getNicknameFromBookmark(this.get('jid'));
return this.__super__.getAndPersistNickname.call(this, nick);
}
}
},
},
},

initialize () {
initialize() {
// Configuration values for this plugin
// ====================================
// Refer to docs/source/configuration.rst for explanations of these
// configuration settings.
api.settings.extend({
allow_bookmarks: true,
allow_public_bookmarks: false,
muc_respect_autojoin: true
muc_respect_autojoin: true,
});

api.promises.add('bookmarksInitialized');

const exports = { Bookmark, Bookmarks };
Object.assign(api, bookmarks_api);

const exports = { Bookmark, Bookmarks };
Object.assign(_converse, exports); // TODO: DEPRECATED
Object.assign(_converse.exports, exports);

api.listen.on(
'parseMUCPresence',
/**
* @param {Element} _stanza
* @param {import('../muc/types').MUCPresenceAttributes} attrs
*/
(_stanza, attrs) => {
if (attrs.is_self && attrs.codes.includes('303')) {
api.bookmarks.get(attrs.muc_jid).then(
/** @param {Bookmark} bookmark */ (bookmark) => {
if (!bookmark) log.warn('parseMUCPresence: no bookmark returned');

const { nick, muc_jid: jid } = attrs;
api.bookmarks.set({
jid,
nick,
autojoin: bookmark?.get('autojoin') ?? true,
password: bookmark?.get('password') ?? '',
name: bookmark?.get('name') ?? '',
extensions: bookmark?.get('extensions') ?? [],
});
}
);
}
return attrs;
}
);

api.listen.on(
'enteredNewRoom',
/** @param {import('../muc/muc').default} muc */
({ attributes }) => {
const { bookmarks } = _converse.state;
if (!bookmarks) return;

const { jid, nick, password, name } = /** @type {import("../muc/types").MUCAttributes} */(attributes);

bookmarks.setBookmark({
async ({ attributes }) => {
const { jid, nick, password, name } = /** @type {import("../muc/types").MUCAttributes} */ (attributes);
await api.bookmarks.set({
jid,
autojoin: true,
nick,
Expand All @@ -83,35 +109,34 @@ converse.plugins.add('converse-bookmarks', {
api.listen.on(
'leaveRoom',
/** @param {import('../muc/muc').default} muc */
({ attributes }) => {
const { bookmarks } = _converse.state;
if (!bookmarks) return;

const { jid } = /** @type {import("../muc/types").MUCAttributes} */(attributes);

bookmarks.setBookmark({
jid,
autojoin: false,
}, false);
async ({ attributes }) => {
const { jid } = /** @type {import("../muc/types").MUCAttributes} */ (attributes);
await api.bookmarks.set(
{
jid,
autojoin: false,
},
false
);
}
);

api.listen.on('addClientFeatures', () => {
if (api.settings.get('allow_bookmarks')) {
api.disco.own.features.add(Strophe.NS.BOOKMARKS + '+notify')
api.disco.own.features.add(Strophe.NS.BOOKMARKS + '+notify');
}
})
});

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

api.listen.on('connected', async () => {
api.listen.on('connected', async () => {
// Add a handler for bookmarks pushed from other connected clients
const bare_jid = _converse.session.get('bare_jid');
const connection = api.connection.get();
Expand All @@ -120,5 +145,5 @@ converse.plugins.add('converse-bookmarks', {
await Promise.all([api.waitUntil('chatBoxesFetched')]);
initBookmarks();
});
}
},
});
130 changes: 110 additions & 20 deletions src/headless/plugins/bookmarks/tests/bookmarks.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
/* global mock, converse */
const { Strophe, sizzle, stx, u } = converse.env;

describe("A chat room", function () {

describe("A bookmark", function () {

beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));

it("is automatically bookmarked when opened", mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
const { bare_jid } = _converse;
it("is automatically created when a MUC is entered", mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilDiscoConfirmed(
_converse, bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
[
'http://jabber.org/protocol/pubsub#publish-options',
'urn:xmpp:bookmarks:1#compat'
]
);
await mock.waitUntilBookmarksReturned(_converse);

const nick = 'JC';
const muc_jid = 'theplay@conference.shakespeare.lit';
Expand Down Expand Up @@ -64,9 +57,6 @@ describe("A chat room", function () {
</iq>`
);

/* Server acknowledges successful storage
* <iq to='juliet@capulet.lit/balcony' type='result' id='pip1'/>
*/
const stanza = stx`<iq
xmlns="jabber:client"
to="${_converse.api.connection.get().jid}"
Expand All @@ -76,12 +66,111 @@ describe("A chat room", function () {

expect(muc.get('bookmarked')).toBeTruthy();
}));
});

it("will be updated when a user changes their nickname in a MUC", mock.initConverse(
[], {}, async function (_converse) {

describe("A bookmark", function () {
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilBookmarksReturned(_converse);

beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
const nick = 'JC';
const muc_jid = 'theplay@conference.shakespeare.lit';
const settings = { name: "Play's the thing", password: 'secret' };
const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, nick, [], [], true, settings);

const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
let sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('iq publish[node="urn:xmpp:bookmarks:1"]', s).length).pop());

const stanza = stx`<iq
xmlns="jabber:client"
to="${_converse.api.connection.get().jid}"
type="result"
id="${sent_stanza.getAttribute('id')}"/>`;
_converse.api.connection.get()._dataRecv(mock.createRequest(stanza));

const newnick = 'BAP';
muc.setNickname(newnick);

const sent_IQs = _converse.api.connection.get().IQ_stanzas;
while (sent_IQs.length) { sent_IQs.pop(); }

_converse.api.connection.get()._dataRecv(mock.createRequest(
stx`<presence
xmlns="jabber:server"
from='${muc_jid}/${nick}'
id='DC352437-C019-40EC-B590-AF29E879AF98'
to='${_converse.jid}'
type='unavailable'>
<x xmlns='http://jabber.org/protocol/muc#user'>
<item affiliation='member'
jid='${_converse.jid}'
nick='${newnick}'
role='participant'/>
<status code='303'/>
<status code='110'/>
</x>
</presence>`
));

await u.waitUntil(() => muc.get('nick') === newnick);

_converse.api.connection.get()._dataRecv(mock.createRequest(
stx`<presence
xmlns="jabber:server"
from='${muc_jid}/${newnick}'
id='5B4F27A4-25ED-43F7-A699-382C6B4AFC67'
to='${_converse.jid}'>
<x xmlns='http://jabber.org/protocol/muc#user'>
<item affiliation='member'
jid='${_converse.jid}'
role='participant'/>
<status code='110'/>
</x>
</presence>`
));

sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('iq publish[node="urn:xmpp:bookmarks:1"]', s).length).pop());

expect(sent_stanza).toEqualStanza(
stx`<iq from="${_converse.bare_jid}"
to="${_converse.bare_jid}"
id="${sent_stanza.getAttribute('id')}"
type="set"
xmlns="jabber:client">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<publish node="urn:xmpp:bookmarks:1">
<item id="${muc_jid}">
<conference xmlns="urn:xmpp:bookmarks:1" name="${settings.name}" autojoin="true">
<nick>${newnick}</nick>
<password>${settings.password}</password>
</conference>
</item>
</publish>
<publish-options>
<x type="submit" xmlns="jabber:x:data">
<field type="hidden" var="FORM_TYPE">
<value>http://jabber.org/protocol/pubsub#publish-options</value>
</field>
<field var='pubsub#persist_items'>
<value>true</value>
</field>
<field var='pubsub#max_items'>
<value>max</value>
</field>
<field var='pubsub#send_last_published_item'>
<value>never</value>
</field>
<field var='pubsub#access_model'>
<value>whitelist</value>
</field>
</x>
</publish-options>
</pubsub>
</iq>`
);
}));

describe("when autojoin is set", function () {

Expand Down Expand Up @@ -203,8 +292,9 @@ describe("A bookmark", function () {
const bare_jid = _converse.session.get('bare_jid');
const muc1_jid = 'theplay@conference.shakespeare.lit';
const { bookmarks } = _converse.state;
const { api } = _converse;

bookmarks.setBookmark({
await api.bookmarks.set({
jid: muc1_jid,
autojoin: true,
name: 'Hamlet',
Expand Down Expand Up @@ -247,7 +337,7 @@ describe("A bookmark", function () {


const muc2_jid = 'balcony@conference.shakespeare.lit';
bookmarks.setBookmark({
await api.bookmarks.set({
jid: muc2_jid,
autojoin: true,
name: 'Balcony',
Expand Down Expand Up @@ -293,7 +383,7 @@ describe("A bookmark", function () {
</iq>`);

const muc3_jid = 'garden@conference.shakespeare.lit';
bookmarks.setBookmark({
await api.bookmarks.set({
jid: muc3_jid,
autojoin: false,
name: 'Garden',
Expand Down
Loading

0 comments on commit 5586d49

Please sign in to comment.