mod_csi_simple: Consider messages encrypted payload as important (fixes part of ...
[prosody.git] / plugins / mod_pubsub / pubsub.lib.lua
blob1bd5fa33f7cee4dc69df4068c77d59e6d2514dd3
1 local t_unpack = table.unpack or unpack; -- luacheck: ignore 113
2 local time_now = os.time;
4 local jid_prep = require "util.jid".prep;
5 local set = require "util.set";
6 local st = require "util.stanza";
7 local it = require "util.iterators";
8 local uuid_generate = require "util.uuid".generate;
9 local dataform = require"util.dataforms".new;
11 local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
12 local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
13 local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
15 local _M = {};
17 local handlers = {};
18 _M.handlers = handlers;
20 local pubsub_errors = {
21 ["conflict"] = { "cancel", "conflict" };
22 ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
23 ["jid-required"] = { "modify", "bad-request", nil, "jid-required" };
24 ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" };
25 ["item-not-found"] = { "cancel", "item-not-found" };
26 ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
27 ["invalid-options"] = { "modify", "bad-request", nil, "invalid-options" };
28 ["forbidden"] = { "auth", "forbidden" };
29 ["not-allowed"] = { "cancel", "not-allowed" };
30 ["not-acceptable"] = { "modify", "not-acceptable" };
31 ["internal-server-error"] = { "wait", "internal-server-error" };
32 ["precondition-not-met"] = { "cancel", "conflict", nil, "precondition-not-met" };
33 ["invalid-item"] = { "modify", "bad-request", "invalid item" };
35 local function pubsub_error_reply(stanza, error)
36 local e = pubsub_errors[error];
37 local reply = st.error_reply(stanza, t_unpack(e, 1, 3));
38 if e[4] then
39 reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
40 end
41 return reply;
42 end
43 _M.pubsub_error_reply = pubsub_error_reply;
45 local function dataform_error_message(err) -- ({ string : string }) -> string?
46 local out = {};
47 for field, errmsg in pairs(err) do
48 table.insert(out, ("%s: %s"):format(field, errmsg))
49 end
50 return table.concat(out, "; ");
51 end
53 -- Note: If any config options are added that are of complex types,
54 -- (not simply strings/numbers) then the publish-options code will
55 -- need to be revisited
56 local node_config_form = dataform {
58 type = "hidden";
59 var = "FORM_TYPE";
60 value = "http://jabber.org/protocol/pubsub#node_config";
63 type = "text-single";
64 name = "title";
65 var = "pubsub#title";
66 label = "Title";
69 type = "text-single";
70 name = "description";
71 var = "pubsub#description";
72 label = "Description";
75 type = "text-single";
76 name = "payload_type";
77 var = "pubsub#type";
78 label = "The type of node data, usually specified by the namespace of the payload (if any)";
81 type = "text-single";
82 datatype = "xs:integer";
83 name = "max_items";
84 var = "pubsub#max_items";
85 label = "Max # of items to persist";
88 type = "boolean";
89 name = "persist_items";
90 var = "pubsub#persist_items";
91 label = "Persist items to storage";
94 type = "list-single";
95 name = "access_model";
96 var = "pubsub#access_model";
97 label = "Specify the subscriber model";
98 options = {
99 "authorize",
100 "open",
101 "presence",
102 "roster",
103 "whitelist",
107 type = "list-single";
108 name = "publish_model";
109 var = "pubsub#publish_model";
110 label = "Specify the publisher model";
111 options = {
112 "publishers";
113 "subscribers";
114 "open";
118 type = "boolean";
119 value = true;
120 label = "Whether to deliver event notifications";
121 name = "notify_items";
122 var = "pubsub#deliver_notifications";
125 type = "boolean";
126 value = true;
127 label = "Whether to deliver payloads with event notifications";
128 name = "include_payload";
129 var = "pubsub#deliver_payloads";
132 type = "list-single";
133 name = "notification_type";
134 var = "pubsub#notification_type";
135 label = "Specify the delivery style for notifications";
136 options = {
137 { label = "Messages of type normal", value = "normal" },
138 { label = "Messages of type headline", value = "headline", default = true },
142 type = "boolean";
143 label = "Whether to notify subscribers when the node is deleted";
144 name = "notify_delete";
145 var = "pubsub#notify_delete";
146 value = true;
149 type = "boolean";
150 label = "Whether to notify subscribers when items are removed from the node";
151 name = "notify_retract";
152 var = "pubsub#notify_retract";
153 value = true;
157 local subscribe_options_form = dataform {
159 type = "hidden";
160 var = "FORM_TYPE";
161 value = "http://jabber.org/protocol/pubsub#subscribe_options";
164 type = "boolean";
165 name = "pubsub#include_body";
166 label = "Receive message body in addition to payload?";
170 local node_metadata_form = dataform {
172 type = "hidden";
173 var = "FORM_TYPE";
174 value = "http://jabber.org/protocol/pubsub#meta-data";
177 type = "text-single";
178 name = "pubsub#title";
181 type = "text-single";
182 name = "pubsub#description";
185 type = "text-single";
186 name = "pubsub#type";
190 local service_method_feature_map = {
191 add_subscription = { "subscribe", "subscription-options" };
192 create = { "create-nodes", "instant-nodes", "item-ids", "create-and-configure" };
193 delete = { "delete-nodes" };
194 get_items = { "retrieve-items" };
195 get_subscriptions = { "retrieve-subscriptions" };
196 node_defaults = { "retrieve-default" };
197 publish = { "publish", "multi-items", "publish-options" };
198 purge = { "purge-nodes" };
199 retract = { "delete-items", "retract-items" };
200 set_node_config = { "config-node", "meta-data" };
201 set_affiliation = { "modify-affiliations" };
203 local service_config_feature_map = {
204 autocreate_on_publish = { "auto-create" };
207 function _M.get_feature_set(service)
208 local supported_features = set.new();
210 for method, features in pairs(service_method_feature_map) do
211 if service[method] then
212 for _, feature in ipairs(features) do
213 if feature then
214 supported_features:add(feature);
220 for option, features in pairs(service_config_feature_map) do
221 if service.config[option] then
222 for _, feature in ipairs(features) do
223 if feature then
224 supported_features:add(feature);
230 for affiliation in pairs(service.config.capabilities) do
231 if affiliation ~= "none" and affiliation ~= "owner" then
232 supported_features:add(affiliation.."-affiliation");
236 if service.node_defaults.access_model then
237 supported_features:add("access-"..service.node_defaults.access_model);
240 if rawget(service.config, "itemstore") and rawget(service.config, "nodestore") then
241 supported_features:add("persistent-items");
244 return supported_features;
247 function _M.handle_disco_info_node(event, service)
248 local stanza, reply, node = event.stanza, event.reply, event.node;
249 local ok, ret = service:get_nodes(stanza.attr.from);
250 local node_obj = ret[node];
251 if not ok or not node_obj then
252 return;
254 event.exists = true;
255 reply:tag("identity", { category = "pubsub", type = "leaf" }):up();
256 if node_obj.config then
257 reply:add_child(node_metadata_form:form({
258 ["pubsub#title"] = node_obj.config.title;
259 ["pubsub#description"] = node_obj.config.description;
260 ["pubsub#type"] = node_obj.config.payload_type;
261 }, "result"));
265 function _M.handle_disco_items_node(event, service)
266 local stanza, reply, node = event.stanza, event.reply, event.node;
267 local ok, ret = service:get_items(node, stanza.attr.from);
268 if not ok then
269 return;
272 for _, id in ipairs(ret) do
273 reply:tag("item", { jid = module.host, name = id }):up();
275 event.exists = true;
278 function _M.handle_pubsub_iq(event, service)
279 local origin, stanza = event.origin, event.stanza;
280 local pubsub_tag = stanza.tags[1];
281 local action = pubsub_tag.tags[1];
282 if not action then
283 return origin.send(st.error_reply(stanza, "cancel", "bad-request"));
285 local prefix = "";
286 if pubsub_tag.attr.xmlns == xmlns_pubsub_owner then
287 prefix = "owner_";
289 local handler = handlers[prefix..stanza.attr.type.."_"..action.name];
290 if handler then
291 handler(origin, stanza, action, service);
292 return true;
296 function handlers.get_items(origin, stanza, items, service)
297 local node = items.attr.node;
298 local item = items:get_child("item");
299 local item_id = item and item.attr.id;
301 if not node then
302 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
303 return true;
305 local ok, results = service:get_items(node, stanza.attr.from, item_id);
306 if not ok then
307 origin.send(pubsub_error_reply(stanza, results));
308 return true;
311 local data = st.stanza("items", { node = node });
312 for _, id in ipairs(results) do
313 data:add_child(results[id]);
315 local reply;
316 if data then
317 reply = st.reply(stanza)
318 :tag("pubsub", { xmlns = xmlns_pubsub })
319 :add_child(data);
320 else
321 reply = pubsub_error_reply(stanza, "item-not-found");
323 origin.send(reply);
324 return true;
327 function handlers.get_subscriptions(origin, stanza, subscriptions, service)
328 local node = subscriptions.attr.node;
329 local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
330 if not ok then
331 origin.send(pubsub_error_reply(stanza, ret));
332 return true;
334 local reply = st.reply(stanza)
335 :tag("pubsub", { xmlns = xmlns_pubsub })
336 :tag("subscriptions");
337 for _, sub in ipairs(ret) do
338 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
340 origin.send(reply);
341 return true;
344 function handlers.owner_get_subscriptions(origin, stanza, subscriptions, service)
345 local node = subscriptions.attr.node;
346 local ok, ret = service:get_subscriptions(node, stanza.attr.from);
347 if not ok then
348 origin.send(pubsub_error_reply(stanza, ret));
349 return true;
351 local reply = st.reply(stanza)
352 :tag("pubsub", { xmlns = xmlns_pubsub_owner })
353 :tag("subscriptions");
354 for _, sub in ipairs(ret) do
355 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
357 origin.send(reply);
358 return true;
361 function handlers.owner_set_subscriptions(origin, stanza, subscriptions, service)
362 local node = subscriptions.attr.node;
363 if not node then
364 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
365 return true;
367 if not service:may(node, stanza.attr.from, "subscribe_other") then
368 origin.send(pubsub_error_reply(stanza, "forbidden"));
369 return true;
372 local node_obj = service.nodes[node];
373 if not node_obj then
374 origin.send(pubsub_error_reply(stanza, "item-not-found"));
375 return true;
378 for subscription_tag in subscriptions:childtags("subscription") do
379 if subscription_tag.attr.subscription == 'subscribed' then
380 local ok, err = service:add_subscription(node, stanza.attr.from, subscription_tag.attr.jid);
381 if not ok then
382 origin.send(pubsub_error_reply(stanza, err));
383 return true;
385 elseif subscription_tag.attr.subscription == 'none' then
386 local ok, err = service:remove_subscription(node, stanza.attr.from, subscription_tag.attr.jid);
387 if not ok then
388 origin.send(pubsub_error_reply(stanza, err));
389 return true;
394 local reply = st.reply(stanza);
395 origin.send(reply);
396 return true;
399 function handlers.set_create(origin, stanza, create, service)
400 local node = create.attr.node;
401 local ok, ret, reply;
402 local config;
403 local configure = stanza.tags[1]:get_child("configure");
404 if configure then
405 local config_form = configure:get_child("x", "jabber:x:data");
406 if not config_form then
407 origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform"));
408 return true;
410 local form_data, err = node_config_form:data(config_form);
411 if err then
412 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
413 return true;
415 config = form_data;
417 if node then
418 ok, ret = service:create(node, stanza.attr.from, config);
419 if ok then
420 reply = st.reply(stanza);
421 else
422 reply = pubsub_error_reply(stanza, ret);
424 else
425 repeat
426 node = uuid_generate();
427 ok, ret = service:create(node, stanza.attr.from, config);
428 until ok or ret ~= "conflict";
429 if ok then
430 reply = st.reply(stanza)
431 :tag("pubsub", { xmlns = xmlns_pubsub })
432 :tag("create", { node = node });
433 else
434 reply = pubsub_error_reply(stanza, ret);
437 origin.send(reply);
438 return true;
441 function handlers.owner_set_delete(origin, stanza, delete, service)
442 local node = delete.attr.node;
444 local reply;
445 if not node then
446 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
447 return true;
449 local ok, ret = service:delete(node, stanza.attr.from);
450 if ok then
451 reply = st.reply(stanza);
452 else
453 reply = pubsub_error_reply(stanza, ret);
455 origin.send(reply);
456 return true;
459 function handlers.set_subscribe(origin, stanza, subscribe, service)
460 local node, jid = subscribe.attr.node, subscribe.attr.jid;
461 jid = jid_prep(jid);
462 if not (node and jid) then
463 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
464 return true;
466 local options_tag, options = stanza.tags[1]:get_child("options"), nil;
467 if options_tag then
468 -- FIXME form parsing errors ignored here, why?
469 local err
470 options, err = subscribe_options_form:data(options_tag.tags[1]);
471 if err then
472 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
473 return true
476 local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
477 local reply;
478 if ok then
479 reply = st.reply(stanza)
480 :tag("pubsub", { xmlns = xmlns_pubsub })
481 :tag("subscription", {
482 node = node,
483 jid = jid,
484 subscription = "subscribed"
485 }):up();
486 if options_tag then
487 reply:add_child(options_tag);
489 else
490 reply = pubsub_error_reply(stanza, ret);
492 origin.send(reply);
495 function handlers.set_unsubscribe(origin, stanza, unsubscribe, service)
496 local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
497 jid = jid_prep(jid);
498 if not (node and jid) then
499 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
500 return true;
502 local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
503 local reply;
504 if ok then
505 reply = st.reply(stanza);
506 else
507 reply = pubsub_error_reply(stanza, ret);
509 origin.send(reply);
510 return true;
513 function handlers.get_options(origin, stanza, options, service)
514 local node, jid = options.attr.node, options.attr.jid;
515 jid = jid_prep(jid);
516 if not (node and jid) then
517 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
518 return true;
520 local ok, ret = service:get_subscription(node, stanza.attr.from, jid);
521 if not ok then
522 origin.send(pubsub_error_reply(stanza, "not-subscribed"));
523 return true;
525 if ret == true then ret = {} end
526 origin.send(st.reply(stanza)
527 :tag("pubsub", { xmlns = xmlns_pubsub })
528 :tag("options", { node = node, jid = jid })
529 :add_child(subscribe_options_form:form(ret)));
530 return true;
533 function handlers.set_options(origin, stanza, options, service)
534 local node, jid = options.attr.node, options.attr.jid;
535 jid = jid_prep(jid);
536 if not (node and jid) then
537 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
538 return true;
540 local ok, ret = service:get_subscription(node, stanza.attr.from, jid);
541 if not ok then
542 origin.send(pubsub_error_reply(stanza, ret));
543 return true;
544 elseif not ret then
545 origin.send(pubsub_error_reply(stanza, "not-subscribed"));
546 return true;
548 local old_subopts = ret;
549 local new_subopts, err = subscribe_options_form:data(options.tags[1], old_subopts);
550 if err then
551 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
552 return true;
554 local ok, err = service:add_subscription(node, stanza.attr.from, jid, new_subopts);
555 if not ok then
556 origin.send(pubsub_error_reply(stanza, err));
557 return true;
559 origin.send(st.reply(stanza));
560 return true;
563 function handlers.set_publish(origin, stanza, publish, service)
564 local node = publish.attr.node;
565 if not node then
566 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
567 return true;
569 local required_config = nil;
570 local publish_options = stanza.tags[1]:get_child("publish-options");
571 if publish_options then
572 -- Ensure that the node configuration matches the values in publish-options
573 local publish_options_form = publish_options:get_child("x", "jabber:x:data");
574 local err;
575 required_config, err = node_config_form:data(publish_options_form);
576 if err then
577 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
578 return true
581 local item = publish:get_child("item");
582 local id = (item and item.attr.id);
583 if not id then
584 id = uuid_generate();
585 if item then
586 item.attr.id = id;
589 local ok, ret = service:publish(node, stanza.attr.from, id, item, required_config);
590 local reply;
591 if ok then
592 if type(ok) == "string" then
593 id = ok;
595 reply = st.reply(stanza)
596 :tag("pubsub", { xmlns = xmlns_pubsub })
597 :tag("publish", { node = node })
598 :tag("item", { id = id });
599 else
600 reply = pubsub_error_reply(stanza, ret);
602 origin.send(reply);
603 return true;
606 function handlers.set_retract(origin, stanza, retract, service)
607 local node, notify = retract.attr.node, retract.attr.notify;
608 notify = (notify == "1") or (notify == "true");
609 local item = retract:get_child("item");
610 local id = item and item.attr.id
611 if not (node and id) then
612 origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
613 return true;
615 local reply, notifier;
616 if notify then
617 notifier = st.stanza("retract", { id = id });
619 local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
620 if ok then
621 reply = st.reply(stanza);
622 else
623 reply = pubsub_error_reply(stanza, ret);
625 origin.send(reply);
626 return true;
629 function handlers.owner_set_purge(origin, stanza, purge, service)
630 local node, notify = purge.attr.node, purge.attr.notify;
631 notify = (notify == "1") or (notify == "true");
632 local reply;
633 if not node then
634 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
635 return true;
637 local ok, ret = service:purge(node, stanza.attr.from, notify);
638 if ok then
639 reply = st.reply(stanza);
640 else
641 reply = pubsub_error_reply(stanza, ret);
643 origin.send(reply);
644 return true;
647 function handlers.owner_get_configure(origin, stanza, config, service)
648 local node = config.attr.node;
649 if not node then
650 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
651 return true;
654 local ok, node_config = service:get_node_config(node, stanza.attr.from);
655 if not ok then
656 origin.send(pubsub_error_reply(stanza, node_config));
657 return true;
660 local reply = st.reply(stanza)
661 :tag("pubsub", { xmlns = xmlns_pubsub_owner })
662 :tag("configure", { node = node })
663 :add_child(node_config_form:form(node_config));
664 origin.send(reply);
665 return true;
668 function handlers.owner_set_configure(origin, stanza, config, service)
669 local node = config.attr.node;
670 if not node then
671 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
672 return true;
674 if not service:may(node, stanza.attr.from, "configure") then
675 origin.send(pubsub_error_reply(stanza, "forbidden"));
676 return true;
678 local config_form = config:get_child("x", "jabber:x:data");
679 if not config_form then
680 origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform"));
681 return true;
683 local ok, old_config = service:get_node_config(node, stanza.attr.from);
684 if not ok then
685 origin.send(pubsub_error_reply(stanza, old_config));
686 return true;
688 local new_config, err = node_config_form:data(config_form, old_config);
689 if err then
690 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
691 return true;
693 local ok, err = service:set_node_config(node, stanza.attr.from, new_config);
694 if not ok then
695 origin.send(pubsub_error_reply(stanza, err));
696 return true;
698 origin.send(st.reply(stanza));
699 return true;
702 function handlers.owner_get_default(origin, stanza, default, service) -- luacheck: ignore 212/default
703 local reply = st.reply(stanza)
704 :tag("pubsub", { xmlns = xmlns_pubsub_owner })
705 :tag("default")
706 :add_child(node_config_form:form(service.node_defaults));
707 origin.send(reply);
708 return true;
711 function handlers.owner_get_affiliations(origin, stanza, affiliations, service)
712 local node = affiliations.attr.node;
713 if not node then
714 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
715 return true;
717 if not service:may(node, stanza.attr.from, "set_affiliation") then
718 origin.send(pubsub_error_reply(stanza, "forbidden"));
719 return true;
722 local node_obj = service.nodes[node];
723 if not node_obj then
724 origin.send(pubsub_error_reply(stanza, "item-not-found"));
725 return true;
728 local reply = st.reply(stanza)
729 :tag("pubsub", { xmlns = xmlns_pubsub_owner })
730 :tag("affiliations", { node = node });
732 for jid, affiliation in pairs(node_obj.affiliations) do
733 reply:tag("affiliation", { jid = jid, affiliation = affiliation }):up();
736 origin.send(reply);
737 return true;
740 function handlers.owner_set_affiliations(origin, stanza, affiliations, service)
741 local node = affiliations.attr.node;
742 if not node then
743 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
744 return true;
746 if not service:may(node, stanza.attr.from, "set_affiliation") then
747 origin.send(pubsub_error_reply(stanza, "forbidden"));
748 return true;
751 local node_obj = service.nodes[node];
752 if not node_obj then
753 origin.send(pubsub_error_reply(stanza, "item-not-found"));
754 return true;
757 for affiliation_tag in affiliations:childtags("affiliation") do
758 local jid = affiliation_tag.attr.jid;
759 local affiliation = affiliation_tag.attr.affiliation;
761 jid = jid_prep(jid);
762 if affiliation == "none" then affiliation = nil; end
764 local ok, err = service:set_affiliation(node, stanza.attr.from, jid, affiliation);
765 if not ok then
766 -- FIXME Incomplete error handling,
767 -- see XEP 60 8.9.2.4 Multiple Simultaneous Modifications
768 origin.send(pubsub_error_reply(stanza, err));
769 return true;
773 local reply = st.reply(stanza);
774 origin.send(reply);
775 return true;
778 local function create_encapsulating_item(id, payload)
779 local item = st.stanza("item", { id = id, xmlns = xmlns_pubsub });
780 item:add_child(payload);
781 return item;
784 local function archive_itemstore(archive, config, user, node)
785 module:log("debug", "Creation of itemstore for node %s with config %s", node, config);
786 local get_set = {};
787 local max_items = config["max_items"];
788 function get_set:items() -- luacheck: ignore 212/self
789 local data, err = archive:find(user, {
790 limit = tonumber(max_items);
791 reverse = true;
793 if not data then
794 module:log("error", "Unable to get items: %s", err);
795 return true;
797 module:log("debug", "Listed items %s", data);
798 return it.reverse(function()
799 local id, payload, when, publisher = data();
800 if id == nil then
801 return;
803 local item = create_encapsulating_item(id, payload, publisher);
804 return id, item;
805 end);
807 function get_set:get(key) -- luacheck: ignore 212/self
808 local data, err = archive:find(user, {
809 key = key;
810 -- Get the last item with that key, if the archive doesn't deduplicate
811 reverse = true,
812 limit = 1;
814 if not data then
815 module:log("error", "Unable to get item: %s", err);
816 return nil, err;
818 local id, payload, when, publisher = data();
819 module:log("debug", "Get item %s (published at %s by %s)", id, when, publisher);
820 if id == nil then
821 return nil;
823 return create_encapsulating_item(id, payload, publisher);
825 function get_set:set(key, value) -- luacheck: ignore 212/self
826 local data, err;
827 if value ~= nil then
828 local publisher = value.attr.publisher;
829 local payload = value.tags[1];
830 data, err = archive:append(user, key, payload, time_now(), publisher);
831 else
832 data, err = archive:delete(user, { key = key; });
834 -- TODO archive support for maintaining maximum items
835 archive:delete(user, {
836 truncate = max_items;
838 if not data then
839 module:log("error", "Unable to set item: %s", err);
840 return nil, err;
842 return data;
844 function get_set:clear() -- luacheck: ignore 212/self
845 return archive:delete(user);
847 function get_set:resize(size) -- luacheck: ignore 212/self
848 max_items = size;
849 return archive:delete(user, {
850 truncate = size;
853 function get_set:head()
854 -- This should conveniently return the most recent item
855 local item = self:get(nil);
856 if item then
857 return item.attr.id, item;
860 return setmetatable(get_set, archive);
862 _M.archive_itemstore = archive_itemstore;
864 return _M;