Mercurial > prosody-modules
comparison mod_pep_plus/pubsub.lib.lua @ 2801:cb2342cf3f3c
mod_pep_plus: Snapshot from Prosody trunk 910d3c3f60a6 including dependencies
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Wed, 18 Oct 2017 09:56:29 +0200 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
2800:8d9aed6d1f87 | 2801:cb2342cf3f3c |
---|---|
1 local t_unpack = table.unpack or unpack; -- luacheck: ignore 113 | |
2 local time_now = os.time; | |
3 | |
4 local set = require "util.set"; | |
5 local st = require "util.stanza"; | |
6 local it = require "util.iterators"; | |
7 local uuid_generate = require "util.uuid".generate; | |
8 local dataform = require"util.dataforms".new; | |
9 | |
10 local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; | |
11 local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; | |
12 local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; | |
13 | |
14 local _M = {}; | |
15 | |
16 local handlers = {}; | |
17 _M.handlers = handlers; | |
18 | |
19 local pubsub_errors = { | |
20 ["conflict"] = { "cancel", "conflict" }; | |
21 ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; | |
22 ["jid-required"] = { "modify", "bad-request", nil, "jid-required" }; | |
23 ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" }; | |
24 ["item-not-found"] = { "cancel", "item-not-found" }; | |
25 ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" }; | |
26 ["forbidden"] = { "auth", "forbidden" }; | |
27 ["not-allowed"] = { "cancel", "not-allowed" }; | |
28 }; | |
29 local function pubsub_error_reply(stanza, error) | |
30 local e = pubsub_errors[error]; | |
31 local reply = st.error_reply(stanza, t_unpack(e, 1, 3)); | |
32 if e[4] then | |
33 reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); | |
34 end | |
35 return reply; | |
36 end | |
37 _M.pubsub_error_reply = pubsub_error_reply; | |
38 | |
39 local node_config_form = dataform { | |
40 { | |
41 type = "hidden"; | |
42 name = "FORM_TYPE"; | |
43 value = "http://jabber.org/protocol/pubsub#node_config"; | |
44 }; | |
45 { | |
46 type = "text-single"; | |
47 name = "pubsub#max_items"; | |
48 label = "Max # of items to persist"; | |
49 }; | |
50 { | |
51 type = "boolean"; | |
52 name = "pubsub#persist_items"; | |
53 label = "Persist items to storage"; | |
54 }; | |
55 }; | |
56 | |
57 local service_method_feature_map = { | |
58 add_subscription = { "subscribe" }; | |
59 create = { "create-nodes", "instant-nodes", "item-ids", "create-and-configure" }; | |
60 delete = { "delete-nodes" }; | |
61 get_items = { "retrieve-items" }; | |
62 get_subscriptions = { "retrieve-subscriptions" }; | |
63 node_defaults = { "retrieve-default" }; | |
64 publish = { "publish" }; | |
65 purge = { "purge-nodes" }; | |
66 retract = { "delete-items", "retract-items" }; | |
67 set_node_config = { "config-node" }; | |
68 }; | |
69 local service_config_feature_map = { | |
70 autocreate_on_publish = { "auto-create" }; | |
71 }; | |
72 | |
73 function _M.get_feature_set(service) | |
74 local supported_features = set.new(); | |
75 | |
76 for method, features in pairs(service_method_feature_map) do | |
77 if service[method] then | |
78 for _, feature in ipairs(features) do | |
79 if feature then | |
80 supported_features:add(feature); | |
81 end | |
82 end | |
83 end | |
84 end | |
85 | |
86 for option, features in pairs(service_config_feature_map) do | |
87 if service.config[option] then | |
88 for _, feature in ipairs(features) do | |
89 if feature then | |
90 supported_features:add(feature); | |
91 end | |
92 end | |
93 end | |
94 end | |
95 | |
96 for affiliation in pairs(service.config.capabilities) do | |
97 if affiliation ~= "none" and affiliation ~= "owner" then | |
98 supported_features:add(affiliation.."-affiliation"); | |
99 end | |
100 end | |
101 | |
102 return supported_features; | |
103 end | |
104 | |
105 function _M.handle_pubsub_iq(event, service) | |
106 local origin, stanza = event.origin, event.stanza; | |
107 local pubsub_tag = stanza.tags[1]; | |
108 local action = pubsub_tag.tags[1]; | |
109 if not action then | |
110 return origin.send(st.error_reply(stanza, "cancel", "bad-request")); | |
111 end | |
112 local prefix = ""; | |
113 if pubsub_tag.attr.xmlns == xmlns_pubsub_owner then | |
114 prefix = "owner_"; | |
115 end | |
116 local handler = handlers[prefix..stanza.attr.type.."_"..action.name]; | |
117 if handler then | |
118 handler(origin, stanza, action, service); | |
119 return true; | |
120 end | |
121 end | |
122 | |
123 function handlers.get_items(origin, stanza, items, service) | |
124 local node = items.attr.node; | |
125 local item = items:get_child("item"); | |
126 local item_id = item and item.attr.id; | |
127 | |
128 if not node then | |
129 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
130 return true; | |
131 end | |
132 local ok, results = service:get_items(node, stanza.attr.from, item_id); | |
133 if not ok then | |
134 origin.send(pubsub_error_reply(stanza, results)); | |
135 return true; | |
136 end | |
137 | |
138 local data = st.stanza("items", { node = node }); | |
139 for _, id in ipairs(results) do | |
140 data:add_child(results[id]); | |
141 end | |
142 local reply; | |
143 if data then | |
144 reply = st.reply(stanza) | |
145 :tag("pubsub", { xmlns = xmlns_pubsub }) | |
146 :add_child(data); | |
147 else | |
148 reply = pubsub_error_reply(stanza, "item-not-found"); | |
149 end | |
150 origin.send(reply); | |
151 return true; | |
152 end | |
153 | |
154 function handlers.get_subscriptions(origin, stanza, subscriptions, service) | |
155 local node = subscriptions.attr.node; | |
156 local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); | |
157 if not ok then | |
158 origin.send(pubsub_error_reply(stanza, ret)); | |
159 return true; | |
160 end | |
161 local reply = st.reply(stanza) | |
162 :tag("pubsub", { xmlns = xmlns_pubsub }) | |
163 :tag("subscriptions"); | |
164 for _, sub in ipairs(ret) do | |
165 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); | |
166 end | |
167 origin.send(reply); | |
168 return true; | |
169 end | |
170 | |
171 function handlers.set_create(origin, stanza, create, service) | |
172 local node = create.attr.node; | |
173 local ok, ret, reply; | |
174 local config; | |
175 local configure = stanza.tags[1]:get_child("configure"); | |
176 if configure then | |
177 local config_form = configure:get_child("x", "jabber:x:data"); | |
178 if not config_form then | |
179 origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform")); | |
180 return true; | |
181 end | |
182 local form_data, err = node_config_form:data(config_form); | |
183 if not form_data then | |
184 origin.send(st.error_reply(stanza, "modify", "bad-request", err)); | |
185 return true; | |
186 end | |
187 config = { | |
188 ["max_items"] = tonumber(form_data["pubsub#max_items"]); | |
189 ["persist_items"] = form_data["pubsub#persist_items"]; | |
190 }; | |
191 end | |
192 if node then | |
193 ok, ret = service:create(node, stanza.attr.from, config); | |
194 if ok then | |
195 reply = st.reply(stanza); | |
196 else | |
197 reply = pubsub_error_reply(stanza, ret); | |
198 end | |
199 else | |
200 repeat | |
201 node = uuid_generate(); | |
202 ok, ret = service:create(node, stanza.attr.from, config); | |
203 until ok or ret ~= "conflict"; | |
204 if ok then | |
205 reply = st.reply(stanza) | |
206 :tag("pubsub", { xmlns = xmlns_pubsub }) | |
207 :tag("create", { node = node }); | |
208 else | |
209 reply = pubsub_error_reply(stanza, ret); | |
210 end | |
211 end | |
212 origin.send(reply); | |
213 return true; | |
214 end | |
215 | |
216 function handlers.owner_set_delete(origin, stanza, delete, service) | |
217 local node = delete.attr.node; | |
218 | |
219 local reply; | |
220 if not node then | |
221 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
222 return true; | |
223 end | |
224 local ok, ret = service:delete(node, stanza.attr.from); | |
225 if ok then | |
226 reply = st.reply(stanza); | |
227 else | |
228 reply = pubsub_error_reply(stanza, ret); | |
229 end | |
230 origin.send(reply); | |
231 return true; | |
232 end | |
233 | |
234 function handlers.set_subscribe(origin, stanza, subscribe, service) | |
235 local node, jid = subscribe.attr.node, subscribe.attr.jid; | |
236 if not (node and jid) then | |
237 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); | |
238 return true; | |
239 end | |
240 --[[ | |
241 local options_tag, options = stanza.tags[1]:get_child("options"), nil; | |
242 if options_tag then | |
243 options = options_form:data(options_tag.tags[1]); | |
244 end | |
245 --]] | |
246 local options_tag, options; -- FIXME | |
247 local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options); | |
248 local reply; | |
249 if ok then | |
250 reply = st.reply(stanza) | |
251 :tag("pubsub", { xmlns = xmlns_pubsub }) | |
252 :tag("subscription", { | |
253 node = node, | |
254 jid = jid, | |
255 subscription = "subscribed" | |
256 }):up(); | |
257 if options_tag then | |
258 reply:add_child(options_tag); | |
259 end | |
260 else | |
261 reply = pubsub_error_reply(stanza, ret); | |
262 end | |
263 origin.send(reply); | |
264 end | |
265 | |
266 function handlers.set_unsubscribe(origin, stanza, unsubscribe, service) | |
267 local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; | |
268 if not (node and jid) then | |
269 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); | |
270 return true; | |
271 end | |
272 local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); | |
273 local reply; | |
274 if ok then | |
275 reply = st.reply(stanza); | |
276 else | |
277 reply = pubsub_error_reply(stanza, ret); | |
278 end | |
279 origin.send(reply); | |
280 return true; | |
281 end | |
282 | |
283 function handlers.set_publish(origin, stanza, publish, service) | |
284 local node = publish.attr.node; | |
285 if not node then | |
286 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
287 return true; | |
288 end | |
289 local item = publish:get_child("item"); | |
290 local id = (item and item.attr.id); | |
291 if not id then | |
292 id = uuid_generate(); | |
293 if item then | |
294 item.attr.id = id; | |
295 end | |
296 end | |
297 local ok, ret = service:publish(node, stanza.attr.from, id, item); | |
298 local reply; | |
299 if ok then | |
300 reply = st.reply(stanza) | |
301 :tag("pubsub", { xmlns = xmlns_pubsub }) | |
302 :tag("publish", { node = node }) | |
303 :tag("item", { id = id }); | |
304 else | |
305 reply = pubsub_error_reply(stanza, ret); | |
306 end | |
307 origin.send(reply); | |
308 return true; | |
309 end | |
310 | |
311 function handlers.set_retract(origin, stanza, retract, service) | |
312 local node, notify = retract.attr.node, retract.attr.notify; | |
313 notify = (notify == "1") or (notify == "true"); | |
314 local item = retract:get_child("item"); | |
315 local id = item and item.attr.id | |
316 if not (node and id) then | |
317 origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required")); | |
318 return true; | |
319 end | |
320 local reply, notifier; | |
321 if notify then | |
322 notifier = st.stanza("retract", { id = id }); | |
323 end | |
324 local ok, ret = service:retract(node, stanza.attr.from, id, notifier); | |
325 if ok then | |
326 reply = st.reply(stanza); | |
327 else | |
328 reply = pubsub_error_reply(stanza, ret); | |
329 end | |
330 origin.send(reply); | |
331 return true; | |
332 end | |
333 | |
334 function handlers.owner_set_purge(origin, stanza, purge, service) | |
335 local node, notify = purge.attr.node, purge.attr.notify; | |
336 notify = (notify == "1") or (notify == "true"); | |
337 local reply; | |
338 if not node then | |
339 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
340 return true; | |
341 end | |
342 local ok, ret = service:purge(node, stanza.attr.from, notify); | |
343 if ok then | |
344 reply = st.reply(stanza); | |
345 else | |
346 reply = pubsub_error_reply(stanza, ret); | |
347 end | |
348 origin.send(reply); | |
349 return true; | |
350 end | |
351 | |
352 function handlers.owner_get_configure(origin, stanza, config, service) | |
353 local node = config.attr.node; | |
354 if not node then | |
355 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
356 return true; | |
357 end | |
358 | |
359 if not service:may(node, stanza.attr.from, "configure") then | |
360 origin.send(pubsub_error_reply(stanza, "forbidden")); | |
361 return true; | |
362 end | |
363 | |
364 local node_obj = service.nodes[node]; | |
365 if not node_obj then | |
366 origin.send(pubsub_error_reply(stanza, "item-not-found")); | |
367 return true; | |
368 end | |
369 | |
370 local node_config = node_obj.config; | |
371 local pubsub_form_data = { | |
372 ["pubsub#max_items"] = tostring(node_config["max_items"]); | |
373 ["pubsub#persist_items"] = node_config["persist_items"] | |
374 } | |
375 local reply = st.reply(stanza) | |
376 :tag("pubsub", { xmlns = xmlns_pubsub_owner }) | |
377 :tag("configure", { node = node }) | |
378 :add_child(node_config_form:form(pubsub_form_data)); | |
379 origin.send(reply); | |
380 return true; | |
381 end | |
382 | |
383 function handlers.owner_set_configure(origin, stanza, config, service) | |
384 local node = config.attr.node; | |
385 if not node then | |
386 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
387 return true; | |
388 end | |
389 if not service:may(node, stanza.attr.from, "configure") then | |
390 origin.send(pubsub_error_reply(stanza, "forbidden")); | |
391 return true; | |
392 end | |
393 local config_form = config:get_child("x", "jabber:x:data"); | |
394 if not config_form then | |
395 origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform")); | |
396 return true; | |
397 end | |
398 local form_data, err = node_config_form:data(config_form); | |
399 if not form_data then | |
400 origin.send(st.error_reply(stanza, "modify", "bad-request", err)); | |
401 return true; | |
402 end | |
403 local new_config = { | |
404 ["max_items"] = tonumber(form_data["pubsub#max_items"]); | |
405 ["persist_items"] = form_data["pubsub#persist_items"]; | |
406 }; | |
407 local ok, err = service:set_node_config(node, stanza.attr.from, new_config); | |
408 if not ok then | |
409 origin.send(pubsub_error_reply(stanza, err)); | |
410 return true; | |
411 end | |
412 origin.send(st.reply(stanza)); | |
413 return true; | |
414 end | |
415 | |
416 function handlers.owner_get_default(origin, stanza, default, service) -- luacheck: ignore 212/default | |
417 local pubsub_form_data = { | |
418 ["pubsub#max_items"] = tostring(service.node_defaults["max_items"]); | |
419 ["pubsub#persist_items"] = service.node_defaults["persist_items"] | |
420 } | |
421 local reply = st.reply(stanza) | |
422 :tag("pubsub", { xmlns = xmlns_pubsub_owner }) | |
423 :tag("default") | |
424 :add_child(node_config_form:form(pubsub_form_data)); | |
425 origin.send(reply); | |
426 return true; | |
427 end | |
428 | |
429 local function create_encapsulating_item(id, payload) | |
430 local item = st.stanza("item", { id = id, xmlns = xmlns_pubsub }); | |
431 item:add_child(payload); | |
432 return item; | |
433 end | |
434 | |
435 local function archive_itemstore(archive, config, user, node) | |
436 module:log("debug", "Creation of itemstore for node %s with config %s", node, config); | |
437 local get_set = {}; | |
438 function get_set:items() -- luacheck: ignore 212/self | |
439 local data, err = archive:find(user, { | |
440 limit = tonumber(config["pubsub#max_items"]); | |
441 reverse = true; | |
442 }); | |
443 if not data then | |
444 module:log("error", "Unable to get items: %s", err); | |
445 return true; | |
446 end | |
447 module:log("debug", "Listed items %s", data); | |
448 return it.reverse(function() | |
449 local id, payload, when, publisher = data(); | |
450 if id == nil then | |
451 return; | |
452 end | |
453 local item = create_encapsulating_item(id, payload, publisher); | |
454 return id, item; | |
455 end); | |
456 end | |
457 function get_set:get(key) -- luacheck: ignore 212/self | |
458 local data, err = archive:find(user, { | |
459 key = key; | |
460 -- Get the last item with that key, if the archive doesn't deduplicate | |
461 reverse = true, | |
462 limit = 1; | |
463 }); | |
464 if not data then | |
465 module:log("error", "Unable to get item: %s", err); | |
466 return nil, err; | |
467 end | |
468 local id, payload, when, publisher = data(); | |
469 module:log("debug", "Get item %s (published at %s by %s)", id, when, publisher); | |
470 if id == nil then | |
471 return nil; | |
472 end | |
473 return create_encapsulating_item(id, payload, publisher); | |
474 end | |
475 function get_set:set(key, value) -- luacheck: ignore 212/self | |
476 local data, err; | |
477 if value ~= nil then | |
478 local publisher = value.attr.publisher; | |
479 local payload = value.tags[1]; | |
480 data, err = archive:append(user, key, payload, time_now(), publisher); | |
481 else | |
482 data, err = archive:delete(user, { key = key; }); | |
483 end | |
484 if not data then | |
485 module:log("error", "Unable to set item: %s", err); | |
486 return nil, err; | |
487 end | |
488 return data; | |
489 end | |
490 function get_set:clear() -- luacheck: ignore 212/self | |
491 return archive:delete(user); | |
492 end | |
493 return setmetatable(get_set, archive); | |
494 end | |
495 _M.archive_itemstore = archive_itemstore; | |
496 | |
497 return _M; |