Skip to content
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
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Next release

- #3815: Removal of boomark leads to new bookmark named `Symbol(lit-nothing)`
- #3824: Dates and times are not translated
- #3829: Rich text from LibreOffice Calc is sent as screenshots
- #3830: The message textarea blocks undo of the pasted text
Expand Down
44 changes: 37 additions & 7 deletions src/headless/plugins/bookmarks/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ class Bookmarks extends Collection {
.then((bm) => this.markRoomAsBookmarked(bm))
.catch((e) => log.fatal(e))
);
this.on('remove', this.leaveRoom, this);
this.on('change:autojoin', this.onAutoJoinChanged, this);
this.on(
'remove',
/** @param {Bookmark} bookmark */
(_, bookmark) => this.sendBookmarkStanza(bookmark),
this
/** @param { Bookmark } bookmark }*/ (bookmark) => {
this.sendRemoveBookmarkStanza(bookmark);
this.leaveRoom(bookmark);
}
);

const { session } = _converse;
Expand Down Expand Up @@ -132,12 +132,42 @@ class Bookmarks extends Collection {
}

/**
* @param {'urn:xmpp:bookmarks:1'|'storage:bookmarks'} node
* @param {Bookmark} bookmark
* @returns {Promise<void|Element>}
*/
async sendRemoveBookmarkStanza(bookmark) {
const bare_jid = _converse.session.get('bare_jid');
const node = (await api.disco.supports(`${Strophe.NS.BOOKMARKS2}#compat`, bare_jid))
? Strophe.NS.BOOKMARKS2
: Strophe.NS.BOOKMARKS;

if (node === Strophe.NS.BOOKMARKS2) {
const stanza = stx`
<iq from="${bare_jid}"
to="${bare_jid}"
type="set"
xmlns="jabber:client">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<retract node="${node}" notify="true">
<item id="${bookmark.get('jid')}"/>
</retract>
</pubsub>
</iq>`;
return api.sendIQ(stanza);
}

return this.sendBookmarkStanza().catch((iq) => this.onBookmarkError(iq));
}

/**
* @param {'urn:xmpp:bookmarks:1'|'storage:bookmarks'} node
* @param {Bookmark} [bookmark]
* @returns {Stanza|Stanza[]}
*/
getPublishedItems(node, bookmark) {
if (node === Strophe.NS.BOOKMARKS2) {
if (!bookmark) throw new Error('getPublishedItems: missing bookmark');

const extensions = bookmark.get('extensions') ?? [];
return stx`<item id="${bookmark.get('jid')}">
<conference xmlns="${Strophe.NS.BOOKMARKS2}"
Expand Down Expand Up @@ -169,7 +199,7 @@ class Bookmarks extends Collection {
}

/**
* @param {Bookmark} bookmark
* @param {Bookmark} [bookmark]
* @returns {Promise<void|Element>}
*/
async sendBookmarkStanza(bookmark) {
Expand All @@ -190,7 +220,7 @@ class Bookmarks extends Collection {
* @param {Element} iq
*/
onBookmarkError(iq) {
log.error('Error while trying to add bookmark');
log.error('Error while trying to update bookmarks');
log.error(iq);
}

Expand Down
61 changes: 61 additions & 0 deletions src/headless/plugins/bookmarks/tests/bookmarks.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,4 +468,65 @@ describe("A bookmark", function () {
expect(result).toBe(true);
})
);

it(
'can be removed and sends out a retract stanza',
mock.initConverse(['connected', 'chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilDiscoConfirmed(
_converse,
_converse.bare_jid,
[{ 'category': 'pubsub', 'type': 'pep' }],
['http://jabber.org/protocol/pubsub#publish-options', 'urn:xmpp:bookmarks:1#compat']
);
await mock.waitUntilBookmarksReturned(_converse);

const bare_jid = _converse.session.get('bare_jid');
const muc_jid = '[email protected]';
const { api } = _converse;

// First create a bookmark
await api.bookmarks.set({
jid: muc_jid,
autojoin: true,
name: 'The Play',
nick: 'romeo',
});

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

// Server acknowledges successful storage
const result_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(result_stanza));

// Clear previous stanzas
while (IQ_stanzas.length) IQ_stanzas.pop();

// Now remove the bookmark
const bookmark = _converse.state.bookmarks.findWhere({ jid: muc_jid });
expect(bookmark).toBeTruthy();
_converse.state.bookmarks.remove(bookmark);

// Check that a retract stanza is sent as per XEP-0402
sent_stanza = await u.waitUntil(() =>
IQ_stanzas.filter((s) => sizzle('retract[node="urn:xmpp:bookmarks:1"]', s).length).pop()
);

expect(sent_stanza).toEqualStanza(stx`
<iq from="${bare_jid}" to="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<retract node="urn:xmpp:bookmarks:1" notify="true">
<item id="${muc_jid}"/>
</retract>
</pubsub>
</iq>`);
})
);
});
101 changes: 101 additions & 0 deletions src/headless/plugins/bookmarks/tests/deprecated.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,105 @@ describe("A bookmark", function () {
</pubsub>
</iq>`);
}));

