diff options
-rw-r--r-- | plugins/muc/mod_muc.lua | 19 | ||||
-rw-r--r-- | plugins/muc/muc.lib.lua | 779 | ||||
-rw-r--r-- | plugins/muc/occupant.lib.lua | 85 |
3 files changed, 503 insertions, 380 deletions
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index 8759cba4..a8a6388d 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -228,27 +228,14 @@ module.restore = function(data) hosts[module:get_host()].muc = { rooms = rooms }; end -function shutdown_room(room, stanza) - for nick, occupant in pairs(room._occupants) do - stanza.attr.from = nick; - for jid in pairs(occupant.sessions) do - stanza.attr.to = jid; - room:_route_stanza(stanza); - room._jid_nick[jid] = nil; - end - room._occupants[nick] = nil; - end -end function shutdown_component() if not saved then - local stanza = st.presence({type = "unavailable"}) - :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) - :tag("item", { affiliation='none', role='none' }):up() + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) :tag("status", { code = "332"}):up(); for roomjid, room in pairs(rooms) do - shutdown_room(room, stanza); + room:clear(x); end - shutdown_room(host_room, stanza); + host_room:clear(x); end end module.unload = shutdown_component; diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index 27c50cd4..76fd7ee2 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -25,23 +25,9 @@ local setmetatable = setmetatable; local base64 = require "util.encodings".base64; local md5 = require "util.hashes".md5; -local default_history_length, max_history_length = 20, math.huge; +local occupant_lib = module:require "muc/occupant" -local get_filtered_presence do - local presence_filters = { - ["http://jabber.org/protocol/muc"] = true; - ["http://jabber.org/protocol/muc#user"] = true; - } - local function presence_filter(tag) - if presence_filters[tag.attr.xmlns] then - return nil; - end - return tag; - end - function get_filtered_presence(stanza) - return st.clone(stanza):maptags(presence_filter); - end -end +local default_history_length, max_history_length = 20, math.huge; local is_kickable_error do local kickable_error_conditions = { @@ -96,28 +82,103 @@ function room_mt:is_locked() return not not self.locked end -function room_mt:route_to_occupant(o_data, stanza) +--- Occupant functions +function room_mt:new_occupant(bare_real_jid, nick) + local occupant = occupant_lib.new(bare_real_jid, nick); + local affiliation = self:get_affiliation(bare_real_jid); + occupant.role = self:get_default_role(affiliation); + return occupant; +end + +function room_mt:get_occupant_by_nick(nick) + local occupant = self._occupants[nick]; + if occupant == nil then return nil end + return occupant_lib.copy(occupant); +end + +do + local function next_copied_occupant(occupants, occupant_jid) + local next_occupant_jid, raw_occupant = next(occupants, occupant_jid); + 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) + return next_copied_occupant, self._occupants, nil; + end +end + +function room_mt:get_occupant_by_real_jid(real_jid) + local occupant_jid = self:get_occupant_jid(real_jid); + if occupant_jid == nil then return nil end + return self:get_occupant_by_nick(occupant_jid); +end + +function room_mt:save_occupant(occupant) + occupant = occupant_lib.copy(occupant); -- So that occupant can be modified more + local id = occupant.nick + + -- Need to maintain _jid_nick secondary index + local old_occupant = self._occupants[id]; + if old_occupant then + for real_jid in pairs(old_occupant.sessions) do + self._jid_nick[real_jid] = nil; + end + end + if occupant.role ~= nil and next(occupant.sessions) then + for real_jid, presence in occupant:each_session() do + self._jid_nick[real_jid] = occupant.nick; + end + else + occupant = nil + end + self._occupants[id] = occupant +end + +function room_mt:route_to_occupant(occupant, stanza) local to = stanza.attr.to; - for jid in pairs(o_data.sessions) do - stanza.attr.to = jid; - self:_route_stanza(stanza); + for jid, pr in occupant:each_session() do + if pr.attr.type ~= "unavailable" then + stanza.attr.to = jid; + self:route_stanza(stanza); + end end stanza.attr.to = to; end -function room_mt:broadcast_presence(stanza, sid, code, nick) - stanza = get_filtered_presence(stanza); - local occupant = self._occupants[stanza.attr.from]; - stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) - :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none", nick=nick}):up(); - if code then - stanza:tag("status", {code=code}):up(); +-- Adds an item to an "x" element. +-- actor is the attribute table +local function add_item(x, affiliation, role, jid, nick, actor, reason) + x:tag("item", {affiliation = affiliation; role = role; jid = jid; nick = nick;}) + if actor then + x:tag("actor", actor):up() + end + if reason then + x:tag("reason"):text(reason):up() + end + x:up(); + return x +end +-- actor is (real) jid +function room_mt:build_item_list(occupant, x, is_anonymous, nick, actor, reason) + local affiliation = self:get_affiliation(occupant.bare_jid); + local role = occupant.role; + local actor_jid = actor and self:get_occupant_jid(actor); + if actor then + actor = {nick = select(3,jid_split(actor_jid))}; + end + if is_anonymous then + add_item(x, affiliation, role, nil, nick, actor, reason); + else + if actor_jid then + actor.jid = actor_jid; + end + for real_jid, session in occupant:each_session() do + add_item(x, affiliation, role, real_jid, nick, actor, reason); + end end - self:broadcast_except_nick(stanza, stanza.attr.from); - stanza:tag("status", {code='110'}):up(); - stanza.attr.to = sid; - self:_route_stanza(stanza); + return x end + function room_mt:broadcast_message(stanza, historic) module:fire_event("muc-broadcast-message", {room = self, stanza = stanza, historic = historic}); self:broadcast(stanza); @@ -139,31 +200,89 @@ module:hook("muc-broadcast-message", function(event) t_insert(history, entry); while #history > room:get_historylength() do t_remove(history, 1) end end -end) - -function room_mt:broadcast_except_nick(stanza, nick) - return self:broadcast(stanza, function(rnick, occupant) return rnick ~= nick end) -end +end); -- Broadcast a stanza to all occupants in the room. --- optionally checks conditional called with nicl +-- optionally checks conditional called with (nick, occupant) function room_mt:broadcast(stanza, cond_func) - for nick, occupant in pairs(self._occupants) do + for nick, occupant in self:each_occupant() do if cond_func == nil or cond_func(nick, occupant) then self:route_to_occupant(occupant, stanza) end end end -function room_mt:send_occupant_list(to) - local current_nick = self:get_occupant_jid(to); - for occupant, o_data in pairs(self._occupants) do - if occupant ~= current_nick then - local pres = get_filtered_presence(o_data.sessions[o_data.jid]); - pres.attr.to, pres.attr.from = to, occupant; - pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) - :tag("item", {affiliation=o_data.affiliation or "none", role=o_data.role or "none"}):up(); - self:_route_stanza(pres); +-- Broadcasts an occupant's presence to the whole room +-- Takes (and modifies) the x element that goes into the stanzas +function room_mt:publicise_occupant_status(occupant, full_x, actor, reason) + local anon_x; + local has_anonymous = self:get_whois() ~= "anyone"; + if has_anonymous then + anon_x = st.clone(full_x); + self:build_item_list(occupant, anon_x, true, nil, actor, reason); + end + self:build_item_list(occupant,full_x, false, nil, actor, reason); + + -- General populance + local full_p + if occupant.role ~= nil then + -- Try to use main jid's presence + local pr = occupant:get_presence(); + if pr ~= nil then + full_p = st.clone(pr); + end + end + if full_p == nil then + full_p = st.presence{from=occupant.nick; type="unavailable"}; + end + local anon_p; + if has_anonymous then + anon_p = st.clone(full_p); + anon_p:add_child(anon_x); + end + full_p:add_child(full_x); + + for nick, n_occupant in self:each_occupant() do + if nick ~= occupant.nick or n_occupant.role == nil then + local pr = full_p; + if has_anonymous and n_occupant.role ~= "moderators" and occupant.bare_jid ~= n_occupant.bare_jid then + pr = anon_p; + end + self:route_to_occupant(n_occupant, pr); + end + end + + -- Presences for occupant itself + full_x:tag("status", {code = "110";}):up(); + if occupant.role == nil then + -- They get an unavailable + self:route_to_occupant(occupant, full_p); + else + -- use their own presences as templates + for full_jid, pr in occupant:each_session() do + if pr.attr.type ~= "unavailable" then + pr = st.clone(pr); + pr.attr.to = full_jid; + -- You can always see your own full jids + pr:add_child(full_x); + self:route_stanza(pr); + end + end + end +end + +function room_mt:send_occupant_list(to, filter) + local to_occupant = self:get_occupant_by_real_jid(to); + local has_anonymous = self:get_whois() ~= "anyone" + for occupant_jid, occupant in self:each_occupant() do + if filter and filter(occupant_jid, occupant) then + local x = st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'}); + local is_anonymous = has_anonymous and occupant.role ~= "moderator" and to_occupant.bare_jid ~= occupant.bare_jid; + self:build_item_list(occupant, x, is_anonymous); + local pres = st.clone(occupant:get_presence()); + pres.attr.to = to; + pres:add_child(x); + self:route_stanza(pres); end end end @@ -248,12 +367,12 @@ function room_mt:send_history(stanza) } module:fire_event("muc-get-history", event) for msg in event.next_stanza , event do - self:_route_stanza(msg); + self:route_stanza(msg); end end function room_mt:get_disco_info(stanza) - local count = 0; for _ in pairs(self._occupants) do count = count + 1; end + local count = 0; for _ in self:each_occupant() do count = count + 1; end return st.reply(stanza):query("http://jabber.org/protocol/disco#info") :tag("identity", {category="conference", type="text", name=self:get_name()}):up() :tag("feature", {var="http://jabber.org/protocol/muc"}):up() @@ -272,7 +391,7 @@ function room_mt:get_disco_info(stanza) end function room_mt:get_disco_items(stanza) local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); - for room_jid in pairs(self._occupants) do + for room_jid in self:each_occupant() do reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up(); end return reply; @@ -291,7 +410,7 @@ function room_mt:send_subject(to) local msg = create_subject_message(subject) msg.attr.from = from msg.attr.to = to - self:_route_stanza(msg); + self:route_stanza(msg); end end function room_mt:set_subject(current_nick, subject) @@ -306,14 +425,20 @@ function room_mt:set_subject(current_nick, subject) end function room_mt:handle_kickable(origin, stanza) + local real_jid = stanza.attr.from; + local occupant = self:get_occupant_by_real_jid(real_jid); + if occupant == nil then return nil; end local type, condition, text = stanza:get_error(); local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error"); if text then error_message = error_message..": "..text; end - local kick_stanza = st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to}) - :tag('status'):text(error_message); - self:handle_unavailable_to_occupant(origin, kick_stanza); -- send unavailable + occupant:set_session(real_jid, st.presence({type="unavailable"}) + :tag('status'):text(error_message)); + self:save_occupant(occupant); + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}) + :tag("status", {code = "307"}) + self:publicise_occupant_status(occupant, x); return true; end @@ -428,91 +553,26 @@ function room_mt:get_whois() return self._data.whois; end -function room_mt:handle_unavailable_to_occupant(origin, stanza) - local from = stanza.attr.from; - local current_nick = self:get_occupant_jid(from); - if not current_nick then - return true; -- discard - end - local pr = get_filtered_presence(stanza); - pr.attr.from = current_nick; - log("debug", "%s leaving %s", current_nick, self.jid); - self._jid_nick[from] = nil; - local occupant = self._occupants[current_nick]; - local new_jid = next(occupant.sessions); - if new_jid == from then new_jid = next(occupant.sessions, new_jid); end - if new_jid then - local jid = occupant.jid; - occupant.jid = new_jid; - occupant.sessions[from] = nil; - pr.attr.to = from; - pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) - :tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up() - :tag("status", {code='110'}):up(); - self:_route_stanza(pr); - if jid ~= new_jid then - pr = st.clone(occupant.sessions[new_jid]) - :tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) - :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none"}); - pr.attr.from = current_nick; - self:broadcast_except_nick(pr, current_nick); - end - else - occupant.role = 'none'; - self:broadcast_presence(pr, from); - self._occupants[current_nick] = nil; - module:fire_event("muc-occupant-left", { room = self; nick = current_nick; }); +module:hook("muc-room-pre-create", function(event) + local room = event.room; + if room:is_locked() and not event.stanza:get_child("x", "http://jabber.org/protocol/muc") then + room:unlock(); -- Older groupchat protocol doesn't lock end - return true; -end +end, 10); -function room_mt:handle_occupant_presence(origin, stanza) - local from = stanza.attr.from; - local pr = get_filtered_presence(stanza); - local current_nick = stanza.attr.to - pr.attr.from = current_nick; - log("debug", "%s broadcasted presence", current_nick); - self._occupants[current_nick].sessions[from] = pr; - self:broadcast_presence(pr, from); - return true; -end - -function room_mt:handle_change_nick(origin, stanza, current_nick, to) - local from = stanza.attr.from; - local occupant = self._occupants[current_nick]; - local is_multisession = next(occupant.sessions, next(occupant.sessions)); - if self._occupants[to] or is_multisession then - log("debug", "%s couldn't change nick", current_nick); - local reply = st.error_reply(stanza, "cancel", "conflict"):up(); - reply.tags[1].attr.code = "409"; - origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); - return true; - else - local to_nick = select(3, jid_split(to)); - log("debug", "%s (%s) changing nick to %s", current_nick, occupant.jid, to); - local p = st.presence({type='unavailable', from=current_nick}); - self:broadcast_presence(p, from, '303', to_nick); - self._occupants[current_nick] = nil; - self._occupants[to] = occupant; - self._jid_nick[from] = to; - local pr = get_filtered_presence(stanza); - pr.attr.from = to; - self._occupants[to].sessions[from] = pr; - self:broadcast_presence(pr, from); - return true; - end -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"); +end, -1); module:hook("muc-occupant-pre-join", function(event) return module:fire_event("muc-occupant-pre-join/affiliation", event) or module:fire_event("muc-occupant-pre-join/password", event) - or module:fire_event("muc-occupant-pre-join/locked", event) - or module:fire_event("muc-occupant-pre-join/nick-conflict", event) + or module:fire_event("muc-occupant-pre-join/locked", event); end, -1) module:hook("muc-occupant-pre-join/password", function(event) local room, stanza = event.room, event.stanza; - local from, to = stanza.attr.from, stanza.attr.to; local password = stanza:get_child("x", "http://jabber.org/protocol/muc"); password = password and password:get_child_text("password", "http://jabber.org/protocol/muc"); if not password or password == "" then password = nil; end @@ -526,81 +586,19 @@ module:hook("muc-occupant-pre-join/password", function(event) end end, -1) -module:hook("muc-occupant-pre-join/nick-conflict", function(event) - local room, stanza = event.room, event.stanza; - local from, to = stanza.attr.from, stanza.attr.to; - local occupant = room._occupants[to] - if occupant -- occupant already exists - and jid_bare(from) ~= jid_bare(occupant.jid) then -- and has different bare real jid - log("debug", "%s couldn't join due to nick conflict: %s", from, to); - local reply = st.error_reply(stanza, "cancel", "conflict"):up(); - reply.tags[1].attr.code = "409"; - event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); - return true; - end -end, -1) - module:hook("muc-occupant-pre-join/locked", function(event) if event.room:is_locked() then -- Deny entry event.origin.send(st.error_reply(event.stanza, "cancel", "item-not-found")); return true; end -end, -1) - -function room_mt:handle_join(origin, stanza) - local from, to = stanza.attr.from, stanza.attr.to; - local affiliation = self:get_affiliation(from); - if affiliation == nil and next(self._affiliations) == nil then -- new room, no owners - affiliation = "owner"; - self._affiliations[jid_bare(from)] = affiliation; - if self:is_locked() and not stanza:get_child("x", "http://jabber.org/protocol/muc") then - self:unlock(); -- Older groupchat protocol doesn't lock - end - end - if module:fire_event("muc-occupant-pre-join", { - room = self; - origin = origin; - stanza = stanza; - affiliation = affiliation; - }) then return true; end - log("debug", "%s joining as %s", from, to); - - local role = self:get_default_role(affiliation) - if role then -- new occupant - local is_merge = not not self._occupants[to] - if not is_merge then - self._occupants[to] = {affiliation=affiliation, role=role, jid=from, sessions={[from]=get_filtered_presence(stanza)}}; - else - self._occupants[to].sessions[from] = get_filtered_presence(stanza); - end - self._jid_nick[from] = to; - self:send_occupant_list(from); - local pr = get_filtered_presence(stanza); - pr.attr.from = to; - pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) - :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up(); - if not is_merge then - self:broadcast_except_nick(pr, to); - end - pr:tag("status", {code='110'}):up(); - if self:get_whois() == 'anyone' then - pr:tag("status", {code='100'}):up(); - end - if self:is_locked() then - pr:tag("status", {code='201'}):up(); - end - pr.attr.to = from; - self:_route_stanza(pr); - self:send_history(from, stanza); - self:send_subject(from); - return true; - end -end +end, -1); -- registration required for entering members-only room module:hook("muc-occupant-pre-join/affiliation", function(event) - if event.affiliation == nil and event.room:get_members_only() then - local reply = st.error_reply(event.stanza, "auth", "registration-required"):up(); + local room, stanza = event.room, event.stanza; + local affiliation = room:get_affiliation(stanza.attr.from); + if affiliation == nil and event.room:get_members_only() then + local reply = st.error_reply(stanza, "auth", "registration-required"):up(); reply.tags[1].attr.code = "407"; event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); return true; @@ -609,43 +607,173 @@ end, -1) -- banned module:hook("muc-occupant-pre-join/affiliation", function(event) - if event.affiliation == "outcast" then - local reply = st.error_reply(event.stanza, "auth", "forbidden"):up(); + local room, stanza = event.room, event.stanza; + local affiliation = room:get_affiliation(stanza.attr.from); + if affiliation == "outcast" then + local reply = st.error_reply(stanza, "auth", "forbidden"):up(); reply.tags[1].attr.code = "403"; event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); return true; end end, -1) -function room_mt:handle_available_to_occupant(origin, stanza) - local from, to = stanza.attr.from, stanza.attr.to; - local current_nick = self:get_occupant_jid(from); - if current_nick then - --if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence - if current_nick == to then -- simple presence - return self:handle_occupant_presence(origin, stanza) - else -- change nick - return self:handle_change_nick(origin, stanza, current_nick, to) - end - --else -- possible rejoin - -- log("debug", "%s had connection replaced", current_nick); - -- self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}) - -- :tag('status'):text('Replaced by new connection'):up()); -- send unavailable - -- self:handle_to_occupant(origin, stanza); -- resend available - --end - else -- enter room - return self:handle_join(origin, stanza) - end -end +module:hook("muc-occupant-joined", function(event) + local room, stanza = event.room, event.stanza; + local real_jid = stanza.attr.from; + room:send_occupant_list(real_jid, function(nick, occupant) + -- Don't include self + return occupant.sessions[real_jid] == nil + end); + room:send_history(stanza); + room:send_subject(real_jid); +end, -1); function room_mt:handle_presence_to_occupant(origin, stanza) local type = stanza.attr.type; if type == "error" then -- error, kick em out! return self:handle_kickable(origin, stanza) - elseif type == "unavailable" then -- unavailable - return self:handle_unavailable_to_occupant(origin, stanza) - elseif not type then -- available - return self:handle_available_to_occupant(origin, stanza) + elseif type == nil or type == "unavailable" then + local real_jid = stanza.attr.from; + local bare_jid = jid_bare(real_jid); + local orig_occupant, dest_occupant; + local is_new_room = next(self._affiliations) == nil; + if is_new_room then + if type == "unavailable" then return true; end -- Unavailable from someone not in the room + if module:fire_event("muc-room-pre-create", { + room = self; + origin = origin; + stanza = stanza; + }) then return true; end + else + orig_occupant = self:get_occupant_by_real_jid(real_jid); + 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 + -- 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); + dest_occupant = orig_occupant; + else + local dest_jid = stanza.attr.to; + dest_occupant = self:get_occupant_by_nick(dest_jid); + if dest_occupant == nil then + log("debug", "no occupant found for %s; creating new occupant object for %s", dest_jid, real_jid); + is_first_dest_session = true; + dest_occupant = self:new_occupant(bare_jid, dest_jid); + else + is_first_dest_session = false; + end + end + local is_last_orig_session; + if orig_occupant ~= nil then + -- Is there are least 2 sessions? + is_last_orig_session = next(orig_occupant.sessions, next(orig_occupant.sessions)) == nil; + end + + local event, event_name = { + room = self; + origin = origin; + stanza = stanza; + is_first_session = is_first_dest_session; + is_last_session = is_last_orig_session; + }; + if orig_occupant == nil then + event_name = "muc-occupant-pre-join"; + event.is_new_room = is_new_room; + elseif dest_occupant == nil then + event_name = "muc-occupant-pre-leave"; + else + event_name = "muc-occupant-pre-change"; + end + if module:fire_event(event_name, event) then return true; end + + -- Check for nick conflicts + if dest_occupant ~= nil and not is_first_dest_session and bare_jid ~= jid_bare(dest_occupant.bare_jid) then -- new nick or has different bare real jid + log("debug", "%s couldn't join due to nick conflict: %s", real_jid, dest_occupant.nick); + local reply = st.error_reply(stanza, "cancel", "conflict"):up(); + reply.tags[1].attr.code = "409"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + return true; + end + + -- Send presence stanza about original occupant + if orig_occupant ~= nil and orig_occupant ~= dest_occupant then + local orig_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); + + if dest_occupant == nil then -- Session is leaving + log("debug", "session %s is leaving occupant %s", real_jid, orig_occupant.nick); + orig_occupant:set_session(real_jid, stanza); + else + log("debug", "session %s is changing from occupant %s to %s", real_jid, orig_occupant.nick, dest_occupant.nick); + orig_occupant:remove_session(real_jid); -- If we are moving to a new nick; we don't want to get our own presence + + local dest_nick = select(3, jid_split(dest_occupant.nick)); + local affiliation = self:get_affiliation(bare_jid); + + -- This session + if not is_first_dest_session then -- User is swapping into another pre-existing session + log("debug", "session %s is swapping into multisession %s, showing it leave.", real_jid, dest_occupant.nick); + -- Show the other session leaving + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}) + :tag("status"):text("you are joining pre-existing session " .. dest_nick):up(); + add_item(x, affiliation, "none"); + local pr = st.presence{from = dest_occupant.nick, to = real_jid, type = "unavailable"} + :add_child(x); + self:route_stanza(pr); + else + if is_last_orig_session then -- User is moving to a new session + log("debug", "no sessions in %s left; marking as nick change", orig_occupant.nick); + -- Everyone gets to see this as a nick change + local jid = self:get_whois() ~= "anyone" and real_jid or nil; -- FIXME: mods should see real jids + add_item(orig_x, affiliation, orig_occupant.role, jid, dest_nick); + orig_x:tag("status", {code = "303";}):up(); + end + end + -- The session itself always sees a nick change + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); + add_item(x, affiliation, orig_occupant.role, real_jid, dest_nick); + -- self:build_item_list(orig_occupant, x, false); -- COMPAT + x:tag("status", {code = "303";}):up(); + x:tag("status", {code = "110";}):up(); + self:route_stanza(st.presence{from = dest_occupant.nick, to = real_jid, type = "unavailable"}:add_child(x)); + end + self:save_occupant(orig_occupant); + self:publicise_occupant_status(orig_occupant, orig_x); + + if is_last_orig_session then + module:fire_event("muc-occupant-left", {room = self; nick = orig_occupant.nick;}); + end + end + + if dest_occupant ~= nil then + dest_occupant:set_session(real_jid, stanza); + local dest_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); + if is_new_room then + dest_x:tag("status", {code = "201"}):up(); + end + if orig_occupant == nil and self:get_whois() == "anyone" then + dest_x:tag("status", {code = "100"}):up(); + end + self:save_occupant(dest_occupant); + self:publicise_occupant_status(dest_occupant, dest_x); + + if orig_occupant ~= nil and orig_occupant ~= dest_occupant and not is_last_orig_session then -- If user is swapping and wasn't last original session + log("debug", "session %s split nicks; showing %s rejoining", real_jid, orig_occupant.nick); + -- Show the original nick joining again + local pr = st.clone(orig_occupant:get_presence()); + pr.attr.to = real_jid; + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); + self:build_item_list(orig_occupant, x, false); + -- TODO: new status code to inform client this was the multi-session it left? + pr:add_child(x); + self:route_stanza(pr); + end + + if orig_occupant == nil and is_first_dest_session then + module:fire_event("muc-occupant-joined", {room = self; nick = dest_occupant.nick; stanza = stanza;}); + end + end elseif type ~= 'result' then -- bad type if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error? @@ -659,14 +787,14 @@ function room_mt:handle_iq_to_occupant(origin, stanza) local type = stanza.attr.type; local id = stanza.attr.id; local current_nick = self:get_occupant_jid(from); - local o_data = self._occupants[to]; + local occupant = self:get_occupant_by_nick(to); if (type == "error" or type == "result") then do -- deconstruct_stanza_id - if not current_nick or not o_data then return nil; end + if not current_nick or not occupant then return nil; end local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$"); if not(from == from_jid or from == jid_bare(from_jid)) then return nil; end local session_jid - for to_jid in pairs(o_data.sessions) do + for to_jid in occupant:each_session() do if md5(to_jid) == to_jid_hash then session_jid = to_jid; break; @@ -676,7 +804,7 @@ function room_mt:handle_iq_to_occupant(origin, stanza) stanza.attr.from, stanza.attr.to, stanza.attr.id = current_nick, session_jid, id end log("debug", "%s sent private iq stanza to %s (%s)", from, to, stanza.attr.to); - self:_route_stanza(stanza); + self:route_stanza(stanza); stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; return true; else -- Type is "get" or "set" @@ -684,19 +812,19 @@ function room_mt:handle_iq_to_occupant(origin, stanza) origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); return true; end - if not o_data then -- recipient not in room + if not occupant then -- recipient not in room origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); return true; end do -- construct_stanza_id - stanza.attr.id = base64.encode(o_data.jid.."\0"..stanza.attr.id.."\0"..md5(from)); + stanza.attr.id = base64.encode(occupant.jid.."\0"..stanza.attr.id.."\0"..md5(from)); end - stanza.attr.from, stanza.attr.to = current_nick, o_data.jid; - log("debug", "%s sent private iq stanza to %s (%s)", from, to, o_data.jid); + stanza.attr.from, stanza.attr.to = current_nick, occupant.jid; + log("debug", "%s sent private iq stanza to %s (%s)", from, to, occupant.jid); if stanza.tags[1].attr.xmlns == 'vcard-temp' then stanza.attr.to = jid_bare(stanza.attr.to); end - self:_route_stanza(stanza); + self:route_stanza(stanza); stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; return true; end @@ -720,7 +848,7 @@ function room_mt:handle_message_to_occupant(origin, stanza) return self:handle_kickable(origin, stanza); -- send unavailable end - local o_data = self._occupants[to]; + local o_data = self:get_occupant_by_nick(to); if not o_data then origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); return true; @@ -870,23 +998,29 @@ function room_mt:process_form(origin, stanza) end end -function room_mt:destroy(newjid, reason, password) - local pr = st.presence({type = "unavailable"}) - :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) - :tag("item", { affiliation='none', role='none' }):up() - :tag("destroy", {jid=newjid}) - if reason then pr:tag("reason"):text(reason):up(); end - if password then pr:tag("password"):text(password):up(); end - for nick, occupant in pairs(self._occupants) do - pr.attr.from = nick; - for jid in pairs(occupant.sessions) do - pr.attr.to = jid; - self:_route_stanza(pr); - self._jid_nick[jid] = nil; - end - self._occupants[nick] = nil; - module:fire_event("muc-occupant-left", { room = self; nick = nick; }); +-- Removes everyone from the room +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 + occupant.role = nil; + self:save_occupant(occupant); + occupants_updated[occupant] = true; end + for occupant in pairs(occupants_updated) do + self:publicise_occupant_status(occupant, x); + module:fire_event("muc-occupant-left", { room = self; nick = occupant.nick; }); + end +end + +function room_mt:destroy(newjid, reason, password) + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + :tag("item", { affiliation='none', role='none' }):up() + :tag("destroy", {jid=newjid}); + if reason then x:tag("reason"):text(reason):up(); end + if password then x:tag("password"):text(password):up(); end + x:up(); + self:clear(x); self:set_persistent(false); module:fire_event("muc-room-destroyed", { room = self }); end @@ -911,7 +1045,7 @@ function room_mt:handle_admin_query_set_command(origin, stanza) end end if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation - local occupant = self._occupants[self.jid.."/"..item.attr.nick]; + local occupant = self:get_occupant_by_nick(self.jid.."/"..item.attr.nick); if occupant then item.attr.jid = occupant.jid; end elseif not item.attr.nick and item.attr.jid then local nick = self:get_occupant_jid(item.attr.jid); @@ -958,18 +1092,7 @@ function room_mt:handle_admin_query_get_command(origin, stanza) local role = self:get_role(self:get_occupant_jid(actor)) or self:get_default_role(affiliation); if role == "moderator" then if _rol == "none" then _rol = nil; end - local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); - for occupant_jid, occupant in pairs(self._occupants) do - if occupant.role == _rol then - reply:tag("item", { - nick = select(3, jid_split(occupant_jid)), - role = _rol or "none", - affiliation = occupant.affiliation or "none", - jid = occupant.jid - }):up(); - end - end - origin.send(reply); + self:send_occupant_list(actor, function(occupant_jid, occupant) return occupant.role == _rol end); return true; else origin.send(st.error_reply(stanza, "auth", "forbidden")); @@ -1015,8 +1138,7 @@ end function room_mt:handle_groupchat_to_room(origin, stanza) local from = stanza.attr.from; - local current_nick = self:get_occupant_jid(from); - local occupant = self._occupants[current_nick]; + local occupant = self:get_occupant_by_real_jid(from); if not occupant then -- not in room origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); return true; @@ -1025,12 +1147,12 @@ function room_mt:handle_groupchat_to_room(origin, stanza) return true; else local from = stanza.attr.from; - stanza.attr.from = current_nick; + stanza.attr.from = occupant.nick; local subject = stanza:get_child_text("subject"); if subject then if occupant.role == "moderator" or ( self:get_changesubject() and occupant.role == "participant" ) then -- and participant - self:set_subject(current_nick, subject); + self:set_subject(occupant.nick, subject); else stanza.attr.from = from; origin.send(st.error_reply(stanza, "auth", "forbidden")); @@ -1096,7 +1218,7 @@ function room_mt:handle_mediated_invite(origin, stanza) end module:hook("muc-invite", function(event) - event.room:_route_stanza(event.stanza); + event.room:route_stanza(event.stanza); return true; end, -1) @@ -1210,64 +1332,46 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) end self._affiliations[jid] = affiliation; local role = self:get_default_role(affiliation); - local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) - :tag("item", {affiliation=affiliation or "none", role=role or "none"}) - :tag("reason"):text(reason or ""):up() - :up(); - local presence_type = nil; + local occupants_updated = {}; + for nick, occupant in self:each_occupant() do + if occupant.bare_jid == jid then + occupant.role = role; + self:save_occupant(occupant); + occupants_updated[occupant] = true; + end + end + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}); if not role then -- getting kicked - presence_type = "unavailable"; if affiliation == "outcast" then x:tag("status", {code="301"}):up(); -- banned else x:tag("status", {code="321"}):up(); -- affiliation change end end - local modified_nicks = {}; - for nick, occupant in pairs(self._occupants) do - if jid_bare(occupant.jid) == jid then - if not role then -- getting kicked - self._occupants[nick] = nil; - else - occupant.affiliation, occupant.role = affiliation, role; - end - for jid,pres in pairs(occupant.sessions) do -- remove for all sessions of the nick - if not role then self._jid_nick[jid] = nil; end - local p = st.clone(pres); - p.attr.from = nick; - p.attr.type = presence_type; - p.attr.to = jid; - p:add_child(x); - self:_route_stanza(p); - if occupant.jid == jid then - modified_nicks[nick] = p; - end - end - end + for occupant in pairs(occupants_updated) do + self:publicise_occupant_status(occupant, x, actor, reason); end if self.save then self:save(); end if callback then callback(); end - for nick,p in pairs(modified_nicks) do - p.attr.from = nick; - self:broadcast_except_nick(p, nick); - end return true; end function room_mt:get_role(nick) - local session = self._occupants[nick]; - return session and session.role or nil; + local occupant = self:get_occupant_by_nick(nick); + return occupant and occupant.role or nil; end function room_mt:can_set_role(actor_jid, occupant_jid, role) - local occupant = self._occupants[occupant_jid]; + local occupant = self:get_occupant_by_nick(occupant_jid); if not occupant or not actor_jid then return nil, "modify", "not-acceptable"; end if actor_jid == true then return true; end - local actor = self._occupants[self:get_occupant_jid(actor_jid)]; + local actor = self:get_occupant_by_real_jid(actor_jid); if actor.role == "moderator" then - if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then - if actor.affiliation == "owner" or actor.affiliation == "admin" then + local occupant_affiliation = self:get_affiliation(occupant.bare_jid) + local actor_affiliation = self:get_affiliation(actor.bare_jid) + if occupant_affiliation ~= "owner" and occupant_affiliation ~= "admin" then + if actor_affiliation == "owner" or actor_affiliation == "admin" then return true; elseif occupant.role ~= "moderator" and role ~= "moderator" then return true; @@ -1281,73 +1385,20 @@ function room_mt:set_role(actor, occupant_jid, role, callback, reason) if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end local allowed, err_type, err_condition = self:can_set_role(actor, occupant_jid, role); if not allowed then return allowed, err_type, err_condition; end - local occupant = self._occupants[occupant_jid]; - local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) - :tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"}) - :tag("reason"):text(reason or ""):up() - :up(); - local presence_type = nil; - if not role then -- kick - presence_type = "unavailable"; - self._occupants[occupant_jid] = nil; - for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick - self._jid_nick[jid] = nil; - end + + local occupant = self:get_occupant_by_nick(occupant_jid); + local occupant_affiliation = self:get_affiliation(occupant.bare_jid); + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}); + if not role then x:tag("status", {code = "307"}):up(); - else - occupant.role = role; - end - local bp; - for jid,pres in pairs(occupant.sessions) do -- send to all sessions of the nick - local p = st.clone(pres); - p.attr.from = occupant_jid; - p.attr.type = presence_type; - p.attr.to = jid; - p:add_child(x); - self:_route_stanza(p); - if occupant.jid == jid then - bp = p; - end end + occupant.role = role; + self:save_occupant(occupant); + self:publicise_occupant_status(occupant, x, actor, reason); if callback then callback(); end - if bp then - self:broadcast_except_nick(bp, occupant_jid); - end return true; end -function room_mt:_route_stanza(stanza) - local muc_child; - if stanza.name == "presence" then - local to_occupant = self._occupants[self:get_occupant_jid(stanza.attr.to)]; - local from_occupant = self._occupants[stanza.attr.from]; - if to_occupant and from_occupant then - if self:get_whois() == 'anyone' then - muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); - else - if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then - muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); - end - end - end - if muc_child then - for item in muc_child:childtags("item") do - if from_occupant == to_occupant then - item.attr.jid = stanza.attr.to; - else - item.attr.jid = from_occupant.jid; - end - end - end - end - self:route_stanza(stanza); - if muc_child then - for item in muc_child:childtags("item") do - item.attr.jid = nil; - end - end -end - local _M = {}; -- module "muc" function _M.new_room(jid, config) diff --git a/plugins/muc/occupant.lib.lua b/plugins/muc/occupant.lib.lua new file mode 100644 index 00000000..b4b12390 --- /dev/null +++ b/plugins/muc/occupant.lib.lua @@ -0,0 +1,85 @@ +local next = next; +local pairs = pairs; +local setmetatable = setmetatable; +local st = require "util.stanza"; + +local get_filtered_presence do + local presence_filters = { + ["http://jabber.org/protocol/muc"] = true; + ["http://jabber.org/protocol/muc#user"] = true; + } + local function presence_filter(tag) + if presence_filters[tag.attr.xmlns] then + return nil; + end + return tag; + end + function get_filtered_presence(stanza) + return st.clone(stanza):maptags(presence_filter); + end +end + +local occupant_mt = {}; +occupant_mt.__index = occupant_mt; + +local function new_occupant(bare_real_jid, nick) + return setmetatable({ + bare_jid = bare_real_jid; + nick = nick; -- in-room jid + sessions = {}; -- hash from real_jid to presence stanzas. stanzas should not be modified + role = nil; + jid = nil; -- Primary session + }, occupant_mt); +end + +-- Deep copy an occupant +local function copy_occupant(occupant) + local sessions = {}; + for full_jid, presence_stanza in pairs(occupant.sessions) do + if presence_stanza.attr.type ~= "unavailable" then + sessions[full_jid] = presence_stanza; + end + end + return setmetatable({ + bare_jid = occupant.bare_jid; + nick = occupant.nick; + sessions = sessions; + role = occupant.role; + jid = occupant.jid; + }, occupant_mt); +end + +function occupant_mt:set_session(real_jid, presence_stanza, replace_primary) + local pr = get_filtered_presence(presence_stanza); + pr.attr.from = self.nick; + pr.attr.to = real_jid; + + self.sessions[real_jid] = pr; + if replace_primary or self.jid == nil then + self.jid = real_jid; + end +end + +function occupant_mt:remove_session(real_jid) + -- Delete original session + local presence_stanza = self.sessions[real_jid]; + self.sessions[real_jid] = nil; + if self.jid == real_jid then + -- find another session to be the primary (might be nil) + self.jid = next(self.sessions); + end +end + +function occupant_mt:each_session() + return pairs(self.sessions) +end + +function occupant_mt:get_presence(real_jid) + return self.sessions[real_jid or self.jid] +end + +return { + new = new_occupant; + copy = copy_occupant; + mt = occupant_mt; +} |