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 s_find
= string.find
;
14 local tonumber = tonumber;
16 local core_post_stanza
= prosody
.core_post_stanza
;
17 local st
= require
"util.stanza";
18 local jid_split
= require
"util.jid".split
;
19 local jid_bare
= require
"util.jid".bare
;
20 local datetime
= require
"util.datetime";
21 local hosts
= prosody
.hosts
;
22 local bare_sessions
= prosody
.bare_sessions
;
23 local full_sessions
= prosody
.full_sessions
;
26 local rostermanager
= require
"core.rostermanager";
27 local sessionmanager
= require
"core.sessionmanager";
29 local recalc_resource_map
= require
"util.presence".recalc_resource_map
;
31 local ignore_presence_priority
= module
:get_option_boolean("ignore_presence_priority", false);
33 function handle_normal_presence(origin
, stanza
)
34 if ignore_presence_priority
then
35 local priority
= stanza
:get_child("priority");
36 if priority
and priority
[1] ~= "0" then
37 for i
=#priority
.tags
,1,-1 do priority
.tags
[i
] = nil; end
38 for i
=#priority
,2,-1 do priority
[i
] = nil; end
42 local priority
= stanza
:get_child_text("priority");
43 if priority
and s_find(priority
, "^[+-]?[0-9]+$") then
44 priority
= tonumber(priority
);
45 if priority
< -128 then priority
= -128 end
46 if priority
> 127 then priority
= 127 end
47 else priority
= 0; end
48 if full_sessions
[origin
.full_jid
] then -- if user is still connected
49 origin
.send(stanza
); -- reflect their presence back to them
51 local roster
= origin
.roster
;
52 local node
, host
= origin
.username
, origin
.host
;
53 local user
= bare_sessions
[node
.."@"..host
];
54 for _
, res
in pairs(user
and user
.sessions
or NULL
) do -- broadcast to all resources
55 if res
~= origin
and res
.presence
then -- to resource
56 stanza
.attr
.to
= res
.full_jid
;
57 core_post_stanza(origin
, stanza
, true);
60 for jid
, item
in pairs(roster
) do -- broadcast to all interested contacts
61 if item
.subscription
== "both" or item
.subscription
== "from" then
63 core_post_stanza(origin
, stanza
, true);
67 if stanza
.attr
.type == nil and not origin
.presence
then -- initial presence
68 module
:fire_event("presence/initial", { origin
= origin
, stanza
= stanza
} );
69 origin
.presence
= stanza
; -- FIXME repeated later
70 local probe
= st
.presence({from
= origin
.full_jid
, type = "probe"});
71 for jid
, item
in pairs(roster
) do -- probe all contacts we are subscribed to
72 if item
.subscription
== "both" or item
.subscription
== "to" then
74 core_post_stanza(origin
, probe
, true);
77 for _
, res
in pairs(user
and user
.sessions
or NULL
) do -- broadcast from all available resources
78 if res
~= origin
and res
.presence
then
79 res
.presence
.attr
.to
= origin
.full_jid
;
80 core_post_stanza(res
, res
.presence
, true);
81 res
.presence
.attr
.to
= nil;
84 for jid
in pairs(roster
[false].pending
) do -- resend incoming subscription requests
85 origin
.send(st
.presence({type="subscribe", from
=jid
})); -- TODO add to attribute? Use original?
87 local request
= st
.presence({type="subscribe", from
=origin
.username
.."@"..origin
.host
});
88 for jid
, item
in pairs(roster
) do -- resend outgoing subscription requests
90 request
.attr
.to
= jid
;
91 core_post_stanza(origin
, request
, true);
96 local event
= { origin
= origin
}
97 module
:fire_event('message/offline/broadcast', event
);
100 if stanza
.attr
.type == "unavailable" then
101 origin
.presence
= nil;
102 if origin
.priority
then
103 origin
.priority
= nil;
104 recalc_resource_map(user
);
106 if origin
.directed
then
107 for jid
in pairs(origin
.directed
) do
108 stanza
.attr
.to
= jid
;
109 core_post_stanza(origin
, stanza
, true);
111 origin
.directed
= nil;
114 origin
.presence
= stanza
;
115 stanza
:tag("delay", { xmlns
= "urn:xmpp:delay", from
= host
, stamp
= datetime
.datetime() }):up();
116 if origin
.priority
~= priority
then
117 origin
.priority
= priority
;
118 recalc_resource_map(user
);
121 stanza
.attr
.to
= nil; -- reset it
124 -- luacheck: ignore 212/recipient_session
125 -- TODO This argument is used in 3rd party modules
126 function send_presence_of_available_resources(user
, host
, jid
, recipient_session
, stanza
)
127 local h
= hosts
[host
];
129 if h
and h
.type == "local" then
130 local u
= h
.sessions
[user
];
132 for _
, session
in pairs(u
.sessions
) do
133 local pres
= session
.presence
;
135 if stanza
then pres
= stanza
; pres
.attr
.from
= session
.full_jid
; end
137 core_post_stanza(session
, pres
, true);
144 log("debug", "broadcasted presence of %d resources from %s@%s to %s", count
, user
, host
, jid
);
148 function handle_outbound_presence_subscriptions_and_probes(origin
, stanza
, from_bare
, to_bare
)
149 local node
, host
= jid_split(from_bare
);
150 if to_bare
== from_bare
then return; end -- No self contacts
151 local st_from
, st_to
= stanza
.attr
.from
, stanza
.attr
.to
;
152 stanza
.attr
.from
, stanza
.attr
.to
= from_bare
, to_bare
;
153 log("debug", "outbound presence %s from %s for %s", stanza
.attr
.type, from_bare
, to_bare
);
154 if stanza
.attr
.type == "probe" then
155 stanza
.attr
.from
, stanza
.attr
.to
= st_from
, st_to
;
157 elseif stanza
.attr
.type == "subscribe" then
159 -- 2. roster push (subscription = none, ask = subscribe)
160 if rostermanager
.set_contact_pending_out(node
, host
, to_bare
) then
161 rostermanager
.roster_push(node
, host
, to_bare
);
162 end -- else file error
163 core_post_stanza(origin
, stanza
);
164 elseif stanza
.attr
.type == "unsubscribe" then
166 -- 2. roster push (subscription = none or from)
167 if rostermanager
.unsubscribe(node
, host
, to_bare
) then
168 rostermanager
.roster_push(node
, host
, to_bare
); -- FIXME do roster push when roster has in fact not changed?
169 end -- else file error
170 core_post_stanza(origin
, stanza
);
171 elseif stanza
.attr
.type == "subscribed" then
174 -- 3. send_presence_of_available_resources
175 if rostermanager
.subscribed(node
, host
, to_bare
) then
176 rostermanager
.roster_push(node
, host
, to_bare
);
178 core_post_stanza(origin
, stanza
);
179 send_presence_of_available_resources(node
, host
, to_bare
, origin
);
180 if rostermanager
.is_user_subscribed(node
, host
, to_bare
) then
181 core_post_stanza(origin
, st
.presence({ type = "probe", from
= from_bare
, to
= to_bare
}));
183 elseif stanza
.attr
.type == "unsubscribed" then
184 -- 1. send unavailable
186 -- 3. roster push (subscription = from or both)
187 local success
, pending_in
, subscribed
= rostermanager
.unsubscribed(node
, host
, to_bare
);
190 rostermanager
.roster_push(node
, host
, to_bare
);
192 core_post_stanza(origin
, stanza
);
194 send_presence_of_available_resources(node
, host
, to_bare
, origin
, st
.presence({ type = "unavailable" }));
198 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", "Invalid presence type"));
200 stanza
.attr
.from
, stanza
.attr
.to
= st_from
, st_to
;
204 function handle_inbound_presence_subscriptions_and_probes(origin
, stanza
, from_bare
, to_bare
)
205 local node
, host
= jid_split(to_bare
);
206 local st_from
, st_to
= stanza
.attr
.from
, stanza
.attr
.to
;
207 stanza
.attr
.from
, stanza
.attr
.to
= from_bare
, to_bare
;
208 log("debug", "inbound presence %s from %s for %s", stanza
.attr
.type, from_bare
, to_bare
);
210 if stanza
.attr
.type == "probe" then
211 local result
, err
= rostermanager
.is_contact_subscribed(node
, host
, from_bare
);
213 if 0 == send_presence_of_available_resources(node
, host
, st_from
, origin
) then
214 core_post_stanza(hosts
[host
], st
.presence({from
=to_bare
, to
=st_from
, type="unavailable"}), true); -- TODO send last activity
217 core_post_stanza(hosts
[host
], st
.presence({from
=to_bare
, to
=from_bare
, type="unsubscribed"}), true);
219 elseif stanza
.attr
.type == "subscribe" then
220 if rostermanager
.is_contact_subscribed(node
, host
, from_bare
) then
221 core_post_stanza(hosts
[host
], st
.presence({from
=to_bare
, to
=from_bare
, type="subscribed"}), true); -- already subscribed
222 -- Sending presence is not clearly stated in the RFC, but it seems appropriate
223 if 0 == send_presence_of_available_resources(node
, host
, from_bare
, origin
) then
224 core_post_stanza(hosts
[host
], st
.presence({from
=to_bare
, to
=from_bare
, type="unavailable"}), true); -- TODO send last activity
227 core_post_stanza(hosts
[host
], st
.presence({from
=to_bare
, to
=from_bare
, type="unavailable"}), true); -- acknowledging receipt
228 if not rostermanager
.is_contact_pending_in(node
, host
, from_bare
) then
229 if rostermanager
.set_contact_pending_in(node
, host
, from_bare
) then
230 sessionmanager
.send_to_available_resources(node
, host
, stanza
);
231 end -- TODO else return error, unable to save
234 elseif stanza
.attr
.type == "unsubscribe" then
235 if rostermanager
.process_inbound_unsubscribe(node
, host
, from_bare
) then
236 sessionmanager
.send_to_interested_resources(node
, host
, stanza
);
237 rostermanager
.roster_push(node
, host
, from_bare
);
239 elseif stanza
.attr
.type == "subscribed" then
240 if rostermanager
.process_inbound_subscription_approval(node
, host
, from_bare
) then
241 sessionmanager
.send_to_interested_resources(node
, host
, stanza
);
242 rostermanager
.roster_push(node
, host
, from_bare
);
244 elseif stanza
.attr
.type == "unsubscribed" then
245 if rostermanager
.process_inbound_subscription_cancellation(node
, host
, from_bare
) then
246 sessionmanager
.send_to_interested_resources(node
, host
, stanza
);
247 rostermanager
.roster_push(node
, host
, from_bare
);
250 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", "Invalid presence type"));
252 stanza
.attr
.from
, stanza
.attr
.to
= st_from
, st_to
;
256 local outbound_presence_handler
= function(data
)
257 -- outbound presence received
258 local origin
, stanza
= data
.origin
, data
.stanza
;
260 local to
= stanza
.attr
.to
;
262 local t
= stanza
.attr
.type;
263 if t
~= nil and t
~= "unavailable" and t
~= "error" then -- check for subscriptions and probes
264 return handle_outbound_presence_subscriptions_and_probes(origin
, stanza
, jid_bare(stanza
.attr
.from
), jid_bare(stanza
.attr
.to
));
267 local to_bare
= jid_bare(to
);
268 local roster
= origin
.roster
;
269 if roster
and not(roster
[to_bare
] and (roster
[to_bare
].subscription
== "both" or roster
[to_bare
].subscription
== "from")) then -- directed presence
270 origin
.directed
= origin
.directed
or {};
271 if t
then -- removing from directed presence list on sending an error or unavailable
272 origin
.directed
[to
] = nil; -- FIXME does it make more sense to add to_bare rather than to?
274 origin
.directed
[to
] = true; -- FIXME does it make more sense to add to_bare rather than to?
277 end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers?
280 module
:hook("pre-presence/full", outbound_presence_handler
);
281 module
:hook("pre-presence/bare", outbound_presence_handler
);
282 module
:hook("pre-presence/host", outbound_presence_handler
);
284 module
:hook("presence/bare", function(data
)
285 -- inbound presence to bare JID received
286 local origin
, stanza
= data
.origin
, data
.stanza
;
288 local to
= stanza
.attr
.to
;
289 local t
= stanza
.attr
.type;
291 if t
~= nil and t
~= "unavailable" and t
~= "error" then -- check for subscriptions and probes sent to bare JID
292 return handle_inbound_presence_subscriptions_and_probes(origin
, stanza
, jid_bare(stanza
.attr
.from
), jid_bare(stanza
.attr
.to
));
295 local user
= bare_sessions
[to
];
297 for _
, session
in pairs(user
.sessions
) do
298 if session
.presence
then -- only send to available resources
299 session
.send(stanza
);
302 end -- no resources not online, discard
303 elseif not t
or t
== "unavailable" then
304 handle_normal_presence(origin
, stanza
);
306 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", "Invalid presence type"));
310 module
:hook("presence/full", function(data
)
311 -- inbound presence to full JID received
312 local origin
, stanza
= data
.origin
, data
.stanza
;
314 local t
= stanza
.attr
.type;
315 if t
~= nil and t
~= "unavailable" and t
~= "error" then -- check for subscriptions and probes sent to full JID
316 return handle_inbound_presence_subscriptions_and_probes(origin
, stanza
, jid_bare(stanza
.attr
.from
), jid_bare(stanza
.attr
.to
));
319 local session
= full_sessions
[stanza
.attr
.to
];
321 -- TODO fire post processing event
322 session
.send(stanza
);
323 end -- resource not online, discard
326 module
:hook("presence/host", function(data
)
327 -- inbound presence to the host
328 local stanza
= data
.stanza
;
330 local from_bare
= jid_bare(stanza
.attr
.from
);
331 local t
= stanza
.attr
.type;
333 core_post_stanza(hosts
[module
.host
], st
.presence({ from
= module
.host
, to
= from_bare
, id
= stanza
.attr
.id
}));
334 elseif t
== "subscribe" then
335 core_post_stanza(hosts
[module
.host
], st
.presence({ from
= module
.host
, to
= from_bare
, id
= stanza
.attr
.id
, type = "subscribed" }));
336 core_post_stanza(hosts
[module
.host
], st
.presence({ from
= module
.host
, to
= from_bare
, id
= stanza
.attr
.id
}));
341 module
:hook("resource-unbind", function(event
)
342 local session
, err
= event
.session
, event
.error;
343 -- Send unavailable presence
344 if session
.presence
then
345 local pres
= st
.presence
{ type = "unavailable" };
347 pres
:tag("status"):text("Disconnected: "..err
):up();
349 session
:dispatch_stanza(pres
);
350 elseif session
.directed
then
351 local pres
= st
.presence
{ type = "unavailable", from
= session
.full_jid
};
353 pres
:tag("status"):text("Disconnected: "..err
):up();
355 for jid
in pairs(session
.directed
) do
357 core_post_stanza(session
, pres
, true);
359 session
.directed
= nil;
363 module
:hook("roster-item-removed", function (event
)
364 local username
= event
.username
;
365 local session
= event
.origin
;
366 local roster
= event
.roster
or session
and session
.roster
;
367 local jid
= event
.jid
;
368 local item
= event
.item
;
369 local from_jid
= session
.full_jid
or (username
.. "@" .. module
.host
);
371 local subscription
= item
and item
.subscription
or "none";
372 local ask
= item
and item
.ask
;
373 local pending
= roster
and roster
[false].pending
[jid
];
375 if subscription
== "both" or subscription
== "from" or pending
then
376 core_post_stanza(session
, st
.presence({type="unsubscribed", from
=from_jid
, to
=jid
}));
379 if subscription
== "both" or subscription
== "to" or ask
then
380 send_presence_of_available_resources(username
, module
.host
, jid
, session
, st
.presence({type="unavailable"}));
381 core_post_stanza(session
, st
.presence({type="unsubscribe", from
=from_jid
, to
=jid
}));