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);
66 if stanza
.attr
.type == nil and not origin
.presence
then -- initial presence
67 module
:fire_event("presence/initial", { origin
= origin
, stanza
= stanza
} );
68 origin
.presence
= stanza
; -- FIXME repeated later
69 local probe
= st
.presence({from
= origin
.full_jid
, type = "probe"});
70 for jid
, item
in pairs(roster
) do -- probe all contacts we are subscribed to
71 if item
.subscription
== "both" or item
.subscription
== "to" then
73 core_post_stanza(origin
, probe
, true);
76 for _
, res
in pairs(user
and user
.sessions
or NULL
) do -- broadcast from all available resources
77 if res
~= origin
and res
.presence
then
78 res
.presence
.attr
.to
= origin
.full_jid
;
79 core_post_stanza(res
, res
.presence
, true);
80 res
.presence
.attr
.to
= nil;
83 for jid
in pairs(roster
[false].pending
) do -- resend incoming subscription requests
84 origin
.send(st
.presence({type="subscribe", from
=jid
})); -- TODO add to attribute? Use original?
86 local request
= st
.presence({type="subscribe", from
=origin
.username
.."@"..origin
.host
});
87 for jid
, item
in pairs(roster
) do -- resend outgoing subscription requests
89 request
.attr
.to
= jid
;
90 core_post_stanza(origin
, request
, true);
95 local event
= { origin
= origin
}
96 module
:fire_event('message/offline/broadcast', event
);
99 if stanza
.attr
.type == "unavailable" then
100 origin
.presence
= nil;
101 if origin
.priority
then
102 origin
.priority
= nil;
103 recalc_resource_map(user
);
105 if origin
.directed
then
106 for jid
in pairs(origin
.directed
) do
107 stanza
.attr
.to
= jid
;
108 core_post_stanza(origin
, stanza
, true);
110 origin
.directed
= nil;
113 origin
.presence
= stanza
;
114 stanza
:tag("delay", { xmlns
= "urn:xmpp:delay", from
= host
, stamp
= datetime
.datetime() }):up();
115 if origin
.priority
~= priority
then
116 origin
.priority
= priority
;
117 recalc_resource_map(user
);
120 stanza
.attr
.to
= nil; -- reset it
123 -- luacheck: ignore 212/recipient_session
124 -- TODO This argument is used in 3rd party modules
125 function send_presence_of_available_resources(user
, host
, jid
, recipient_session
, stanza
)
126 local h
= hosts
[host
];
128 if h
and h
.type == "local" then
129 local u
= h
.sessions
[user
];
131 for _
, session
in pairs(u
.sessions
) do
132 local pres
= session
.presence
;
134 if stanza
then pres
= stanza
; pres
.attr
.from
= session
.full_jid
; end
136 core_post_stanza(session
, pres
, true);
143 log("debug", "broadcasted presence of %d resources from %s@%s to %s", count
, user
, host
, jid
);
147 function handle_outbound_presence_subscriptions_and_probes(origin
, stanza
, from_bare
, to_bare
)
148 local node
, host
= jid_split(from_bare
);
149 if to_bare
== from_bare
then return; end -- No self contacts
150 local st_from
, st_to
= stanza
.attr
.from
, stanza
.attr
.to
;
151 stanza
.attr
.from
, stanza
.attr
.to
= from_bare
, to_bare
;
152 log("debug", "outbound presence %s from %s for %s", stanza
.attr
.type, from_bare
, to_bare
);
153 if stanza
.attr
.type == "probe" then
154 stanza
.attr
.from
, stanza
.attr
.to
= st_from
, st_to
;
156 elseif stanza
.attr
.type == "subscribe" then
158 -- 2. roster push (subscription = none, ask = subscribe)
159 if rostermanager
.set_contact_pending_out(node
, host
, to_bare
) then
160 rostermanager
.roster_push(node
, host
, to_bare
);
161 end -- else file error
162 core_post_stanza(origin
, stanza
);
163 elseif stanza
.attr
.type == "unsubscribe" then
165 -- 2. roster push (subscription = none or from)
166 if rostermanager
.unsubscribe(node
, host
, to_bare
) then
167 rostermanager
.roster_push(node
, host
, to_bare
); -- FIXME do roster push when roster has in fact not changed?
168 end -- else file error
169 core_post_stanza(origin
, stanza
);
170 elseif stanza
.attr
.type == "subscribed" then
173 -- 3. send_presence_of_available_resources
174 if rostermanager
.subscribed(node
, host
, to_bare
) then
175 rostermanager
.roster_push(node
, host
, to_bare
);
177 core_post_stanza(origin
, stanza
);
178 send_presence_of_available_resources(node
, host
, to_bare
, origin
);
179 if rostermanager
.is_user_subscribed(node
, host
, to_bare
) then
180 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
185 -- 3. roster push (subscription = from or both)
186 local success
, pending_in
, subscribed
= rostermanager
.unsubscribed(node
, host
, to_bare
);
189 rostermanager
.roster_push(node
, host
, to_bare
);
191 core_post_stanza(origin
, stanza
);
193 send_presence_of_available_resources(node
, host
, to_bare
, origin
, st
.presence({ type = "unavailable" }));
197 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", "Invalid presence type"));
199 stanza
.attr
.from
, stanza
.attr
.to
= st_from
, st_to
;
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
);
209 if stanza
.attr
.type == "probe" then
210 local result
, err
= rostermanager
.is_contact_subscribed(node
, host
, from_bare
);
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
216 core_post_stanza(hosts
[host
], st
.presence({from
=to_bare
, to
=from_bare
, type="unsubscribed"}), true);
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
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
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
);
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
);
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
);
249 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", "Invalid presence type"));
251 stanza
.attr
.from
, stanza
.attr
.to
= st_from
, st_to
;
255 local outbound_presence_handler
= function(data
)
256 -- outbound presence received
257 local origin
, stanza
= data
.origin
, data
.stanza
;
259 local to
= stanza
.attr
.to
;
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
));
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?
273 origin
.directed
[to
] = true; -- FIXME does it make more sense to add to_bare rather than to?
276 end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers?
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
);
283 module
:hook("presence/bare", function(data
)
284 -- inbound presence to bare JID received
285 local origin
, stanza
= data
.origin
, data
.stanza
;
287 local to
= stanza
.attr
.to
;
288 local t
= stanza
.attr
.type;
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
));
294 local user
= bare_sessions
[to
];
296 for _
, session
in pairs(user
.sessions
) do
297 if session
.presence
then -- only send to available resources
298 session
.send(stanza
);
301 end -- no resources not online, discard
302 elseif not t
or t
== "unavailable" then
303 handle_normal_presence(origin
, stanza
);
305 origin
.send(st
.error_reply(stanza
, "modify", "bad-request", "Invalid presence type"));
309 module
:hook("presence/full", function(data
)
310 -- inbound presence to full JID received
311 local origin
, stanza
= data
.origin
, data
.stanza
;
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
));
318 local session
= full_sessions
[stanza
.attr
.to
];
320 -- TODO fire post processing event
321 session
.send(stanza
);
322 end -- resource not online, discard
325 module
:hook("presence/host", function(data
)
326 -- inbound presence to the host
327 local stanza
= data
.stanza
;
329 local from_bare
= jid_bare(stanza
.attr
.from
);
330 local t
= stanza
.attr
.type;
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
}));
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" };
346 pres
:tag("status"):text("Disconnected: "..err
):up();
348 session
:dispatch_stanza(pres
);
349 elseif session
.directed
then
350 local pres
= st
.presence
{ type = "unavailable", from
= session
.full_jid
};
352 pres
:tag("status"):text("Disconnected: "..err
):up();
354 for jid
in pairs(session
.directed
) do
356 core_post_stanza(session
, pres
, true);
358 session
.directed
= nil;
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
);
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
];
374 if subscription
== "both" or subscription
== "from" or pending
then
375 core_post_stanza(session
, st
.presence({type="unsubscribed", from
=from_jid
, to
=jid
}));
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
}));