mod_presence: Re-probe for contacts presence after outgoing 'subscribed' (fixes ...
[prosody.git] / plugins / mod_presence.lua
index f78a5950ad3315e6a21b0a65e3cb67f41bdb2d96..a5b4f282a4da5635cedf7b81f7e86ac5c9bc17de 100644 (file)
@@ -1,6 +1,6 @@
 -- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
 -- 
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
@@ -9,33 +9,23 @@
 local log = module._log;
 
 local require = require;
-local pairs, ipairs = pairs, ipairs;
+local pairs = pairs;
 local t_concat, t_insert = table.concat, table.insert;
 local s_find = string.find;
 local tonumber = tonumber;
 
+local core_post_stanza = prosody.core_post_stanza;
 local st = require "util.stanza";
 local jid_split = require "util.jid".split;
 local jid_bare = require "util.jid".bare;
-local hosts = hosts;
+local datetime = require "util.datetime";
+local hosts = prosody.hosts;
+local bare_sessions = prosody.bare_sessions;
+local full_sessions = prosody.full_sessions;
+local NULL = {};
 
 local rostermanager = require "core.rostermanager";
 local sessionmanager = require "core.sessionmanager";
-local offlinemanager = require "core.offlinemanager";
-
-local _core_route_stanza = core_route_stanza;
-local core_route_stanza;
-function core_route_stanza(origin, stanza)
-       if stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" and stanza.attr.type ~= "error" then
-               local node, host = jid_split(stanza.attr.to);
-               host = hosts[host];
-               if host and host.type == "local" then
-                       handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
-                       return;
-               end
-       end
-       _core_route_stanza(origin, stanza);
-end
 
 local function select_top_resources(user)
        local priority = 0;
@@ -54,39 +44,64 @@ local function select_top_resources(user)
        end
        return recipients;
 end
-local function recalc_resource_map(origin)
-       local user = hosts[origin.host].sessions[origin.username];
-       user.top_resources = select_top_resources(user);
-       if #user.top_resources == 0 then user.top_resources = nil; end
+local function recalc_resource_map(user)
+       if user then
+               user.top_resources = select_top_resources(user);
+               if #user.top_resources == 0 then user.top_resources = nil; end
+       end
 end
 
-function handle_normal_presence(origin, stanza, core_route_stanza)
+local ignore_presence_priority = module:get_option("ignore_presence_priority");
+
+function handle_normal_presence(origin, stanza)
+       if ignore_presence_priority then
+               local priority = stanza:child_with_name("priority");
+               if priority and priority[1] ~= "0" then
+                       for i=#priority.tags,1,-1 do priority.tags[i] = nil; end
+                       for i=#priority,1,-1 do priority[i] = nil; end
+                       priority[1] = "0";
+               end
+       end
+       local priority = stanza:child_with_name("priority");
+       if priority and #priority > 0 then
+               priority = t_concat(priority);
+               if s_find(priority, "^[+-]?[0-9]+$") then
+                       priority = tonumber(priority);
+                       if priority < -128 then priority = -128 end
+                       if priority > 127 then priority = 127 end
+               else priority = 0; end
+       else priority = 0; end
+       if full_sessions[origin.full_jid] then -- if user is still connected
+               origin.send(stanza); -- reflect their presence back to them
+       end
        local roster = origin.roster;
        local node, host = origin.username, origin.host;
-       for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast to all resources
+       local user = bare_sessions[node.."@"..host];
+       for _, res in pairs(user and user.sessions or NULL) do -- broadcast to all resources
                if res ~= origin and res.presence then -- to resource
                        stanza.attr.to = res.full_jid;
-                       core_route_stanza(origin, stanza);
+                       core_post_stanza(origin, stanza, true);
                end
        end
        for jid, item in pairs(roster) do -- broadcast to all interested contacts
                if item.subscription == "both" or item.subscription == "from" then
                        stanza.attr.to = jid;
-                       core_route_stanza(origin, stanza);
+                       core_post_stanza(origin, stanza, true);
                end
        end
        if stanza.attr.type == nil and not origin.presence then -- initial presence
+               origin.presence = stanza; -- FIXME repeated later
                local probe = st.presence({from = origin.full_jid, type = "probe"});
                for jid, item in pairs(roster) do -- probe all contacts we are subscribed to
                        if item.subscription == "both" or item.subscription == "to" then
                                probe.attr.to = jid;
-                               core_route_stanza(origin, probe);
+                               core_post_stanza(origin, probe, true);
                        end
                end
-               for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast from all available resources
+               for _, res in pairs(user and user.sessions or NULL) do -- broadcast from all available resources
                        if res ~= origin and res.presence then
                                res.presence.attr.to = origin.full_jid;
-                               core_route_stanza(res, res.presence);
+                               core_post_stanza(res, res.presence, true);
                                res.presence.attr.to = nil;
                        end
                end
@@ -99,50 +114,40 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
                for jid, item in pairs(roster) do -- resend outgoing subscription requests
                        if item.ask then
                                request.attr.to = jid;
-                               core_route_stanza(origin, request);
+                               core_post_stanza(origin, request, true);
                        end
                end
-               local offline = offlinemanager.load(node, host);
-               if offline then
-                       for _, msg in ipairs(offline) do
-                               origin.send(msg); -- FIXME do we need to modify to/from in any way?
-                       end
-                       offlinemanager.deleteAll(node, host);
+
+               if priority >= 0 then
+                       local event = { origin = origin }
+                       module:fire_event('message/offline/broadcast', event);
                end
        end
        if stanza.attr.type == "unavailable" then
                origin.presence = nil;
                if origin.priority then
                        origin.priority = nil;
-                       recalc_resource_map(origin);
+                       recalc_resource_map(user);
                end
                if origin.directed then
                        for jid in pairs(origin.directed) do
                                stanza.attr.to = jid;
-                               core_route_stanza(origin, stanza);
+                               core_post_stanza(origin, stanza, true);
                        end
                        origin.directed = nil;
                end
        else
                origin.presence = stanza;
-               local priority = stanza:child_with_name("priority");
-               if priority and #priority > 0 then
-                       priority = t_concat(priority);
-                       if s_find(priority, "^[+-]?[0-9]+$") then
-                               priority = tonumber(priority);
-                               if priority < -128 then priority = -128 end
-                               if priority > 127 then priority = 127 end
-                       else priority = 0; end
-               else priority = 0; end
+               stanza:tag("delay", { xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime() }):up();
                if origin.priority ~= priority then
                        origin.priority = priority;
-                       recalc_resource_map(origin);
+                       recalc_resource_map(user);
                end
        end
        stanza.attr.to = nil; -- reset it
 end
 
-function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza)
+function send_presence_of_available_resources(user, host, jid, recipient_session, stanza)
        local h = hosts[host];
        local count = 0;
        if h and h.type == "local" then
