Changeset

6032:a9fe4a50f935

mod_bookmarks2: remove merged module
author Menel <menel@snikket.de>
date Thu, 31 Oct 2024 13:53:48 +0100
parents 6031:2c6b14207271
children 6033:8cb37a497e4c
files mod_bookmarks2/README.md mod_bookmarks2/mod_bookmarks2.lua mod_bookmarks2/tests/bookmarks2.scs mod_bookmarks2/tests/conversion.scs
diffstat 4 files changed, 3 insertions(+), 894 deletions(-) [+]
line wrap: on
line diff
--- a/mod_bookmarks2/README.md	Thu Oct 31 13:48:22 2024 +0100
+++ b/mod_bookmarks2/README.md	Thu Oct 31 13:53:48 2024 +0100
@@ -1,37 +1,10 @@
 ---
 labels:
 - 'Stage-Merged'
-summary: Synchronise bookmarks between Private XML, legacy PEP, and PEP
+summary: Synchronise bookmarks between Private XML, legacy PEP, and pep, XEP-0048 and XEP-0402
 ...
 
 ::: {.alert .alert-info}
-**Deprecatation notice:** This module has been merged into Prosody as
-[mod_bookmarks][doc:modules:mod_bookmarks]. Users of Prosody **0.12**
-and later should switch to that.
+This module has been merged into Prosody since version 0.12,
+see [mod_bookmarks][doc:modules:mod_bookmarks].
 :::
