Merge 0.10->trunk
[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                                 session.send(stanza);
79                         end
80                 end
81         end
82 end
83
84 local function roster_metadata(roster, err)
85         local metadata = roster[false];
86         if not metadata then
87                 metadata = { broken = err or nil };
88                 roster[false] = metadata;
89         end
90         if roster.pending and type(roster.pending.subscription) ~= "string" then
91                 metadata.pending = roster.pending;
92                 roster.pending = nil;
93         elseif not metadata.pending then
94                 metadata.pending = {};
95         end
96         return metadata;
97 end
98
99 local function load_roster(username, host)
100         local jid = username.."@"..host;
101         log("debug", "load_roster: asked for: %s", jid);
102         local user = bare_sessions[jid];
103         local roster;
104         if user then
105                 roster = user.roster;
106                 if roster then return roster; end
107                 log("debug", "load_roster: loading for new user: %s@%s", username, host);
108         else -- Attempt to load roster for non-loaded user
109                 log("debug", "load_roster: loading for offline user: %s@%s", username, host);
110         end
111         local roster_store = storagemanager.open(host, "roster", "keyval");
112         local data, err = roster_store:get(username);
113         roster = data or {};
114         if user then user.roster = roster; end
115         roster_metadata(roster, err);
116         if roster[jid] then
117                 roster[jid] = nil;
118                 log("warn", "roster for %s has a self-contact", jid);
119         end
120         if not err then
121                 hosts[host].events.fire_event("roster-load", { username = username, host = host, roster = roster });
122         end
123         return roster, err;
124 end
125
126 function save_roster(username, host, roster, jid)
127         if not um_user_exists(username, host) then
128                 log("debug", "not saving roster for %s@%s: the user doesn't exist", username, host);
129                 return nil;
130         end
131
132         log("debug", "save_roster: saving roster for %s@%s, (%s)", username, host, jid or "all contacts");
133         if not roster then
134                 roster = hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster;
135                 --if not roster then
136                 --      --roster = load_roster(username, host);
137                 --      return true; -- roster unchanged, no reason to save
138                 --end
139         end
140         if roster then
141                 local metadata = roster_metadata(roster);
142                 if metadata.version ~= true then
143                         metadata.version = (metadata.version or 0) + 1;
144                 end
145                 if metadata.broken then return nil, "Not saving broken roster" end
146                 if jid == nil then
147                         local roster_store = storagemanager.open(host, "roster", "keyval");
148                         return roster_store:set(username, roster);
149                 else
150                         local roster_store = storagemanager.open(host, "roster", "map");
151                         return roster_store:set_keys(username, { [false] = metadata, [jid] = roster[jid] or roster_store.remove });
152                 end
153         end
154         log("warn", "save_roster: user had no roster to save");
155         return nil;
156 end
157
158 local function process_inbound_subscription_approval(username, host, jid)
159         local roster = load_roster(username, host);
160         local item = roster[jid];
161         if item and item.ask then
162                 if item.subscription == "none" then
163                         item.subscription = "to";
164                 else -- subscription == from
165                         item.subscription = "both";
166                 end
167                 item.ask = nil;
168                 return save_roster(username, host, roster, jid);
169         end
170 end
171
172 local is_contact_pending_out -- forward declaration
173
174 local function process_inbound_subscription_cancellation(username, host, jid)
175         local roster = load_roster(username, host);
176         local item = roster[jid];
177         local changed = nil;
178         if is_contact_pending_out(username, host, jid) then
179                 item.ask = nil;
180                 changed = true;
181         end
182         if item then
183                 if item.subscription == "to" then
184                         item.subscription = "none";
185                         changed = true;
186                 elseif item.subscription == "both" then
187                         item.subscription = "from";
188                         changed = true;
189                 end
190         end
191         if changed then
192                 return save_roster(username, host, roster, jid);
193         end
194 end
195
196 local is_contact_pending_in -- forward declaration
197
198 local function process_inbound_unsubscribe(username, host, jid)
199         local roster = load_roster(username, host);
200         local item = roster[jid];
201         local changed = nil;
202         if is_contact_pending_in(username, host, jid) then
203                 roster[false].pending[jid] = nil;
204                 changed = true;
205         end
206         if item then
207                 if item.subscription == "from" then
208                         item.subscription = "none";
209                         changed = true;
210                 elseif item.subscription == "both" then
211                         item.subscription = "to";
212                         changed = true;
213                 end
214         end
215         if changed then
216                 return save_roster(username, host, roster, jid);
217         end
218 end
219
220 local function _get_online_roster_subscription(jidA, jidB)
221         local user = bare_sessions[jidA];
222         local item = user and (user.roster[jidB] or { subscription = "none" });
223         return item and item.subscription;
224 end
225 local function is_contact_subscribed(username, host, jid)
226         do
227                 local selfjid = username.."@"..host;
228                 local user_subscription = _get_online_roster_subscription(selfjid, jid);
229                 if user_subscription then return (user_subscription == "both" or user_subscription == "from"); end
230                 local contact_subscription = _get_online_roster_subscription(jid, selfjid);
231                 if contact_subscription then return (contact_subscription == "both" or contact_subscription == "to"); end
232         end
233         local roster, err = load_roster(username, host);
234         local item = roster[jid];
235         return item and (item.subscription == "from" or item.subscription == "both"), err;
236 end
237
238 function is_contact_pending_in(username, host, jid)
239         local roster = load_roster(username, host);
240         return roster[false].pending[jid];
241 end
242 local function set_contact_pending_in(username, host, jid)
243         local roster = load_roster(username, host);
244         local item = roster[jid];
245         if item and (item.subscription == "from" or item.subscription == "both") then
246                 return; -- false
247         end
248         roster[false].pending[jid] = true;
249         return save_roster(username, host, roster, jid);
250 end
251 function is_contact_pending_out(username, host, jid)
252         local roster = load_roster(username, host);
253         local item = roster[jid];
254         return item and item.ask;
255 end
256 local function set_contact_pending_out(username, host, jid) -- subscribe
257         local roster = load_roster(username, host);
258         local item = roster[jid];
259         if item and (item.ask or item.subscription == "to" or item.subscription == "both") then
260                 return true;
261         end
262         if not item then
263                 item = {subscription = "none", groups = {}};
264                 roster[jid] = item;
265         end
266         item.ask = "subscribe";
267         log("debug", "set_contact_pending_out: saving roster; set %s@%s.roster[%q].ask=subscribe", username, host, jid);
268         return save_roster(username, host, roster, jid);
269 end
270 local function unsubscribe(username, host, jid)
271         local roster = load_roster(username, host);
272         local item = roster[jid];
273         if not item then return false; end
274         if (item.subscription == "from" or item.subscription == "none") and not item.ask then
275                 return true;
276         end
277         item.ask = nil;
278         if item.subscription == "both" then
279                 item.subscription = "from";
280         elseif item.subscription == "to" then
281                 item.subscription = "none";
282         end
283         return save_roster(username, host, roster, jid);
284 end
285 local function subscribed(username, host, jid)
286         if is_contact_pending_in(username, host, jid) then
287                 local roster = load_roster(username, host);
288                 local item = roster[jid];
289                 if not item then -- FIXME should roster item be auto-created?
290                         item = {subscription = "none", groups = {}};
291                         roster[jid] = item;
292                 end
293                 if item.subscription == "none" then
294                         item.subscription = "from";
295                 else -- subscription == to
296                         item.subscription = "both";
297                 end
298                 roster[false].pending[jid] = nil;
299                 return save_roster(username, host, roster, jid);
300         end -- TODO else implement optional feature pre-approval (ask = subscribed)
301 end
302 local function unsubscribed(username, host, jid)
303         local roster = load_roster(username, host);
304         local item = roster[jid];
305         local pending = is_contact_pending_in(username, host, jid);
306         if pending then
307                 roster[false].pending[jid] = nil;
308         end
309         local is_subscribed;
310         if item then
311                 if item.subscription == "from" then
312                         item.subscription = "none";
313                         is_subscribed = true;
314                 elseif item.subscription == "both" then
315                         item.subscription = "to";
316                         is_subscribed = true;
317                 end
318         end
319         local success = (pending or is_subscribed) and save_roster(username, host, roster, jid);
320         return success, pending, subscribed;
321 end
322
323 local function process_outbound_subscription_request(username, host, jid)
324         local roster = load_roster(username, host);
325         local item = roster[jid];
326         if item and (item.subscription == "none" or item.subscription == "from") then
327                 item.ask = "subscribe";
328                 return save_roster(username, host, roster, jid);
329         end
330 end
331
332 --[[function process_outbound_subscription_approval(username, host, jid)
333         local roster = load_roster(username, host);
334         local item = roster[jid];
335         if item and (item.subscription == "none" or item.subscription == "from" then
336                 item.ask = "subscribe";
337                 return save_roster(username, host, roster);
338         end
339 end]]
340
341
342
343 return {
344         add_to_roster = add_to_roster;
345         remove_from_roster = remove_from_roster;
346         roster_push = roster_push;
347         load_roster = load_roster;
348         save_roster = save_roster;
349         process_inbound_subscription_approval = process_inbound_subscription_approval;
350         process_inbound_subscription_cancellation = process_inbound_subscription_cancellation;
351         process_inbound_unsubscribe = process_inbound_unsubscribe;
352         is_contact_subscribed = is_contact_subscribed;
353         is_contact_pending_in = is_contact_pending_in;
354         set_contact_pending_in = set_contact_pending_in;
355         is_contact_pending_out = is_contact_pending_out;
356         set_contact_pending_out = set_contact_pending_out;
357         unsubscribe = unsubscribe;
358         subscribed = subscribed;
359         unsubscribed = unsubscribed;
360         process_outbound_subscription_request = process_outbound_subscription_request;
361 };