1 local pubsub = require "util.pubsub";
2 local st = require "util.stanza";
3 local jid_bare = require "util.jid".bare;
4 local uuid_generate = require "util.uuid".generate;
6 local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
7 local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
8 local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
9 local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
11 local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
12 local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
13 local pubsub_disco_name = module:get_option("name");
14 if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end
20 function handle_pubsub_iq(event)
21 local origin, stanza = event.origin, event.stanza;
22 local pubsub = stanza.tags[1];
23 local action = pubsub.tags[1];
24 local handler = handlers[stanza.attr.type.."_"..action.name];
26 handler(origin, stanza, action);
31 local pubsub_errors = {
32 ["conflict"] = { "cancel", "conflict" };
33 ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
34 ["item-not-found"] = { "cancel", "item-not-found" };
35 ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
36 ["forbidden"] = { "cancel", "forbidden" };
38 function pubsub_error_reply(stanza, error)
39 local e = pubsub_errors[error];
40 local reply = st.error_reply(stanza, unpack(e, 1, 3));
42 reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
47 function handlers.get_items(origin, stanza, items)
48 local node = items.attr.node;
49 local item = items:get_child("item");
50 local id = item and item.attr.id;
52 local ok, results = service:get_items(node, stanza.attr.from, id);
54 return origin.send(pubsub_error_reply(stanza, results));
57 local data = st.stanza("items", { node = node });
58 for _, entry in pairs(results) do
59 data:add_child(entry);
63 reply = st.reply(stanza)
64 :tag("pubsub", { xmlns = xmlns_pubsub })
67 reply = pubsub_error_reply(stanza, "item-not-found");
69 return origin.send(reply);
72 function handlers.get_subscriptions(origin, stanza, subscriptions)
73 local node = subscriptions.attr.node;
74 local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
76 return origin.send(pubsub_error_reply(stanza, ret));
78 local reply = st.reply(stanza)
79 :tag("pubsub", { xmlns = xmlns_pubsub })
80 :tag("subscriptions");
81 for _, sub in ipairs(ret) do
82 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
84 return origin.send(reply);
87 function handlers.set_create(origin, stanza, create)
88 local node = create.attr.node;
91 ok, ret = service:create(node, stanza.attr.from);
93 reply = st.reply(stanza);
95 reply = pubsub_error_reply(stanza, ret);
99 node = uuid_generate();
100 ok, ret = service:create(node, stanza.attr.from);
101 until ok or ret ~= "conflict";
103 reply = st.reply(stanza)
104 :tag("pubsub", { xmlns = xmlns_pubsub })
105 :tag("create", { node = node });
107 reply = pubsub_error_reply(stanza, ret);
110 return origin.send(reply);
113 function handlers.set_subscribe(origin, stanza, subscribe)
114 local node, jid = subscribe.attr.node, subscribe.attr.jid;
116 local options_tag, options = stanza.tags[1]:get_child("options"), nil;
118 options = options_form:data(options_tag.tags[1]);
121 local options_tag, options; -- FIXME
122 local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
125 reply = st.reply(stanza)
126 :tag("pubsub", { xmlns = xmlns_pubsub })
127 :tag("subscription", {
130 subscription = "subscribed"
133 reply:add_child(options_tag);
136 reply = pubsub_error_reply(stanza, ret);
140 -- Send all current items
141 local ok, items = service:get_items(node, stanza.attr.from);
143 local jids = { [jid] = options or true };
144 for id, item in pairs(items) do
145 service.config.broadcaster(node, jids, item);
151 function handlers.set_unsubscribe(origin, stanza, unsubscribe)
152 local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
153 local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
156 reply = st.reply(stanza);
158 reply = pubsub_error_reply(stanza, ret);
160 return origin.send(reply);
163 function handlers.set_publish(origin, stanza, publish)
164 local node = publish.attr.node;
165 local item = publish:get_child("item");
166 local id = (item and item.attr.id) or uuid_generate();
167 local ok, ret = service:publish(node, stanza.attr.from, id, item);
170 reply = st.reply(stanza)
171 :tag("pubsub", { xmlns = xmlns_pubsub })
172 :tag("publish", { node = node })
173 :tag("item", { id = id });
175 reply = pubsub_error_reply(stanza, ret);
177 return origin.send(reply);
180 function handlers.set_retract(origin, stanza, retract)
181 local node, notify = retract.attr.node, retract.attr.notify;
182 notify = (notify == "1") or (notify == "true");
183 local item = retract:get_child("item");
184 local id = item and item.attr.id
185 if not (node and id) then
186 origin.send(st.error_reply(stanza, "modify", "bad-request"));
189 local reply, notifier;
191 notifier = st.stanza("retract", { id = id });
193 local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
195 reply = st.reply(stanza);
197 reply = pubsub_error_reply(stanza, ret);
199 return origin.send(reply);
202 function handlers.set_purge(origin, stanza, purge)
203 local node, notify = purge.attr.node, purge.attr.notify;
204 notify = (notify == "1") or (notify == "true");
207 origin.send(st.error_reply(stanza, "modify", "bad-request"));
210 local ok, ret = service:purge(node, stanza.attr.from, notify);
212 reply = st.reply(stanza);
214 reply = pubsub_error_reply(stanza, ret);
216 return origin.send(reply);
219 function simple_broadcast(kind, node, jids, item)
221 item = st.clone(item);
222 item.attr.xmlns = nil; -- Clear the pubsub namespace
224 local message = st.message({ from = module.host, type = "headline" })
225 :tag("event", { xmlns = xmlns_pubsub_event })
226 :tag(kind, { node = node })
228 for jid in pairs(jids) do
229 module:log("debug", "Sending notification to %s", jid);
230 message.attr.to = jid;
231 module:send(message);
235 module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
236 module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
240 local feature_map = {
241 create = { "create-nodes", "instant-nodes", "item-ids" };
242 retract = { "delete-items", "retract-items" };
243 purge = { "purge-nodes" };
244 publish = { "publish", autocreate_on_publish and "auto-create" };
245 get_items = { "retrieve-items" };
246 add_subscription = { "subscribe" };
247 get_subscriptions = { "retrieve-subscriptions" };
250 local function add_disco_features_from_service(disco, service)
251 for method, features in pairs(feature_map) do
252 if service[method] then
253 for _, feature in ipairs(features) do
255 disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up();
260 for affiliation in pairs(service.config.capabilities) do
261 if affiliation ~= "none" and affiliation ~= "owner" then
262 disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up();
267 local function build_disco_info(service)
268 local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" })
269 :tag("identity", { category = "pubsub", type = "service", name = pubsub_disco_name }):up()
270 :tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up();
271 add_disco_features_from_service(disco_info, service);
275 module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event)
276 local origin, stanza = event.origin, event.stanza;
277 local node = stanza.tags[1].attr.node;
279 return origin.send(st.reply(stanza):add_child(disco_info));
281 local ok, ret = service:get_nodes(stanza.attr.from);
282 if ok and not ret[node] then
283 ok, ret = false, "item-not-found";
286 return origin.send(pubsub_error_reply(stanza, ret));
288 local reply = st.reply(stanza)
289 :tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node })
290 :tag("identity", { category = "pubsub", type = "leaf" });
291 return origin.send(reply);
295 local function handle_disco_items_on_node(event)
296 local stanza, origin = event.stanza, event.origin;
297 local query = stanza.tags[1];
298 local node = query.attr.node;
299 local ok, ret = service:get_items(node, stanza.attr.from);
301 return origin.send(pubsub_error_reply(stanza, ret));
304 local reply = st.reply(stanza)
305 :tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node });
307 for id, item in pairs(ret) do
308 reply:tag("item", { jid = module.host, name = id }):up();
311 return origin.send(reply);
315 module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event)
316 if event.stanza.tags[1].attr.node then
317 return handle_disco_items_on_node(event);
319 local ok, ret = service:get_nodes(event.stanza.attr.from);
321 event.origin.send(pubsub_error_reply(stanza, ret));
323 local reply = st.reply(event.stanza)
324 :tag("query", { xmlns = "http://jabber.org/protocol/disco#items" });
325 for node, node_obj in pairs(ret) do
326 reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up();
328 event.origin.send(reply);
333 local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
334 local function get_affiliation(jid)
335 local bare_jid = jid_bare(jid);
336 if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then
341 function set_service(new_service)
342 service = new_service;
343 module.environment.service = service;
344 disco_info = build_disco_info(service);
347 function module.save()
348 return { service = service };
351 function module.restore(data)
352 set_service(data.service);
355 set_service(pubsub.new({
365 get_subscription = true;
366 get_subscriptions = true;
369 subscribe_other = false;
370 unsubscribe_other = false;
371 get_subscription_other = false;
372 get_subscriptions_other = false;
374 be_subscribed = true;
375 be_unsubscribed = true;
377 set_affiliation = false;
387 get_subscription = true;
388 get_subscriptions = true;
391 subscribe_other = false;
392 unsubscribe_other = false;
393 get_subscription_other = false;
394 get_subscriptions_other = false;
396 be_subscribed = true;
397 be_unsubscribed = true;
399 set_affiliation = false;
409 get_subscription = true;
410 get_subscriptions = true;
414 subscribe_other = true;
415 unsubscribe_other = true;
416 get_subscription_other = true;
417 get_subscriptions_other = true;
419 be_subscribed = true;
420 be_unsubscribed = true;
422 set_affiliation = true;
426 autocreate_on_publish = autocreate_on_publish;
427 autocreate_on_subscribe = autocreate_on_subscribe;
429 broadcaster = simple_broadcast;
430 get_affiliation = get_affiliation;
432 normalize_jid = jid_bare;