2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
9 local log = module._log;
11 local require = require;
13 local t_concat = table.concat;
14 local s_find = string.find;
15 local tonumber = tonumber;
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;
27 local rostermanager = require "core.rostermanager";
28 local sessionmanager = require "core.sessionmanager";
30 local recalc_resource_map = require "util.presence".recalc_resource_map;
32 local ignore_presence_priority = module:get_option_boolean("ignore_presence_priority", false);
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
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
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);
64 for jid, item in pairs(roster) do -- broadcast to all interested contacts
65 if item.subscription == "both" or item.subscription == "from" then
67 core_post_stanza(origin, stanza, true);
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
77 core_post_stanza(origin, probe, true);
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;
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?
90 local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host});
91 for jid, item in pairs(roster) do -- resend outgoing subscription requests
93 request.attr.to = jid;
94 core_post_stanza(origin, request, true);
99 local event = { origin = origin }
100 module:fire_event('message/offline/broadcast', event);
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);
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);
114 origin.directed = nil;
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);
124 stanza.attr.to = nil; -- reset it
127 function send_presence_of_available_resources(user, host, jid, recipient_session, stanza)
128 local h = hosts[host];
130 if h and h.type == "local" then
131 local u = h.sessions[user];
133 for k, session in pairs(u.sessions) do
134 local pres = session.presence;
136 if stanza then pres = stanza; pres.attr.from = session.full_jid; end
138 core_post_stanza(session, pres, true);
145 log("debug", "broadcasted presence of %d resources from %s@%s to %s", count, user, host, jid);
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;
158 elseif stanza.attr.type == "subscribe" then
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
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
175 -- 3. send_presence_of_available_resources
176 if rostermanager.subscribed(node, host, to_bare) then
177 rostermanager.roster_push(node, host, to_bare);
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
184 -- 3. roster push (subscription = from or both)
185 local success, pending_in, subscribed = rostermanager.unsubscribed(node, host, to_bare);
188 rostermanager.roster_push(node, host, to_bare);
190 core_post_stanza(origin, stanza);
192 send_presence_of_available_resources(node, host, to_bare, origin, st.presence({ type = "unavailable" }));
196 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
198 stanza.attr.from, stanza.attr.to = st_from, st_to;
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);
208 if stanza.attr.type == "probe" then
209 local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
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
215 core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true);
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
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
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);
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);
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);
248 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
250 stanza.attr.from, stanza.attr.to = st_from, st_to;
254 local outbound_presence_handler = function(data)
255 -- outbound presence recieved
256 local origin, stanza = data.origin, data.stanza;
258 local to = stanza.attr.to;
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));
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?
272 origin.directed[to] = true; -- FIXME does it make more sense to add to_bare rather than to?
275 end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers?
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);
282 module:hook("presence/bare", function(data)
283 -- inbound presence to bare JID recieved
284 local origin, stanza = data.origin, data.stanza;
286 local to = stanza.attr.to;
287 local t = stanza.attr.type;
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));
293 local user = bare_sessions[to];
295 for _, session in pairs(user.sessions) do
296 if session.presence then -- only send to available resources
297 session.send(stanza);
300 end -- no resources not online, discard
301 elseif not t or t == "unavailable" then
302 handle_normal_presence(origin, stanza);
304 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
308 module:hook("presence/full", function(data)
309 -- inbound presence to full JID recieved
310 local origin, stanza = data.origin, data.stanza;
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));
317 local session = full_sessions[stanza.attr.to];
319 -- TODO fire post processing event
320 session.send(stanza);
321 end -- resource not online, discard
324 module:hook("presence/host", function(data)
325 -- inbound presence to the host
326 local stanza = data.stanza;
328 local from_bare = jid_bare(stanza.attr.from);
329 local t = stanza.attr.type;
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 }));
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" };
345 pres:tag("status"):text("Disconnected: "..err):up();
347 session:dispatch_stanza(pres);
348 elseif session.directed then
349 local pres = st.presence{ type = "unavailable", from = session.full_jid };
351 pres:tag("status"):text("Disconnected: "..err):up();
353 for jid in pairs(session.directed) do
355 core_post_stanza(session, pres, true);
357 session.directed = nil;
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 local from_jid = session.full_jid or (username .. "@" .. module.host);
369 local subscription = item and item.subscription or "none";
370 local ask = item and item.ask;
371 local pending = roster and roster[false].pending[jid];
373 if subscription == "both" or subscription == "from" or pending then
374 core_post_stanza(session, st.presence({type="unsubscribed", from=from_jid, to=jid}));
377 if subscription == "both" or subscription == "to" or ask then
378 send_presence_of_available_resources(username, module.host, jid, session, st.presence({type="unavailable"}));
379 core_post_stanza(session, st.presence({type="unsubscribe", from=from_jid, to=jid}));