MUC: Fix logic for when to broadcast unavailable presence (actual fix for 14170d161b39)
[prosody.git] / plugins / muc / muc.lib.lua
index 8fec0ffca557ba63a0ede668a67a9db4640ab1c4..e98e99b8a50c8e1d1989e8bf798d5d731e4d7422 100644 (file)
@@ -8,7 +8,7 @@
 --
 
 local select = select;
-local pairs, ipairs = pairs, ipairs;
+local pairs = pairs;
 local next = next;
 local setmetatable = setmetatable;
 
@@ -19,10 +19,11 @@ local jid_bare = require "util.jid".bare;
 local jid_prep = require "util.jid".prep;
 local jid_join = require "util.jid".join;
 local st = require "util.stanza";
-local log = require "util.logger".init("mod_muc");
 local base64 = require "util.encodings".base64;
 local md5 = require "util.hashes".md5;
 
+local log = module._log;
+
 local occupant_lib = module:require "muc/occupant"
 local muc_util = module:require "muc/util";
 local is_kickable_error = muc_util.is_kickable_error;
@@ -35,6 +36,10 @@ function room_mt:__tostring()
        return "MUC room ("..self.jid..")";
 end
 
+function room_mt.save()
+       -- overriden by mod_muc.lua
+end
+
 function room_mt:get_occupant_jid(real_jid)
        return self._jid_nick[real_jid]
 end
@@ -75,7 +80,8 @@ do
                if next_occupant_jid == nil then return nil end
                return next_occupant_jid, occupant_lib.copy(raw_occupant);
        end
-       function room_mt:each_occupant(read_only)
+       -- FIXME Explain what 'read_only' is supposed to be
+       function room_mt:each_occupant(read_only) -- luacheck: ignore 212
                return next_copied_occupant, self._occupants, nil;
        end
 end
@@ -123,7 +129,7 @@ end
 
 function room_mt:route_to_occupant(occupant, stanza)
        local to = stanza.attr.to;
-       for jid, pr in occupant:each_session() do
+       for jid in occupant:each_session() do
                stanza.attr.to = jid;
                self:route_stanza(stanza);
        end
