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