Software / code / prosody
Comparison
plugins/mod_bookmarks.lua @ 12148:b63bb2c4b6d9
mod_bookmarks: Import mod_bookmarks2 from prosody-modules @ ad7767a9f3ea
| author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
|---|---|
| date | Tue, 04 Jan 2022 23:04:14 +0100 |
| child | 12149:bbbf0dd90b6d |
comparison
equal
deleted
inserted
replaced
| 12147:02481502c3dc | 12148:b63bb2c4b6d9 |
|---|---|
| 1 local mm = require "core.modulemanager"; | |
| 2 if mm.get_modules_for_host(module.host):contains("bookmarks2") then | |
| 3 error("mod_bookmarks and mod_bookmarks2 are conflicting, please disable one of them.", 0); | |
| 4 end | |
| 5 | |
| 6 local st = require "util.stanza"; | |
| 7 local jid_split = require "util.jid".split; | |
| 8 | |
| 9 local mod_pep = module:depends "pep"; | |
| 10 local private_storage = module:open_store("private", "map"); | |
| 11 | |
| 12 local namespace = "urn:xmpp:bookmarks:1"; | |
| 13 local namespace_private = "jabber:iq:private"; | |
| 14 local namespace_legacy = "storage:bookmarks"; | |
| 15 | |
| 16 local default_options = { | |
| 17 ["persist_items"] = true; | |
| 18 ["max_items"] = "max"; | |
| 19 ["send_last_published_item"] = "never"; | |
| 20 ["access_model"] = "whitelist"; | |
| 21 }; | |
| 22 | |
| 23 if not mod_pep.check_node_config(nil, nil, default_options) then | |
| 24 -- 0.11 or earlier not supporting max_items="max" trows an error here | |
| 25 module:log("debug", "Setting max_items=pep_max_items because 'max' is not supported in this version"); | |
| 26 default_options["max_items"] = module:get_option_number("pep_max_items", 256); | |
| 27 end | |
| 28 | |
| 29 module:hook("account-disco-info", function (event) | |
| 30 -- This Time it’s Serious! | |
| 31 event.reply:tag("feature", { var = namespace.."#compat" }):up(); | |
| 32 event.reply:tag("feature", { var = namespace.."#compat-pep" }):up(); | |
| 33 end); | |
| 34 | |
| 35 -- This must be declared on the domain JID, not the account JID. Note that | |
| 36 -- this isn’t defined in the XEP. | |
| 37 module:add_feature(namespace_private); | |
| 38 | |
| 39 local function generate_legacy_storage(items) | |
| 40 local storage = st.stanza("storage", { xmlns = namespace_legacy }); | |
| 41 for _, item_id in ipairs(items) do | |
| 42 local item = items[item_id]; | |
| 43 local bookmark = item:get_child("conference", namespace); | |
| 44 local conference = st.stanza("conference", { | |
| 45 jid = item.attr.id, | |
| 46 name = bookmark.attr.name, | |
| 47 autojoin = bookmark.attr.autojoin, | |
| 48 }); | |
| 49 local nick = bookmark:get_child_text("nick"); | |
| 50 if nick ~= nil then | |
| 51 conference:text_tag("nick", nick):up(); | |
| 52 end | |
| 53 local password = bookmark:get_child_text("password"); | |
| 54 if password ~= nil then | |
| 55 conference:text_tag("password", password):up(); | |
| 56 end | |
| 57 storage:add_child(conference); | |
| 58 end | |
| 59 | |
| 60 return storage; | |
| 61 end | |
| 62 | |
| 63 local function on_retrieve_legacy_pep(event) | |
| 64 local stanza, session = event.stanza, event.origin; | |
| 65 local pubsub = stanza:get_child("pubsub", "http://jabber.org/protocol/pubsub"); | |
| 66 if pubsub == nil then | |
| 67 return; | |
| 68 end | |
| 69 | |
| 70 local items = pubsub:get_child("items"); | |
| 71 if items == nil then | |
| 72 return; | |
| 73 end | |
| 74 | |
| 75 local node = items.attr.node; | |
| 76 if node ~= namespace_legacy then | |
| 77 return; | |
| 78 end | |
| 79 | |
| 80 local username = session.username; | |
| 81 local jid = username.."@"..session.host; | |
| 82 local service = mod_pep.get_pep_service(username); | |
| 83 local ok, ret = service:get_items(namespace, session.full_jid); | |
| 84 if not ok then | |
| 85 module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, ret); | |
| 86 session.send(st.error_reply(stanza, "cancel", ret, "Failed to retrive bookmarks from PEP")); | |
| 87 return true; | |
| 88 end | |
| 89 | |
| 90 local storage = generate_legacy_storage(ret); | |
| 91 | |
| 92 module:log("debug", "Sending back legacy PEP for %s: %s", jid, storage); | |
| 93 session.send(st.reply(stanza) | |
| 94 :tag("pubsub", {xmlns = "http://jabber.org/protocol/pubsub"}) | |
| 95 :tag("items", {node = namespace_legacy}) | |
| 96 :tag("item", {id = "current"}) | |
| 97 :add_child(storage)); | |
| 98 return true; | |
| 99 end | |
| 100 | |
| 101 local function on_retrieve_private_xml(event) | |
| 102 local stanza, session = event.stanza, event.origin; | |
| 103 local query = stanza:get_child("query", namespace_private); | |
| 104 if query == nil then | |
| 105 return; | |
| 106 end | |
| 107 | |
| 108 local bookmarks = query:get_child("storage", namespace_legacy); | |
| 109 if bookmarks == nil then | |
| 110 return; | |
| 111 end | |
| 112 | |
| 113 module:log("debug", "Getting private bookmarks: %s", bookmarks); | |
| 114 | |
| 115 local username = session.username; | |
| 116 local jid = username.."@"..session.host; | |
| 117 local service = mod_pep.get_pep_service(username); | |
| 118 local ok, ret = service:get_items(namespace, session.full_jid); | |
| 119 if not ok then | |
| 120 if ret == "item-not-found" then | |
| 121 module:log("debug", "Got no PEP bookmarks item for %s, returning empty private bookmarks", jid); | |
| 122 session.send(st.reply(stanza):add_child(query)); | |
| 123 else | |
| 124 module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, ret); | |
| 125 session.send(st.error_reply(stanza, "cancel", ret, "Failed to retrive bookmarks from PEP")); | |
| 126 end | |
| 127 return true; | |
| 128 end | |
| 129 | |
| 130 local storage = generate_legacy_storage(ret); | |
| 131 | |
| 132 module:log("debug", "Sending back private for %s: %s", jid, storage); | |
| 133 session.send(st.reply(stanza):query(namespace_private):add_child(storage)); | |
| 134 return true; | |
| 135 end | |
| 136 | |
| 137 local function compare_bookmark2(a, b) | |
| 138 if a == nil or b == nil then | |
| 139 return false; | |
| 140 end | |
| 141 local a_conference = a:get_child("conference", namespace); | |
| 142 local b_conference = b:get_child("conference", namespace); | |
| 143 local a_nick = a_conference:get_child_text("nick"); | |
| 144 local b_nick = b_conference:get_child_text("nick"); | |
| 145 local a_password = a_conference:get_child_text("password"); | |
| 146 local b_password = b_conference:get_child_text("password"); | |
| 147 return (a.attr.id == b.attr.id and | |
| 148 a_conference.attr.name == b_conference.attr.name and | |
| 149 a_conference.attr.autojoin == b_conference.attr.autojoin and | |
| 150 a_nick == b_nick and | |
| 151 a_password == b_password); | |
| 152 end | |
| 153 | |
| 154 local function publish_to_pep(jid, bookmarks, synchronise) | |
| 155 local service = mod_pep.get_pep_service(jid_split(jid)); | |
| 156 | |
| 157 if #bookmarks.tags == 0 then | |
| 158 if synchronise then | |
| 159 -- If we set zero legacy bookmarks, purge the bookmarks 2 node. | |
| 160 module:log("debug", "No bookmark in the set, purging instead."); | |
| 161 return service:purge(namespace, jid, true); | |
| 162 else | |
| 163 return true; | |
| 164 end | |
| 165 end | |
| 166 | |
| 167 -- Retrieve the current bookmarks2. | |
| 168 module:log("debug", "Retrieving the current bookmarks 2."); | |
| 169 local has_bookmarks2, ret = service:get_items(namespace, jid); | |
| 170 local bookmarks2; | |
| 171 if not has_bookmarks2 and ret == "item-not-found" then | |
| 172 module:log("debug", "Got item-not-found, assuming it was empty until now, creating."); | |
| 173 local ok, err = service:create(namespace, jid, default_options); | |
| 174 if not ok then | |
| 175 module:log("error", "Creating bookmarks 2 node failed: %s", err); | |
| 176 return ok, err; | |
| 177 end | |
| 178 bookmarks2 = {}; | |
| 179 elseif not has_bookmarks2 then | |
| 180 module:log("debug", "Got %s error, aborting.", ret); | |
| 181 return false, ret; | |
| 182 else | |
| 183 module:log("debug", "Got existing bookmarks2."); | |
| 184 bookmarks2 = ret; | |
| 185 end | |
| 186 | |
| 187 -- Get a list of all items we may want to remove. | |
| 188 local to_remove = {}; | |
| 189 for i in ipairs(bookmarks2) do | |
| 190 to_remove[bookmarks2[i]] = true; | |
| 191 end | |
| 192 | |
| 193 for bookmark in bookmarks:childtags("conference", namespace_legacy) do | |
| 194 -- Create the new conference element by copying everything from the legacy one. | |
| 195 local conference = st.stanza("conference", { | |
| 196 xmlns = namespace, | |
| 197 name = bookmark.attr.name, | |
| 198 autojoin = bookmark.attr.autojoin, | |
| 199 }); | |
| 200 local nick = bookmark:get_child_text("nick"); | |
| 201 if nick ~= nil then | |
| 202 conference:text_tag("nick", nick):up(); | |
| 203 end | |
| 204 local password = bookmark:get_child_text("password"); | |
| 205 if password ~= nil then | |
| 206 conference:text_tag("password", password):up(); | |
| 207 end | |
| 208 | |
| 209 -- Create its wrapper. | |
| 210 local item = st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = bookmark.attr.jid }) | |
| 211 :add_child(conference); | |
| 212 | |
| 213 -- Then publish it only if it’s a new one or updating a previous one. | |
| 214 if compare_bookmark2(item, bookmarks2[bookmark.attr.jid]) then | |
| 215 module:log("debug", "Item %s identical to the previous one, skipping.", item.attr.id); | |
| 216 to_remove[bookmark.attr.jid] = nil; | |
| 217 else | |
| 218 if bookmarks2[bookmark.attr.jid] == nil then | |
| 219 module:log("debug", "Item %s not existing previously, publishing.", item.attr.id); | |
| 220 else | |
| 221 module:log("debug", "Item %s different from the previous one, publishing.", item.attr.id); | |
| 222 to_remove[bookmark.attr.jid] = nil; | |
| 223 end | |
| 224 local ok, err = service:publish(namespace, jid, bookmark.attr.jid, item, default_options); | |
| 225 if not ok then | |
| 226 module:log("error", "Publishing item %s failed: %s", item.attr.id, err); | |
| 227 return ok, err; | |
| 228 end | |
| 229 end | |
| 230 end | |
| 231 | |
| 232 -- Now handle retracting items that have been removed. | |
| 233 if synchronise then | |
| 234 for id in pairs(to_remove) do | |
| 235 module:log("debug", "Item %s removed from bookmarks.", id); | |
| 236 local ok, err = service:retract(namespace, jid, id, st.stanza("retract", { id = id })); | |
| 237 if not ok then | |
| 238 module:log("error", "Retracting item %s failed: %s", id, err); | |
| 239 return ok, err; | |
| 240 end | |
| 241 end | |
| 242 end | |
| 243 return true; | |
| 244 end | |
| 245 | |
| 246 -- Synchronise legacy PEP to PEP. | |
| 247 local function on_publish_legacy_pep(event) | |
| 248 local stanza, session = event.stanza, event.origin; | |
| 249 local pubsub = stanza:get_child("pubsub", "http://jabber.org/protocol/pubsub"); | |
| 250 if pubsub == nil then | |
| 251 return; | |
| 252 end | |
| 253 | |
| 254 local publish = pubsub:get_child("publish"); | |
| 255 if publish == nil or publish.attr.node ~= namespace_legacy then | |
| 256 return; | |
| 257 end | |
| 258 | |
| 259 local item = publish:get_child("item"); | |
| 260 if item == nil then | |
| 261 return; | |
| 262 end | |
| 263 | |
| 264 -- Here we ignore the item id, it’ll be generated as 'current' anyway. | |
| 265 | |
| 266 local bookmarks = item:get_child("storage", namespace_legacy); | |
| 267 if bookmarks == nil then | |
| 268 return; | |
| 269 end | |
| 270 | |
| 271 -- We also ignore the publish-options. | |
| 272 | |
| 273 module:log("debug", "Legacy PEP bookmarks set by client, publishing to PEP."); | |
| 274 | |
| 275 local ok, err = publish_to_pep(session.full_jid, bookmarks, true); | |
| 276 if not ok then | |
| 277 module:log("error", "Failed to publish to PEP bookmarks for %s@%s: %s", session.username, session.host, err); | |
| 278 session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP")); | |
| 279 return true; | |
| 280 end | |
| 281 | |
| 282 session.send(st.reply(stanza)); | |
| 283 return true; | |
| 284 end | |
| 285 | |
| 286 -- Synchronise Private XML to PEP. | |
| 287 local function on_publish_private_xml(event) | |
| 288 local stanza, session = event.stanza, event.origin; | |
| 289 local query = stanza:get_child("query", namespace_private); | |
| 290 if query == nil then | |
| 291 return; | |
| 292 end | |
| 293 | |
| 294 local bookmarks = query:get_child("storage", namespace_legacy); | |
| 295 if bookmarks == nil then | |
| 296 return; | |
| 297 end | |
| 298 | |
| 299 module:log("debug", "Private bookmarks set by client, publishing to PEP."); | |
| 300 | |
| 301 local ok, err = publish_to_pep(session.full_jid, bookmarks, true); | |
| 302 if not ok then | |
| 303 module:log("error", "Failed to publish to PEP bookmarks for %s@%s: %s", session.username, session.host, err); | |
| 304 session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP")); | |
| 305 return true; | |
| 306 end | |
| 307 | |
| 308 session.send(st.reply(stanza)); | |
| 309 return true; | |
| 310 end | |
| 311 | |
| 312 local function migrate_legacy_bookmarks(event) | |
| 313 local session = event.session; | |
| 314 local username = session.username; | |
| 315 local service = mod_pep.get_pep_service(username); | |
| 316 local jid = username.."@"..session.host; | |
| 317 | |
| 318 local ok, ret = service:get_items(namespace_legacy, session.full_jid); | |
| 319 if ok then | |
| 320 module:log("debug", "Legacy PEP bookmarks found for %s, migrating.", jid); | |
| 321 local failed = false; | |
| 322 for _, item_id in ipairs(ret) do | |
| 323 local item = ret[item_id]; | |
| 324 if item.attr.id ~= "current" then | |
| 325 module:log("warn", "Legacy PEP bookmarks for %s isn’t using 'current' as its id: %s", jid, item.attr.id); | |
| 326 end | |
| 327 local bookmarks = item:get_child("storage", namespace_legacy); | |
| 328 module:log("debug", "Got legacy PEP bookmarks of %s: %s", jid, bookmarks); | |
| 329 | |
| 330 local ok, err = publish_to_pep(session.full_jid, bookmarks, false); | |
| 331 if not ok then | |
| 332 module:log("error", "Failed to store legacy PEP bookmarks to bookmarks 2 for %s, aborting migration: %s", jid, err); | |
| 333 failed = true; | |
| 334 break; | |
| 335 end | |
| 336 end | |
| 337 if not failed then | |
| 338 module:log("debug", "Successfully migrated legacy PEP bookmarks of %s to bookmarks 2, attempting deletion of the node.", jid); | |
| 339 local ok, err = service:delete(namespace_legacy, jid); | |
| 340 if not ok then | |
| 341 module:log("error", "Failed to delete legacy PEP bookmarks for %s: %s", jid, err); | |
| 342 end | |
| 343 end | |
| 344 end | |
| 345 | |
| 346 local data, err = private_storage:get(username, "storage:storage:bookmarks"); | |
| 347 if not data then | |
| 348 module:log("debug", "No existing legacy bookmarks for %s, migration already done: %s", jid, err); | |
| 349 local ok, ret2 = service:get_items(namespace, session.full_jid); | |
| 350 if not ok or not ret2 then | |
| 351 module:log("debug", "Additionally, no bookmarks 2 were existing for %s, assuming empty.", jid); | |
| 352 module:fire_event("bookmarks/empty", { session = session }); | |
| 353 end | |
| 354 return; | |
| 355 end | |
| 356 local bookmarks = st.deserialize(data); | |
| 357 module:log("debug", "Got legacy bookmarks of %s: %s", jid, bookmarks); | |
| 358 | |
| 359 module:log("debug", "Going to store legacy bookmarks to bookmarks 2 %s.", jid); | |
| 360 local ok, err = publish_to_pep(session.full_jid, bookmarks, false); | |
| 361 if not ok then | |
| 362 module:log("error", "Failed to store legacy bookmarks to bookmarks 2 for %s, aborting migration: %s", jid, err); | |
| 363 return; | |
| 364 end | |
| 365 module:log("debug", "Stored legacy bookmarks to bookmarks 2 for %s.", jid); | |
| 366 | |
| 367 local ok, err = private_storage:set(username, "storage:storage:bookmarks", nil); | |
| 368 if not ok then | |
| 369 module:log("error", "Failed to remove legacy bookmarks of %s: %s", jid, err); | |
| 370 return; | |
| 371 end | |
| 372 module:log("debug", "Removed legacy bookmarks of %s, migration done!", jid); | |
| 373 end | |
| 374 | |
| 375 local function on_node_created(event) | |
| 376 local service, node, actor = event.service, event.node, event.actor; | |
| 377 if node ~= namespace_legacy then | |
| 378 return; | |
| 379 end | |
| 380 | |
| 381 module:log("debug", "Something tried to create legacy PEP bookmarks for %s.", actor); | |
| 382 local ok, err = service:delete(namespace_legacy, actor); | |
| 383 if not ok then | |
| 384 module:log("error", "Failed to delete legacy PEP bookmarks for %s: %s", actor, err); | |
| 385 end | |
| 386 module:log("debug", "Legacy PEP bookmarks node of %s deleted.", actor); | |
| 387 end | |
| 388 | |
| 389 module:hook("iq/bare/jabber:iq:private:query", function (event) | |
| 390 if event.stanza.attr.type == "get" then | |
| 391 return on_retrieve_private_xml(event); | |
| 392 else | |
| 393 return on_publish_private_xml(event); | |
| 394 end | |
| 395 end, 1); | |
| 396 module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function (event) | |
| 397 if event.stanza.attr.type == "get" then | |
| 398 return on_retrieve_legacy_pep(event); | |
| 399 else | |
| 400 return on_publish_legacy_pep(event); | |
| 401 end | |
| 402 end, 1); | |
| 403 module:hook("resource-bind", migrate_legacy_bookmarks); | |
| 404 module:handle_items("pep-service", function (event) | |
| 405 local service = event.item.service; | |
| 406 module:hook_object_event(service.events, "node-created", on_node_created); | |
| 407 end, function () end, true); |