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";
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
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
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
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
;
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
);
282 for _
, id
in ipairs(ret
) do
283 reply
:tag("item", { jid
= module
.host
, name
= id
}):up();
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];
293 return origin
.send(st
.error_reply(stanza
, "cancel", "bad-request"));
296 if pubsub_tag
.attr
.xmlns
== xmlns_pubsub_owner
then
299 local handler
= handlers
[prefix
..stanza
.attr
.type.."_"..action
.name
];
301 handler(origin
, stanza
, action
, service
);
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;
318 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
321 local ok
, results
= service
:get_items(node
, stanza
.attr
.from
, requested_items
);
323 origin
.send(pubsub_error_reply(stanza
, results
));
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
})
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
);
342 origin
.send(pubsub_error_reply(stanza
, ret
));
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();
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
);
359 origin
.send(pubsub_error_reply(stanza
, ret
));
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();
372 function handlers
.owner_set_subscriptions(origin
, stanza
, subscriptions
, service
)
373 local node
= subscriptions
.attr
.node
;
375 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
378 if not service
:may(node
, stanza
.attr
.from
, "subscribe_other") then
379 origin
.send(pubsub_error_reply(stanza
, "forbidden"));
383 local node_obj
= service
.nodes
[node
];
385 origin
.send(pubsub_error_reply(stanza
, "item-not-found"));
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
);
393 origin
.send(pubsub_error_reply(stanza
, err
));
396 elseif subscription_tag
.attr
.subscription
== 'none' then
397 local ok
, err
= service
:remove_subscription(node
, stanza
.attr
.from
, subscription_tag
.attr
.jid
);
399 origin
.send(pubsub_error_reply(stanza
, err
));
405 local reply
= st
.reply(stanza
);
410 function handlers
.set_create(origin
, stanza
, create
, service
)
411 local node
= create
.attr
.node
;
412 local ok
, ret
, reply
;
414 local configure
= stanza
.tags
[1]:get_child("configure");
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"));
421 local form_data
, err
= node_config_form
:data(config_form
);
423 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", dataform_error_message(err
)));
429 ok
, ret
= service
:create(node
, stanza
.attr
.from
, config
);
431 reply
= st
.reply(stanza
);
433 reply
= pubsub_error_reply(stanza
, ret
);
437 node
= uuid_generate();
438 ok
, ret
= service
:create(node
, stanza
.attr
.from
, config
);
439 until ok
or ret
~= "conflict";
441 reply
= st
.reply(stanza
)
442 :tag("pubsub", { xmlns
= xmlns_pubsub
})
443 :tag("create", { node
= node
});
445 reply
= pubsub_error_reply(stanza
, ret
);
452 function handlers
.owner_set_delete(origin
, stanza
, delete
, service
)
453 local node
= delete
.attr
.node
;
457 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
460 local ok
, ret
= service
:delete(node
, stanza
.attr
.from
);
462 reply
= st
.reply(stanza
);
464 reply
= pubsub_error_reply(stanza
, ret
);
470 function handlers
.set_subscribe(origin
, stanza
, subscribe
, service
)
471 local node
, jid
= subscribe
.attr
.node
, subscribe
.attr
.jid
;
473 if not (node
and jid
) then
474 origin
.send(pubsub_error_reply(stanza
, jid
and "nodeid-required" or "invalid-jid"));
477 local options_tag
, options
= stanza
.tags
[1]:get_child("options"), nil;
479 -- FIXME form parsing errors ignored here, why?
481 options
, err
= subscribe_options_form
:data(options_tag
.tags
[1]);
483 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", dataform_error_message(err
)));
487 local ok
, ret
= service
:add_subscription(node
, stanza
.attr
.from
, jid
, options
);
490 reply
= st
.reply(stanza
)
491 :tag("pubsub", { xmlns
= xmlns_pubsub
})
492 :tag("subscription", {
495 subscription
= "subscribed"
498 reply
:add_child(options_tag
);
501 reply
= pubsub_error_reply(stanza
, ret
);
506 function handlers
.set_unsubscribe(origin
, stanza
, unsubscribe
, service
)
507 local node
, jid
= unsubscribe
.attr
.node
, unsubscribe
.attr
.jid
;
509 if not (node
and jid
) then
510 origin
.send(pubsub_error_reply(stanza
, jid
and "nodeid-required" or "invalid-jid"));
513 local ok
, ret
= service
:remove_subscription(node
, stanza
.attr
.from
, jid
);
516 reply
= st
.reply(stanza
);
518 reply
= pubsub_error_reply(stanza
, ret
);
524 function handlers
.get_options(origin
, stanza
, options
, service
)
525 local node
, jid
= options
.attr
.node
, options
.attr
.jid
;
527 if not (node
and jid
) then
528 origin
.send(pubsub_error_reply(stanza
, jid
and "nodeid-required" or "invalid-jid"));
531 local ok
, ret
= service
:get_subscription(node
, stanza
.attr
.from
, jid
);
533 origin
.send(pubsub_error_reply(stanza
, "not-subscribed"));
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
)));
544 function handlers
.set_options(origin
, stanza
, options
, service
)
545 local node
, jid
= options
.attr
.node
, options
.attr
.jid
;
547 if not (node
and jid
) then
548 origin
.send(pubsub_error_reply(stanza
, jid
and "nodeid-required" or "invalid-jid"));
551 local ok
, ret
= service
:get_subscription(node
, stanza
.attr
.from
, jid
);
553 origin
.send(pubsub_error_reply(stanza
, ret
));
556 origin
.send(pubsub_error_reply(stanza
, "not-subscribed"));
559 local old_subopts
= ret
;
560 local new_subopts
, err
= subscribe_options_form
:data(options
.tags
[1], old_subopts
);
562 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", dataform_error_message(err
)));
565 local ok
, err
= service
:add_subscription(node
, stanza
.attr
.from
, jid
, new_subopts
);
567 origin
.send(pubsub_error_reply(stanza
, err
));
570 origin
.send(st
.reply(stanza
));
574 function handlers
.set_publish(origin
, stanza
, publish
, service
)
575 local node
= publish
.attr
.node
;
577 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
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");
586 required_config
, err
= node_config_form
:data(publish_options_form
);
588 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", dataform_error_message(err
)));
592 local item
= publish
:get_child("item");
593 local id
= (item
and item
.attr
.id
);
595 id
= uuid_generate();
600 local ok
, ret
= service
:publish(node
, stanza
.attr
.from
, id
, item
, required_config
);
603 if type(ok
) == "string" then
606 reply
= st
.reply(stanza
)
607 :tag("pubsub", { xmlns
= xmlns_pubsub
})
608 :tag("publish", { node
= node
})
609 :tag("item", { id
= id
});
611 reply
= pubsub_error_reply(stanza
, ret
);
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"));
626 local reply
, notifier
;
628 notifier
= st
.stanza("retract", { id
= id
});
630 local ok
, ret
= service
:retract(node
, stanza
.attr
.from
, id
, notifier
);
632 reply
= st
.reply(stanza
);
634 reply
= pubsub_error_reply(stanza
, ret
);
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");
645 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
648 local ok
, ret
= service
:purge(node
, stanza
.attr
.from
, notify
);
650 reply
= st
.reply(stanza
);
652 reply
= pubsub_error_reply(stanza
, ret
);
658 function handlers
.owner_get_configure(origin
, stanza
, config
, service
)
659 local node
= config
.attr
.node
;
661 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
665 local ok
, node_config
= service
:get_node_config(node
, stanza
.attr
.from
);
667 origin
.send(pubsub_error_reply(stanza
, node_config
));
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
));
679 function handlers
.owner_set_configure(origin
, stanza
, config
, service
)
680 local node
= config
.attr
.node
;
682 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
685 if not service
:may(node
, stanza
.attr
.from
, "configure") then
686 origin
.send(pubsub_error_reply(stanza
, "forbidden"));
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"));
694 local ok
, old_config
= service
:get_node_config(node
, stanza
.attr
.from
);
696 origin
.send(pubsub_error_reply(stanza
, old_config
));
699 local new_config
, err
= node_config_form
:data(config_form
, old_config
);
701 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", dataform_error_message(err
)));
704 local ok
, err
= service
:set_node_config(node
, stanza
.attr
.from
, new_config
);
706 origin
.send(pubsub_error_reply(stanza
, err
));
709 origin
.send(st
.reply(stanza
));
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
})
717 :add_child(node_config_form
:form(service
.node_defaults
));
722 function handlers
.owner_get_affiliations(origin
, stanza
, affiliations
, service
)
723 local node
= affiliations
.attr
.node
;
725 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
728 if not service
:may(node
, stanza
.attr
.from
, "set_affiliation") then
729 origin
.send(pubsub_error_reply(stanza
, "forbidden"));
733 local node_obj
= service
.nodes
[node
];
735 origin
.send(pubsub_error_reply(stanza
, "item-not-found"));
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();
751 function handlers
.owner_set_affiliations(origin
, stanza
, affiliations
, service
)
752 local node
= affiliations
.attr
.node
;
754 origin
.send(pubsub_error_reply(stanza
, "nodeid-required"));
757 if not service
:may(node
, stanza
.attr
.from
, "set_affiliation") then
758 origin
.send(pubsub_error_reply(stanza
, "forbidden"));
762 local node_obj
= service
.nodes
[node
];
764 origin
.send(pubsub_error_reply(stanza
, "item-not-found"));
768 for affiliation_tag
in affiliations
:childtags("affiliation") do
769 local jid
= affiliation_tag
.attr
.jid
;
770 local affiliation
= affiliation_tag
.attr
.affiliation
;
773 if affiliation
== "none" then affiliation
= nil; end
775 local ok
, err
= service
:set_affiliation(node
, stanza
.attr
.from
, jid
, affiliation
);
777 -- FIXME Incomplete error handling,
778 -- see XEP 60 8.9.2.4 Multiple Simultaneous Modifications
779 origin
.send(pubsub_error_reply(stanza
, err
));
784 local reply
= st
.reply(stanza
);
789 local function create_encapsulating_item(id
, payload
)
790 local item
= st
.stanza("item", { id
= id
, xmlns
= xmlns_pubsub
});
791 item
:add_child(payload
);
795 local function archive_itemstore(archive
, config
, user
, node
)
796 module
:log("debug", "Creation of itemstore for node %s with config %s", node
, config
);
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
);
805 module
:log("error", "Unable to get items: %s", err
);
808 module
:log("debug", "Listed items %s", data
);
809 return it
.reverse(function()
810 local id
, payload
, when
, publisher
= data();
814 local item
= create_encapsulating_item(id
, payload
, publisher
);
818 function get_set
:get(key
) -- luacheck: ignore 212/self
819 local data
, err
= archive
:find(user
, {
821 -- Get the last item with that key, if the archive doesn't deduplicate
826 module
:log("error", "Unable to get item: %s", err
);
829 local id
, payload
, when
, publisher
= data();
830 module
:log("debug", "Get item %s (published at %s by %s)", id
, when
, publisher
);
834 return create_encapsulating_item(id
, payload
, publisher
);
836 function get_set
:set(key
, value
) -- luacheck: ignore 212/self
839 local publisher
= value
.attr
.publisher
;
840 local payload
= value
.tags
[1];
841 data
, err
= archive
:append(user
, key
, payload
, time_now(), publisher
);
843 data
, err
= archive
:delete(user
, { key
= key
; });
845 -- TODO archive support for maintaining maximum items
846 archive
:delete(user
, {
847 truncate
= max_items
;
850 module
:log("error", "Unable to set item: %s", err
);
855 function get_set
:clear() -- luacheck: ignore 212/self
856 return archive
:delete(user
);
858 function get_set
:resize(size
) -- luacheck: ignore 212/self
860 return archive
:delete(user
, {
864 function get_set
:head()
865 -- This should conveniently return the most recent item
866 local item
= self
:get(nil);
868 return item
.attr
.id
, item
;
871 return setmetatable(get_set
, archive
);
873 _M
.archive_itemstore
= archive_itemstore
;