util.pubsub: Add service:jids_equal() and new config option normalize_jid
[prosody.git] / util / pubsub.lua
1 module("pubsub", package.seeall);
2
3 local service = {};
4 local service_mt = { __index = service };
5
6 local default_config = {
7         broadcaster = function () end;
8         get_affiliation = function () end;
9         capabilities = {};
10 };
11
12 function new(config)
13         config = config or {};
14         return setmetatable({
15                 config = setmetatable(config, { __index = default_config });
16                 affiliations = {};
17                 nodes = {};
18         }, service_mt);
19 end
20
21 function service:jids_equal(jid1, jid2)
22         local normalize = self.config.normalize_jid;
23         return normalize(jid1) == normalize(jid2);
24 end
25
26 function service:may(node, actor, action)
27         if actor == true then return true; end
28         
29         
30         local node_obj = self.nodes[node];
31         local node_aff = node_obj and node_obj.affiliations[actor];
32         local service_aff = self.affiliations[actor]
33                          or self.config.get_affiliation(actor, node, action)
34                          or "none";
35         
36         local node_capabilities = node_obj and node_obj.capabilities;
37         local service_capabilities = self.config.capabilities;
38         
39         -- Check if node allows/forbids it      
40         if node_capabilities then
41                 local caps = node_capabilities[node_aff or service_aff];
42                 if caps then
43                         local can = caps[action];
44                         if can ~= nil then
45                                 return can;
46                         end
47                 end
48         end
49         -- Check service-wide capabilities instead
50         local caps = service_capabilities[node_aff or service_aff];
51         if caps then
52                 local can = caps[action];
53                 if can ~= nil then
54                         return can;
55                 end
56         end
57         
58         return false;
59 end
60
61 function service:set_affiliation(node, actor, jid, affiliation)
62         -- Access checking
63         if not self:may(node, actor, "set_affiliation") then
64                 return false, "forbidden";
65         end
66         --
67         local node_obj = self.nodes[node];
68         if not node_obj then
69                 return false, "item-not-found";
70         end
71         node_obj.affiliations[jid] = affiliation;
72         local _, jid_sub = self:get_subscription(node, nil, jid);
73         if not jid_sub and not self:may(node, jid, "be_unsubscribed") then
74                 local ok, err = self:add_subscription(node, nil, jid);
75                 if not ok then
76                         return ok, err;
77                 end
78         elseif jid_sub and not self:may(node, jid, "be_subscribed") then
79                 local ok, err = self:add_subscription(node, nil, jid);
80                 if not ok then
81                         return ok, err;
82                 end
83         end
84         return true;
85 end
86
87 function service:add_subscription(node, actor, jid, options)
88         -- Access checking
89         local cap;
90         if jid == actor or self:jids_equal(actor, jid) then
91                 cap = "subscribe";
92         else
93                 cap = "subscribe_other";
94         end
95         if not self:may(node, actor, cap) then
96                 return false, "forbidden";
97         end
98         if not self:may(node, jid, "be_subscribed") then
99                 return false, "forbidden";
100         end
101         --
102         local node_obj = self.nodes[node];
103         if not node_obj then
104                 if not self.config.autocreate_on_subscribe then
105                         return false, "item-not-found";
106                 else
107                         local ok, err = self:create(node, actor);
108                         if not ok then
109                                 return ok, err;
110                         end
111                 end
112         end
113         node_obj.subscribers[jid] = options or true;
114         return true;
115 end
116
117 function service:remove_subscription(node, actor, jid)
118         -- Access checking
119         local cap;
120         if jid == actor or self:jids_equal(actor, jid) then
121                 cap = "unsubscribe";
122         else
123                 cap = "unsubscribe_other";
124         end
125         if not self:may(node, actor, cap) then
126                 return false, "forbidden";
127         end
128         if not self:may(node, jid, "be_unsubscribed") then
129                 return false, "forbidden";
130         end
131         --
132         local node_obj = self.nodes[node];
133         if not node_obj then
134                 return false, "item-not-found";
135         end
136         if not node_obj.subscribers[jid] then
137                 return false, "not-subscribed";
138         end
139         node_obj.subscribers[jid] = nil;
140         return true;
141 end
142
143 function service:get_subscription(node, actor, jid)
144         -- Access checking
145         local cap;
146         if jid == actor or self:jids_equal(actor, jid) then
147                 cap = "get_subscription";
148         else
149                 cap = "get_subscription_other";
150         end
151         if not self:may(node, actor, cap) then
152                 return false, "forbidden";
153         end
154         --
155         local node_obj = self.nodes[node];
156         if node_obj then
157                 return true, node_obj.subscribers[jid];
158         end
159 end
160
161 function service:create(node, actor)
162         -- Access checking
163         if not self:may(node, actor, "create") then
164                 return false, "forbidden";
165         end
166         --
167         if self.nodes[node] then
168                 return false, "conflict";
169         end
170         
171         self.nodes[node] = {
172                 name = node;
173                 subscribers = {};
174                 config = {};
175                 data = {};
176                 affiliations = {};
177         };
178         local ok, err = self:set_affiliation(node, true, actor, "owner");
179         if not ok then
180                 self.nodes[node] = nil;
181         end
182         return ok, err;
183 end
184
185 function service:publish(node, actor, id, item)
186         -- Access checking
187         if not self:may(node, actor, "publish") then
188                 return false, "forbidden";
189         end
190         --
191         local node_obj = self.nodes[node];
192         if not node_obj then
193                 if not self.config.autocreate_on_publish then
194                         return false, "item-not-found";
195                 end
196                 local ok, err = self:create(node, actor);
197                 if not ok then
198                         return ok, err;
199                 end
200                 node_obj = self.nodes[node];
201         end
202         node_obj.data[id] = item;
203         self.config.broadcaster(node, node_obj.subscribers, item);
204         return true;
205 end
206
207 function service:retract(node, actor, id, retract)
208         -- Access checking
209         if not self:may(node, actor, "retract") then
210                 return false, "forbidden";
211         end
212         --
213         local node_obj = self.nodes[node];
214         if (not node_obj) or (not node_obj.data[id]) then
215                 return false, "item-not-found";
216         end
217         node_obj.data[id] = nil;
218         if retract then
219                 self.config.broadcaster(node, node_obj.subscribers, retract);
220         end
221         return true
222 end
223
224 function service:get_items(node, actor, id)
225         -- Access checking
226         if not self:may(node, actor, "get_items") then
227                 return false, "forbidden";
228         end
229         --
230         local node_obj = self.nodes[node];
231         if not node_obj then
232                 return false, "item-not-found";
233         end
234         if id then -- Restrict results to a single specific item
235                 return true, { [id] = node_obj.data[id] };
236         else
237                 return true, node_obj.data;
238         end
239 end
240
241 function service:get_nodes(actor)
242         -- Access checking
243         if not self:may(nil, actor, "get_nodes") then
244                 return false, "forbidden";
245         end
246         --
247         return true, self.nodes;
248 end
249
250 -- Access models only affect 'none' affiliation caps, service/default access level...
251 function service:set_node_capabilities(node, actor, capabilities)
252         -- Access checking
253         if not self:may(node, actor, "configure") then
254                 return false, "forbidden";
255         end
256         --
257         local node_obj = self.nodes[node];
258         if not node_obj then
259                 return false, "item-not-found";
260         end
261         node_obj.capabilities = capabilities;
262         return true;
263 end
264
265 return _M;