util.encodings: Spell out all IDNA 2008 options ICU has
[prosody.git] / plugins / mod_pubsub / pubsub.lib.lua
blobd59e3d8526075fc0a73365a8db72fd24aa56413a
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";
189 type = "text-single";
190 name = "pubsub#access_model";
193 type = "text-single";
194 name = "pubsub#publish_model";
198 local service_method_feature_map = {
199 add_subscription = { "subscribe", "subscription-options" };
200 create = { "create-nodes", "instant-nodes", "item-ids", "create-and-configure" };
201 delete = { "delete-nodes" };
202 get_items = { "retrieve-items" };
203 get_subscriptions = { "retrieve-subscriptions" };
204 node_defaults = { "retrieve-default" };
205 publish = { "publish", "multi-items", "publish-options" };
206 purge = { "purge-nodes" };
207 retract = { "delete-items", "retract-items" };
208 set_node_config = { "config-node", "meta-data" };
209 set_affiliation = { "modify-affiliations" };
211 local service_config_feature_map = {
212 autocreate_on_publish = { "auto-create" };
215 function _M.get_feature_set(service)
216 local supported_features = set.new();
218 for method, features in pairs(service_method_feature_map) do
219 if service[method] then
220 for _, feature in ipairs(features) do
221 if feature then
222 supported_features:add(feature);
228 for option, features in pairs(service_config_feature_map) do
229 if service.config[option] then
230 for _, feature in ipairs(features) do
231 if feature then
232 supported_features:add(feature);
238 for affiliation in pairs(service.config.capabilities) do
239 if affiliation ~= "none" and affiliation ~= "owner" then
240 supported_features:add(affiliation.."-affiliation");
244 if service.node_defaults.access_model then
245 supported_features:add("access-"..service.node_defaults.access_model);
248 if rawget(service.config, "itemstore") and rawget(service.config, "nodestore") then
249 supported_features:add("persistent-items");
252 return supported_features;
255 function _M.handle_disco_info_node(event, service)
256 local stanza, reply, node = event.stanza, event.reply, event.node;
257 local ok, ret = service:get_nodes(stanza.attr.from);
258 local node_obj = ret[node];
259 if not ok or not node_obj then
260 return;
262 event.exists = true;
263 reply:tag("identity", { category = "pubsub", type = "leaf" }):up();
264 if node_obj.config then
265 reply:add_child(node_metadata_form:form({
266 ["pubsub#title"] = node_obj.config.title;
267 ["pubsub#description"] = node_obj.config.description;
268 ["pubsub#type"] = node_obj.config.payload_type;
269 ["pubsub#access_model"] = node_obj.config.access_model;
270 ["pubsub#publish_model"] = node_obj.config.publish_model;
271 }, "result"));
275 function _M.handle_disco_items_node(event, service)
276 local stanza, reply, node = event.stanza, event.reply, event.node;
277 local ok, ret = service:get_items(node, stanza.attr.from);
278 if not ok then
279 return;
282 for _, id in ipairs(ret) do
283 reply:tag("item", { jid = module.host, name = id }):up();
285 event.exists = true;
288 function _M.handle_pubsub_iq(event, service)
289 local origin, stanza = event.origin, event.stanza;
290 local pubsub_tag = stanza.tags[1];
291 local action = pubsub_tag.tags[1];
292 if not action then
293 return origin.send(st.error_reply(stanza, "cancel", "bad-request"));
295 local prefix = "";
296 if pubsub_tag.attr.xmlns == xmlns_pubsub_owner then
297 prefix = "owner_";
299 local handler = handlers[prefix..stanza.attr.type.."_"..action.name];
300 if handler then
301 handler(origin, stanza, action, service);
302 return true;
306 function handlers.get_items(origin, stanza, items, service)
307 local node = items.attr.node;
309 local requested_items = {};
310 for item in items:childtags("item") do
311 table.insert(requested_items, item.attr.id);
313 if requested_items[1] == nil then
314 requested_items = nil;
317 if not node then
318 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
319 return true;
321 local ok, results = service:get_items(node, stanza.attr.from, requested_items);
322 if not ok then
323 origin.send(pubsub_error_reply(stanza, results));
324 return true;
327 local data = st.stanza("items", { node = node });
328 for _, id in ipairs(results) do
329 data:add_child(results[id]);
331 local reply = st.reply(stanza)
332 :tag("pubsub", { xmlns = xmlns_pubsub })
333 :add_child(data);
334 origin.send(reply);
335 return true;
338 function handlers.get_subscriptions(origin, stanza, subscriptions, service)
339 local node = subscriptions.attr.node;
340 local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
341 if not ok then
342 origin.send(pubsub_error_reply(stanza, ret));
343 return true;
345 local reply = st.reply(stanza)
346 :tag("pubsub", { xmlns = xmlns_pubsub })
347 :tag("subscriptions");
348 for _, sub in ipairs(ret) do
349 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
351 origin.send(reply);
352 return true;
355 function handlers.owner_get_subscriptions(origin, stanza, subscriptions, service)
356 local node = subscriptions.attr.node;
357 local ok, ret = service:get_subscriptions(node, stanza.attr.from);
358 if not ok then
359 origin.send(pubsub_error_reply(stanza, ret));
360 return true;
362 local reply = st.reply(stanza)
363 :tag("pubsub", { xmlns = xmlns_pubsub_owner })
364 :tag("subscriptions");
365 for _, sub in ipairs(ret) do
366 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
368 origin.send(reply);
369 return true;
372 function handlers.owner_set_subscriptions(origin, stanza, subscriptions, service)
373 local node = subscriptions.attr.node;
374 if not node then
375 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
376 return true;
378 if not service:may(node, stanza.attr.from, "subscribe_other") then
379 origin.send(pubsub_error_reply(stanza, "forbidden"));
380 return true;
383 local node_obj = service.nodes[node];
384 if not node_obj then
385 origin.send(pubsub_error_reply(stanza, "item-not-found"));
386 return true;
389 for subscription_tag in subscriptions:childtags("subscription") do
390 if subscription_tag.attr.subscription == 'subscribed' then
391 local ok, err = service:add_subscription(node, stanza.attr.from, subscription_tag.attr.jid);
392 if not ok then
393 origin.send(pubsub_error_reply(stanza, err));
394 return true;
396 elseif subscription_tag.attr.subscription == 'none' then
397 local ok, err = service:remove_subscription(node, stanza.attr.from, subscription_tag.attr.jid);
398 if not ok then
399 origin.send(pubsub_error_reply(stanza, err));
400 return true;
405 local reply = st.reply(stanza);
406 origin.send(reply);
407 return true;
410 function handlers.set_create(origin, stanza, create, service)
411 local node = create.attr.node;
412 local ok, ret, reply;
413 local config;
414 local configure = stanza.tags[1]:get_child("configure");
415 if configure then
416 local config_form = configure:get_child("x", "jabber:x:data");
417 if not config_form then
418 origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform"));
419 return true;
421 local form_data, err = node_config_form:data(config_form);
422 if err then
423 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
424 return true;
426 config = form_data;
428 if node then
429 ok, ret = service:create(node, stanza.attr.from, config);
430 if ok then
431 reply = st.reply(stanza);
432 else
433 reply = pubsub_error_reply(stanza, ret);
435 else
436 repeat
437 node = uuid_generate();
438 ok, ret = service:create(node, stanza.attr.from, config);
439 until ok or ret ~= "conflict";
440 if ok then
441 reply = st.reply(stanza)
442 :tag("pubsub", { xmlns = xmlns_pubsub })
443 :tag("create", { node = node });
444 else
445 reply = pubsub_error_reply(stanza, ret);
448 origin.send(reply);
449 return true;
452 function handlers.owner_set_delete(origin, stanza, delete, service)
453 local node = delete.attr.node;
455 local reply;
456 if not node then
457 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
458 return true;
460 local ok, ret = service:delete(node, stanza.attr.from);
461 if ok then
462 reply = st.reply(stanza);
463 else
464 reply = pubsub_error_reply(stanza, ret);
466 origin.send(reply);
467 return true;
470 function handlers.set_subscribe(origin, stanza, subscribe, service)
471 local node, jid = subscribe.attr.node, subscribe.attr.jid;
472 jid = jid_prep(jid);
473 if not (node and jid) then
474 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
475 return true;
477 local options_tag, options = stanza.tags[1]:get_child("options"), nil;
478 if options_tag then
479 -- FIXME form parsing errors ignored here, why?
480 local err
481 options, err = subscribe_options_form:data(options_tag.tags[1]);
482 if err then
483 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
484 return true
487 local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
488 local reply;
489 if ok then
490 reply = st.reply(stanza)
491 :tag("pubsub", { xmlns = xmlns_pubsub })
492 :tag("subscription", {
493 node = node,
494 jid = jid,
495 subscription = "subscribed"
496 }):up();
497 if options_tag then
498 reply:add_child(options_tag);
500 else
501 reply = pubsub_error_reply(stanza, ret);
503 origin.send(reply);
506 function handlers.set_unsubscribe(origin, stanza, unsubscribe, service)
507 local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
508 jid = jid_prep(jid);
509 if not (node and jid) then
510 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
511 return true;
513 local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
514 local reply;
515 if ok then
516 reply = st.reply(stanza);
517 else
518 reply = pubsub_error_reply(stanza, ret);
520 origin.send(reply);
521 return true;
524 function handlers.get_options(origin, stanza, options, service)
525 local node, jid = options.attr.node, options.attr.jid;
526 jid = jid_prep(jid);
527 if not (node and jid) then
528 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
529 return true;
531 local ok, ret = service:get_subscription(node, stanza.attr.from, jid);
532 if not ok then
533 origin.send(pubsub_error_reply(stanza, "not-subscribed"));
534 return true;
536 if ret == true then ret = {} end
537 origin.send(st.reply(stanza)
538 :tag("pubsub", { xmlns = xmlns_pubsub })
539 :tag("options", { node = node, jid = jid })
540 :add_child(subscribe_options_form:form(ret)));
541 return true;
544 function handlers.set_options(origin, stanza, options, service)
545 local node, jid = options.attr.node, options.attr.jid;
546 jid = jid_prep(jid);
547 if not (node and jid) then
548 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
549 return true;
551 local ok, ret = service:get_subscription(node, stanza.attr.from, jid);
552 if not ok then
553 origin.send(pubsub_error_reply(stanza, ret));
554 return true;
555 elseif not ret then
556 origin.send(pubsub_error_reply(stanza, "not-subscribed"));
557 return true;
559 local old_subopts = ret;
560 local new_subopts, err = subscribe_options_form:data(options.tags[1], old_subopts);
561 if err then
562 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
563 return true;
565 local ok, err = service:add_subscription(node, stanza.attr.from, jid, new_subopts);
566 if not ok then
567 origin.send(pubsub_error_reply(stanza, err));
568 return true;
570 origin.send(st.reply(stanza));
571 return true;
574 function handlers.set_publish(origin, stanza, publish, service)
575 local node = publish.attr.node;
576 if not node then
577 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
578 return true;
580 local required_config = nil;
581 local publish_options = stanza.tags[1]:get_child("publish-options");
582 if publish_options then
583 -- Ensure that the node configuration matches the values in publish-options
584 local publish_options_form = publish_options:get_child("x", "jabber:x:data");
585 local err;
586 required_config, err = node_config_form:data(publish_options_form);
587 if err then
588 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
589 return true
592 local item = publish:get_child("item");
593 local id = (item and item.attr.id);
594 if not id then
595 id = uuid_generate();
596 if item then
597 item.attr.id = id;
600 local ok, ret = service:publish(node, stanza.attr.from, id, item, required_config);
601 local reply;
602 if ok then
603 if type(ok) == "string" then
604 id = ok;
606 reply = st.reply(stanza)
607 :tag("pubsub", { xmlns = xmlns_pubsub })
608 :tag("publish", { node = node })
609 :tag("item", { id = id });
610 else
611 reply = pubsub_error_reply(stanza, ret);
613 origin.send(reply);
614 return true;
617 function handlers.set_retract(origin, stanza, retract, service)
618 local node, notify = retract.attr.node, retract.attr.notify;
619 notify = (notify == "1") or (notify == "true");
620 local item = retract:get_child("item");
621 local id = item and item.attr.id
622 if not (node and id) then
623 origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
624 return true;
626 local reply, notifier;
627 if notify then
628 notifier = st.stanza("retract", { id = id });
630 local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
631 if ok then
632 reply = st.reply(stanza);
633 else
634 reply = pubsub_error_reply(stanza, ret);
636 origin.send(reply);
637 return true;
640 function handlers.owner_set_purge(origin, stanza, purge, service)
641 local node, notify = purge.attr.node, purge.attr.notify;
642 notify = (notify == "1") or (notify == "true");
643 local reply;
644 if not node then
645 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
646 return true;
648 local ok, ret = service:purge(node, stanza.attr.from, notify);
649 if ok then
650 reply = st.reply(stanza);
651 else
652 reply = pubsub_error_reply(stanza, ret);
654 origin.send(reply);
655 return true;
658 function handlers.owner_get_configure(origin, stanza, config, service)
659 local node = config.attr.node;
660 if not node then
661 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
662 return true;
665 local ok, node_config = service:get_node_config(node, stanza.attr.from);
666 if not ok then
667 origin.send(pubsub_error_reply(stanza, node_config));
668 return true;
671 local reply = st.reply(stanza)
672 :tag("pubsub", { xmlns = xmlns_pubsub_owner })
673 :tag("configure", { node = node })
674 :add_child(node_config_form:form(node_config));
675 origin.send(reply);
676 return true;
679 function handlers.owner_set_configure(origin, stanza, config, service)
680 local node = config.attr.node;
681 if not node then
682 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
683 return true;
685 if not service:may(node, stanza.attr.from, "configure") then
686 origin.send(pubsub_error_reply(stanza, "forbidden"));
687 return true;
689 local config_form = config:get_child("x", "jabber:x:data");
690 if not config_form then
691 origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform"));
692 return true;
694 local ok, old_config = service:get_node_config(node, stanza.attr.from);
695 if not ok then
696 origin.send(pubsub_error_reply(stanza, old_config));
697 return true;
699 local new_config, err = node_config_form:data(config_form, old_config);
700 if err then
701 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
702 return true;
704 local ok, err = service:set_node_config(node, stanza.attr.from, new_config);
705 if not ok then
706 origin.send(pubsub_error_reply(stanza, err));
707 return true;
709 origin.send(st.reply(stanza));
710 return true;
713 function handlers.owner_get_default(origin, stanza, default, service) -- luacheck: ignore 212/default
714 local reply = st.reply(stanza)
715 :tag("pubsub", { xmlns = xmlns_pubsub_owner })
716 :tag("default")
717 :add_child(node_config_form:form(service.node_defaults));
718 origin.send(reply);
719 return true;
722 function handlers.owner_get_affiliations(origin, stanza, affiliations, service)
723 local node = affiliations.attr.node;
724 if not node then
725 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
726 return true;
728 if not service:may(node, stanza.attr.from, "set_affiliation") then
729 origin.send(pubsub_error_reply(stanza, "forbidden"));
730 return true;
733 local node_obj = service.nodes[node];
734 if not node_obj then
735 origin.send(pubsub_error_reply(stanza, "item-not-found"));
736 return true;
739 local reply = st.reply(stanza)
740 :tag("pubsub", { xmlns = xmlns_pubsub_owner })
741 :tag("affiliations", { node = node });
743 for jid, affiliation in pairs(node_obj.affiliations) do
744 reply:tag("affiliation", { jid = jid, affiliation = affiliation }):up();
747 origin.send(reply);
748 return true;
751 function handlers.owner_set_affiliations(origin, stanza, affiliations, service)
752 local node = affiliations.attr.node;
753 if not node then
754 origin.send(pubsub_error_reply(stanza, "nodeid-required"));
755 return true;
757 if not service:may(node, stanza.attr.from, "set_affiliation") then
758 origin.send(pubsub_error_reply(stanza, "forbidden"));
759 return true;
762 local node_obj = service.nodes[node];
763 if not node_obj then
764 origin.send(pubsub_error_reply(stanza, "item-not-found"));
765 return true;
768 for affiliation_tag in affiliations:childtags("affiliation") do
769 local jid = affiliation_tag.attr.jid;
770 local affiliation = affiliation_tag.attr.affiliation;
772 jid = jid_prep(jid);
773 if affiliation == "none" then affiliation = nil; end
775 local ok, err = service:set_affiliation(node, stanza.attr.from, jid, affiliation);
776 if not ok then
777 -- FIXME Incomplete error handling,
778 -- see XEP 60 8.9.2.4 Multiple Simultaneous Modifications
779 origin.send(pubsub_error_reply(stanza, err));
780 return true;
784 local reply = st.reply(stanza);
785 origin.send(reply);
786 return true;
789 local function create_encapsulating_item(id, payload)
790 local item = st.stanza("item", { id = id, xmlns = xmlns_pubsub });
791 item:add_child(payload);
792 return item;
795 local function archive_itemstore(archive, config, user, node)
796 module:log("debug", "Creation of itemstore for node %s with config %s", node, config);
797 local get_set = {};
798 local max_items = config["max_items"];
799 function get_set:items() -- luacheck: ignore 212/self
800 local data, err = archive:find(user, {
801 limit = tonumber(max_items);
802 reverse = true;
804 if not data then
805 module:log("error", "Unable to get items: %s", err);
806 return true;
808 module:log("debug", "Listed items %s", data);
809 return it.reverse(function()
810 local id, payload, when, publisher = data();
811 if id == nil then
812 return;
814 local item = create_encapsulating_item(id, payload, publisher);
815 return id, item;
816 end);
818 function get_set:get(key) -- luacheck: ignore 212/self
819 local data, err = archive:find(user, {
820 key = key;
821 -- Get the last item with that key, if the archive doesn't deduplicate
822 reverse = true,
823 limit = 1;
825 if not data then
826 module:log("error", "Unable to get item: %s", err);
827 return nil, err;
829 local id, payload, when, publisher = data();
830 module:log("debug", "Get item %s (published at %s by %s)", id, when, publisher);
831 if id == nil then
832 return nil;
834 return create_encapsulating_item(id, payload, publisher);
836 function get_set:set(key, value) -- luacheck: ignore 212/self
837 local data, err;
838 if value ~= nil then
839 local publisher = value.attr.publisher;
840 local payload = value.tags[1];
841 data, err = archive:append(user, key, payload, time_now(), publisher);
842 else
843 data, err = archive:delete(user, { key = key; });
845 -- TODO archive support for maintaining maximum items
846 archive:delete(user, {
847 truncate = max_items;
849 if not data then
850 module:log("error", "Unable to set item: %s", err);
851 return nil, err;
853 return data;
855 function get_set:clear() -- luacheck: ignore 212/self
856 return archive:delete(user);
858 function get_set:resize(size) -- luacheck: ignore 212/self
859 max_items = size;
860 return archive:delete(user, {
861 truncate = size;
864 function get_set:head()
865 -- This should conveniently return the most recent item
866 local item = self:get(nil);
867 if item then
868 return item.attr.id, item;
871 return setmetatable(get_set, archive);
873 _M.archive_itemstore = archive_itemstore;
875 return _M;