diff options
Diffstat (limited to 'plugins/muc/muc.lib.lua')
-rw-r--r-- | plugins/muc/muc.lib.lua | 311 |
1 files changed, 158 insertions, 153 deletions
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index e8d565f2..8930feeb 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -1,7 +1,7 @@ -- Prosody IM -- 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. -- @@ -27,28 +27,16 @@ local muc_domain = nil; --module:get_host(); local default_history_length, max_history_length = 20, math.huge; ------------ -local function filter_xmlns_from_array(array, filters) - local count = 0; - for i=#array,1,-1 do - local attr = array[i].attr; - if filters[attr and attr.xmlns] then - t_remove(array, i); - count = count + 1; - end - end - return count; -end -local function filter_xmlns_from_stanza(stanza, filters) - if filters then - if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then - return stanza, filter_xmlns_from_array(stanza, filters); - end +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 stanza, 0; + return tag; end -local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; + local function get_filtered_presence(stanza) - return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters); + return st.clone(stanza):maptags(presence_filter); end local kickable_error_conditions = { ["gone"] = true; @@ -72,17 +60,6 @@ local function is_kickable_error(stanza) local cond = get_error_condition(stanza); return kickable_error_conditions[cond] and cond; end -local function getUsingPath(stanza, path, getText) - local tag = stanza; - for _, name in ipairs(path) do - if type(tag) ~= 'table' then return; end - tag = tag:child_with_name(name); - end - if tag and getText then tag = table.concat(tag); end - return tag; -end -local function getTag(stanza, path) return getUsingPath(stanza, path); end -local function getText(stanza, path) return getUsingPath(stanza, path, true); end ----------- local room_mt = {}; @@ -98,8 +75,8 @@ function room_mt:get_default_role(affiliation) elseif affiliation == "member" then return "participant"; elseif not affiliation then - if not self:is_members_only() then - return self:is_moderated() and "visitor" or "participant"; + if not self:get_members_only() then + return self:get_moderated() and "visitor" or "participant"; end end end @@ -146,18 +123,21 @@ function room_mt:broadcast_message(stanza, historic) end stanza.attr.to = to; if historic then -- add to history - local history = self._data['history']; - if not history then history = {}; self._data['history'] = history; end - stanza = st.clone(stanza); - stanza.attr.to = ""; - local stamp = datetime.datetime(); - stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = self.jid, stamp = stamp}):up(); -- XEP-0203 - stanza:tag("x", {xmlns = "jabber:x:delay", from = self.jid, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) - local entry = { stanza = stanza, stamp = stamp }; - t_insert(history, entry); - while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end + return self:save_to_history(stanza) end end +function room_mt:save_to_history(stanza) + local history = self._data['history']; + if not history then history = {}; self._data['history'] = history; end + stanza = st.clone(stanza); + stanza.attr.to = ""; + local stamp = datetime.datetime(); + stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = self.jid, stamp = stamp}):up(); -- XEP-0203 + stanza:tag("x", {xmlns = "jabber:x:delay", from = self.jid, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) + local entry = { stanza = stanza, stamp = stamp }; + t_insert(history, entry); + while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end +end function room_mt:broadcast_except_nick(stanza, nick) for rnick, occupant in pairs(self._occupants) do if rnick ~= nick then @@ -186,10 +166,10 @@ function room_mt:send_history(to, stanza) if history then local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc"); local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc"); - + local maxchars = history_tag and tonumber(history_tag.attr.maxchars); if maxchars then maxchars = math.floor(maxchars); end - + local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history); if not history_tag then maxstanzas = 20; end @@ -202,7 +182,7 @@ function room_mt:send_history(to, stanza) local n = 0; local charcount = 0; - + for i=#history,1,-1 do local entry = history[i]; if maxchars then @@ -223,26 +203,35 @@ function room_mt:send_history(to, stanza) self:_route_stanza(msg); end end +end +function room_mt:send_subject(to) self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject'])); end function room_mt:get_disco_info(stanza) local count = 0; for _ in pairs(self._occupants) do count = count + 1; end - return st.reply(stanza):query("http://jabber.org/protocol/disco#info") + local reply = 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() :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up() - :tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up() - :tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up() - :tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up() - :tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up() + :tag("feature", {var=self:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up() + :tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up() + :tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up() + :tag("feature", {var=self:get_hidden() and "muc_hidden" or "muc_public"}):up() :tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up() - :add_child(dataform.new({ - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }, - { name = "muc#roominfo_description", label = "Description", value = "" }, - { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) } - }):form({["muc#roominfo_description"] = self:get_description()}, 'result')) ; + local dataform = dataform.new({ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }, + { name = "muc#roominfo_description", label = "Description", value = "" }, + { name = "muc#roominfo_occupants", label = "Number of occupants", value = "" } + }); + local formdata = { + ["muc#roominfo_description"] = self:get_description(), + ["muc#roominfo_occupants"] = tostring(count), + }; + module:fire_event("muc-disco#info", { room = self, reply = reply, form = dataform, formdata = formdata }); + reply:add_child(dataform:form(formdata, 'result')) + return reply; end function room_mt:get_disco_items(stanza) local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); @@ -252,7 +241,6 @@ function room_mt:get_disco_items(stanza) return reply; end function room_mt:set_subject(current_nick, subject) - -- TODO check nick's authority if subject == "" then subject = nil; end self._data['subject'] = subject; self._data['subject_from'] = current_nick; @@ -310,7 +298,7 @@ function room_mt:set_moderated(moderated) if self.save then self:save(true); end end end -function room_mt:is_moderated() +function room_mt:get_moderated() return self._data.moderated; end function room_mt:set_members_only(members_only) @@ -320,7 +308,7 @@ function room_mt:set_members_only(members_only) if self.save then self:save(true); end end end -function room_mt:is_members_only() +function room_mt:get_members_only() return self._data.members_only; end function room_mt:set_persistent(persistent) @@ -330,7 +318,7 @@ function room_mt:set_persistent(persistent) if self.save then self:save(true); end end end -function room_mt:is_persistent() +function room_mt:get_persistent() return self._data.persistent; end function room_mt:set_hidden(hidden) @@ -340,9 +328,15 @@ function room_mt:set_hidden(hidden) if self.save then self:save(true); end end end -function room_mt:is_hidden() +function room_mt:get_hidden() return self._data.hidden; end +function room_mt:get_public() + return not self:get_hidden(); +end +function room_mt:set_public(public) + return self:set_hidden(not public); +end function room_mt:set_changesubject(changesubject) changesubject = changesubject and true or nil; if self._data.changesubject ~= changesubject then @@ -365,12 +359,25 @@ function room_mt:set_historylength(length) end +local valid_whois = { moderators = true, anyone = true }; + +function room_mt:set_whois(whois) + if valid_whois[whois] and self._data.whois ~= whois then + self._data.whois = whois; + if self.save then self:save(true); end + end +end + +function room_mt:get_whois() + return self._data.whois; +end + local function construct_stanza_id(room, stanza) local from_jid, to_nick = stanza.attr.from, stanza.attr.to; local from_nick = room._jid_nick[from_jid]; local occupant = room._occupants[to_nick]; local to_jid = occupant.jid; - + return from_nick, to_jid, base64.encode(to_jid.."\0"..stanza.attr.id.."\0"..md5(from_jid)); end local function deconstruct_stanza_id(room, stanza) @@ -442,6 +449,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc self._occupants[current_nick].sessions[from] = pr; self:broadcast_presence(pr, from); else -- change nick + -- a MUC service MUST NOT allow empty or invisible Room Nicknames + -- (i.e., Room Nicknames that consist only of one or more space characters). + if not select(3, jid_split(to)):find("[^ ]") then -- resourceprep turns all whitespace into 0x20 + module:log("debug", "Rejecting invisible nickname"); + origin.send(st.error_reply(stanza, "cancel", "not-allowed")); + return; + end local occupant = self._occupants[current_nick]; local is_multisession = next(occupant.sessions, next(occupant.sessions)); if self._occupants[to] or is_multisession then @@ -474,6 +488,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc -- self:handle_to_occupant(origin, stanza); -- resend available --end else -- enter room + -- a MUC service MUST NOT allow empty or invisible Room Nicknames + -- (i.e., Room Nicknames that consist only of one or more space characters). + if not select(3, jid_split(to)):find("[^ ]") then -- resourceprep turns all whitespace into 0x20 + module:log("debug", "Rejecting invisible nickname"); + origin.send(st.error_reply(stanza, "cancel", "not-allowed")); + return; + end local new_nick = to; local is_merge; if self._occupants[to] then @@ -499,6 +520,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc log("debug", "%s joining as %s", from, to); if not next(self._affiliations) then -- new room, no owners self._affiliations[jid_bare(from)] = "owner"; + if self.locked and not stanza:get_child("x", "http://jabber.org/protocol/muc") then + self.locked = nil; -- Older groupchat protocol doesn't lock + end + elseif self.locked then -- Deny entry + module:log("debug", "Room is locked, denying entry"); + origin.send(st.error_reply(stanza, "cancel", "item-not-found")); + return; end local affiliation = self:get_affiliation(from); local role = self:get_default_role(affiliation) @@ -520,9 +548,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc if self._data.whois == 'anyone' then pr:tag("status", {code='100'}):up(); end + if self.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); elseif not affiliation then -- registration required for entering members-only room local reply = st.error_reply(stanza, "auth", "registration-required"):up(); reply.tags[1].attr.code = "407"; @@ -574,6 +606,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc end stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; else -- message + stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up(); stanza.attr.from = current_nick; for jid in pairs(o_data.sessions) do stanza.attr.to = jid; @@ -589,11 +622,11 @@ end function room_mt:send_form(origin, stanza) origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") - :add_child(self:get_form_layout():form()) + :add_child(self:get_form_layout(stanza.attr.from):form()) ); end -function room_mt:get_form_layout() +function room_mt:get_form_layout(actor) local form = dataform.new({ title = "Configuration for "..self.jid, instructions = "Complete and submit this form to configure the room.", @@ -618,13 +651,13 @@ function room_mt:get_form_layout() name = 'muc#roomconfig_persistentroom', type = 'boolean', label = 'Make Room Persistent?', - value = self:is_persistent() + value = self:get_persistent() }, { name = 'muc#roomconfig_publicroom', type = 'boolean', label = 'Make Room Publicly Searchable?', - value = not self:is_hidden() + value = not self:get_hidden() }, { name = 'muc#roomconfig_changesubject', @@ -651,13 +684,13 @@ function room_mt:get_form_layout() name = 'muc#roomconfig_moderatedroom', type = 'boolean', label = 'Make Room Moderated?', - value = self:is_moderated() + value = self:get_moderated() }, { name = 'muc#roomconfig_membersonly', type = 'boolean', label = 'Make Room Members-Only?', - value = self:is_members_only() + value = self:get_members_only() }, { name = 'muc#roomconfig_historylength', @@ -666,14 +699,9 @@ function room_mt:get_form_layout() value = tostring(self:get_historylength()) } }); - return module:fire_event("muc-config-form", { room = self, form = form }) or form; + return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form; end -local valid_whois = { - moderators = true, - anyone = true, -} - function room_mt:process_form(origin, stanza) local query = stanza.tags[1]; local form; @@ -689,86 +717,52 @@ function room_mt:process_form(origin, stanza) return true; end - - local fields = self:get_form_layout():data(form); - if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); return; end - - local dirty = false - - local event = { room = self, fields = fields, changed = dirty }; - module:fire_event("muc-config-submitted", event); - dirty = event.changed or dirty; - - local name = fields['muc#roomconfig_roomname']; - if name ~= self:get_name() then - self:set_name(name); - end - - local description = fields['muc#roomconfig_roomdesc']; - if description ~= self:get_description() then - self:set_description(description); + local fields, errors, present = self:get_form_layout(stanza.attr.from):data(form); + if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then + origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); + return; end - local persistent = fields['muc#roomconfig_persistentroom']; - dirty = dirty or (self:is_persistent() ~= persistent) - module:log("debug", "persistent=%s", tostring(persistent)); - - local moderated = fields['muc#roomconfig_moderatedroom']; - dirty = dirty or (self:is_moderated() ~= moderated) - module:log("debug", "moderated=%s", tostring(moderated)); - - local membersonly = fields['muc#roomconfig_membersonly']; - dirty = dirty or (self:is_members_only() ~= membersonly) - module:log("debug", "membersonly=%s", tostring(membersonly)); - - local public = fields['muc#roomconfig_publicroom']; - dirty = dirty or (self:is_hidden() ~= (not public and true or nil)) + local changed = {}; - local changesubject = fields['muc#roomconfig_changesubject']; - dirty = dirty or (self:get_changesubject() ~= (not changesubject and true or nil)) - module:log('debug', 'changesubject=%s', changesubject and "true" or "false") - - local historylength = tonumber(fields['muc#roomconfig_historylength']); - dirty = dirty or (historylength and (self:get_historylength() ~= historylength)); - module:log('debug', 'historylength=%s', historylength) - - - local whois = fields['muc#roomconfig_whois']; - if not valid_whois[whois] then - origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'")); - return; + local function handle_option(name, field, allowed) + if not present[field] then return; end + local new = fields[field]; + if allowed and not allowed[new] then return; end + if new == self["get_"..name](self) then return; end + changed[name] = true; + self["set_"..name](self, new); end - local whois_changed = self._data.whois ~= whois - self._data.whois = whois - module:log('debug', 'whois=%s', whois) - local password = fields['muc#roomconfig_roomsecret']; - if self:get_password() ~= password then - self:set_password(password); - end - self:set_moderated(moderated); - self:set_members_only(membersonly); - self:set_persistent(persistent); - self:set_hidden(not public); - self:set_changesubject(changesubject); - self:set_historylength(historylength); + local event = { room = self, fields = fields, changed = changed, stanza = stanza, origin = origin, update_option = handle_option }; + module:fire_event("muc-config-submitted", event); + + handle_option("name", "muc#roomconfig_roomname"); + handle_option("description", "muc#roomconfig_roomdesc"); + handle_option("persistent", "muc#roomconfig_persistentroom"); + handle_option("moderated", "muc#roomconfig_moderatedroom"); + handle_option("members_only", "muc#roomconfig_membersonly"); + handle_option("public", "muc#roomconfig_publicroom"); + handle_option("changesubject", "muc#roomconfig_changesubject"); + handle_option("historylength", "muc#roomconfig_historylength"); + handle_option("whois", "muc#roomconfig_whois", valid_whois); + handle_option("password", "muc#roomconfig_roomsecret"); if self.save then self:save(true); end + if self.locked then + module:fire_event("muc-room-unlocked", { room = self }); + self.locked = nil; + end origin.send(st.reply(stanza)); - if dirty or whois_changed then + if next(changed) then local msg = st.message({type='groupchat', from=self.jid}) - :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}); - - if dirty then - msg.tags[1]:tag('status', {code = '104'}):up(); - end - if whois_changed then - local code = (whois == 'moderators') and "173" or "172"; + :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) + :tag('status', {code = '104'}):up(); + if changed.whois then + local code = (self:get_whois() == 'moderators') and "173" or "172"; msg.tags[1]:tag('status', {code = code}):up(); end - msg:up(); - self:broadcast_message(msg, false) end end @@ -791,6 +785,7 @@ function room_mt:destroy(newjid, reason, password) end self:set_persistent(false); module:fire_event("muc-room-destroyed", { room = self }); + return true; end function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc @@ -838,7 +833,8 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha local _aff = item.attr.affiliation; local _rol = item.attr.role; if _aff and not _rol then - if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then + if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") + or (affiliation and affiliation ~= "outcast" and self:get_members_only() and self:get_whois() == "anyone") then local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); for jid, affiliation in pairs(self._affiliations) do if affiliation == _aff then @@ -904,7 +900,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end elseif stanza.name == "message" and type == "groupchat" then - local from, to = stanza.attr.from, stanza.attr.to; + local from = stanza.attr.from; local current_nick = self._jid_nick[from]; local occupant = self._occupants[current_nick]; if not occupant then -- not in room @@ -914,11 +910,11 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha else local from = stanza.attr.from; stanza.attr.from = current_nick; - local subject = getText(stanza, {"subject"}); + local subject = stanza:get_child_text("subject"); if subject then if occupant.role == "moderator" or ( self._data.changesubject and occupant.role == "participant" ) then -- and participant - self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza + self:set_subject(current_nick, subject); else stanza.attr.from = from; origin.send(st.error_reply(stanza, "auth", "forbidden")); @@ -966,7 +962,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha :tag('body') -- Add a plain message for clients which don't support invites :text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or "")) :up(); - if self:is_members_only() and not self:get_affiliation(_invitee) then + if self:get_members_only() and not self:get_affiliation(_invitee) then log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to); self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from]) end @@ -1041,6 +1037,9 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) x:tag("status", {code="321"}):up(); -- affiliation change end end + -- Your own presence should have status 110 + local self_x = st.clone(x); + self_x:tag("status", {code="110"}); local modified_nicks = {}; for nick, occupant in pairs(self._occupants) do if jid_bare(occupant.jid) == jid then @@ -1055,11 +1054,14 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) 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; + -- Broadcast this presence to everyone else later, with the public <x> variant + local bp = st.clone(p); + bp:add_child(x); + modified_nicks[nick] = bp; end + p:add_child(self_x); + self:_route_stanza(p); end end end @@ -1115,17 +1117,20 @@ function room_mt:set_role(actor, occupant_jid, role, callback, reason) else occupant.role = role; end + local self_x = st.clone(x); + self_x:tag("status", {code = "110"}):up(); 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; + bp = st.clone(p); + bp:add_child(x); end + p:add_child(self_x); + self:_route_stanza(p); end if callback then callback(); end if bp then |