@@ -150,7 +156,7 @@ function room_mt:build_item_list(occupant, x, is_anonymous, nick, actor_nick, ac
        if is_anonymous then
                add_item(x, affiliation, role, nil, nick, actor_nick, actor_jid, reason);
        else
-               for real_jid, session in occupant:each_session() do
+               for real_jid in occupant:each_session() do
                        add_item(x, affiliation, role, real_jid, nick, actor_nick, actor_jid, reason);
                end
        end
@@ -190,7 +196,7 @@ function room_mt:publicise_occupant_status(occupant, base_x, nick, actor, reason
        local base_presence do
                -- Try to use main jid's presence
                local pr = occupant:get_presence();
-               if pr and (pr.attr.type ~= "unavailable" or occupant.role == nil) then
+               if pr and (pr.attr.type ~= "unavailable" and occupant.role ~= nil) then
                        base_presence = st.clone(pr);
                else -- user is leaving but didn't send a leave presence. make one for them
                        base_presence = st.presence {from = occupant.nick; type = "unavailable";};
@@ -249,8 +255,8 @@ function room_mt:publicise_occupant_status(occupant, base_x, nick, actor, reason
        end
 
        -- General populance
-       for nick, n_occupant in self:each_occupant() do
-               if nick ~= occupant.nick then
+       for occupant_nick, n_occupant in self:each_occupant() do
+               if occupant_nick ~= occupant.nick then
                        local pr;
                        if can_see_real_jids(whois, n_occupant) then
                                pr = get_full_p();
@@ -309,16 +315,17 @@ function room_mt:get_disco_info(stanza)
        local form = dataform.new {
                { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" };
        };
-       module:fire_event("muc-disco#info", {room = self; reply = reply; form = form;});
-       reply:add_child(form:form(nil, "result"));
+       local formdata = {};
+       module:fire_event("muc-disco#info", {room = self; reply = reply; form = form, formdata = formdata ;});
+       reply:add_child(form:form(formdata, "result"));
        return reply;
 end
 module:hook("muc-disco#info", function(event)
        event.reply:tag("feature", {var = "http://jabber.org/protocol/muc"}):up();
 end);
 module:hook("muc-disco#info", function(event)
-       local count = iterators.count(event.room:each_occupant());
-       table.insert(event.form, { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) });
+       table.insert(event.form, { name = "muc#roominfo_occupants", label = "Number of occupants" });
+       event.formdata["muc#roominfo_occupants"] = tostring(iterators.count(event.room:each_occupant()));
 end);
 
 function room_mt:get_disco_items(stanza)
@@ -329,7 +336,7 @@ function room_mt:get_disco_items(stanza)
        return reply;
 end
 
-function room_mt:handle_kickable(origin, stanza)
+function room_mt:handle_kickable(origin, stanza) -- luacheck: ignore 212
        local real_jid = stanza.attr.from;
        local occupant = self:get_occupant_by_real_jid(real_jid);
        if occupant == nil then return nil; end
@@ -350,6 +357,16 @@ function room_mt:handle_kickable(origin, stanza)
        return true;
 end
 
+if not module:get_option_boolean("muc_compat_create", true) then
+       module:hook("muc-room-pre-create", function(event)
+               local origin, stanza = event.origin, event.stanza;
+               if not stanza:get_child("x", "http://jabber.org/protocol/muc") then
+                       origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+                       return true;
+               end
+       end, -1);
+end
+
 -- Give the room creator owner affiliation
 module:hook("muc-room-pre-create", function(event)
        event.room:set_affiliation(true, jid_bare(event.stanza.attr.from), "owner");
@@ -388,7 +405,8 @@ function room_mt:handle_presence_to_occupant(origin, stanza)
                        if type == "unavailable" and orig_occupant == nil then return true; end -- Unavailable from someone not in the room
                end
                local is_first_dest_session;
-               if type == "unavailable" then
+               if type == "unavailable" then -- luacheck: ignore 542
+                       -- FIXME Why the empty if branch?
                        -- dest_occupant = nil
                elseif orig_occupant and orig_occupant.nick == stanza.attr.to then -- Just a presence update
                        log("debug", "presence update for %s from session %s", orig_occupant.nick, real_jid);
@@ -487,7 +505,13 @@ function room_mt:handle_presence_to_occupant(origin, stanza)
                        self:publicise_occupant_status(orig_occupant, orig_x, dest_nick);
 
                        if is_last_orig_session then
-                               module:fire_event("muc-occupant-left", {room = self; nick = orig_occupant.nick; occupant = orig_occupant;});
+                               module:fire_event("muc-occupant-left", {
+                                       room = self;
+                                       nick = orig_occupant.nick;
+                                       occupant = orig_occupant;
+                                       origin = origin;
+                                       stanza = stanza;
+                               });
                        end
                end
 
@@ -504,7 +528,7 @@ function room_mt:handle_presence_to_occupant(origin, stanza)
 
                        if orig_occupant == nil then
                                -- Send occupant list to newly joined user
-                               self:send_occupant_list(real_jid, function(nick, occupant)
+                               self:send_occupant_list(real_jid, function(nick, occupant) -- luacheck: ignore 212
                                        -- Don't include self
                                        return occupant:get_presence(real_jid) == nil;
                                end)
@@ -525,9 +549,22 @@ function room_mt:handle_presence_to_occupant(origin, stanza)
 
                        if orig_occupant == nil then
                                if is_first_dest_session then
-                                       module:fire_event("muc-occupant-joined", {room = self; nick = dest_occupant.nick; occupant = dest_occupant;});
+                                       module:fire_event("muc-occupant-joined", {
+                                               room = self;
+                                               nick = dest_occupant.nick;
+                                               occupant = dest_occupant;
+                                               stanza = stanza;
+                                               origin = origin;
+                                       });
                                end
-                               module:fire_event("muc-occupant-session-new", {room = self; nick = dest_occupant.nick; occupant = dest_occupant; stanza = stanza; jid = real_jid;});
+                               module:fire_event("muc-occupant-session-new", {
+                                       room = self;
+                                       nick = dest_occupant.nick;
+                                       occupant = dest_occupant;
+                                       stanza = stanza;
+                                       origin = origin;
+                                       jid = real_jid;
+                               });
                        end
                end
        elseif type ~= 'result' then -- bad type
@@ -546,7 +583,7 @@ function room_mt:handle_iq_to_occupant(origin, stanza)
        if (type == "error" or type == "result") then
                do -- deconstruct_stanza_id
                        if not occupant then return nil; end
-                       local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(%Z+)%z(%Z*)%z(.+)$");
+                       local from_jid, orig_id, to_jid_hash = (base64.decode(id) or ""):match("^(%Z+)%z(%Z*)%z(.+)$");
                        if not(from == from_jid or from == jid_bare(from_jid)) then return nil; end
                        local from_occupant_jid = self:get_occupant_jid(from_jid);
                        if from_occupant_jid == nil then return nil; end
@@ -558,7 +595,7 @@ function room_mt:handle_iq_to_occupant(origin, stanza)
                                end
                        end
                        if session_jid == nil then return nil; end
-                       stanza.attr.from, stanza.attr.to, stanza.attr.id = from_occupant_jid, session_jid, id;
+                       stanza.attr.from, stanza.attr.to, stanza.attr.id = from_occupant_jid, session_jid, orig_id;
                end
                log("debug", "%s sent private iq stanza to %s (%s)", from, to, stanza.attr.to);
                self:route_stanza(stanza);
@@ -672,7 +709,7 @@ function room_mt:process_form(origin, stanza)
                end
                event.field, event.value = nil, nil;
 
-               if self.save then self:save(true); end
+               self:save(true);
                origin.send(st.reply(stanza));
 
                if next(event.status_codes) then
@@ -694,7 +731,7 @@ end
 function room_mt:clear(x)
        x = x or st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'});
        local occupants_updated = {};
-       for nick, occupant in self:each_occupant() do
+       for nick, occupant in self:each_occupant() do -- luacheck: ignore 213
                occupant.role = nil;
                self:save_occupant(occupant);
                occupants_updated[occupant] = true;
@@ -755,6 +792,7 @@ function room_mt:handle_admin_query_set_command(origin, stanza)
        else
                success, errtype, err = nil, "cancel", "bad-request";
        end
+       self:save();
        if not success then
                origin.send(st.error_reply(stanza, errtype, err));
        else
@@ -883,7 +921,7 @@ end
 -- Need visitor role or higher to invite
 module:hook("muc-pre-invite", function(event)
        local room, stanza = event.room, event.stanza;
-       local _from, _to = stanza.attr.from, stanza.attr.to;
+       local _from = stanza.attr.from;
        local inviter = room:get_occupant_by_real_jid(_from);
        local role = inviter and inviter.role or room:get_default_role(room:get_affiliation(_from));
        if valid_roles[role or "none"] <= valid_roles.visitor then
@@ -955,7 +993,7 @@ function room_mt:handle_mediated_decline(origin, stanza)
                        :up()
                :up();
        if not module:fire_event("muc-decline", {room = self, stanza = decline, origin = origin, incoming = stanza}) then
-               local declinee = decline.attr.to; -- re-fetch, in case event modified it
+               declinee = decline.attr.to; -- re-fetch, in case event modified it
                local occupant
                if jid_bare(declinee) == self.jid then -- declinee jid is already an in-room jid
                        occupant = self:get_occupant_by_nick(declinee);
@@ -991,7 +1029,7 @@ function room_mt:handle_message_to_room(origin, stanza)
                local x = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
                if x then
                        local payload = x.tags[1];
-                       if payload == nil then
+                       if payload == nil then --luacheck: ignore 542
                                -- fallthrough
                        elseif payload.name == "invite" and payload.attr.to then
                                return self:handle_mediated_invite(origin, stanza)
@@ -1004,7 +1042,7 @@ function room_mt:handle_message_to_room(origin, stanza)
        end
 end
 
-function room_mt:route_stanza(stanza)
+function room_mt:route_stanza(stanza) -- luacheck: ignore 212
        module:send(stanza);
 end
 
@@ -1078,7 +1116,7 @@ function room_mt:set_affiliation(actor, jid, affiliation, reason)
        local role = self:get_default_role(affiliation);
        local role_rank = valid_roles[role or "none"];
        local occupants_updated = {}; -- Filled with old roles
-       for nick, occupant in self:each_occupant() do
+       for nick, occupant in self:each_occupant() do -- luacheck: ignore 213
                if occupant.bare_jid == jid or (
                        -- Outcast can be by host.
                        is_host_only and affiliation == "outcast" and select(2, jid_split(occupant.bare_jid)) == host
@@ -1114,14 +1152,14 @@ function room_mt:set_affiliation(actor, jid, affiliation, reason)
                        (old_role ~= "moderator" and occupant.role == "moderator") then -- Has gained or lost moderator status
                        -- Send everyone else's presences (as jid visibility has changed)
                        for real_jid in occupant:each_session() do
-                               self:send_occupant_list(real_jid, function(occupant_jid, occupant)
+                               self:send_occupant_list(real_jid, function(occupant_jid, occupant) --luacheck: ignore 212 433
                                        return occupant.bare_jid ~= jid;
                                end);
                        end
                end
        end
 
-       if self.save then self:save(); end
+       self:save(true);
 
        module:fire_event("muc-set-affiliation", {
                room = self;
@@ -1145,7 +1183,7 @@ function room_mt:set_role(actor, occupant_jid, role, reason)
        if not actor then return nil, "modify", "not-acceptable"; end
 
        local occupant = self:get_occupant_by_nick(occupant_jid);
-       if not occupant then return nil, "modify", "not-acceptable"; end
+       if not occupant then return nil, "modify", "item-not-found"; end
 
        if valid_roles[role or "none"] == nil then
                return nil, "modify", "not-acceptable";
@@ -1200,12 +1238,94 @@ function _M.new_room(jid, config)
                jid = jid;
                _jid_nick = {};
                _occupants = {};
-               _data = {
-               };
+               _data = config or {};
                _affiliations = {};
        }, room_mt);
 end
 
+function room_mt:freeze(live)
+       local frozen = {
+               _jid = self.jid;
+               _data = self._data;
+       };
+       for user, affiliation in pairs(self._affiliations) do
+               frozen[user] = affiliation;
+       end
+       if live then
+               for nick, occupant in self:each_occupant() do
+                       frozen[nick] = {
+                               bare_jid = occupant.bare_jid;
+                               role = occupant.role;
+                               jid = occupant.jid;
+                       }
+                       for jid, presence in occupant:each_session() do
+                               frozen[jid] = st.preserialize(presence);
+                       end
+               end
+       end
+       return frozen;
+end
+
+function _M.restore_room(frozen)
+       -- COMPAT
+       if frozen.jid and frozen._affiliations then
+               local room = _M.new_room(frozen.jid, frozen._data);
+               room._affiliations = frozen._affiliations;
+               return room;
+       end
+
+       local room_jid = frozen._jid;
+       local room = _M.new_room(room_jid, frozen._data);
+
+       local occupants = {};
+       local occupant_sessions = {};
+       local room_name, room_host = jid_split(room_jid);
+       for jid, data in pairs(frozen) do
+               local node, host, resource = jid_split(jid);
+               if node or host:sub(1,1) ~= "_" then
+                       if not resource then
+                               -- bare jid: affiliation
+                               room._affiliations[jid] = data;
+                       elseif host == room_host and node == room_name then
+                               -- full room jid: bare real jid and role
+                               local bare_jid = data.bare_jid;
+                               local   occupant = occupant_lib.new(bare_jid, jid);
+                               occupant.jid = data.jid;
+                               occupant.role = data.role;
+                               occupants[bare_jid] = occupant;
+                               local sessions = occupant_sessions[bare_jid];
+                               if sessions then
+                                       for full_jid, presence in pairs(sessions) do
+                                               occupant:set_session(full_jid, presence);
+                                       end
+                               end
+                               occupant_sessions[bare_jid] = nil;
+                       else
+                               -- full user jid: presence
+                               local presence = st.deserialize(data);
+                               local bare_jid = jid_bare(jid);
+                               local occupant = occupants[bare_jid];
+                               local sessions = occupant_sessions[bare_jid];
+                               if occupant then
+                                       occupant:set_session(jid, presence);
+                               elseif sessions then
+                                       sessions[jid] = presence;
+                               else
+                                       occupant_sessions[bare_jid] = {
+                                               [jid] = presence;
+                                       };
+                               end
+                       end
+               end
+       end
+
+       for _, occupant in pairs(occupants) do
+               room:save_occupant(occupant);
+       end
+
+       return room;
+end
+
 _M.room_mt = room_mt;
 
 return _M;