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