6b706cd5a98c7ccfd021793db341eb721f4d20f1
[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         if origin.roster then\r
65                 for jid, item in pairs(origin.roster) do -- broadcast to all interested contacts\r
66                         if item.subscription == "both" or item.subscription == "from" then\r
67                                 stanza.attr.to = jid;\r
68                                 core_route_stanza(origin, stanza);\r
69                         end\r
70                 end\r
71                 local node, host = origin.username, origin.host;\r
72                 for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast to all resources\r
73                         if res ~= origin and res.presence then -- to resource\r
74                                 stanza.attr.to = res.full_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(origin.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 origin.roster.pending then -- resend incoming subscription requests\r
94                                 for jid in pairs(origin.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(origin.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         else\r
144                 log("warn", "presence recieved from client with no roster");\r
145         end\r
146 end\r
147 \r
148 function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza)\r
149         local h = hosts[host];\r
150         local count = 0;\r
151         if h and h.type == "local" then\r
152                 local u = h.sessions[user];\r
153                 if u then\r
154                         for k, session in pairs(u.sessions) do\r
155                                 local pres = session.presence;\r
156                                 if pres then\r
157                                         pres.attr.to = jid;\r
158                                         core_route_stanza(session, pres);\r
159                                         pres.attr.to = nil;\r
160                                         count = count + 1;\r
161                                 end\r
162                         end\r
163                 end\r
164         end\r
165         log("debug", "broadcasted presence of "..count.." resources from "..user.."@"..host.." to "..jid);\r
166         return count;\r
167 end\r
168 \r
169 function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)\r
170         local node, host = jid_split(from_bare);\r
171         local st_from, st_to = stanza.attr.from, stanza.attr.to;\r
172         stanza.attr.from, stanza.attr.to = from_bare, to_bare;\r
173         log("debug", "outbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);\r
174         if stanza.attr.type == "subscribe" then\r
175                 -- 1. route stanza\r
176                 -- 2. roster push (subscription = none, ask = subscribe)\r
177                 if rostermanager.set_contact_pending_out(node, host, to_bare) then\r
178                         rostermanager.roster_push(node, host, to_bare);\r
179                 end -- else file error\r
180                 core_route_stanza(origin, stanza);\r
181         elseif stanza.attr.type == "unsubscribe" then\r
182                 -- 1. route stanza\r
183                 -- 2. roster push (subscription = none or from)\r
184                 if rostermanager.unsubscribe(node, host, to_bare) then\r
185                         rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed?\r
186                 end -- else file error\r
187                 core_route_stanza(origin, stanza);\r
188         elseif stanza.attr.type == "subscribed" then\r
189                 -- 1. route stanza\r
190                 -- 2. roster_push ()\r
191                 -- 3. send_presence_of_available_resources\r
192                 if rostermanager.subscribed(node, host, to_bare) then\r
193                         rostermanager.roster_push(node, host, to_bare);\r
194                 end\r
195                 core_route_stanza(origin, stanza);\r
196                 send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza);\r
197         elseif stanza.attr.type == "unsubscribed" then\r
198                 -- 1. route stanza\r
199                 -- 2. roster push (subscription = none or to)\r
200                 if rostermanager.unsubscribed(node, host, to_bare) then\r
201                         rostermanager.roster_push(node, host, to_bare);\r
202                 end\r
203                 core_route_stanza(origin, stanza);\r
204         end\r
205         stanza.attr.from, stanza.attr.to = st_from, st_to;\r
206 end\r
207 \r
208 function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)\r
209         local node, host = jid_split(to_bare);\r
210         local st_from, st_to = stanza.attr.from, stanza.attr.to;\r
211         stanza.attr.from, stanza.attr.to = from_bare, to_bare;\r
212         log("debug", "inbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);\r
213         if stanza.attr.type == "probe" then\r
214                 if rostermanager.is_contact_subscribed(node, host, from_bare) then\r
215                         if 0 == send_presence_of_available_resources(node, host, from_bare, origin, core_route_stanza) then\r
216                                 -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too)\r
217                         end\r
218                 else\r
219                         core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="unsubscribed"}));\r
220                 end\r
221         elseif stanza.attr.type == "subscribe" then\r
222                 if rostermanager.is_contact_subscribed(node, host, from_bare) then\r
223                         core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed\r
224                         -- Sending presence is not clearly stated in the RFC, but it seems appropriate\r
225                         if 0 == send_presence_of_available_resources(node, host, from_bare, origin, core_route_stanza) then\r
226                                 -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too)\r
227                         end\r
228                 else\r
229                         if not rostermanager.is_contact_pending_in(node, host, from_bare) then\r
230                                 if rostermanager.set_contact_pending_in(node, host, from_bare) then\r
231                                         sessionmanager.send_to_available_resources(node, host, stanza);\r
232                                 end -- TODO else return error, unable to save\r
233                         end\r
234                 end\r
235         elseif stanza.attr.type == "unsubscribe" then\r
236                 if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then\r
237                         rostermanager.roster_push(node, host, from_bare);\r
238                 end\r
239         elseif stanza.attr.type == "subscribed" then\r
240                 if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then\r
241                         rostermanager.roster_push(node, host, from_bare);\r
242                 end\r
243         elseif stanza.attr.type == "unsubscribed" then\r
244                 if rostermanager.process_inbound_subscription_cancellation(node, host, from_bare) then\r
245                         rostermanager.roster_push(node, host, from_bare);\r
246                 end\r
247         end -- discard any other type\r
248         stanza.attr.from, stanza.attr.to = st_from, st_to;\r
249 end\r
250 \r
251 local outbound_presence_handler = function(data)\r
252         -- outbound presence recieved\r
253         local origin, stanza = data.origin, data.stanza;\r
254 \r
255         local to = stanza.attr.to;\r
256         if to then\r
257                 local t = stanza.attr.type;\r
258                 if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes\r
259                         handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);\r
260                         return true;\r
261                 end\r
262 \r
263                 local to_bare = jid_bare(to);\r
264                 if not(origin.roster[to_bare] and (origin.roster[to_bare].subscription == "both" or origin.roster[to_bare].subscription == "from")) then -- directed presence\r
265                         origin.directed = origin.directed or {};\r
266                         if t then -- removing from directed presence list on sending an error or unavailable\r
267                                 origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to?\r
268                         else\r
269                                 origin.directed[to] = true; -- FIXME does it make more sense to add to_bare rather than to?\r
270                         end\r
271                 end\r
272         end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers?\r
273 end\r
274 \r
275 module:hook("pre-presence/full", outbound_presence_handler);\r
276 module:hook("pre-presence/bare", outbound_presence_handler);\r
277 module:hook("pre-presence/host", outbound_presence_handler);\r
278 \r
279 module:hook("presence/bare", function(data)\r
280         -- inbound presence to bare JID recieved\r
281         local origin, stanza = data.origin, data.stanza;\r
282 \r
283         local to = stanza.attr.to;\r
284         local t = stanza.attr.type;\r
285         if to then\r
286                 if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID\r
287                         handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);\r
288                         return true;\r
289                 end\r
290         \r
291                 local user = bare_sessions[to];\r
292                 if user then\r
293                         for _, session in pairs(user.sessions) do\r
294                                 if session.presence then -- only send to available resources\r
295                                         session.send(stanza);\r
296                                 end\r
297                         end\r
298                 end -- no resources not online, discard\r
299         elseif not t or t == "unavailable" then\r
300                 handle_normal_presence(origin, stanza, core_route_stanza);\r
301         end\r
302         return true;\r
303 end);\r
304 module:hook("presence/full", function(data)\r
305         -- inbound presence to full JID recieved\r
306         local origin, stanza = data.origin, data.stanza;\r
307 \r
308         local t = stanza.attr.type;\r
309         if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID\r
310                 handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);\r
311                 return true;\r
312         end\r
313 \r
314         local session = full_sessions[stanza.attr.to];\r
315         if session then\r
316                 -- TODO fire post processing event\r
317                 session.send(stanza);\r
318         end -- resource not online, discard\r
319         return true;\r
320 end);\r