# HG changeset patch # User Emmanuel Gil Peyrot # Date 1569673633 -7200 # Node ID 90f88a643973038f1f5d8e8a0e5f1736bdd75f18 # Parent 957e870672314c0f45f02ce1af87326532343d54 mod_bookmarks2: Add new module. This is the result of hacking during the Stockholm XMPP Sprint, for compatibility with older clients only doing Private XML XEP-0048. This module shouldn’t be loaded at the same time as mod_bookmarks, as both implement Private XML to achieve their conversion. diff -r 957e87067231 -r 90f88a643973 mod_bookmarks2/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_bookmarks2/README.markdown Sat Sep 28 14:27:13 2019 +0200 @@ -0,0 +1,32 @@ +--- +labels: +- 'Stage-Alpha' +summary: Synchronise bookmarks between Private XML and PEP +... + +Introduction +------------ + +This module fetches users’ bookmarks from Private XML and pushes them +to PEP on login, and then redirects any Private XML query to PEP. This +allows interop between older clients that use [XEP-0048: Bookmarks +version 1.0](https://xmpp.org/extensions/attic/xep-0048-1.0.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 +------------- + + ------- --------------- + trunk Works + 0.11 Works + 0.10 Does not work + 0.9 Does not work + ------- --------------- diff -r 957e87067231 -r 90f88a643973 mod_bookmarks2/mod_bookmarks2.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_bookmarks2/mod_bookmarks2.lua Sat Sep 28 14:27:13 2019 +0200 @@ -0,0 +1,279 @@ +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 legacy_ns = "storage:bookmarks"; +local ns = "urn:xmpp:bookmarks:0"; + +local default_options = { + ["persist_items"] = true; + ["max_items"] = 255; + ["send_last_published_item"] = "never"; + ["access_model"] = "whitelist"; +}; + +module:hook("account-disco-info", function (event) + -- This Time it’s Serious! + event.reply:tag("feature", { var = "urn:xmpp:bookmarks:0#compat" }):up(); +end); + +local function on_retrieve_private_xml(event) + local stanza, session = event.stanza, event.origin; + local query = stanza:get_child("query", "jabber:iq:private"); + if query == nil then + return; + end + + local bookmarks = query:get_child("storage", "storage:bookmarks"); + 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("urn:xmpp:bookmarks:0", 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, id); + session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to retrive bookmarks from PEP")); + end + return true; + end + + local storage = st.stanza("storage", { xmlns = "storage:bookmarks" }); + for i in ipairs(ret) do + local item = ret[ret[i]]; + local conference = st.stanza("conference"); + conference.attr.jid = item.attr.id; + local bookmark = item:get_child("conference", "urn:xmpp:bookmarks:0"); + conference.attr.name = bookmark.attr.name; + conference.attr.autojoin = bookmark.attr.autojoin; + local nick = bookmark:get_child_text("nick", "urn:xmpp:bookmarks:0"); + if nick ~= nil then + conference:text_tag("nick", nick, { xmlns = "storage:bookmarks" }):up(); + end + local password = bookmark:get_child_text("password", "urn:xmpp:bookmarks:0"); + if password ~= nil then + conference:text_tag("password", password):up(); + end + storage:add_child(conference); + end + + module:log("debug", "Sending back private for %s: %s", jid, storage); + session.send(st.reply(stanza):query("jabber:iq: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", "urn:xmpp:bookmarks:0"); + local b_conference = b:get_child("conference", "urn:xmpp:bookmarks:0"); + local a_nick = a:get_child_text("nick", "urn:xmpp:bookmarks:0"); + local b_nick = b:get_child_text("nick", "urn:xmpp:bookmarks:0"); + local a_password = a:get_child_text("password", "urn:xmpp:bookmarks:0"); + local b_password = b:get_child_text("password", "urn:xmpp:bookmarks:0"); + 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) + local service = mod_pep.get_pep_service(jid_split(jid)); + + -- If we set zero legacy bookmarks, purge the bookmarks 2 node. + if #bookmarks.tags == 0 then + module:log("debug", "No bookmark in the set, purging instead."); + return service:purge("urn:xmpp:bookmarks:0", jid, true); + end + + -- Retrieve the current bookmarks2. + module:log("debug", "Retrieving the current bookmarks 2."); + local has_bookmarks2, ret = service:get_items("urn:xmpp:bookmarks:0", 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("urn:xmpp:bookmarks:0", 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; + 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", "storage:bookmarks") do + -- Create the new conference element by copying everything from the legacy one. + local conference = st.stanza("conference", { xmlns = "urn:xmpp:bookmarks:0" }); + conference.attr.name = bookmark.attr.name; + conference.attr.autojoin = bookmark.attr.autojoin; + local nick = bookmark:get_child_text("nick", "storage:bookmarks"); + if nick ~= nil then + conference:text_tag("nick", nick, { xmlns = "urn:xmpp:bookmarks:0" }):up(); + end + local password = bookmark:get_child_text("password", "storage:bookmarks"); + if password ~= nil then + conference:text_tag("password", password, { xmlns = "urn:xmpp:bookmarks:0" }):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("urn:xmpp:bookmarks:0", 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. + for id in pairs(to_remove) do + module:log("debug", "Item %s removed from bookmarks.", id); + local ok, err = service:retract("urn:xmpp:bookmarks:0", 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 + 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", "jabber:iq:private"); + if query == nil then + return; + end + + local bookmarks = query:get_child("storage", legacy_ns); + 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); + 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 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, ret = service:get_items("urn:xmpp:bookmarks:0", session.full_jid); + if not ok or #ret.tags == 0 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); + + -- We don’t care if deleting succeeds or not, we only want to start with a non-existent node. + module:log("debug", "Deleting possibly existing PEP item for %s.", jid); + service:delete("urn:xmpp:bookmarks:0", jid); + + module:log("debug", "Going to store PEP item for %s.", jid); + local ok, err = publish_to_pep(session.full_jid, bookmarks); + if not ok then + module:log("error", "Failed to store bookmarks to PEP for %s, aborting migration: %s", jid, err); + return; + end + module:log("debug", "Stored bookmarks to PEP for %s.", jid); + + local ok, err = private_storage:set(username, "storage:storage:bookmarks", nil); + if not ok then + module:log("error", "Failed to remove private bookmarks of %s: %s", jid, err); + return; + end + module:log("debug", "Removed private 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 ~= "storage:bookmarks" then + return; + end + local ok, node_config = service:get_node_config(node, actor); + if not ok then + module:log("error", "Failed to get node config of %s: %s", node, node_config); + return; + end + local changed = false; + for config_field, value in pairs(default_options) do + if node_config[config_field] ~= value then + node_config[config_field] = value; + changed = true; + end + end + if not changed then + return; + end + local ok, err = service:set_node_config(node, actor, node_config); + if not ok then + module:log("error", "Failed to set node config of %s: %s", node, err); + return; + end +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("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);