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);