@@ -151,38 +156,42 @@ function send_presence_of_available_resources(user, host, jid, recipient_session
                        for k, session in pairs(u.sessions) do
                                local pres = session.presence;
                                if pres then
+                                       if stanza then pres = stanza; pres.attr.from = session.full_jid; end
                                        pres.attr.to = jid;
-                                       core_route_stanza(session, pres);
+                                       core_post_stanza(session, pres, true);
                                        pres.attr.to = nil;
                                        count = count + 1;
                                end
                        end
                end
        end
-       log("debug", "broadcasted presence of "..count.." resources from "..user.."@"..host.." to "..jid);
+       log("debug", "broadcasted presence of %d resources from %s@%s to %s", count, user, host, jid);
        return count;
 end
 
-function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)
+function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
        local node, host = jid_split(from_bare);
-       if to_bare == origin.username.."@"..origin.host then return; end -- No self contacts
+       if to_bare == from_bare then return; end -- No self contacts
        local st_from, st_to = stanza.attr.from, stanza.attr.to;
        stanza.attr.from, stanza.attr.to = from_bare, to_bare;
-       log("debug", "outbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);
-       if stanza.attr.type == "subscribe" then
+       log("debug", "outbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare);
+       if stanza.attr.type == "probe" then
+               stanza.attr.from, stanza.attr.to = st_from, st_to;
+               return;
+       elseif stanza.attr.type == "subscribe" then
                -- 1. route stanza
                -- 2. roster push (subscription = none, ask = subscribe)
                if rostermanager.set_contact_pending_out(node, host, to_bare) then
                        rostermanager.roster_push(node, host, to_bare);
                end -- else file error
-               core_route_stanza(origin, stanza);
+               core_post_stanza(origin, stanza);
        elseif stanza.attr.type == "unsubscribe" then
                -- 1. route stanza
                -- 2. roster push (subscription = none or from)
                if rostermanager.unsubscribe(node, host, to_bare) then
                        rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed?
                end -- else file error
-               core_route_stanza(origin, stanza);
+               core_post_stanza(origin, stanza);
        elseif stanza.attr.type == "subscribed" then
                -- 1. route stanza
                -- 2. roster_push ()
@@ -190,45 +199,54 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_
                if rostermanager.subscribed(node, host, to_bare) then
                        rostermanager.roster_push(node, host, to_bare);
                end
-               core_route_stanza(origin, stanza);
-               send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza);
+               core_post_stanza(origin, stanza);
+               send_presence_of_available_resources(node, host, to_bare, origin);
+               core_post_stanza(origin, st.presence({ type = "probe", from = from_bare, to = to_bare }));
        elseif stanza.attr.type == "unsubscribed" then
-               -- 1. route stanza
-               -- 2. roster push (subscription = none or to)
-               if rostermanager.unsubscribed(node, host, to_bare) then
-                       rostermanager.roster_push(node, host, to_bare);
+               -- 1. send unavailable
+               -- 2. route stanza
+               -- 3. roster push (subscription = from or both)
+               local success, pending_in, subscribed = rostermanager.unsubscribed(node, host, to_bare);
+               if success then
+                       if subscribed then
+                               rostermanager.roster_push(node, host, to_bare);
+                       end
+                       core_post_stanza(origin, stanza);
+                       if subscribed then
+                               send_presence_of_available_resources(node, host, to_bare, origin, st.presence({ type = "unavailable" }));
+                       end
                end
-               core_route_stanza(origin, stanza);
+       else
+               origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
        end
        stanza.attr.from, stanza.attr.to = st_from, st_to;
+       return true;
 end
 
-function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)
+function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
        local node, host = jid_split(to_bare);
        local st_from, st_to = stanza.attr.from, stanza.attr.to;
        stanza.attr.from, stanza.attr.to = from_bare, to_bare;
