Merged with trunk
[prosody.git] / plugins / mod_presence.lua
1 -- Prosody IM v0.4\r
2 -- Copyright (C) 2008-2009 Matthew Wild\r
3 -- Copyright (C) 2008-2009 Waqas Hussain\r
4 -- \r
5 -- This project is MIT/X11 licensed. Please see the\r
6 -- COPYING file in the source package for more information.\r
7 --\r
8 \r
9 local log = module._log;\r
10 \r
11 local require = require;\r
12 local pairs, ipairs = pairs, ipairs;\r
13 local t_concat, t_insert = table.concat, table.insert;\r
14 local s_find = string.find;\r
15 local tonumber = tonumber;\r
16 \r
17 local st = require "util.stanza";\r
18 local jid_split = require "util.jid".split;\r
19 local jid_bare = require "util.jid".bare;\r
20 local hosts = hosts;\r
21 \r
22 local rostermanager = require "core.rostermanager";\r
23 local sessionmanager = require "core.sessionmanager";\r
24 local offlinemanager = require "core.offlinemanager";\r
25 \r
26 local _core_route_stanza = core_route_stanza;\r
27 local core_route_stanza;\r
28 function core_route_stanza(origin, stanza)\r
29         if stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" and stanza.attr.type ~= "error" then\r
30                 local node, host = jid_split(stanza.attr.to);\r
31                 host = hosts[host];\r
32                 if host and host.type == "local" then\r
33                         handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);\r
34                         return;\r
35                 end\r
36         end\r
37         _core_route_stanza(origin, stanza);\r
38 end\r
39 \r
40 local function select_top_resources(user)\r
41         local priority = 0;\r
42         local recipients = {};\r
43         for _, session in pairs(user.sessions) do -- find resource with greatest priority\r
44                 if session.presence then\r
45                         -- TODO check active privacy list for session\r
46                         local p = session.priority;\r
47                         if p > priority then\r
48                                 priority = p;\r
49                                 recipients = {session};\r
50                         elseif p == priority then\r
51                                 t_insert(recipients, session);\r
52                         end\r
53                 end\r
54         end\r
55         return recipients;\r
56 end\r
57 local function recalc_resource_map(origin)\r
58         local user = hosts[origin.host].sessions[origin.username];\r
59         user.top_resources = select_top_resources(user);\r
60         if #user.top_resources == 0 then user.top_resources = nil; end\r
61 end\r
62 \r
63 function handle_normal_presence(origin, stanza, core_route_stanza)\r
64         local roster = origin.roster;\r
65         local node, host = origin.username, origin.host;\r
66         for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast to all resources\r
67                 if res ~= origin and res.presence then -- to resource\r
68                         stanza.attr.to = res.full_jid;\r
69                         core_route_stanza(origin, stanza);\r
70                 end\r
71         end\r
72         for jid, item in pairs(roster) do -- broadcast to all interested contacts\r
73                 if item.subscription == "both" or item.subscription == "from" then\r
74                         stanza.attr.to = jid;\r
75                         core_route_stanza(origin, stanza);\r
76                 end\r
77         end\r
78         if stanza.attr.type == nil and not origin.presence then -- initial presence\r
79                 local probe = st.presence({from = origin.full_jid, type = "probe"});\r
80                 for jid, item in pairs(roster) do -- probe all contacts we are subscribed to\r
81                         if item.subscription == "both" or item.subscription == "to" then\r
82                                 probe.attr.to = jid;\r
83                                 core_route_stanza(origin, probe);\r
84                         end\r
85                 end\r
86                 for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast from all available resources\r
87                         if res ~= origin and res.presence then\r
88                                 res.presence.attr.to = origin.full_jid;\r
89                                 core_route_stanza(res, res.presence);\r
90                                 res.presence.attr.to = nil;\r
91                         end\r
92                 end\r
93                 if roster.pending then -- resend incoming subscription requests\r
94                         for jid in pairs(roster.pending) do\r
95                                 origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original?\r
96                         end\r
97                 end\r
98                 local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host});\r
99                 for jid, item in pairs(roster) do -- resend outgoing subscription requests\r
100                         if item.ask then\r
101                                 request.attr.to = jid;\r
102                                 core_route_stanza(origin, request);\r
103                         end\r
104                 end\r
105                 local offline = offlinemanager.load(node, host);\r
106                 if offline then\r
107                         for _, msg in ipairs(offline) do\r
108                                 origin.send(msg); -- FIXME do we need to modify to/from in any way?\r
109                         end\r
110                         offlinemanager.deleteAll(node, host);\r
111                 end\r
112         end\r
113         if stanza.attr.type == "unavailable" then\r
114                 origin.presence = nil;\r
115                 if origin.priority then\r
116                         origin.priority = nil;\r
117                         recalc_resource_map(origin);\r
118                 end\r
119                 if origin.directed then\r
120                         for jid in pairs(origin.directed) do\r
121                                 stanza.attr.to = jid;\r
122                                 core_route_stanza(origin, stanza);\r
123                         end\r
124                         origin.directed = nil;\r
125                 end\r
126         else\r
127                 origin.presence = stanza;\r
128                 local priority = stanza:child_with_name("priority");\r
129                 if priority and #priority > 0 then\r
130                         priority = t_concat(priority);\r
131                         if s_find(priority, "^[+-]?[0-9]+$") then\r
132                                 priority = tonumber(priority);\r
133                                 if priority < -128 then priority = -128 end\r
134                                 if priority > 127 then priority = 127 end\r
135                         else priority = 0; end\r
136                 else priority = 0; end\r
137                 if origin.priority ~= priority then\r
138                         origin.priority = priority;\r
139                         recalc_resource_map(origin);\r
140                 end\r
141         end\r
142         stanza.attr.to = nil; -- reset it\r
143 end\r
144 \r
145 function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza)\r
146         local h = hosts[host];\r
147         local count = 0;\r
148         if h and h.type == "local" then\r
149                 local u = h.sessions[user];\r
150                 if u then\r
151                         for k, session in pairs(u.sessions) do\r
152                                 local pres = session.presence;\r
153                                 if pres then\r
154                                         pres.attr.to = jid;\r
155                                         core_route_stanza(session, pres);\r
156                                         pres.attr.to = nil;\r
157                                         count = count + 1;\r
158                                 end\r
159                         end\r
160                 end\r
161         end\r
162         log("debug", "broadcasted presence of "..count.." resources from "..user.."@"..host.." to "..jid);\r
163         return count;\r
164 end\r
165 \r
166 function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)\r
167         local node, host = jid_split(from_bare);\r
168         local st_from, st_to = stanza.attr.from, stanza.attr.to;\r
169         stanza.attr.from, stanza.attr.to = from_bare, to_bare;\r
170         log("debug", "outbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);\r
171         if stanza.attr.type == "subscribe" then\r
172                 -- 1. route stanza\r
173                 -- 2. roster push (subscription = none, ask = subscribe)\r
174                 if rostermanager.set_contact_pending_out(node, host, to_bare) then\r
175                         rostermanager.roster_push(node, host, to_bare);\r
176                 end -- else file error\r
177                 core_route_stanza(origin, stanza);\r
178         elseif stanza.attr.type == "unsubscribe" then\r
179                 -- 1. route stanza\r
180                 -- 2. roster push (subscription = none or from)\r
181                 if rostermanager.unsubscribe(node, host, to_bare) then\r
182                         rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed?\r
183                 end -- else file error\r
184                 core_route_stanza(origin, stanza);\r
185         elseif stanza.attr.type == "subscribed" then\r
186                 -- 1. route stanza\r
187                 -- 2. roster_push ()\r
188                 -- 3. send_presence_of_available_resources\r
189                 if rostermanager.subscribed(node, host, to_bare) then\r
190                         rostermanager.roster_push(node, host, to_bare);\r
191                 end\r
192                 core_route_stanza(origin, stanza);\r
193                 send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza);\r
194         elseif stanza.attr.type == "unsubscribed" then\r
195                 -- 1. route stanza\r
196                 -- 2. roster push (subscription = none or to)\r
197                 if rostermanager.unsubscribed(node, host, to_bare) then\r
198                         rostermanager.roster_push(node, host, to_bare);\r
199                 end\r
200                 core_route_stanza(origin, stanza);\r
201         end\r
202         stanza.attr.from, stanza.attr.to = st_from, st_to;\r
203 end\r
204 \r
205 function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)\r
206         local node, host = jid_split(to_bare);\r
207         local st_from, st_to = stanza.attr.from, stanza.attr.to;\r
208         stanza.attr.from, stanza.attr.to = from_bare, to_bare;\r
209         log("debug", "inbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);\r
210         if stanza.attr.type == "probe" then\r
211                 if rostermanager.is_contact_subscribed(node, host, from_bare) then\r
212                         if 0 == send_presence_of_available_resources(node, host, st_from, origin, core_route_stanza) then\r
213                                 -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too)\r
214                         end\r
215                 else\r
216                         core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="unsubscribed"}));\r
217                 end\r
218         elseif stanza.attr.type == "subscribe" then\r
219                 if rostermanager.is_contact_subscribed(node, host, from_bare) then\r
220                         core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed\r
221                         -- Sending presence is not clearly stated in the RFC, but it seems appropriate\r
222                         if 0 == send_presence_of_available_resources(node, host, from_bare, origin, core_route_stanza) then\r
223                                 -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too)\r
224                         end\r
225                 else\r
226                         if not rostermanager.is_contact_pending_in(node, host, from_bare) then\r
227                                 if rostermanager.set_contact_pending_in(node, host, from_bare) then\r
228                                         sessionmanager.send_to_available_resources(node, host, stanza);\r
229                                 end -- TODO else return error, unable to save\r
230                         end\r
231                 end\r
232         elseif stanza.attr.type == "unsubscribe" then\r
233                 if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then\r
234                         rostermanager.roster_push(node, host, from_bare);\r
235                 end\r
236         elseif stanza.attr.type == "subscribed" then\r
237                 if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then\r
238                         rostermanager.roster_push(node, host, from_bare);\r
239                 end\r
240         elseif stanza.attr.type == "unsubscribed" then\r
241                 if rostermanager.process_inbound_subscription_cancellation(node, host, from_bare) then\r
242                         rostermanager.roster_push(node, host, from_bare);\r
243                 end\r
244         end -- discard any other type\r
245         stanza.attr.from, stanza.attr.to = st_from, st_to;\r
246 end\r
247 \r
248 local outbound_presence_handler = function(data)\r
249         -- outbound presence recieved\r
250         local origin, stanza = data.origin, data.stanza;\r
251 \r
252         local to = stanza.attr.to;\r
253         if to then\r
254                 local t = stanza.attr.type;\r
255                 if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes\r
256                         handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);\r
257                         return true;\r
258                 end\r
259 \r
260                 local to_bare = jid_bare(to);\r
261                 if not(origin.roster[to_bare] and (origin.roster[to_bare].subscription == "both" or origin.roster[to_bare].subscription == "from")) then -- directed presence\r
262                         origin.directed = origin.directed or {};\r
263                         if t then -- removing from directed presence list on sending an error or unavailable\r
264                                 origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to?\r
265                         else\r
266                                 origin.directed[to] = true; -- FIXME does it make more sense to add to_bare rather than to?\r
267                         end\r
268                 end\r
269         end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers?\r
270 end\r
271 \r
272 module:hook("pre-presence/full", outbound_presence_handler);\r
273 module:hook("pre-presence/bare", outbound_presence_handler);\r
274 module:hook("pre-presence/host", outbound_presence_handler);\r
275 \r
276 module:hook("presence/bare", function(data)\r
277         -- inbound presence to bare JID recieved\r
278         local origin, stanza = data.origin, data.stanza;\r
279 \r
280         local to = stanza.attr.to;\r
281         local t = stanza.attr.type;\r
282         if to then\r
283                 if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID\r
284                         handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);\r
285                         return true;\r
286                 end\r
287         \r
288                 local user = bare_sessions[to];\r
289                 if user then\r
290                         for _, session in pairs(user.sessions) do\r
291                                 if session.presence then -- only send to available resources\r
292                                         session.send(stanza);\r
293                                 end\r
294                         end\r
295                 end -- no resources not online, discard\r
296         elseif not t or t == "unavailable" then\r
297                 handle_normal_presence(origin, stanza, core_route_stanza);\r
298         end\r
299         return true;\r
300 end);\r
301 module:hook("presence/full", function(data)\r
302         -- inbound presence to full JID recieved\r
303         local origin, stanza = data.origin, data.stanza;\r
304 \r
305         local t = stanza.attr.type;\r
306         if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID\r
307                 handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);\r
308                 return true;\r
309         end\r
310 \r
311         local session = full_sessions[stanza.attr.to];\r
312         if session then\r
313                 -- TODO fire post processing event\r
314                 session.send(stanza);\r
315         end -- resource not online, discard\r
316         return true;\r
317 end);\r
318 \r
319 module:hook("resource-unbind", function(event)\r
320         local session, err = event.session, event.error;\r
321         -- Send unavailable presence\r
322         if session.presence then\r
323                 local pres = st.presence{ type = "unavailable" };\r
324                 if not(err) or err == "closed" then err = "connection closed"; end\r
325                 pres:tag("status"):text("Disconnected: "..err):up();\r
326                 session:dispatch_stanza(pres);\r
327         elseif session.directed then\r
328                 local pres = st.presence{ type = "unavailable" };\r
329                 if not(err) or err == "closed" then err = "connection closed"; end\r
330                 pres:tag("status"):text("Disconnected: "..err):up();\r
331                 for jid in pairs(session.directed) do\r
332                         pres.attr.to = jid;\r
333                         core_route_stanza(session, pres);\r
334                 end\r
335                 session.directed = nil;\r
336         end\r
337 end);\r