56fabaec68dad24f8166447bc72f2ed8ca16d228
[prosody.git] / plugins / mod_pubsub.lua
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;
5
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";
10
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
15
16 local service;
17
18 local handlers = {};
19
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];
25         if handler then
26                 handler(origin, stanza, action);
27                 return true;
28         end
29 end
30
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" };
37 };
38 function pubsub_error_reply(stanza, error)
39         local e = pubsub_errors[error];
40         local reply = st.error_reply(stanza, unpack(e, 1, 3));
41         if e[4] then
42                 reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
43         end
44         return reply;
45 end
46
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;
51         
52         local ok, results = service:get_items(node, stanza.attr.from, id);
53         if not ok then
54                 return origin.send(pubsub_error_reply(stanza, results));
55         end
56         
57         local data = st.stanza("items", { node = node });
58         for _, entry in pairs(results) do
59                 data:add_child(entry);
60         end
61         local reply;
62         if data then
63                 reply = st.reply(stanza)
64                         :tag("pubsub", { xmlns = xmlns_pubsub })
65                                 :add_child(data);
66         else
67                 reply = pubsub_error_reply(stanza, "item-not-found");
68         end
69         return origin.send(reply);
70 end
71
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);
75         if not ok then
76                 return origin.send(pubsub_error_reply(stanza, ret));
77         end
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();
83         end
84         return origin.send(reply);
85 end
86
87 function handlers.set_create(origin, stanza, create)
88         local node = create.attr.node;
89         local ok, ret, reply;
90         if node then
91                 ok, ret = service:create(node, stanza.attr.from);
92                 if ok then
93                         reply = st.reply(stanza);
94                 else
95                         reply = pubsub_error_reply(stanza, ret);
96                 end
97         else
98                 repeat
99                         node = uuid_generate();
100                         ok, ret = service:create(node, stanza.attr.from);
101                 until ok or ret ~= "conflict";
102                 if ok then
103                         reply = st.reply(stanza)
104                                 :tag("pubsub", { xmlns = xmlns_pubsub })
105                                         :tag("create", { node = node });
106                 else
107                         reply = pubsub_error_reply(stanza, ret);
108                 end
109         end
110         return origin.send(reply);
111 end
112
113 function handlers.set_subscribe(origin, stanza, subscribe)
114         local node, jid = subscribe.attr.node, subscribe.attr.jid;
115         --[[
116         local options_tag, options = stanza.tags[1]:get_child("options"), nil;
117         if options_tag then
118                 options = options_form:data(options_tag.tags[1]);
119         end
120         --]]
121         local options_tag, options; -- FIXME
122         local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
123         local reply;
124         if ok then
125                 reply = st.reply(stanza)
126                         :tag("pubsub", { xmlns = xmlns_pubsub })
127                                 :tag("subscription", {
128                                         node = node,
129                                         jid = jid,
130                                         subscription = "subscribed"
131                                 }):up();
132                 if options_tag then
133                         reply:add_child(options_tag);
134                 end
135         else
136                 reply = pubsub_error_reply(stanza, ret);
137         end
138         origin.send(reply);
139         if ok then
140                 -- Send all current items
141                 local ok, items = service:get_items(node, stanza.attr.from);
142                 if items then
143                         local jids = { [jid] = options or true };
144                         for id, item in pairs(items) do
145                                 service.config.broadcaster(node, jids, item);
146                         end
147                 end
148         end
149 end
150
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);
154         local reply;
155         if ok then
156                 reply = st.reply(stanza);
157         else
158                 reply = pubsub_error_reply(stanza, ret);
159         end
160         return origin.send(reply);
161 end
162
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);
168         local reply;
169         if ok then
170                 reply = st.reply(stanza)
171                         :tag("pubsub", { xmlns = xmlns_pubsub })
172                                 :tag("publish", { node = node })
173                                         :tag("item", { id = id });
174         else
175                 reply = pubsub_error_reply(stanza, ret);
176         end
177         return origin.send(reply);
178 end
179
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"));
187                 return true;
188         end
189         local reply, notifier;
190         if notify then
191                 notifier = st.stanza("retract", { id = id });
192         end
193         local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
194         if ok then
195                 reply = st.reply(stanza);
196         else
197                 reply = pubsub_error_reply(stanza, ret);
198         end
199         return origin.send(reply);
200 end
201
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");
205         local reply;
206         if not node then
207                 origin.send(st.error_reply(stanza, "modify", "bad-request"));
208                 return true;
209         end
210         local ok, ret = service:purge(node, stanza.attr.from, notify);
211         if ok then
212                 reply = st.reply(stanza);
213         else
214                 reply = pubsub_error_reply(stanza, ret);
215         end
216         return origin.send(reply);
217 end
218
219 function simple_broadcast(kind, node, jids, item)
220         if item then
221                 item = st.clone(item);
222                 item.attr.xmlns = nil; -- Clear the pubsub namespace
223         end
224         local message = st.message({ from = module.host, type = "headline" })
225                 :tag("event", { xmlns = xmlns_pubsub_event })
226                         :tag(kind, { node = node })
227                                 :add_child(item);
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);
232         end
233 end
234
235 module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
236 module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
237
238 local disco_info;
239
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" };
248 };
249
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
254                                 if feature then
255                                         disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up();
256                                 end
257                         end
258                 end
259         end
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();
263                 end
264         end
265 end
266
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);
272         return disco_info;
273 end
274
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;
278         if not node then
279                 return origin.send(st.reply(stanza):add_child(disco_info));
280         else
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";
284                 end
285                 if not ok then
286                         return origin.send(pubsub_error_reply(stanza, ret));
287                 end
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);
292         end
293 end);
294
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);
300         if not ok then
301                 return origin.send(pubsub_error_reply(stanza, ret));
302         end
303         
304         local reply = st.reply(stanza)
305                 :tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node });
306         
307         for id, item in pairs(ret) do
308                 reply:tag("item", { jid = module.host, name = id }):up();
309         end
310         
311         return origin.send(reply);
312 end
313
314
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);
318         end
319         local ok, ret = service:get_nodes(event.stanza.attr.from);
320         if not ok then
321                 event.origin.send(pubsub_error_reply(stanza, ret));
322         else
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();
327                 end
328                 event.origin.send(reply);
329         end
330         return true;
331 end);
332
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
337                 return admin_aff;
338         end
339 end
340
341 function set_service(new_service)
342         service = new_service;
343         module.environment.service = service;
344         disco_info = build_disco_info(service);
345 end
346
347 function module.save()
348         return { service = service };
349 end
350
351 function module.restore(data)
352         set_service(data.service);
353 end
354
355 set_service(pubsub.new({
356         capabilities = {
357                 none = {
358                         create = false;
359                         publish = false;
360                         retract = false;
361                         get_nodes = true;
362                         
363                         subscribe = true;
364                         unsubscribe = true;
365                         get_subscription = true;
366                         get_subscriptions = true;
367                         get_items = true;
368                         
369                         subscribe_other = false;
370                         unsubscribe_other = false;
371                         get_subscription_other = false;
372                         get_subscriptions_other = false;
373                         
374                         be_subscribed = true;
375                         be_unsubscribed = true;
376                         
377                         set_affiliation = false;
378                 };
379                 publisher = {
380                         create = false;
381                         publish = true;
382                         retract = true;
383                         get_nodes = true;
384                         
385                         subscribe = true;
386                         unsubscribe = true;
387                         get_subscription = true;
388                         get_subscriptions = true;
389                         get_items = true;
390                         
391                         subscribe_other = false;
392                         unsubscribe_other = false;
393                         get_subscription_other = false;
394                         get_subscriptions_other = false;
395                         
396                         be_subscribed = true;
397                         be_unsubscribed = true;
398                         
399                         set_affiliation = false;
400                 };
401                 owner = {
402                         create = true;
403                         publish = true;
404                         retract = true;
405                         get_nodes = true;
406                         
407                         subscribe = true;
408                         unsubscribe = true;
409                         get_subscription = true;
410                         get_subscriptions = true;
411                         get_items = true;
412                         
413                         
414                         subscribe_other = true;
415                         unsubscribe_other = true;
416                         get_subscription_other = true;
417                         get_subscriptions_other = true;
418                         
419                         be_subscribed = true;
420                         be_unsubscribed = true;
421                         
422                         set_affiliation = true;
423                 };
424         };
425         
426         autocreate_on_publish = autocreate_on_publish;
427         autocreate_on_subscribe = autocreate_on_subscribe;
428         
429         broadcaster = simple_broadcast;
430         get_affiliation = get_affiliation;
431         
432         normalize_jid = jid_bare;
433 }));