Merge 0.10->trunk
[prosody.git] / util / pubsub.lua
1 local events = require "util.events";
2 local t_remove = table.remove;
3
4 local service = {};
5 local service_mt = { __index = service };
6
7 local default_config = { __index = {
8         broadcaster = function () end;
9         get_affiliation = function () end;
10         capabilities = {};
11 } };
12 local default_node_config = { __index = {
13         ["pubsub#max_items"] = "20";
14 } };
15
16 local function new(config)
17         config = config or {};
18         return setmetatable({
19                 config = setmetatable(config, default_config);
20                 node_defaults = setmetatable(config.node_defaults or {}, default_node_config);
21                 affiliations = {};
22                 subscriptions = {};
23                 nodes = {};
24                 data = {};
25                 events = events.new();
26         }, service_mt);
27 end
28
29 function service:jids_equal(jid1, jid2)
30         local normalize = self.config.normalize_jid;
31         return normalize(jid1) == normalize(jid2);
32 end
33
34 function service:may(node, actor, action)
35         if actor == true then return true; end
36
37         local node_obj = self.nodes[node];
38         local node_aff = node_obj and node_obj.affiliations[actor];
39         local service_aff = self.affiliations[actor]
40                          or self.config.get_affiliation(actor, node, action)
41                          or "none";
42
43         -- Check if node allows/forbids it
44         local node_capabilities = node_obj and node_obj.capabilities;
45         if node_capabilities then
46                 local caps = node_capabilities[node_aff or service_aff];
47                 if caps then
48                         local can = caps[action];
49                         if can ~= nil then
50                                 return can;
51                         end
52                 end
53         end
54
55         -- Check service-wide capabilities instead
56         local service_capabilities = self.config.capabilities;
57         local caps = service_capabilities[node_aff or service_aff];
58         if caps then
59                 local can = caps[action];
60                 if can ~= nil then
61                         return can;
62                 end
63         end
64
65         return false;
66 end
67
68 function service:set_affiliation(node, actor, jid, affiliation)
69         -- Access checking
70         if not self:may(node, actor, "set_affiliation") then
71                 return false, "forbidden";
72         end
73         --
74         local node_obj = self.nodes[node];
75         if not node_obj then
76                 return false, "item-not-found";
77         end
78         node_obj.affiliations[jid] = affiliation;
79         local _, jid_sub = self:get_subscription(node, true, jid);
80         if not jid_sub and not self:may(node, jid, "be_unsubscribed") then
81                 local ok, err = self:add_subscription(node, true, jid);
82                 if not ok then
83                         return ok, err;
84                 end
85         elseif jid_sub and not self:may(node, jid, "be_subscribed") then
86                 local ok, err = self:add_subscription(node, true, jid);
87                 if not ok then
88                         return ok, err;
89                 end
90         end
91         return true;
92 end
93
94 function service:add_subscription(node, actor, jid, options)
95         -- Access checking
96         local cap;
97         if actor == true or jid == actor or self:jids_equal(actor, jid) then
98                 cap = "subscribe";
99         else
100                 cap = "subscribe_other";
101         end
102         if not self:may(node, actor, cap) then
103                 return false, "forbidden";
104         end
105         if not self:may(node, jid, "be_subscribed") then
106                 return false, "forbidden";
107         end
108         --
109         local node_obj = self.nodes[node];
110         if not node_obj then
111                 if not self.config.autocreate_on_subscribe then
112                         return false, "item-not-found";
113                 else
114                         local ok, err = self:create(node, true);
115                         if not ok then
116                                 return ok, err;
117                         end
118                         node_obj = self.nodes[node];
119                 end
120         end
121         node_obj.subscribers[jid] = options or true;
122         local normal_jid = self.config.normalize_jid(jid);
123         local subs = self.subscriptions[normal_jid];
124         if subs then
125                 if not subs[jid] then
126                         subs[jid] = { [node] = true };
127                 else
128                         subs[jid][node] = true;
129                 end
130         else
131                 self.subscriptions[normal_jid] = { [jid] = { [node] = true } };
132         end
133         self.events.fire_event("subscription-added", { node = node, jid = jid, normalized_jid = normal_jid, options = options });
134         return true;
135 end
136
137 function service:remove_subscription(node, actor, jid)
138         -- Access checking
139         local cap;
140         if actor == true or jid == actor or self:jids_equal(actor, jid) then
141                 cap = "unsubscribe";
142         else
143                 cap = "unsubscribe_other";
144         end
145         if not self:may(node, actor, cap) then
146                 return false, "forbidden";
147         end
148         if not self:may(node, jid, "be_unsubscribed") then
149                 return false, "forbidden";
150         end
151         --
152         local node_obj = self.nodes[node];
153         if not node_obj then
154                 return false, "item-not-found";
155         end
156         if not node_obj.subscribers[jid] then
157                 return false, "not-subscribed";
158         end
159         node_obj.subscribers[jid] = nil;
160         local normal_jid = self.config.normalize_jid(jid);
161         local subs = self.subscriptions[normal_jid];
162         if subs then
163                 local jid_subs = subs[jid];
164                 if jid_subs then
165                         jid_subs[node] = nil;
166                         if next(jid_subs) == nil then
167                                 subs[jid] = nil;
168                         end
169                 end
170                 if next(subs) == nil then
171                         self.subscriptions[normal_jid] = nil;
172                 end
173         end
174         self.events.fire_event("subscription-removed", { node = node, jid = jid, normalized_jid = normal_jid });
175         return true;
176 end
177
178 function service:remove_all_subscriptions(actor, jid)
179         local normal_jid = self.config.normalize_jid(jid);
180         local subs = self.subscriptions[normal_jid]
181         subs = subs and subs[jid];
182         if subs then
183                 for node in pairs(subs) do
184                         self:remove_subscription(node, true, jid);
185                 end
186         end
187         return true;
188 end
189
190 function service:get_subscription(node, actor, jid)
191         -- Access checking
192         local cap;
193         if actor == true or jid == actor or self:jids_equal(actor, jid) then
194                 cap = "get_subscription";
195         else
196                 cap = "get_subscription_other";
197         end
198         if not self:may(node, actor, cap) then
199                 return false, "forbidden";
200         end
201         --
202         local node_obj = self.nodes[node];
203         if not node_obj then
204                 return false, "item-not-found";
205         end
206         return true, node_obj.subscribers[jid];
207 end
208
209 function service:create(node, actor, options)
210         -- Access checking
211         if not self:may(node, actor, "create") then
212                 return false, "forbidden";
213         end
214         --
215         if self.nodes[node] then
216                 return false, "conflict";
217         end
218
219         self.data[node] = {};
220         self.nodes[node] = {
221                 name = node;
222                 subscribers = {};
223                 config = setmetatable(options or {}, {__index=self.node_defaults});
224                 affiliations = {};
225         };
226         setmetatable(self.nodes[node], { __index = { data = self.data[node] } }); -- COMPAT
227         self.events.fire_event("node-created", { node = node, actor = actor });
228         local ok, err = self:set_affiliation(node, true, actor, "owner");
229         if not ok then
230                 self.nodes[node] = nil;
231                 self.data[node] = nil;
232         end
233         return ok, err;
234 end
235
236 function service:delete(node, actor)
237         -- Access checking
238         if not self:may(node, actor, "delete") then
239                 return false, "forbidden";
240         end
241         --
242         local node_obj = self.nodes[node];
243         if not node_obj then
244                 return false, "item-not-found";
245         end
246         self.nodes[node] = nil;
247         self.data[node] = nil;
248         self.events.fire_event("node-deleted", { node = node, actor = actor });
249         self.config.broadcaster("delete", node, node_obj.subscribers);
250         return true;
251 end
252
253 local function remove_item_by_id(data, id)
254         if not data[id] then return end
255         data[id] = nil;
256         for i, _id in ipairs(data) do
257                 if id == _id then
258                         t_remove(data, i);
259                         return i;
260                 end
261         end
262 end
263
264 local function trim_items(data, max)
265         max = tonumber(max);
266         if not max or #data <= max then return end
267         repeat
268                 data[t_remove(data, 1)] = nil;
269         until #data <= max
270 end
271
272 function service:publish(node, actor, id, item)
273         -- Access checking
274         if not self:may(node, actor, "publish") then
275                 return false, "forbidden";
276         end
277         --
278         local node_obj = self.nodes[node];
279         if not node_obj then
280                 if not self.config.autocreate_on_publish then
281                         return false, "item-not-found";
282                 end
283                 local ok, err = self:create(node, true);
284                 if not ok then
285                         return ok, err;
286                 end
287                 node_obj = self.nodes[node];
288         end
289         local node_data = self.data[node];
290         remove_item_by_id(node_data, id);
291         node_data[#node_data + 1] = id;
292         node_data[id] = item;
293         trim_items(node_data, node_obj.config["pubsub#max_items"]);
294         self.events.fire_event("item-published", { node = node, actor = actor, id = id, item = item });
295         self.config.broadcaster("items", node, node_obj.subscribers, item, actor);
296         return true;
297 end
298
299 function service:retract(node, actor, id, retract)
300         -- Access checking
301         if not self:may(node, actor, "retract") then
302                 return false, "forbidden";
303         end
304         --
305         local node_obj = self.nodes[node];
306         if (not node_obj) or (not self.data[node][id]) then
307                 return false, "item-not-found";
308         end
309         self.events.fire_event("item-retracted", { node = node, actor = actor, id = id });
310         remove_item_by_id(self.data[node], id);
311         if retract then
312                 self.config.broadcaster("items", node, node_obj.subscribers, retract);
313         end
314         return true
315 end
316
317 function service:purge(node, actor, notify)
318         -- Access checking
319         if not self:may(node, actor, "retract") then
320                 return false, "forbidden";
321         end
322         --
323         local node_obj = self.nodes[node];
324         if not node_obj then
325                 return false, "item-not-found";
326         end
327         self.data[node] = {}; -- Purge
328         self.events.fire_event("node-purged", { node = node, actor = actor });
329         if notify then
330                 self.config.broadcaster("purge", node, node_obj.subscribers);
331         end
332         return true
333 end
334
335 function service:get_items(node, actor, id)
336         -- Access checking
337         if not self:may(node, actor, "get_items") then
338                 return false, "forbidden";
339         end
340         --
341         local node_obj = self.nodes[node];
342         if not node_obj then
343                 return false, "item-not-found";
344         end
345         if id then -- Restrict results to a single specific item
346                 return true, { id, [id] = self.data[node][id] };
347         else
348                 return true, self.data[node];
349         end
350 end
351
352 function service:get_nodes(actor)
353         -- Access checking
354         if not self:may(nil, actor, "get_nodes") then
355                 return false, "forbidden";
356         end
357         --
358         return true, self.nodes;
359 end
360
361 function service:get_subscriptions(node, actor, jid)
362         -- Access checking
363         local cap;
364         if actor == true or jid == actor or self:jids_equal(actor, jid) then
365                 cap = "get_subscriptions";
366         else
367                 cap = "get_subscriptions_other";
368         end
369         if not self:may(node, actor, cap) then
370                 return false, "forbidden";
371         end
372         --
373         local node_obj;
374         if node then
375                 node_obj = self.nodes[node];
376                 if not node_obj then
377                         return false, "item-not-found";
378                 end
379         end
380         local normal_jid = self.config.normalize_jid(jid);
381         local subs = self.subscriptions[normal_jid];
382         -- We return the subscription object from the node to save
383         -- a get_subscription() call for each node.
384         local ret = {};
385         if subs then
386                 for jid, subscribed_nodes in pairs(subs) do
387                         if node then -- Return only subscriptions to this node
388                                 if subscribed_nodes[node] then
389                                         ret[#ret+1] = {
390                                                 node = node;
391                                                 jid = jid;
392                                                 subscription = node_obj.subscribers[jid];
393                                         };
394                                 end
395                         else -- Return subscriptions to all nodes
396                                 local nodes = self.nodes;
397                                 for subscribed_node in pairs(subscribed_nodes) do
398                                         ret[#ret+1] = {
399                                                 node = subscribed_node;
400                                                 jid = jid;
401                                                 subscription = nodes[subscribed_node].subscribers[jid];
402                                         };
403                                 end
404                         end
405                 end
406         end
407         return true, ret;
408 end
409
410 -- Access models only affect 'none' affiliation caps, service/default access level...
411 function service:set_node_capabilities(node, actor, capabilities)
412         -- Access checking
413         if not self:may(node, actor, "configure") then
414                 return false, "forbidden";
415         end
416         --
417         local node_obj = self.nodes[node];
418         if not node_obj then
419                 return false, "item-not-found";
420         end
421         node_obj.capabilities = capabilities;
422         return true;
423 end
424
425 function service:set_node_config(node, actor, new_config)
426         if not self:may(node, actor, "configure") then
427                 return false, "forbidden";
428         end
429
430         local node_obj = self.nodes[node];
431         if not node_obj then
432                 return false, "item-not-found";
433         end
434
435         for k,v in pairs(new_config) do
436                 node_obj.config[k] = v;
437         end
438         trim_items(self.data[node], node_obj.config["pubsub#max_items"]);
439
440         return true;
441 end
442
443 return {
444         new = new;
445 };