Merge trunk->0.10
[prosody.git] / core / rostermanager.lua
1 -- Prosody IM
2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
4 --
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
7 --
8
9
10
11
12 local log = require "util.logger".init("rostermanager");
13
14 local pairs = pairs;
15 local tostring = tostring;
16 local type = type;
17
18 local hosts = hosts;
19 local bare_sessions = prosody.bare_sessions;
20
21 local um_user_exists = require "core.usermanager".user_exists;
22 local st = require "util.stanza";
23 local storagemanager = require "core.storagemanager";
24
25 local _ENV = nil;
26
27 local save_roster; -- forward declaration
28
29 local function add_to_roster(session, jid, item)
30         if session.roster then
31                 local old_item = session.roster[jid];
32                 session.roster[jid] = item;
33                 if save_roster(session.username, session.host, nil, jid) then
34                         return true;
35                 else
36                         session.roster[jid] = old_item;
37                         return nil, "wait", "internal-server-error", "Unable to save roster";
38                 end
39         else
40                 return nil, "auth", "not-authorized", "Session's roster not loaded";
41         end
42 end
43
44 local function remove_from_roster(session, jid)
45         if session.roster then
46                 local old_item = session.roster[jid];
47                 session.roster[jid] = nil;
48                 if save_roster(session.username, session.host, nil, jid) then
49                         return true;
50                 else
51                         session.roster[jid] = old_item;
52                         return nil, "wait", "internal-server-error", "Unable to save roster";
53                 end
54         else
55                 return nil, "auth", "not-authorized", "Session's roster not loaded";
56         end
57 end
58
59 local function roster_push(username, host, jid)
60         local roster = jid and hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster;
61         if roster then
62                 local item = hosts[host].sessions[username].roster[jid];
63                 local stanza = st.iq({type="set"});
64                 stanza:tag("query", {xmlns = "jabber:iq:roster", ver = tostring(roster[false].version or "1")  });
65                 if item then
66                         stanza:tag("item", {jid = jid, subscription = item.subscription, name = item.name, ask = item.ask});
67                         for group in pairs(item.groups) do
68                                 stanza:tag("group"):text(group):up();
69                         end
70                 else
71                         stanza:tag("item", {jid = jid, subscription = "remove"});
72                 end
73                 stanza:up(); -- move out from item
74                 stanza:up(); -- move out from stanza
75                 -- stanza ready
76                 for _, session in pairs(hosts[host].sessions[username].sessions) do
77                         if session.interested then
78                                 -- FIXME do we need to set stanza.attr.to?
79                                 session.send(stanza);
80                         end
81                 end
82         end
83 end
84
85 local function roster_metadata(roster, err)
86         local metadata = roster[false];
87         if not metadata then
88                 metadata = { broken = err or nil };
89                 roster[false] = metadata;
90         end
91         if roster.pending and type(roster.pending.subscription) ~= "string" then
92                 metadata.pending = roster.pending;
93                 roster.pending = nil;
94         elseif not metadata.pending then
95                 metadata.pending = {};
96         end
97         return metadata;
98 end
99
100 local function load_roster(username, host)
101         local jid = username.."@"..host;
102         log("debug", "load_roster: asked for: %s", jid);
103         local user = bare_sessions[jid];
104         local roster;
105         if user then
106                 roster = user.roster;
107                 if roster then return roster; end
108                 log("debug", "load_roster: loading for new user: %s@%s", username, host);
109         else -- Attempt to load roster for non-loaded user
110                 log("debug", "load_roster: loading for offline user: %s@%s", username, host);
111         end
112         local roster_store = storagemanager.open(host, "roster", "keyval");
113         local data, err = roster_store:get(username);
114         roster = data or {};
115         if user then user.roster = roster; end
116         roster_metadata(roster, err);
117         if roster[jid] then
118                 roster[jid] = nil;
119                 log("warn", "roster for %s has a self-contact", jid);
120         end
121         if not err then
122                 hosts[host].events.fire_event("roster-load", { username = username, host = host, roster = roster });
123         end
124         return roster, err;
125 end
126
127 function save_roster(username, host, roster, jid)
128         if not um_user_exists(username, host) then
129                 log("debug", "not saving roster for %s@%s: the user doesn't exist", username, host);
130                 return nil;
131         end
132
133         log("debug", "save_roster: saving roster for %s@%s, (%s)", username, host, jid or "all contacts");
134         if not roster then
135                 roster = hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster;
136                 --if not roster then
137                 --      --roster = load_roster(username, host);
138                 --      return true; -- roster unchanged, no reason to save
139                 --end
140         end
141         if roster then
142                 local metadata = roster_metadata(roster);
143                 if metadata.version ~= true then
144                         metadata.version = (metadata.version or 0) + 1;
145                 end
146                 if metadata.broken then return nil, "Not saving broken roster" end
147                 if jid == nil then
148                         local roster_store = storagemanager.open(host, "roster", "keyval");
149                         return roster_store:set(username, roster);
150                 else
151                         local roster_store = storagemanager.open(host, "roster", "map");
152                         return roster_store:set_keys(username, { [false] = metadata, [jid] = roster[jid] or roster_store.remove });
153                 end
154         end
155         log("warn", "save_roster: user had no roster to save");
156         return nil;
157 end
158
159 local function process_inbound_subscription_approval(username, host, jid)
160         local roster = load_roster(username, host);
161         local item = roster[jid];
162         if item and item.ask then
163                 if item.subscription == "none" then
164                         item.subscription = "to";
165                 else -- subscription == from
166                         item.subscription = "both";
167                 end
168                 item.ask = nil;
169                 return save_roster(username, host, roster, jid);
170         end
171 end
172
173 local is_contact_pending_out -- forward declaration
174
175 local function process_inbound_subscription_cancellation(username, host, jid)
176         local roster = load_roster(username, host);
177         local item = roster[jid];
178         local changed = nil;
179         if is_contact_pending_out(username, host, jid) then
180                 item.ask = nil;
181                 changed = true;
182         end
183         if item then
184                 if item.subscription == "to" then
185                         item.subscription = "none";
186                         changed = true;
187                 elseif item.subscription == "both" then
188                         item.subscription = "from";
189                         changed = true;
190                 end
191         end
192         if changed then
193                 return save_roster(username, host, roster, jid);
194         end
195 end
196
197 local is_contact_pending_in -- forward declaration
198
199 local function process_inbound_unsubscribe(username, host, jid)
200         local roster = load_roster(username, host);
201         local item = roster[jid];
202         local changed = nil;
203         if is_contact_pending_in(username, host, jid) then
204                 roster[false].pending[jid] = nil;
205                 changed = true;
206         end
207         if item then
208                 if item.subscription == "from" then
209                         item.subscription = "none";
210                         changed = true;
211                 elseif item.subscription == "both" then
212                         item.subscription = "to";
213                         changed = true;
214                 end
215         end
216         if changed then
217                 return save_roster(username, host, roster, jid);
218         end
219 end
220
221 local function _get_online_roster_subscription(jidA, jidB)
222         local user = bare_sessions[jidA];
223         local item = user and (user.roster[jidB] or { subscription = "none" });
224         return item and item.subscription;
225 end
226 local function is_contact_subscribed(username, host, jid)
227         do
228                 local selfjid = username.."@"..host;
229                 local user_subscription = _get_online_roster_subscription(selfjid, jid);
230                 if user_subscription then return (user_subscription == "both" or user_subscription == "from"); end
231                 local contact_subscription = _get_online_roster_subscription(jid, selfjid);
232                 if contact_subscription then return (contact_subscription == "both" or contact_subscription == "to"); end
233         end
234         local roster, err = load_roster(username, host);
235         local item = roster[jid];
236         return item and (item.subscription == "from" or item.subscription == "both"), err;
237 end
238
239 function is_contact_pending_in(username, host, jid)
240         local roster = load_roster(username, host);
241         return roster[false].pending[jid];
242 end
243 local function set_contact_pending_in(username, host, jid)
244         local roster = load_roster(username, host);
245         local item = roster[jid];
246         if item and (item.subscription == "from" or item.subscription == "both") then
247                 return; -- false
248         end
249         roster[false].pending[jid] = true;
250         return save_roster(username, host, roster, jid);
251 end
252 function is_contact_pending_out(username, host, jid)
253         local roster = load_roster(username, host);
254         local item = roster[jid];
255         return item and item.ask;
256 end
257 local function set_contact_pending_out(username, host, jid) -- subscribe
258         local roster = load_roster(username, host);
259         local item = roster[jid];
260         if item and (item.ask or item.subscription == "to" or item.subscription == "both") then
261                 return true;
262         end
263         if not item then
264                 item = {subscription = "none", groups = {}};
265                 roster[jid] = item;
266         end
267         item.ask = "subscribe";
268         log("debug", "set_contact_pending_out: saving roster; set %s@%s.roster[%q].ask=subscribe", username, host, jid);
269         return save_roster(username, host, roster, jid);
270 end
271 local function unsubscribe(username, host, jid)
272         local roster = load_roster(username, host);
273         local item = roster[jid];
274         if not item then return false; end
275         if (item.subscription == "from" or item.subscription == "none") and not item.ask then
276                 return true;
277         end
278         item.ask = nil;
279         if item.subscription == "both" then
280                 item.subscription = "from";
281         elseif item.subscription == "to" then
282                 item.subscription = "none";
283         end
284         return save_roster(username, host, roster, jid);
285 end
286 local function subscribed(username, host, jid)
287         if is_contact_pending_in(username, host, jid) then
288                 local roster = load_roster(username, host);
289                 local item = roster[jid];
290                 if not item then -- FIXME should roster item be auto-created?
291                         item = {subscription = "none", groups = {}};
292                         roster[jid] = item;
293                 end
294                 if item.subscription == "none" then
295                         item.subscription = "from";
296                 else -- subscription == to
297                         item.subscription = "both";
298                 end
299                 roster[false].pending[jid] = nil;
300                 return save_roster(username, host, roster, jid);
301         end -- TODO else implement optional feature pre-approval (ask = subscribed)
302 end
303 local function unsubscribed(username, host, jid)
304         local roster = load_roster(username, host);
305         local item = roster[jid];
306         local pending = is_contact_pending_in(username, host, jid);
307         if pending then
308                 roster[false].pending[jid] = nil;
309         end
310         local is_subscribed;
311         if item then
312                 if item.subscription == "from" then
313                         item.subscription = "none";
314                         is_subscribed = true;
315                 elseif item.subscription == "both" then
316                         item.subscription = "to";
317                         is_subscribed = true;
318                 end
319         end
320         local success = (pending or is_subscribed) and save_roster(username, host, roster, jid);
321         return success, pending, subscribed;
322 end
323
324 local function process_outbound_subscription_request(username, host, jid)
325         local roster = load_roster(username, host);
326         local item = roster[jid];
327         if item and (item.subscription == "none" or item.subscription == "from") then
328                 item.ask = "subscribe";
329                 return save_roster(username, host, roster, jid);
330         end
331 end
332
333 --[[function process_outbound_subscription_approval(username, host, jid)
334         local roster = load_roster(username, host);
335         local item = roster[jid];
336         if item and (item.subscription == "none" or item.subscription == "from" then
337                 item.ask = "subscribe";
338                 return save_roster(username, host, roster);
339         end
340 end]]
341
342
343
344 return {
345         add_to_roster = add_to_roster;
346         remove_from_roster = remove_from_roster;
347         roster_push = roster_push;
348         load_roster = load_roster;
349         save_roster = save_roster;
350         process_inbound_subscription_approval = process_inbound_subscription_approval;
351         process_inbound_subscription_cancellation = process_inbound_subscription_cancellation;
352         process_inbound_unsubscribe = process_inbound_unsubscribe;
353         is_contact_subscribed = is_contact_subscribed;
354         is_contact_pending_in = is_contact_pending_in;
355         set_contact_pending_in = set_contact_pending_in;
356         is_contact_pending_out = is_contact_pending_out;
357         set_contact_pending_out = set_contact_pending_out;
358         unsubscribe = unsubscribe;
359         subscribed = subscribed;
360         unsubscribed = unsubscribed;
361         process_outbound_subscription_request = process_outbound_subscription_request;
362 };