-
-Introduction
-------------
-
-This module fetches users’ bookmarks from Private XML (or legacy PEP) and
-pushes them to PEP on login, and then redirects any Private XML query (or
-legacy PEP) to PEP.  This allows interoperability between older clients that
-use [XEP-0048](https://xmpp.org/extensions/xep-0048.html) and recent clients
-which use [XEP-0402](https://xmpp.org/extensions/xep-0402.html).
-
-Configuration
--------------
-
-Simply [enable it like most other
-modules](https://prosody.im/doc/installing_modules#prosody-modules), no
-further configuration is needed.
-
-Compatibility
--------------
-
-  ------- -----------------------------------------
-  0.12    [Use the official mod_bookmarks module instead][doc:modules:mod_bookmarks]
-  0.11    Works
-  0.10    Does not work
-  0.9     Does not work
-  ------- -----------------------------------------
--- a/mod_bookmarks2/mod_bookmarks2.lua	Thu Oct 31 13:48:22 2024 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,429 +0,0 @@
-
-local st = require "util.stanza";
-local jid_split = require "util.jid".split;
-
-local mod_pep = module:depends "pep";
-local private_storage = module:open_store("private", "map");
-
-local namespace = "urn:xmpp:bookmarks:1";
-local namespace_private = "jabber:iq:private";
-local namespace_legacy = "storage:bookmarks";
-
-local default_options = {
-	["persist_items"] = true;
-	["max_items"] = "max";
-	["send_last_published_item"] = "never";
-	["access_model"] = "whitelist";
-};
-
-if not pcall(mod_pep.check_node_config, nil, nil, default_options) then
-	-- 0.11 or earlier not supporting max_items="max" trows an error here
-	module:log("debug", "Setting max_items=pep_max_items because 'max' is not supported in this version");
-	default_options["max_items"] = module:get_option_number("pep_max_items", 256);
-	default_options["send_last_published_item"] = nil; -- not available in 0.11
-end
-
-module:hook("account-disco-info", function (event)
-	-- This Time it’s Serious!
-	event.reply:tag("feature", { var = namespace.."#compat" }):up();
-	event.reply:tag("feature", { var = namespace.."#compat-pep" }):up();
-end);
-
--- This must be declared on the domain JID, not the account JID.  Note that
--- this isn’t defined in the XEP.
-module:add_feature(namespace_private);
-
-local function generate_legacy_storage(items)
-	local storage = st.stanza("storage", { xmlns = namespace_legacy });
-	for _, item_id in ipairs(items) do
-		local item = items[item_id];
-		local bookmark = item:get_child("conference", namespace);
-		local conference = st.stanza("conference", {
-			jid = item.attr.id,
-			name = bookmark.attr.name,
-			autojoin = bookmark.attr.autojoin,
-		});
-		local nick = bookmark:get_child_text("nick");
-		if nick ~= nil then
-			conference:text_tag("nick", nick):up();
-		end
-		local password = bookmark:get_child_text("password");
-		if password ~= nil then
-			conference:text_tag("password", password):up();
-		end
-		storage:add_child(conference);
-	end
-
-	return storage;
-end
-
-local function on_retrieve_legacy_pep(event)
-	local stanza, session = event.stanza, event.origin;
-	local pubsub = stanza:get_child("pubsub", "http://jabber.org/protocol/pubsub");
-	if pubsub == nil then
-		return;
-	end
-
-	local items = pubsub:get_child("items");
-	if items == nil then
-		return;
-	end
-
-	local node = items.attr.node;
-	if node ~= namespace_legacy then
-		return;
-	end
-
-	local username = session.username;
-	local jid = username.."@"..session.host;
-	local service = mod_pep.get_pep_service(username);
-	local ok, ret = service:get_items(namespace, session.full_jid);
-	if not ok then
-		module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, ret);
-		session.send(st.error_reply(stanza, "cancel", ret, "Failed to retrive bookmarks from PEP"));
-		return true;
-	end
-
-	local storage = generate_legacy_storage(ret);
-
-	module:log("debug", "Sending back legacy PEP for %s: %s", jid, storage);
-	session.send(st.reply(stanza)
-		:tag("pubsub", {xmlns = "http://jabber.org/protocol/pubsub"})
-			:tag("items", {node = namespace_legacy})
-				:tag("item", {id = "current"})
-					:add_child(storage));
-	return true;
-end
-
-local function on_retrieve_private_xml(event)
-	local stanza, session = event.stanza, event.origin;
-	local query = stanza:get_child("query", namespace_private);
-	if query == nil then
-		return;
-	end
-
-	local bookmarks = query:get_child("storage", namespace_legacy);
-	if bookmarks == nil then
-		return;
-	end
-
-	module:log("debug", "Getting private bookmarks: %s", bookmarks);
-
-	local username = session.username;
-	local jid = username.."@"..session.host;
-	local service = mod_pep.get_pep_service(username);
-	local ok, ret = service:get_items(namespace, session.full_jid);
-	if not ok then
-		if ret == "item-not-found" then
-			module:log("debug", "Got no PEP bookmarks item for %s, returning empty private bookmarks", jid);
-			session.send(st.reply(stanza):add_child(query));
-		else
-			module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, ret);
-			session.send(st.error_reply(stanza, "cancel", ret, "Failed to retrive bookmarks from PEP"));
-		end
-		return true;
-	end
-
-	local storage = generate_legacy_storage(ret);
-
-	module:log("debug", "Sending back private for %s: %s", jid, storage);
-	session.send(st.reply(stanza):query(namespace_private):add_child(storage));
-	return true;
-end
-
-local function compare_bookmark2(a, b)
-	if a == nil or b == nil then
-		return false;
-	end
-	local a_conference = a:get_child("conference", namespace);
-	local b_conference = b:get_child("conference", namespace);
-	local a_nick = a_conference:get_child_text("nick");
-	local b_nick = b_conference:get_child_text("nick");
-	local a_password = a_conference:get_child_text("password");
-	local b_password = b_conference:get_child_text("password");
-	return (a.attr.id == b.attr.id and
-	        a_conference.attr.name == b_conference.attr.name and
-	        a_conference.attr.autojoin == b_conference.attr.autojoin and
-	        a_nick == b_nick and
-	        a_password == b_password);
-end
-
-local function publish_to_pep(jid, bookmarks, synchronise)
-	local service = mod_pep.get_pep_service(jid_split(jid));
-
-	if #bookmarks.tags == 0 then
-		if synchronise then
-			-- If we set zero legacy bookmarks, purge the bookmarks 2 node.
-			module:log("debug", "No bookmark in the set, purging instead.");
-			local ok, err = service:purge(namespace, jid, true);
-			if not ok and err == "item-not-found" then
-				-- Nothing there already, all is well.
-				return true;
-			end
-			return ok, err;
-		else
-			return true;
-		end
-	end
-
-	-- Retrieve the current bookmarks2.
-	module:log("debug", "Retrieving the current bookmarks 2.");
-	local has_bookmarks2, ret = service:get_items(namespace, jid);
-	local bookmarks2;
-	if not has_bookmarks2 and ret == "item-not-found" then
-		module:log("debug", "Got item-not-found, assuming it was empty until now, creating.");
-		local ok, err = service:create(namespace, jid, default_options);
-		if not ok then
-			module:log("error", "Creating bookmarks 2 node failed: %s", err);
-			return ok, err;
-		end
-		bookmarks2 = {};
-	elseif not has_bookmarks2 then
-		module:log("debug", "Got %s error, aborting.", ret);
-		return false, ret;
-	else
-		module:log("debug", "Got existing bookmarks2.");
-		bookmarks2 = ret;
-
-		local ok, err = service:get_node_config(namespace, jid);
-		if not ok then
-			module:log("error", "Retrieving bookmarks 2 node config failed: %s", err);
-			return ok, err;
-		end
-
-		local options = err;
-		for key, value in pairs(default_options) do
-			if options[key] and options[key] ~= value then
-				module:log("warn", "Overriding bookmarks 2 configuration for %s, from %s to %s", jid, options[key], value);
-				options[key] = value;
-			end
-		end
-
-		local ok, err = service:set_node_config(namespace, jid, options);
-		if not ok then
-			module:log("error", "Setting bookmarks 2 node config failed: %s", err);
-			return ok, err;
-		end
-	end
-
-	-- Get a list of all items we may want to remove.
-	local to_remove = {};
-	for i in ipairs(bookmarks2) do
-		to_remove[bookmarks2[i]] = true;
-	end
-
-	for bookmark in bookmarks:childtags("conference", namespace_legacy) do
-		-- Create the new conference element by copying everything from the legacy one.
-		local conference = st.stanza("conference", {
-			xmlns = namespace,
-			name = bookmark.attr.name,
-			autojoin = bookmark.attr.autojoin,
-		});
-		local nick = bookmark:get_child_text("nick");
-		if nick ~= nil then
-			conference:text_tag("nick", nick):up();
-		end
-		local password = bookmark:get_child_text("password");
-		if password ~= nil then
-			conference:text_tag("password", password):up();
-		end
-
-		-- Create its wrapper.
-		local item = st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = bookmark.attr.jid })
-			:add_child(conference);
-
-		-- Then publish it only if it’s a new one or updating a previous one.
-		if compare_bookmark2(item, bookmarks2[bookmark.attr.jid]) then
-			module:log("debug", "Item %s identical to the previous one, skipping.", item.attr.id);
-			to_remove[bookmark.attr.jid] = nil;
-		else
-			if bookmarks2[bookmark.attr.jid] == nil then
-				module:log("debug", "Item %s not existing previously, publishing.", item.attr.id);
-			else
-				module:log("debug", "Item %s different from the previous one, publishing.", item.attr.id);
-				to_remove[bookmark.attr.jid] = nil;
-			end
-			local ok, err = service:publish(namespace, jid, bookmark.attr.jid, item, default_options);
-			if not ok then
-				module:log("error", "Publishing item %s failed: %s", item.attr.id, err);
-				return ok, err;
-			end
-		end
-	end
-
-	-- Now handle retracting items that have been removed.
-	if synchronise then
-		for id in pairs(to_remove) do
-			module:log("debug", "Item %s removed from bookmarks.", id);
-			local ok, err = service:retract(namespace, jid, id, st.stanza("retract", { id = id }));
-			if not ok then
-				module:log("error", "Retracting item %s failed: %s", id, err);
-				return ok, err;
-			end
-		end
-	end
-	return true;
-end
-
--- Synchronise legacy PEP to PEP.
-local function on_publish_legacy_pep(event)
-	local stanza, session = event.stanza, event.origin;
-	local pubsub = stanza:get_child("pubsub", "http://jabber.org/protocol/pubsub");
-	if pubsub == nil then
-		return;
-	end
-
-	local publish = pubsub:get_child("publish");
-	if publish == nil or publish.attr.node ~= namespace_legacy then
-		return;
-	end
-
-	local item = publish:get_child("item");
-	if item == nil then
-		return;
-	end
-
-	-- Here we ignore the item id, it’ll be generated as 'current' anyway.
-
-	local bookmarks = item:get_child("storage", namespace_legacy);
-	if bookmarks == nil then
-		return;
-	end
-
-	-- We also ignore the publish-options.
-
-	module:log("debug", "Legacy PEP bookmarks set by client, publishing to PEP.");
-
-	local ok, err = publish_to_pep(session.full_jid, bookmarks, true);
-	if not ok then
-		module:log("error", "Failed to publish to PEP bookmarks for %s@%s: %s", session.username, session.host, err);
-		session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP"));
-		return true;
-	end
-
-	session.send(st.reply(stanza));
-	return true;
-end
-
--- Synchronise Private XML to PEP.
-local function on_publish_private_xml(event)
-	local stanza, session = event.stanza, event.origin;
-	local query = stanza:get_child("query", namespace_private);
-	if query == nil then
-		return;
-	end
-
-	local bookmarks = query:get_child("storage", namespace_legacy);
-	if bookmarks == nil then
-		return;
-	end
-
-	module:log("debug", "Private bookmarks set by client, publishing to PEP.");
-
-	local ok, err = publish_to_pep(session.full_jid, bookmarks, true);
-	if not ok then
-		module:log("error", "Failed to publish to PEP bookmarks for %s@%s: %s", session.username, session.host, err);
-		session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP"));
-		return true;
-	end
-
-	session.send(st.reply(stanza));
-	return true;
-end
-
-local function migrate_legacy_bookmarks(event)
-	local session = event.session;
-	local username = session.username;
-	local service = mod_pep.get_pep_service(username);
-	local jid = username.."@"..session.host;
-
-	local ok, ret = service:get_items(namespace_legacy, session.full_jid);
-	if ok then
-		module:log("debug", "Legacy PEP bookmarks found for %s, migrating.", jid);
-		local failed = false;
-		for _, item_id in ipairs(ret) do
-			local item = ret[item_id];
-			if item.attr.id ~= "current" then
-				module:log("warn", "Legacy PEP bookmarks for %s isn’t using 'current' as its id: %s", jid, item.attr.id);
-			end
-			local bookmarks = item:get_child("storage", namespace_legacy);
-			module:log("debug", "Got legacy PEP bookmarks of %s: %s", jid, bookmarks);
-
-			local ok, err = publish_to_pep(session.full_jid, bookmarks, false);
-			if not ok then
-				module:log("error", "Failed to store legacy PEP bookmarks to bookmarks 2 for %s, aborting migration: %s", jid, err);
-				failed = true;
-				break;
-			end
-		end
-		if not failed then
-			module:log("debug", "Successfully migrated legacy PEP bookmarks of %s to bookmarks 2, attempting deletion of the node.", jid);
-			local ok, err = service:delete(namespace_legacy, jid);
-			if not ok then
-				module:log("error", "Failed to delete legacy PEP bookmarks for %s: %s", jid, err);
-			end
-		end
-	end
-
-	local data, err = private_storage:get(username, "storage:storage:bookmarks");
-	if not data then
-		module:log("debug", "No existing legacy bookmarks for %s, migration already done: %s", jid, err);
-		local ok, ret2 = service:get_items(namespace, session.full_jid);
-		if not ok or not ret2 then
-			module:log("debug", "Additionally, no bookmarks 2 were existing for %s, assuming empty.", jid);
-			module:fire_event("bookmarks/empty", { session = session });
-		end
-		return;
-	end
-	local bookmarks = st.deserialize(data);
-	module:log("debug", "Got legacy bookmarks of %s: %s", jid, bookmarks);
-
-	module:log("debug", "Going to store legacy bookmarks to bookmarks 2 %s.", jid);
-	local ok, err = publish_to_pep(session.full_jid, bookmarks, false);
-	if not ok then
-		module:log("error", "Failed to store legacy bookmarks to bookmarks 2 for %s, aborting migration: %s", jid, err);
-		return;
-	end
-	module:log("debug", "Stored legacy bookmarks to bookmarks 2 for %s.", jid);
-
-	local ok, err = private_storage:set(username, "storage:storage:bookmarks", nil);
-	if not ok then
-		module:log("error", "Failed to remove legacy bookmarks of %s: %s", jid, err);
-		return;
-	end
-	module:log("debug", "Removed legacy bookmarks of %s, migration done!", jid);
-end
-
-local function on_node_created(event)
-	local service, node, actor = event.service, event.node, event.actor;
-	if node ~= namespace_legacy then
-		return;
-	end
-
-	module:log("debug", "Something tried to create legacy PEP bookmarks for %s.", actor);
-	local ok, err = service:delete(namespace_legacy, actor);
-	if not ok then
-		module:log("error", "Failed to delete legacy PEP bookmarks for %s: %s", actor, err);
-	end
-	module:log("debug", "Legacy PEP bookmarks node of %s deleted.", actor);
-end
-
-module:hook("iq/bare/jabber:iq:private:query", function (event)
-	if event.stanza.attr.type == "get" then
-		return on_retrieve_private_xml(event);
-	else
-		return on_publish_private_xml(event);
-	end
-end, 1);
-module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function (event)
-	if event.stanza.attr.type == "get" then
-		return on_retrieve_legacy_pep(event);
-	else
-		return on_publish_legacy_pep(event);
-	end
-end, 1);
-module:hook("resource-bind", migrate_legacy_bookmarks);
-module:handle_items("pep-service", function (event)
-	local service = event.item.service;
-	module:hook_object_event(service.events, "node-created", on_node_created);
-end, function () end, true);
--- a/mod_bookmarks2/tests/bookmarks2.scs	Thu Oct 31 13:48:22 2024 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,181 +0,0 @@
-# Pubsub: Bookmarks 2.0
-
-[Client] Juliet
-	jid: admin@localhost
-	password: password
-
-// admin@localhost is assumed to have node creation privileges
-
----------
-
-Juliet connects
-
--- Generated with https://gitlab.com/xmpp-rs/xmpp-parsers:
--- cargo run --example=generate-caps https://code.matthewwild.co.uk/scansion/ <<< "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' name='scansion' type='bot'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='urn:xmpp:bookmarks:1+notify'/></query>"
-Juliet sends:
-	<presence id='presence0'>
-		<c xmlns='http://jabber.org/protocol/caps'
-		   hash='sha-1'
-		   node='https://code.matthewwild.co.uk/scansion/'
-		   ver='CPuQARM1gCTq2f6/ZjHUzWL2QHg='/>
-		<c xmlns='urn:xmpp:caps'>
-			<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>OTy9GPCvBZRvqzOHmD/ThA1WbBH3tNoeKbdqKQCRPHc=</hash>
-			<hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>f/rxDeTf6HyjQ382V3GEG/UfAs5IeclC05jBSBnVQCI=</hash>
-			<hash xmlns='urn:xmpp:hashes:2' algo='blake2b-256'>ucfqg/NrLj0omE+26hYMrbpcmxHcU4Z3hfAQIF+6tt0=</hash>
-		</c>
-	</presence>
-
-Juliet receives:
-	<iq from="${Juliet's JID}" id='disco' type='get'>
-		<query xmlns='http://jabber.org/protocol/disco#info' node='https://code.matthewwild.co.uk/scansion/#CPuQARM1gCTq2f6/ZjHUzWL2QHg='/>
-	</iq>
-
-Juliet sends:
-	<iq to="${Juliet's JID}" id='disco' type='result'>
-		<query xmlns='http://jabber.org/protocol/disco#info' node='https://code.matthewwild.co.uk/scansion/#CPuQARM1gCTq2f6/ZjHUzWL2QHg='>
-			<identity category='client' name='scansion' type='bot'/>
-			<feature var='http://jabber.org/protocol/disco#info'/>
-			<feature var='urn:xmpp:bookmarks:1+notify'/>
-		</query>
-	</iq>
-
-Juliet sends:
-	<iq type='set' id='pub0'>
-		<pubsub xmlns='http://jabber.org/protocol/pubsub'>
-			<publish node='urn:xmpp:bookmarks:1'>
-				<item id='theplay@conference.shakespeare.lit'>
-					<conference xmlns='urn:xmpp:bookmarks:1'
-					            name='The Play&apos;s the Thing'
-					            autojoin='true'>
-						<nick>JC</nick>
-					</conference>
-				</item>
-			</publish>
-			<publish-options>
-				<x xmlns='jabber:x:data' type='submit'>
-					<field var='FORM_TYPE' type='hidden'>
-						<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>255</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>
-
-Juliet receives:
-	<message type='headline' from="${Juliet's JID}">
-		<event xmlns='http://jabber.org/protocol/pubsub#event'>
-			<items node='urn:xmpp:bookmarks:1'>
-				<item id='theplay@conference.shakespeare.lit' publisher="${Juliet's JID}">
-					<conference xmlns='urn:xmpp:bookmarks:1'
-					            name='The Play&apos;s the Thing'
-					            autojoin='true'>
-						<nick>JC</nick>
-					</conference>
-				</item>
-			</items>
-		</event>
-	</message>
-
-Juliet receives:
-	<iq type='result' id='pub0'>
-		<pubsub xmlns='http://jabber.org/protocol/pubsub'>
-			<publish node='urn:xmpp:bookmarks:1'>
-				<item id='theplay@conference.shakespeare.lit'/>
-			</publish>
-		</pubsub>
-	</iq>
-
-Juliet sends:
-	<iq type='set' id='pub1'>
-		<pubsub xmlns='http://jabber.org/protocol/pubsub'>
-			<publish node='urn:xmpp:bookmarks:1'>
-				<item id='orchard@conference.shakespeare.lit'>
-					<conference xmlns='urn:xmpp:bookmarks:1'
-					            name='The Orchard'
-					            autojoin='true'>
-						<nick>JC</nick>
-					</conference>
-				</item>
-			</publish>
-			<publish-options>
-				<x xmlns='jabber:x:data' type='submit'>
-					<field var='FORM_TYPE' type='hidden'>
-						<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>255</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>
-
-Juliet receives:
-	<message type='headline' from="${Juliet's JID}">
-		<event xmlns='http://jabber.org/protocol/pubsub#event'>
-			<items node='urn:xmpp:bookmarks:1'>
-				<item id='orchard@conference.shakespeare.lit' publisher="${Juliet's JID}">
-					<conference xmlns='urn:xmpp:bookmarks:1'
-					            name='The Orchard'
-					            autojoin='true'>
-						<nick>JC</nick>
-					</conference>
-				</item>
-			</items>
-		</event>
-	</message>
-
-Juliet receives:
-	<iq type='result' id='pub1'>
-		<pubsub xmlns='http://jabber.org/protocol/pubsub'>
-			<publish node='urn:xmpp:bookmarks:1'>
-				<item id='orchard@conference.shakespeare.lit'/>
-			</publish>
-		</pubsub>
-	</iq>
-
-Juliet sends:
-	<iq type='set' id='retract0'>
-		<pubsub xmlns='http://jabber.org/protocol/pubsub'>
-			<retract node='urn:xmpp:bookmarks:1' notify='1'>
-				<item id='theplay@conference.shakespeare.lit'/>
-			</retract>
-		</pubsub>
-	</iq>
-
-Juliet receives:
-	<message type='headline' from="${Juliet's JID}">
-		<event xmlns='http://jabber.org/protocol/pubsub#event'>
-			<items node='urn:xmpp:bookmarks:1'>
-				<retract id='theplay@conference.shakespeare.lit'/>
-			</items>
-		</event>
-	</message>
-
-Juliet receives:
-	<iq type='result' id='retract0'/>
-
-Juliet disconnects
-
-// vim: syntax=xml:
--- a/mod_bookmarks2/tests/conversion.scs	Thu Oct 31 13:48:22 2024 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,254 +0,0 @@
-# Pubsub: Bookmarks 2.0
-
-[Client] Juliet-old
-	jid: admin@localhost
-	password: password
-
-[Client] Juliet-new
-	jid: admin@localhost
-	password: password
-
-// admin@localhost is assumed to have node creation privileges
-
----------
-
-Juliet-new connects
-
--- Generated with https://gitlab.com/xmpp-rs/xmpp-parsers:
--- cargo run --example=generate-caps https://code.matthewwild.co.uk/scansion/ <<< "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' name='scansion' type='bot'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='urn:xmpp:bookmarks:1+notify'/></query>"
-Juliet-new sends:
-	<presence id='presence0'>
-		<c xmlns='http://jabber.org/protocol/caps'
-		   hash='sha-1'
-		   node='https://code.matthewwild.co.uk/scansion/'
-		   ver='CPuQARM1gCTq2f6/ZjHUzWL2QHg='/>
-		<c xmlns='urn:xmpp:caps'>
-			<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>OTy9GPCvBZRvqzOHmD/ThA1WbBH3tNoeKbdqKQCRPHc=</hash>
-			<hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>f/rxDeTf6HyjQ382V3GEG/UfAs5IeclC05jBSBnVQCI=</hash>
-			<hash xmlns='urn:xmpp:hashes:2' algo='blake2b-256'>ucfqg/NrLj0omE+26hYMrbpcmxHcU4Z3hfAQIF+6tt0=</hash>
-		</c>
-	</presence>
-
-Juliet-new receives:
-	<iq from="${Juliet-new's JID}" id='disco' type='get'>
-		<query xmlns='http://jabber.org/protocol/disco#info' node='https://code.matthewwild.co.uk/scansion/#CPuQARM1gCTq2f6/ZjHUzWL2QHg='/>
-	</iq>
-
-Juliet-new sends:
-	<iq to="${Juliet-new's JID}" id='disco' type='result'>
-		<query xmlns='http://jabber.org/protocol/disco#info' node='https://code.matthewwild.co.uk/scansion/#CPuQARM1gCTq2f6/ZjHUzWL2QHg='>
-			<identity category='client' name='scansion' type='bot'/>
-			<feature var='http://jabber.org/protocol/disco#info'/>
-			<feature var='urn:xmpp:bookmarks:1+notify'/>
-		</query>
-	</iq>
-
-Juliet-old connects
-
-Juliet-old sends:
-	<iq type='get' id='get0'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'/>
-		</query>
-	</iq>
-
-Juliet-old receives:
-	<iq type='result' id='get0'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'/>
-		</query>
-	</iq>
-
-Juliet-old sends:
-	<iq type='set' id='pub0'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'>
-				<conference name='The Play&apos;s the Thing'
-					    autojoin='true'
-					    jid='theplay@conference.shakespeare.lit'>
-					<nick>JC</nick>
-				</conference>
-			</storage>
-		</query>
-	</iq>
-
-Juliet-new receives:
-	<message type='headline' from="${Juliet-new's JID}">
-		<event xmlns='http://jabber.org/protocol/pubsub#event'>
-			<items node='urn:xmpp:bookmarks:1'>
-				<item id='theplay@conference.shakespeare.lit'>
-					<conference xmlns='urn:xmpp:bookmarks:1'
-					            name='The Play&apos;s the Thing'
-					            autojoin='true'>
-						<nick>JC</nick>
-					</conference>
-				</item>
-			</items>
-		</event>
-	</message>
-
-Juliet-old receives:
-	<iq type='result' id='pub0'/>
-
-Juliet-old sends:
-	<iq type='get' id='get1'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'/>
-		</query>
-	</iq>
-
-Juliet-old receives:
-	<iq type='result' id='get1'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'>
-				<conference name='The Play&apos;s the Thing'
-					    autojoin='true'
-					    jid='theplay@conference.shakespeare.lit'>
-					<nick>JC</nick>
-				</conference>
-			</storage>
-		</query>
-	</iq>
-
-Juliet-old sends:
-	<iq type='set' id='pub1'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'>
-				<conference name='The Play&apos;s the Thing'
-					    autojoin='true'
-					    jid='theplay@conference.shakespeare.lit'>
-					<nick>JC</nick>
-				</conference>
-				<conference name='The Orchard'
-					    autojoin='true'
-					    jid='orchard@conference.shakespeare.lit'>
-					<nick>JC</nick>
-				</conference>
-			</storage>
-		</query>
-	</iq>
-
-Juliet-new receives:
-	<message type='headline' from="${Juliet-new's JID}">
-		<event xmlns='http://jabber.org/protocol/pubsub#event'>
-			<items node='urn:xmpp:bookmarks:1'>
-				<item id='orchard@conference.shakespeare.lit'>
-					<conference xmlns='urn:xmpp:bookmarks:1'
-					            name='The Orchard'
-					            autojoin='true'>
-						<nick>JC</nick>
-					</conference>
-				</item>
-			</items>
-		</event>
-	</message>
-
-Juliet-old receives:
-	<iq type='result' id='pub1'/>
-
-Juliet-old sends:
-	<iq type='get' id='get2'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'/>
-		</query>
-	</iq>
-
-Juliet-old receives:
-	<iq type='result' id='get2'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'>
-				<conference name='The Play&apos;s the Thing'
-					    autojoin='true'
-					    jid='theplay@conference.shakespeare.lit'>
-					<nick>JC</nick>
-				</conference>
-				<conference name='The Orchard'
-					    autojoin='true'
-					    jid='orchard@conference.shakespeare.lit'>
-					<nick>JC</nick>
-				</conference>
-			</storage>
-		</query>
-	</iq>
-
-Juliet-old sends:
-	<iq type='set' id='retract0'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'>
-				<conference name='The Orchard'
-					    autojoin='true'
-					    jid='orchard@conference.shakespeare.lit'>
-					<nick>JC</nick>
-				</conference>
-			</storage>
-		</query>
-	</iq>
-
-Juliet-new receives:
-	<message type='headline' from="${Juliet-new's JID}">
-		<event xmlns='http://jabber.org/protocol/pubsub#event'>
-			<items node='urn:xmpp:bookmarks:1'>
-				<retract id='theplay@conference.shakespeare.lit'/>
-			</items>
-		</event>
-	</message>
-
-Juliet-old receives:
-	<iq type='result' id='retract0'/>
-
-Juliet-old sends:
-	<iq type='get' id='get3'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'/>
-		</query>
-	</iq>
-
-Juliet-old receives:
-	<iq type='result' id='get3'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'>
-				<conference name='The Orchard'
-					    autojoin='true'
-					    jid='orchard@conference.shakespeare.lit'>
-					<nick>JC</nick>
-				</conference>
-			</storage>
-		</query>
-	</iq>
-
-Juliet-old sends:
-	<iq type='set' id='purge0'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'/>
-		</query>
-	</iq>
-
-Juliet-new receives:
-	<message type='headline' from="${Juliet-new's JID}">
-		<event xmlns='http://jabber.org/protocol/pubsub#event'>
-			<purge node='urn:xmpp:bookmarks:1'/>
-		</event>
-	</message>
-
-Juliet-old receives:
-	<iq type='result' id='purge0'/>
-
-Juliet-old sends:
-	<iq type='get' id='get4'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'/>
-		</query>
-	</iq>
-
-Juliet-old receives:
-	<iq type='result' id='get4'>
-		<query xmlns='jabber:iq:private'>
-			<storage xmlns='storage:bookmarks'/>
-		</query>
-	</iq>
-
-Juliet-old disconnects
-
-Juliet-new disconnects
-
-// vim: syntax=xml: