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";
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));
39 reply
:tag(e
[4], { xmlns
= xmlns_pubsub_errors
}):up();
43 _M
.pubsub_error_reply
= pubsub_error_reply
;
45 local function dataform_error_message(err
) -- ({ string : string }) -> string?
47 for field
, errmsg
in pairs(err
) do
48 table.insert(out
, ("%s: %s"):format(field
, errmsg
))
50 return table.concat(out
, "; ");
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
{
60 value
= "http://jabber.org/protocol/pubsub#node_config";
71 var
= "pubsub#description";
72 label
= "Description";
76 name
= "payload_type";
78 label
= "The type of node data, usually specified by the namespace of the payload (if any)";
82 datatype
= "xs:integer";
84 var
= "pubsub#max_items";
85 label
= "Max # of items to persist";
89 name
= "persist_items";
90 var
= "pubsub#persist_items";
91 label
= "Persist items to storage";
95 name
= "access_model";
96 var
= "pubsub#access_model";
97 label
= "Specify the subscriber model";
107 type = "list-single";
108 name
= "publish_model";
109 var
= "pubsub#publish_model";
110 label
= "Specify the publisher model";
120 label
= "Whether to deliver event notifications";
121 name
= "notify_items";
122 var
= "pubsub#deliver_notifications";
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";
137 { label
= "Messages of type normal", value
= "normal" },
138 { label
= "Messages of type headline", value
= "headline", default
= true },
143 label
= "Whether to notify subscribers when the node is deleted";
144 name
= "notify_delete";
145 var
= "pubsub#notify_delete";
150 label
= "Whether to notify subscribers when items are removed from the node";
151 name
= "notify_retract";
152 var
= "pubsub#notify_retract";
157 local subscribe_options_form
= dataform
{
161 value
= "http://jabber.org/protocol/pubsub#subscribe_options";
165 name
= "pubsub#include_body";
166 label
= "Receive message body in addition to payload?";
170 local node_metadata_form
= dataform
{
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
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
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
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
;
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
);
272 for _
, id
in ipairs(ret
) do
273 reply
:tag("item", { jid
= module
.host
, name
= id
}):up();
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];
283 return origin
.send(st
.error_reply(stanza
, "cancel", "bad-request"));
286 if pubsub_tag
.attr
.xmlns
== xmlns_pubsub_owner
then
289 local handler
= handlers
[prefix
..stanza
.attr
.type.."_"..action
.name
];
291 handler(origin
, stanza
, action
, service
);
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
;
302 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
305 local ok
, results
= service
:get_items(node
, stanza
.attr
.from
, item_id
);
307 origin
.send(pubsub_error_reply(stanza
, results
));
311 local data
= st
.stanza("items", { node
= node
});
312 for _
, id
in ipairs(results
) do
313 data
:add_child(results
[id
]);
317 reply
= st
.reply(stanza
)
318 :tag("pubsub", { xmlns
= xmlns_pubsub
})
321 reply
= pubsub_error_reply(stanza
, "item-not-found");
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
);
331 origin
.send(pubsub_error_reply(stanza
, ret
));
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();
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
);
348 origin
.send(pubsub_error_reply(stanza
, ret
));
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();
361 function handlers
.owner_set_subscriptions(origin
, stanza
, subscriptions
, service
)
362 local node
= subscriptions
.attr
.node
;
364 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
367 if not service
:may(node
, stanza
.attr
.from
, "subscribe_other") then
368 origin
.send(pubsub_error_reply(stanza
, "forbidden"));
372 local node_obj
= service
.nodes
[node
];
374 origin
.send(pubsub_error_reply(stanza
, "item-not-found"));
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
);
382 origin
.send(pubsub_error_reply(stanza
, err
));
385 elseif subscription_tag
.attr
.subscription
== 'none' then
386 local ok
, err
= service
:remove_subscription(node
, stanza
.attr
.from
, subscription_tag
.attr
.jid
);
388 origin
.send(pubsub_error_reply(stanza
, err
));
394 local reply
= st
.reply(stanza
);
399 function handlers
.set_create(origin
, stanza
, create
, service
)
400 local node
= create
.attr
.node
;
401 local ok
, ret
, reply
;
403 local configure
= stanza
.tags
[1]:get_child("configure");
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"));
410 local form_data
, err
= node_config_form
:data(config_form
);
412 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", dataform_error_message(err
)));
418 ok
, ret
= service
:create(node
, stanza
.attr
.from
, config
);
420 reply
= st
.reply(stanza
);
422 reply
= pubsub_error_reply(stanza
, ret
);
426 node
= uuid_generate();
427 ok
, ret
= service
:create(node
, stanza
.attr
.from
, config
);
428 until ok
or ret
~= "conflict";
430 reply
= st
.reply(stanza
)
431 :tag("pubsub", { xmlns
= xmlns_pubsub
})
432 :tag("create", { node
= node
});
434 reply
= pubsub_error_reply(stanza
, ret
);
441 function handlers
.owner_set_delete(origin
, stanza
, delete
, service
)
442 local node
= delete
.attr
.node
;
446 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
449 local ok
, ret
= service
:delete(node
, stanza
.attr
.from
);
451 reply
= st
.reply(stanza
);
453 reply
= pubsub_error_reply(stanza
, ret
);
459 function handlers
.set_subscribe(origin
, stanza
, subscribe
, service
)
460 local node
, jid
= subscribe
.attr
.node
, subscribe
.attr
.jid
;
462 if not (node
and jid
) then
463 origin
.send(pubsub_error_reply(stanza
, jid
and "nodeid-required" or "invalid-jid"));
466 local options_tag
, options
= stanza
.tags
[1]:get_child("options"), nil;
468 -- FIXME form parsing errors ignored here, why?
470 options
, err
= subscribe_options_form
:data(options_tag
.tags
[1]);
472 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", dataform_error_message(err
)));
476 local ok
, ret
= service
:add_subscription(node
, stanza
.attr
.from
, jid
, options
);
479 reply
= st
.reply(stanza
)
480 :tag("pubsub", { xmlns
= xmlns_pubsub
})
481 :tag("subscription", {
484 subscription
= "subscribed"
487 reply
:add_child(options_tag
);
490 reply
= pubsub_error_reply(stanza
, ret
);
495 function handlers
.set_unsubscribe(origin
, stanza
, unsubscribe
, service
)
496 local node
, jid
= unsubscribe
.attr
.node
, unsubscribe
.attr
.jid
;
498 if not (node
and jid
) then
499 origin
.send(pubsub_error_reply(stanza
, jid
and "nodeid-required" or "invalid-jid"));
502 local ok
, ret
= service
:remove_subscription(node
, stanza
.attr
.from
, jid
);
505 reply
= st
.reply(stanza
);
507 reply
= pubsub_error_reply(stanza
, ret
);
513 function handlers
.get_options(origin
, stanza
, options
, service
)
514 local node
, jid
= options
.attr
.node
, options
.attr
.jid
;
516 if not (node
and jid
) then
517 origin
.send(pubsub_error_reply(stanza
, jid
and "nodeid-required" or "invalid-jid"));
520 local ok
, ret
= service
:get_subscription(node
, stanza
.attr
.from
, jid
);
522 origin
.send(pubsub_error_reply(stanza
, "not-subscribed"));
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
)));
533 function handlers
.set_options(origin
, stanza
, options
, service
)
534 local node
, jid
= options
.attr
.node
, options
.attr
.jid
;
536 if not (node
and jid
) then
537 origin
.send(pubsub_error_reply(stanza
, jid
and "nodeid-required" or "invalid-jid"));
540 local ok
, ret
= service
:get_subscription(node
, stanza
.attr
.from
, jid
);
542 origin
.send(pubsub_error_reply(stanza
, ret
));
545 origin
.send(pubsub_error_reply(stanza
, "not-subscribed"));
548 local old_subopts
= ret
;
549 local new_subopts
, err
= subscribe_options_form
:data(options
.tags
[1], old_subopts
);
551 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", dataform_error_message(err
)));
554 local ok
, err
= service
:add_subscription(node
, stanza
.attr
.from
, jid
, new_subopts
);
556 origin
.send(pubsub_error_reply(stanza
, err
));
559 origin
.send(st
.reply(stanza
));
563 function handlers
.set_publish(origin
, stanza
, publish
, service
)
564 local node
= publish
.attr
.node
;
566 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
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");
575 required_config
, err
= node_config_form
:data(publish_options_form
);
577 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", dataform_error_message(err
)));
581 local item
= publish
:get_child("item");
582 local id
= (item
and item
.attr
.id
);
584 id
= uuid_generate();
589 local ok
, ret
= service
:publish(node
, stanza
.attr
.from
, id
, item
, required_config
);
592 if type(ok
) == "string" then
595 reply
= st
.reply(stanza
)
596 :tag("pubsub", { xmlns
= xmlns_pubsub
})
597 :tag("publish", { node
= node
})
598 :tag("item", { id
= id
});
600 reply
= pubsub_error_reply(stanza
, ret
);
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"));
615 local reply
, notifier
;
617 notifier
= st
.stanza("retract", { id
= id
});
619 local ok
, ret
= service
:retract(node
, stanza
.attr
.from
, id
, notifier
);
621 reply
= st
.reply(stanza
);
623 reply
= pubsub_error_reply(stanza
, ret
);
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");
634 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
637 local ok
, ret
= service
:purge(node
, stanza
.attr
.from
, notify
);
639 reply
= st
.reply(stanza
);
641 reply
= pubsub_error_reply(stanza
, ret
);
647 function handlers
.owner_get_configure(origin
, stanza
, config
, service
)
648 local node
= config
.attr
.node
;
650 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
654 local ok
, node_config
= service
:get_node_config(node
, stanza
.attr
.from
);
656 origin
.send(pubsub_error_reply(stanza
, node_config
));
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
));
668 function handlers
.owner_set_configure(origin
, stanza
, config
, service
)
669 local node
= config
.attr
.node
;
671 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
674 if not service
:may(node
, stanza
.attr
.from
, "configure") then
675 origin
.send(pubsub_error_reply(stanza
, "forbidden"));
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"));
683 local ok
, old_config
= service
:get_node_config(node
, stanza
.attr
.from
);
685 origin
.send(pubsub_error_reply(stanza
, old_config
));
688 local new_config
, err
= node_config_form
:data(config_form
, old_config
);
690 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", dataform_error_message(err
)));
693 local ok
, err
= service
:set_node_config(node
, stanza
.attr
.from
, new_config
);
695 origin
.send(pubsub_error_reply(stanza
, err
));
698 origin
.send(st
.reply(stanza
));
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
})
706 :add_child(node_config_form
:form(service
.node_defaults
));
711 function handlers
.owner_get_affiliations(origin
, stanza
, affiliations
, service
)
712 local node
= affiliations
.attr
.node
;
714 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
717 if not service
:may(node
, stanza
.attr
.from
, "set_affiliation") then
718 origin
.send(pubsub_error_reply(stanza
, "forbidden"));
722 local node_obj
= service
.nodes
[node
];
724 origin
.send(pubsub_error_reply(stanza
, "item-not-found"));
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();
740 function handlers
.owner_set_affiliations(origin
, stanza
, affiliations
, service
)
741 local node
= affiliations
.attr
.node
;
743 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
746 if not service
:may(node
, stanza
.attr
.from
, "set_affiliation") then
747 origin
.send(pubsub_error_reply(stanza
, "forbidden"));
751 local node_obj
= service
.nodes
[node
];
753 origin
.send(pubsub_error_reply(stanza
, "item-not-found"));
757 for affiliation_tag
in affiliations
:childtags("affiliation") do
758 local jid
= affiliation_tag
.attr
.jid
;
759 local affiliation
= affiliation_tag
.attr
.affiliation
;
762 if affiliation
== "none" then affiliation
= nil; end
764 local ok
, err
= service
:set_affiliation(node
, stanza
.attr
.from
, jid
, affiliation
);
766 -- FIXME Incomplete error handling,
767 -- see XEP 60 8.9.2.4 Multiple Simultaneous Modifications
768 origin
.send(pubsub_error_reply(stanza
, err
));
773 local reply
= st
.reply(stanza
);
778 local function create_encapsulating_item(id
, payload
)
779 local item
= st
.stanza("item", { id
= id
, xmlns
= xmlns_pubsub
});
780 item
:add_child(payload
);
784 local function archive_itemstore(archive
, config
, user
, node
)
785 module
:log("debug", "Creation of itemstore for node %s with config %s", node
, config
);
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
);
794 module
:log("error", "Unable to get items: %s", err
);
797 module
:log("debug", "Listed items %s", data
);
798 return it
.reverse(function()
799 local id
, payload
, when
, publisher
= data();
803 local item
= create_encapsulating_item(id
, payload
, publisher
);
807 function get_set
:get(key
) -- luacheck: ignore 212/self
808 local data
, err
= archive
:find(user
, {
810 -- Get the last item with that key, if the archive doesn't deduplicate
815 module
:log("error", "Unable to get item: %s", err
);
818 local id
, payload
, when
, publisher
= data();
819 module
:log("debug", "Get item %s (published at %s by %s)", id
, when
, publisher
);
823 return create_encapsulating_item(id
, payload
, publisher
);
825 function get_set
:set(key
, value
) -- luacheck: ignore 212/self
828 local publisher
= value
.attr
.publisher
;
829 local payload
= value
.tags
[1];
830 data
, err
= archive
:append(user
, key
, payload
, time_now(), publisher
);
832 data
, err
= archive
:delete(user
, { key
= key
; });
834 -- TODO archive support for maintaining maximum items
835 archive
:delete(user
, {
836 truncate
= max_items
;
839 module
:log("error", "Unable to set item: %s", err
);
844 function get_set
:clear() -- luacheck: ignore 212/self
845 return archive
:delete(user
);
847 function get_set
:resize(size
) -- luacheck: ignore 212/self
849 return archive
:delete(user
, {
853 function get_set
:head()
854 -- This should conveniently return the most recent item
855 local item
= self
:get(nil);
857 return item
.attr
.id
, item
;
860 return setmetatable(get_set
, archive
);
862 _M
.archive_itemstore
= archive_itemstore
;