it("can be removed and republishes all remaining bookmarks as per XEP-0048", mock.initConverse(
['connected', 'chatBoxesFetched'], {}, async function (_converse) {

await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilBookmarksReturned(
_converse,
[],
['http://jabber.org/protocol/pubsub#publish-options', 'http://jabber.org/protocol/pubsub#config-node-max'],
'storage:bookmarks'
);

const bare_jid = _converse.session.get('bare_jid');
const muc1_jid = '[email protected]';
const muc2_jid = '[email protected]';
const { bookmarks } = _converse.state;

// First create two bookmarks
bookmarks.setBookmark({
jid: muc1_jid,
autojoin: true,
name: 'Hamlet',
nick: ''
});

const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
let sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('item[id="current"]', s).length).pop());

// Server acknowledges successful storage
const result_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(result_stanza));

bookmarks.setBookmark({
jid: muc2_jid,
autojoin: true,
name: 'Balcony',
nick: 'romeo'
});

sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('item[id="current"] conference[name="Balcony"]', s).length).pop());

// Server acknowledges successful storage
const result_stanza2 = 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(result_stanza2));

// Clear previous stanzas
while (IQ_stanzas.length) { IQ_stanzas.pop(); }

// Now remove one bookmark
const bookmark = bookmarks.findWhere({jid: muc1_jid});
expect(bookmark).toBeTruthy();
bookmarks.remove(bookmark);

// Check that a stanza is sent with all remaining bookmarks (XEP-0048 style)
sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('publish[node="storage:bookmarks"]', s).length).pop());

expect(sent_stanza).toEqualStanza(stx`
<iq from="${bare_jid}" to="${bare_jid}" id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<publish node="storage:bookmarks">
<item id="current">
<storage xmlns="storage:bookmarks">
<conference autojoin="true" jid="${muc2_jid}" name="Balcony">
<nick>romeo</nick>
</conference>
</storage>
</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>`);
}));
});
13 changes: 9 additions & 4 deletions src/headless/types/plugins/bookmarks/collection.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,21 @@ declare class Bookmarks extends Collection<Bookmark> {
*/
setBookmark(attrs: import("./types").BookmarkAttrs, create?: boolean, options?: import("@converse/skeletor").FetchOrCreateOptions): Promise<void>;
/**
* @param {'urn:xmpp:bookmarks:1'|'storage:bookmarks'} node
* @param {Bookmark} bookmark
* @returns {Promise<void|Element>}
*/
sendRemoveBookmarkStanza(bookmark: Bookmark): Promise<void | Element>;
/**
* @param {'urn:xmpp:bookmarks:1'|'storage:bookmarks'} node
* @param {Bookmark} [bookmark]
* @returns {Stanza|Stanza[]}
*/
getPublishedItems(node: "urn:xmpp:bookmarks:1" | "storage:bookmarks", bookmark: Bookmark): Stanza | Stanza[];
getPublishedItems(node: "urn:xmpp:bookmarks:1" | "storage:bookmarks", bookmark?: Bookmark): Stanza | Stanza[];
/**
* @param {Bookmark} bookmark
* @param {Bookmark} [bookmark]
* @returns {Promise<void|Element>}
*/
sendBookmarkStanza(bookmark: Bookmark): Promise<void | Element>;
sendBookmarkStanza(bookmark?: Bookmark): Promise<void | Element>;
/**
* @param {Element} iq
*/
Expand Down
Loading