Merge 0.9->0.10
[prosody.git] / plugins / mod_presence.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 local log = module._log;
10
11 local require = require;
12 local pairs = pairs;
13 local t_concat = table.concat;
14 local s_find = string.find;
15 local tonumber = tonumber;
16
17 local core_post_stanza = prosody.core_post_stanza;
18 local st = require "util.stanza";
19 local jid_split = require "util.jid".split;
20 local jid_bare = require "util.jid".bare;
21 local datetime = require "util.datetime";
22 local hosts = prosody.hosts;
23 local bare_sessions = prosody.bare_sessions;
24 local full_sessions = prosody.full_sessions;
25 local NULL = {};
26
27 local rostermanager = require "core.rostermanager";
28 local sessionmanager = require "core.sessionmanager";
29
30 local recalc_resource_map = require "util.presence".recalc_resource_map;
31
32 local ignore_presence_priority = module:get_option_boolean("ignore_presence_priority", false);
33
34 function handle_normal_presence(origin, stanza)
35         if ignore_presence_priority then
36                 local priority = stanza:get_child("priority");
37                 if priority and priority[1] ~= "0" then
38                         for i=#priority.tags,1,-1 do priority.tags[i] = nil; end
39                         for i=#priority,1,-1 do priority[i] = nil; end
40                         priority[1] = "0";
41                 end
42         end
43         local priority = stanza:get_child("priority");
44         if priority and #priority > 0 then
45                 priority = t_concat(priority);
46                 if s_find(priority, "^[+-]?[0-9]+$") then
47                         priority = tonumber(priority);
48                         if priority < -128 then priority = -128 end
49                         if priority > 127 then priority = 127 end
50                 else priority = 0; end
51         else priority = 0; end
52         if full_sessions[origin.full_jid] then -- if user is still connected
53                 origin.send(stanza); -- reflect their presence back to them
54         end
55         local roster = origin.roster;
56         local node, host = origin.username, origin.host;
57         local user = bare_sessions[node.."@"..host];
58         for _, res in pairs(user and user.sessions or NULL) do -- broadcast to all resources
59                 if res ~= origin and res.presence then -- to resource
60                         stanza.attr.to = res.full_jid;
61                         core_post_stanza(origin, stanza, true);
62                 end
63         end
64         for jid, item in pairs(roster) do -- broadcast to all interested contacts
65                 if item.subscription == "both" or item.subscription == "from" then
66                         stanza.attr.to = jid;
67                         core_post_stanza(origin, stanza, true);
68                 end
69         end
70         if stanza.attr.type == nil and not origin.presence then -- initial presence
71                 module:fire_event("presence/initial", { origin = origin, stanza = stanza } );
72                 origin.presence = stanza; -- FIXME repeated later
73                 local probe = st.presence({from = origin.full_jid, type = "probe"});
74                 for jid, item in pairs(roster) do -- probe all contacts we are subscribed to
75                         if item.subscription == "both" or item.subscription == "to" then
76                                 probe.attr.to = jid;
77                                 core_post_stanza(origin, probe, true);
78                         end
79                 end
80                 for _, res in pairs(user and user.sessions or NULL) do -- broadcast from all available resources
81                         if res ~= origin and res.presence then
82                                 res.presence.attr.to = origin.full_jid;
83                                 core_post_stanza(res, res.presence, true);
84                                 res.presence.attr.to = nil;
85                         end
86                 end
87                 for jid in pairs(roster[false].pending) do -- resend incoming subscription requests
88                         origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original?
89                 end
90                 local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host});
91                 for jid, item in pairs(roster) do -- resend outgoing subscription requests
92                         if item.ask then
93                                 request.attr.to = jid;
94                                 core_post_stanza(origin, request, true);
95                         end
96                 end
97
98                 if priority >= 0 then
99                         local event = { origin = origin }
100                         module:fire_event('message/offline/broadcast', event);
101                 end
102         end
103         if stanza.attr.type == "unavailable" then
104                 origin.presence = nil;
105                 if origin.priority then
106                         origin.priority = nil;
107                         recalc_resource_map(user);
108                 end
109                 if origin.directed then
110                         for jid in pairs(origin.directed) do
111                                 stanza.attr.to = jid;
112                                 core_post_stanza(origin, stanza, true);
113                         end
114                         origin.directed = nil;
115                 end
116         else
117                 origin.presence = stanza;
118                 stanza:tag("delay", { xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime() }):up();
119                 if origin.priority ~= priority then
120                         origin.priority = priority;
121                         recalc_resource_map(user);
122                 end
123         end
124         stanza.attr.to = nil; -- reset it
125 end
126
127 function send_presence_of_available_resources(user, host, jid, recipient_session, stanza)
128         local h = hosts[host];
129         local count = 0;
130         if h and h.type == "local" then
131                 local u = h.sessions[user];
132                 if u then
133                         for k, session in pairs(u.sessions) do
134                                 local pres = session.presence;
135                                 if pres then
136                                         if stanza then pres = stanza; pres.attr.from = session.full_jid; end
137                                         pres.attr.to = jid;
138                                         core_post_stanza(session, pres, true);
139                                         pres.attr.to = nil;
140                                         count = count + 1;
141                                 end
142                         end
143                 end
144         end
145         log("debug", "broadcasted presence of %d resources from %s@%s to %s", count, user, host, jid);
146         return count;
147 end
148
149 function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
150         local node, host = jid_split(from_bare);
151         if to_bare == from_bare then return; end -- No self contacts
152         local st_from, st_to = stanza.attr.from, stanza.attr.to;
153         stanza.attr.from, stanza.attr.to = from_bare, to_bare;
154         log("debug", "outbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare);
155         if stanza.attr.type == "probe" then
156                 stanza.attr.from, stanza.attr.to = st_from, st_to;
157                 return;
158         elseif stanza.attr.type == "subscribe" then
159                 -- 1. route stanza
160                 -- 2. roster push (subscription = none, ask = subscribe)
161                 if rostermanager.set_contact_pending_out(node, host, to_bare) then
162                         rostermanager.roster_push(node, host, to_bare);
163                 end -- else file error
164                 core_post_stanza(origin, stanza);
165         elseif stanza.attr.type == "unsubscribe" then
166                 -- 1. route stanza
167                 -- 2. roster push (subscription = none or from)
168                 if rostermanager.unsubscribe(node, host, to_bare) then
169                         rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed?
170                 end -- else file error
171                 core_post_stanza(origin, stanza);
172         elseif stanza.attr.type == "subscribed" then
173                 -- 1. route stanza
174                 -- 2. roster_push ()
175                 -- 3. send_presence_of_available_resources
176                 if rostermanager.subscribed(node, host, to_bare) then
177                         rostermanager.roster_push(node, host, to_bare);
178                 end
179                 core_post_stanza(origin, stanza);
180                 send_presence_of_available_resources(node, host, to_bare, origin);
181                 core_post_stanza(origin, st.presence({ type = "probe", from = from_bare, to = to_bare }));
182         elseif stanza.attr.type == "unsubscribed" then
183                 -- 1. send unavailable
184                 -- 2. route stanza
185                 -- 3. roster push (subscription = from or both)
186                 local success, pending_in, subscribed = rostermanager.unsubscribed(node, host, to_bare);
187                 if success then
188                         if subscribed then
189                                 rostermanager.roster_push(node, host, to_bare);
190                         end
191                         core_post_stanza(origin, stanza);
192                         if subscribed then
193                                 send_presence_of_available_resources(node, host, to_bare, origin, st.presence({ type = "unavailable" }));
194                         end
195                 end
196         else
197                 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
198         end
199         stanza.attr.from, stanza.attr.to = st_from, st_to;
200         return true;
201 end
202
203 function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
204         local node, host = jid_split(to_bare);
205         local st_from, st_to = stanza.attr.from, stanza.attr.to;
206         stanza.attr.from, stanza.attr.to = from_bare, to_bare;
207         log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare);
208
209         if stanza.attr.type == "probe" then
210                 local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
211                 if result then
212                         if 0 == send_presence_of_available_resources(node, host, st_from, origin) then
213                                 core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity
214                         end
215                 elseif not err then
216                         core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true);
217                 end
218         elseif stanza.attr.type == "subscribe" then
219                 if rostermanager.is_contact_subscribed(node, host, from_bare) then
220                         core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); -- already subscribed
221                         -- Sending presence is not clearly stated in the RFC, but it seems appropriate
222                         if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then
223                                 core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity
224                         end
225                 else
226                         core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt
227                         if not rostermanager.is_contact_pending_in(node, host, from_bare) then
228                                 if rostermanager.set_contact_pending_in(node, host, from_bare) then
229                                         sessionmanager.send_to_available_resources(node, host, stanza);
230                                 end -- TODO else return error, unable to save
231                         end
232                 end
233         elseif stanza.attr.type == "unsubscribe" then
234                 if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then
235                         sessionmanager.send_to_interested_resources(node, host, stanza);
236                         rostermanager.roster_push(node, host, from_bare);
237                 end
238         elseif stanza.attr.type == "subscribed" then
239                 if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then
240                         sessionmanager.send_to_interested_resources(node, host, stanza);
241                         rostermanager.roster_push(node, host, from_bare);
242                 end
243         elseif stanza.attr.type == "unsubscribed" then
244                 if rostermanager.process_inbound_subscription_cancellation(node, host, from_bare) then
245                         sessionmanager.send_to_interested_resources(node, host, stanza);
246                         rostermanager.roster_push(node, host, from_bare);
247                 end
248         else
249                 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
250         end
251         stanza.attr.from, stanza.attr.to = st_from, st_to;
252         return true;
253 end
254
255 local outbound_presence_handler = function(data)
256         -- outbound presence recieved
257         local origin, stanza = data.origin, data.stanza;
258
259         local to = stanza.attr.to;
260         if to then
261                 local t = stanza.attr.type;
262                 if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes
263                         return handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
264                 end
265
266                 local to_bare = jid_bare(to);
267                 local roster = origin.roster;
268                 if roster and not(roster[to_bare] and (roster[to_bare].subscription == "both" or roster[to_bare].subscription == "from")) then -- directed presence
269                         origin.directed = origin.directed or {};
270                         if t then -- removing from directed presence list on sending an error or unavailable
271                                 origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to?
272                         else
273                                 origin.directed[to] = true; -- FIXME does it make more sense to add to_bare rather than to?
274                         end
275                 end
276         end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers?
277 end
278
279 module:hook("pre-presence/full", outbound_presence_handler);
280 module:hook("pre-presence/bare", outbound_presence_handler);
281 module:hook("pre-presence/host", outbound_presence_handler);
282
283 module:hook("presence/bare", function(data)
284         -- inbound presence to bare JID recieved
285         local origin, stanza = data.origin, data.stanza;
286
287         local to = stanza.attr.to;
288         local t = stanza.attr.type;
289         if to then
290                 if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID
291                         return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
292                 end
293
294                 local user = bare_sessions[to];
295                 if user then
296                         for _, session in pairs(user.sessions) do
297                                 if session.presence then -- only send to available resources
298                                         session.send(stanza);
299                                 end
300                         end
301                 end -- no resources not online, discard
302         elseif not t or t == "unavailable" then
303                 handle_normal_presence(origin, stanza);
304         else
305                 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
306         end
307         return true;
308 end);
309 module:hook("presence/full", function(data)
310         -- inbound presence to full JID recieved
311         local origin, stanza = data.origin, data.stanza;
312
313         local t = stanza.attr.type;
314         if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID
315                 return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
316         end
317
318         local session = full_sessions[stanza.attr.to];
319         if session then
320                 -- TODO fire post processing event
321                 session.send(stanza);
322         end -- resource not online, discard
323         return true;
324 end);
325 module:hook("presence/host", function(data)
326         -- inbound presence to the host
327         local stanza = data.stanza;
328
329         local from_bare = jid_bare(stanza.attr.from);
330         local t = stanza.attr.type;
331         if t == "probe" then
332                 core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
333         elseif t == "subscribe" then
334                 core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" }));
335                 core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
336         end
337         return true;
338 end);
339
340 module:hook("resource-unbind", function(event)
341         local session, err = event.session, event.error;
342         -- Send unavailable presence
343         if session.presence then
344                 local pres = st.presence{ type = "unavailable" };
345                 if err then
346                         pres:tag("status"):text("Disconnected: "..err):up();
347                 end
348                 session:dispatch_stanza(pres);
349         elseif session.directed then
350                 local pres = st.presence{ type = "unavailable", from = session.full_jid };
351                 if err then
352                         pres:tag("status"):text("Disconnected: "..err):up();
353                 end
354                 for jid in pairs(session.directed) do
355                         pres.attr.to = jid;
356                         core_post_stanza(session, pres, true);
357                 end
358                 session.directed = nil;
359         end
360 end);
361
362 module:hook("roster-item-removed", function (event)
363         local username = event.username;
364         local session = event.origin;
365         local roster = event.roster or session and session.roster;
366         local jid = event.jid;
367         local item = event.item;
368         local from_jid = session.full_jid or (username .. "@" .. module.host);
369
370         local subscription = item and item.subscription or "none";
371         local ask = item and item.ask;
372         local pending = roster and roster[false].pending[jid];
373
374         if subscription == "both" or subscription == "from" or pending then
375                 core_post_stanza(session, st.presence({type="unsubscribed", from=from_jid, to=jid}));
376         end
377
378         if subscription == "both" or subscription == "to" or ask then
379                 send_presence_of_available_resources(username, module.host, jid, session, st.presence({type="unavailable"}));
380                 core_post_stanza(session, st.presence({type="unsubscribe", from=from_jid, to=jid}));
381         end
382
383 end, -1);
384