-       log("debug", "inbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);
-       
-       if not node then
-               log("debug", "dropping presence sent to host or invalid address '%s'", tostring(to_bare));
-       end
+       log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare);
        
        if stanza.attr.type == "probe" then
-               if rostermanager.is_contact_subscribed(node, host, from_bare) then
-                       if 0 == send_presence_of_available_resources(node, host, st_from, origin, core_route_stanza) then
-                               -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too)
+               local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
+               if result then
+                       if 0 == send_presence_of_available_resources(node, host, st_from, origin) then
+                               core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity
                        end
-               else
-                       core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="unsubscribed"}));
+               elseif not err then
+                       core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true);
                end
        elseif stanza.attr.type == "subscribe" then
                if rostermanager.is_contact_subscribed(node, host, from_bare) then
-                       core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed
+                       core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); -- already subscribed
                        -- Sending presence is not clearly stated in the RFC, but it seems appropriate
-                       if 0 == send_presence_of_available_resources(node, host, from_bare, origin, core_route_stanza) then
-                               -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too)
+                       if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then
+                               core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity
                        end
                else
+                       core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt
                        if not rostermanager.is_contact_pending_in(node, host, from_bare) then
                                if rostermanager.set_contact_pending_in(node, host, from_bare) then
                                        sessionmanager.send_to_available_resources(node, host, stanza);
@@ -237,18 +255,24 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b
                end
        elseif stanza.attr.type == "unsubscribe" then
                if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then
+                       sessionmanager.send_to_interested_resources(node, host, stanza);
                        rostermanager.roster_push(node, host, from_bare);
                end
        elseif stanza.attr.type == "subscribed" then
                if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then
+                       sessionmanager.send_to_interested_resources(node, host, stanza);
                        rostermanager.roster_push(node, host, from_bare);
                end
        elseif stanza.attr.type == "unsubscribed" then
                if rostermanager.process_inbound_subscription_cancellation(node, host, from_bare) then
+                       sessionmanager.send_to_interested_resources(node, host, stanza);
                        rostermanager.roster_push(node, host, from_bare);
                end
-       end -- discard any other type
+       else
+               origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
+       end
        stanza.attr.from, stanza.attr.to = st_from, st_to;
+       return true;
 end
 
 local outbound_presence_handler = function(data)
@@ -259,12 +283,12 @@ local outbound_presence_handler = function(data)
        if to then
                local t = stanza.attr.type;
                if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes
-                       handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
-                       return true;
+                       return handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
                end
 
                local to_bare = jid_bare(to);
-               if not(origin.roster[to_bare] and (origin.roster[to_bare].subscription == "both" or origin.roster[to_bare].subscription == "from")) then -- directed presence
+               local roster = origin.roster;
+               if roster and not(roster[to_bare] and (roster[to_bare].subscription == "both" or roster[to_bare].subscription == "from")) then -- directed presence
                        origin.directed = origin.directed or {};
                        if t then -- removing from directed presence list on sending an error or unavailable
                                origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to?
@@ -287,8 +311,7 @@ module:hook("presence/bare", function(data)
        local t = stanza.attr.type;
        if to then
                if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID
-                       handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
-                       return true;
+                       return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
                end
        
                local user = bare_sessions[to];
@@ -300,7 +323,9 @@ module:hook("presence/bare", function(data)
                        end
                end -- no resources not online, discard
        elseif not t or t == "unavailable" then
-               handle_normal_presence(origin, stanza, core_route_stanza);
+               handle_normal_presence(origin, stanza);
+       else
+               origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
        end
        return true;
 end);
@@ -310,8 +335,7 @@ module:hook("presence/full", function(data)
 
        local t = stanza.attr.type;
        if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID
-               handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
-               return true;
+               return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
        end
 
        local session = full_sessions[stanza.attr.to];
@@ -321,22 +345,38 @@ module:hook("presence/full", function(data)
        end -- resource not online, discard
        return true;
 end);
+module:hook("presence/host", function(data)
+       -- inbound presence to the host
+       local stanza = data.stanza;
+       
+       local from_bare = jid_bare(stanza.attr.from);
+       local t = stanza.attr.type;
+       if t == "probe" then
+               core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+       elseif t == "subscribe" then
+               core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" }));
+               core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+       end
+       return true;
+end);
 
 module:hook("resource-unbind", function(event)
        local session, err = event.session, event.error;
        -- Send unavailable presence
        if session.presence then
                local pres = st.presence{ type = "unavailable" };
-               if not(err) or err == "closed" then err = "connection closed"; end
-               pres:tag("status"):text("Disconnected: "..err):up();
+               if err then
+                       pres:tag("status"):text("Disconnected: "..err):up();
+               end
                session:dispatch_stanza(pres);
        elseif session.directed then
                local pres = st.presence{ type = "unavailable", from = session.full_jid };
-               if not(err) or err == "closed" then err = "connection closed"; end
-               pres:tag("status"):text("Disconnected: "..err):up();
+               if err then
+                       pres:tag("status"):text("Disconnected: "..err):up();
+               end
                for jid in pairs(session.directed) do
                        pres.attr.to = jid;
-                       core_route_stanza(session, pres);
+                       core_post_stanza(session, pres, true);
                end
                session.directed = nil;
        end