mod_presence: Send unavailable presence when roster items are removed (fixes #331)
[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         elseif stanza.attr.type == "unsubscribed" then
182                 -- 1. send unavailable
183                 -- 2. route stanza
184                 -- 3. roster push (subscription = from or both)
185                 local success, pending_in, subscribed = rostermanager.unsubscribed(node, host, to_bare);
186                 if success then
187                         if subscribed then
188                                 rostermanager.roster_push(node, host, to_bare);
189                         end
190                         core_post_stanza(origin, stanza);
191                         if subscribed then
192                                 send_presence_of_available_resources(node, host, to_bare, origin, st.presence({ type = "unavailable" }));
193                         end
194                 end
195         else
196                 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
197         end
198         stanza.attr.from, stanza.attr.to = st_from, st_to;
199         return true;
200 end
201
202 function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
203         local node, host = jid_split(to_bare);
204         local st_from, st_to = stanza.attr.from, stanza.attr.to;
205         stanza.attr.from, stanza.attr.to = from_bare, to_bare;
206         log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare);
207
208         if stanza.attr.type == "probe" then
209                 local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
210                 if result then
211                         if 0 == send_presence_of_available_resources(node, host, st_from, origin) then
212                                 core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity
213                         end
214                 elseif not err then
215                         core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true);
216                 end
217         elseif stanza.attr.type == "subscribe" then
218                 if rostermanager.is_contact_subscribed(node, host, from_bare) then
219                         core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); -- already subscribed
220                         -- Sending presence is not clearly stated in the RFC, but it seems appropriate
221                         if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then
222                                 core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity
223                         end
224                 else
225                         core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt
226                         if not rostermanager.is_contact_pending_in(node, host, from_bare) then
227                                 if rostermanager.set_contact_pending_in(node, host, from_bare) then
228                                         sessionmanager.send_to_available_resources(node, host, stanza);
229                                 end -- TODO else return error, unable to save
230                         end
231                 end
232         elseif stanza.attr.type == "unsubscribe" then
233                 if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then
234                         sessionmanager.send_to_interested_resources(node, host, stanza);
235                         rostermanager.roster_push(node, host, from_bare);
236                 end
237         elseif stanza.attr.type == "subscribed" then
238                 if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then
239                         sessionmanager.send_to_interested_resources(node, host, stanza);
240                         rostermanager.roster_push(node, host, from_bare);
241                 end
242         elseif stanza.attr.type == "unsubscribed" then
243                 if rostermanager.process_inbound_subscription_cancellation(node, host, from_bare) then
244                         sessionmanager.send_to_interested_resources(node, host, stanza);
245                         rostermanager.roster_push(node, host, from_bare);
246                 end
247         else
248                 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
249         end
250         stanza.attr.from, stanza.attr.to = st_from, st_to;
251         return true;
252 end
253
254 local outbound_presence_handler = function(data)
255         -- outbound presence recieved
256         local origin, stanza = data.origin, data.stanza;
257
258         local to = stanza.attr.to;
259         if to then
260                 local t = stanza.attr.type;
261                 if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes
262                         return handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
263                 end
264
265                 local to_bare = jid_bare(to);
266                 local roster = origin.roster;
267                 if roster and not(roster[to_bare] and (roster[to_bare].subscription == "both" or roster[to_bare].subscription == "from")) then -- directed presence
268                         origin.directed = origin.directed or {};
269                         if t then -- removing from directed presence list on sending an error or unavailable
270                                 origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to?
271                         else
272                                 origin.directed[to] = true; -- FIXME does it make more sense to add to_bare rather than to?
273                         end
274                 end
275         end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers?
276 end
277
278 module:hook("pre-presence/full", outbound_presence_handler);
279 module:hook("pre-presence/bare", outbound_presence_handler);
280 module:hook("pre-presence/host", outbound_presence_handler);
281
282 module:hook("presence/bare", function(data)
283         -- inbound presence to bare JID recieved
284         local origin, stanza = data.origin, data.stanza;
285
286         local to = stanza.attr.to;
287         local t = stanza.attr.type;
288         if to then
289                 if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID
290                         return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
291                 end
292
293                 local user = bare_sessions[to];
294                 if user then
295                         for _, session in pairs(user.sessions) do
296                                 if session.presence then -- only send to available resources
297                                         session.send(stanza);
298                                 end
299                         end
300                 end -- no resources not online, discard
301         elseif not t or t == "unavailable" then
302                 handle_normal_presence(origin, stanza);
303         else
304                 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
305         end
306         return true;
307 end);
308 module:hook("presence/full", function(data)
309         -- inbound presence to full JID recieved
310         local origin, stanza = data.origin, data.stanza;
311
312         local t = stanza.attr.type;
313         if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID
314                 return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
315         end
316
317         local session = full_sessions[stanza.attr.to];
318         if session then
319                 -- TODO fire post processing event
320                 session.send(stanza);
321         end -- resource not online, discard
322         return true;
323 end);
324 module:hook("presence/host", function(data)
325         -- inbound presence to the host
326         local stanza = data.stanza;
327
328         local from_bare = jid_bare(stanza.attr.from);
329         local t = stanza.attr.type;
330         if t == "probe" then
331                 core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
332         elseif t == "subscribe" then
333                 core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" }));
334                 core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
335         end
336         return true;
337 end);
338
339 module:hook("resource-unbind", function(event)
340         local session, err = event.session, event.error;
341         -- Send unavailable presence
342         if session.presence then
343                 local pres = st.presence{ type = "unavailable" };
344                 if err then
345                         pres:tag("status"):text("Disconnected: "..err):up();
346                 end
347                 session:dispatch_stanza(pres);
348         elseif session.directed then
349                 local pres = st.presence{ type = "unavailable", from = session.full_jid };
350                 if err then
351                         pres:tag("status"):text("Disconnected: "..err):up();
352                 end
353                 for jid in pairs(session.directed) do
354                         pres.attr.to = jid;
355                         core_post_stanza(session, pres, true);
356                 end
357                 session.directed = nil;
358         end
359 end);
360
361 module:hook("roster-item-removed", function (event)
362         local username = event.username;
363         local session = event.origin;
364         local roster = event.roster or session and session.roster;
365         local jid = event.jid;
366         local item = event.item;
367
368         local subscription = item and item.subscription or "none";
369         local ask = item and item.ask;
370         local pending = roster and roster[false].pending[jid];
371
372         if subscription == "both" or subscription == "from" or pending then
373                 core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=jid}));
374         end
375
376         if subscription == "both" or subscription == "to" or ask then
377                 send_presence_of_available_resources(username, module.host, jid, session, st.presence({type="unavailable"}));
378                 core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=jid}));
379         end
380
381 end, -1);
382