comparison mod_bookmarks2/mod_bookmarks2.lua @ 3677:90f88a643973

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.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Sat, 28 Sep 2019 14:27:13 +0200
parents
children 7575399ae544
comparison
equal deleted inserted replaced
3676:957e87067231 3677:90f88a643973
1 local st = require "util.stanza";
2 local jid_split = require "util.jid".split;
3
4 local mod_pep = module:depends "pep";
5 local private_storage = module:open_store("private", "map");
6
7 local legacy_ns = "storage:bookmarks";
8 local ns = "urn:xmpp:bookmarks:0";
9
10 local default_options = {
11 ["persist_items"] = true;
12 ["max_items"] = 255;
13 ["send_last_published_item"] = "never";
14 ["access_model"] = "whitelist";
15 };
16
17 module:hook("account-disco-info", function (event)
18 -- This Time it’s Serious!
19 event.reply:tag("feature", { var = "urn:xmpp:bookmarks:0#compat" }):up();
20 end);
21
22 local function on_retrieve_private_xml(event)
23 local stanza, session = event.stanza, event.origin;
24 local query = stanza:get_child("query", "jabber:iq:private");
25 if query == nil then
26 return;
27 end
28
29 local bookmarks = query:get_child("storage", "storage:bookmarks");
30 if bookmarks == nil then
31 return;
32 end
33
34 module:log("debug", "Getting private bookmarks: %s", bookmarks);
35
36 local username = session.username;
37 local jid = username.."@"..session.host;
38 local service = mod_pep.get_pep_service(username);
39 local ok, ret = service:get_items("urn:xmpp:bookmarks:0", session.full_jid);
40 if not ok then
41 if ret == "item-not-found" then
42 module:log("debug", "Got no PEP bookmarks item for %s, returning empty private bookmarks", jid);
43 session.send(st.reply(stanza):add_child(query));
44 else
45 module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, id);
46 session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to retrive bookmarks from PEP"));
47 end
48 return true;
49 end
50
51 local storage = st.stanza("storage", { xmlns = "storage:bookmarks" });
52 for i in ipairs(ret) do
53 local item = ret[ret[i]];
54 local conference = st.stanza("conference");
55 conference.attr.jid = item.attr.id;
56 local bookmark = item:get_child("conference", "urn:xmpp:bookmarks:0");
57 conference.attr.name = bookmark.attr.name;
58 conference.attr.autojoin = bookmark.attr.autojoin;
59 local nick = bookmark:get_child_text("nick", "urn:xmpp:bookmarks:0");
60 if nick ~= nil then
61 conference:text_tag("nick", nick, { xmlns = "storage:bookmarks" }):up();
62 end
63 local password = bookmark:get_child_text("password", "urn:xmpp:bookmarks:0");
64 if password ~= nil then
65 conference:text_tag("password", password):up();
66 end
67 storage:add_child(conference);
68 end
69
70 module:log("debug", "Sending back private for %s: %s", jid, storage);
71 session.send(st.reply(stanza):query("jabber:iq:private"):add_child(storage));
72 return true;
73 end
74
75 local function compare_bookmark2(a, b)
76 if a == nil or b == nil then
77 return false;
78 end
79 local a_conference = a:get_child("conference", "urn:xmpp:bookmarks:0");
80 local b_conference = b:get_child("conference", "urn:xmpp:bookmarks:0");
81 local a_nick = a:get_child_text("nick", "urn:xmpp:bookmarks:0");
82 local b_nick = b:get_child_text("nick", "urn:xmpp:bookmarks:0");
83 local a_password = a:get_child_text("password", "urn:xmpp:bookmarks:0");
84 local b_password = b:get_child_text("password", "urn:xmpp:bookmarks:0");
85 return (a.attr.id == b.attr.id and
86 a_conference.attr.name == b_conference.attr.name and
87 a_conference.attr.autojoin == b_conference.attr.autojoin and
88 a_nick == b_nick and
89 a_password == b_password);
90 end
91
92 local function publish_to_pep(jid, bookmarks)
93 local service = mod_pep.get_pep_service(jid_split(jid));
94
95 -- If we set zero legacy bookmarks, purge the bookmarks 2 node.
96 if #bookmarks.tags == 0 then
97 module:log("debug", "No bookmark in the set, purging instead.");
98 return service:purge("urn:xmpp:bookmarks:0", jid, true);
99 end
100
101 -- Retrieve the current bookmarks2.
102 module:log("debug", "Retrieving the current bookmarks 2.");
103 local has_bookmarks2, ret = service:get_items("urn:xmpp:bookmarks:0", jid);
104 local bookmarks2;
105 if not has_bookmarks2 and ret == "item-not-found" then
106 module:log("debug", "Got item-not-found, assuming it was empty until now, creating.");
107 local ok, err = service:create("urn:xmpp:bookmarks:0", jid, default_options);
108 if not ok then
109 module:log("error", "Creating bookmarks 2 node failed: %s", err);
110 return ok, err;
111 end
112 bookmarks2 = {};
113 elseif not has_bookmarks2 then
114 module:log("debug", "Got %s error, aborting.", ret);
115 return false, ret;
116 else
117 module:log("debug", "Got existing bookmarks2.");
118 bookmarks2 = ret;
119 end
120
121 -- Get a list of all items we may want to remove.
122 local to_remove = {};
123 for i in ipairs(bookmarks2) do
124 to_remove[bookmarks2[i]] = true;
125 end
126
127 for bookmark in bookmarks:childtags("conference", "storage:bookmarks") do
128 -- Create the new conference element by copying everything from the legacy one.
129 local conference = st.stanza("conference", { xmlns = "urn:xmpp:bookmarks:0" });
130 conference.attr.name = bookmark.attr.name;
131 conference.attr.autojoin = bookmark.attr.autojoin;
132 local nick = bookmark:get_child_text("nick", "storage:bookmarks");
133 if nick ~= nil then
134 conference:text_tag("nick", nick, { xmlns = "urn:xmpp:bookmarks:0" }):up();
135 end
136 local password = bookmark:get_child_text("password", "storage:bookmarks");
137 if password ~= nil then
138 conference:text_tag("password", password, { xmlns = "urn:xmpp:bookmarks:0" }):up();
139 end
140
141 -- Create its wrapper.
142 local item = st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = bookmark.attr.jid })
143 :add_child(conference);
144
145 -- Then publish it only if it’s a new one or updating a previous one.
146 if compare_bookmark2(item, bookmarks2[bookmark.attr.jid]) then
147 module:log("debug", "Item %s identical to the previous one, skipping.", item.attr.id);
148 to_remove[bookmark.attr.jid] = nil;
149 else
150 if bookmarks2[bookmark.attr.jid] == nil then
151 module:log("debug", "Item %s not existing previously, publishing.", item.attr.id);
152 else
153 module:log("debug", "Item %s different from the previous one, publishing.", item.attr.id);
154 to_remove[bookmark.attr.jid] = nil;
155 end
156 local ok, err = service:publish("urn:xmpp:bookmarks:0", jid, bookmark.attr.jid, item, default_options);
157 if not ok then
158 module:log("error", "Publishing item %s failed: %s", item.attr.id, err);
159 return ok, err;
160 end
161 end
162 end
163
164 -- Now handle retracting items that have been removed.
165 for id in pairs(to_remove) do
166 module:log("debug", "Item %s removed from bookmarks.", id);
167 local ok, err = service:retract("urn:xmpp:bookmarks:0", jid, id, st.stanza("retract", { id = id }));
168 if not ok then
169 module:log("error", "Retracting item %s failed: %s", id, err);
170 return ok, err;
171 end
172 end
173 return true;
174 end
175
176 -- Synchronise Private XML to PEP.
177 local function on_publish_private_xml(event)
178 local stanza, session = event.stanza, event.origin;
179 local query = stanza:get_child("query", "jabber:iq:private");
180 if query == nil then
181 return;
182 end
183
184 local bookmarks = query:get_child("storage", legacy_ns);
185 if bookmarks == nil then
186 return;
187 end
188
189 module:log("debug", "Private bookmarks set by client, publishing to pep.");
190
191 local ok, err = publish_to_pep(session.full_jid, bookmarks);
192 if not ok then
193 module:log("error", "Failed to publish to PEP bookmarks for %s@%s: %s", session.username, session.host, err);
194 session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP"));
195 return true;
196 end
197
198 session.send(st.reply(stanza));
199 return true;
200 end
201
202 local function migrate_legacy_bookmarks(event)
203 local session = event.session;
204 local username = session.username;
205 local service = mod_pep.get_pep_service(username);
206 local jid = username.."@"..session.host;
207
208 local data, err = private_storage:get(username, "storage:storage:bookmarks");
209 if not data then
210 module:log("debug", "No existing legacy bookmarks for %s, migration already done: %s", jid, err);
211 local ok, ret = service:get_items("urn:xmpp:bookmarks:0", session.full_jid);
212 if not ok or #ret.tags == 0 then
213 module:log("debug", "Additionally, no bookmarks 2 were existing for %s, assuming empty.", jid);
214 module:fire_event("bookmarks/empty", { session = session });
215 end
216 return;
217 end
218 local bookmarks = st.deserialize(data);
219 module:log("debug", "Got legacy bookmarks of %s: %s", jid, bookmarks);
220
221 -- We don’t care if deleting succeeds or not, we only want to start with a non-existent node.
222 module:log("debug", "Deleting possibly existing PEP item for %s.", jid);
223 service:delete("urn:xmpp:bookmarks:0", jid);
224
225 module:log("debug", "Going to store PEP item for %s.", jid);
226 local ok, err = publish_to_pep(session.full_jid, bookmarks);
227 if not ok then
228 module:log("error", "Failed to store bookmarks to PEP for %s, aborting migration: %s", jid, err);
229 return;
230 end
231 module:log("debug", "Stored bookmarks to PEP for %s.", jid);
232
233 local ok, err = private_storage:set(username, "storage:storage:bookmarks", nil);
234 if not ok then
235 module:log("error", "Failed to remove private bookmarks of %s: %s", jid, err);
236 return;
237 end
238 module:log("debug", "Removed private bookmarks of %s, migration done!", jid);
239 end
240
241 local function on_node_created(event)
242 local service, node, actor = event.service, event.node, event.actor;
243 if node ~= "storage:bookmarks" then
244 return;
245 end
246 local ok, node_config = service:get_node_config(node, actor);
247 if not ok then
248 module:log("error", "Failed to get node config of %s: %s", node, node_config);
249 return;
250 end
251 local changed = false;
252 for config_field, value in pairs(default_options) do
253 if node_config[config_field] ~= value then
254 node_config[config_field] = value;
255 changed = true;
256 end
257 end
258 if not changed then
259 return;
260 end
261 local ok, err = service:set_node_config(node, actor, node_config);
262 if not ok then
263 module:log("error", "Failed to set node config of %s: %s", node, err);
264 return;
265 end
266 end
267
268 module:hook("iq/bare/jabber:iq:private:query", function (event)
269 if event.stanza.attr.type == "get" then
270 return on_retrieve_private_xml(event);
271 else
272 return on_publish_private_xml(event);
273 end
274 end, 1);
275 module:hook("resource-bind", migrate_legacy_bookmarks);
276 module:handle_items("pep-service", function (event)
277 local service = event.item.service;
278 module:hook_object_event(service.events, "node-created", on_node_created);
279 end, function () end, true);