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