Mercurial > prosody-hg
comparison plugins/muc/muc.lib.lua @ 6609:d2faaaca695d
Merge 0.10->trunk
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Fri, 27 Mar 2015 22:24:57 +0000 |
| parents | 3bbd59c03aeb |
| children | 4caef6d53304 |
comparison
equal
deleted
inserted
replaced
| 6608:b6e558febb7a | 6609:d2faaaca695d |
|---|---|
| 1 -- Prosody IM | 1 -- Prosody IM |
| 2 -- Copyright (C) 2008-2010 Matthew Wild | 2 -- Copyright (C) 2008-2010 Matthew Wild |
| 3 -- Copyright (C) 2008-2010 Waqas Hussain | 3 -- Copyright (C) 2008-2010 Waqas Hussain |
| 4 -- Copyright (C) 2014 Daurnimator | |
| 4 -- | 5 -- |
| 5 -- This project is MIT/X11 licensed. Please see the | 6 -- This project is MIT/X11 licensed. Please see the |
| 6 -- COPYING file in the source package for more information. | 7 -- COPYING file in the source package for more information. |
| 7 -- | 8 -- |
| 8 | 9 |
| 9 local select = select; | 10 local select = select; |
| 10 local pairs, ipairs = pairs, ipairs; | 11 local pairs, ipairs = pairs, ipairs; |
| 11 | 12 local next = next; |
| 12 local datetime = require "util.datetime"; | 13 local setmetatable = setmetatable; |
| 13 | 14 |
| 14 local dataform = require "util.dataforms"; | 15 local dataform = require "util.dataforms"; |
| 15 | 16 local iterators = require "util.iterators"; |
| 16 local jid_split = require "util.jid".split; | 17 local jid_split = require "util.jid".split; |
| 17 local jid_bare = require "util.jid".bare; | 18 local jid_bare = require "util.jid".bare; |
| 18 local jid_prep = require "util.jid".prep; | 19 local jid_prep = require "util.jid".prep; |
| 20 local jid_join = require "util.jid".join; | |
| 19 local st = require "util.stanza"; | 21 local st = require "util.stanza"; |
| 20 local log = require "util.logger".init("mod_muc"); | 22 local log = require "util.logger".init("mod_muc"); |
| 21 local t_insert, t_remove = table.insert, table.remove; | |
| 22 local setmetatable = setmetatable; | |
| 23 local base64 = require "util.encodings".base64; | 23 local base64 = require "util.encodings".base64; |
| 24 local md5 = require "util.hashes".md5; | 24 local md5 = require "util.hashes".md5; |
| 25 | 25 |
| 26 local muc_domain = nil; --module:get_host(); | 26 local occupant_lib = module:require "muc/occupant" |
| 27 local default_history_length, max_history_length = 20, math.huge; | 27 local muc_util = module:require "muc/util"; |
| 28 | 28 local is_kickable_error = muc_util.is_kickable_error; |
| 29 ------------ | 29 local valid_roles, valid_affiliations = muc_util.valid_roles, muc_util.valid_affiliations; |
| 30 local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; | |
| 31 local function presence_filter(tag) | |
| 32 if presence_filters[tag.attr.xmlns] then | |
| 33 return nil; | |
| 34 end | |
| 35 return tag; | |
| 36 end | |
| 37 | |
| 38 local function get_filtered_presence(stanza) | |
| 39 return st.clone(stanza):maptags(presence_filter); | |
| 40 end | |
| 41 local kickable_error_conditions = { | |
| 42 ["gone"] = true; | |
| 43 ["internal-server-error"] = true; | |
| 44 ["item-not-found"] = true; | |
| 45 ["jid-malformed"] = true; | |
| 46 ["recipient-unavailable"] = true; | |
| 47 ["redirect"] = true; | |
| 48 ["remote-server-not-found"] = true; | |
| 49 ["remote-server-timeout"] = true; | |
| 50 ["service-unavailable"] = true; | |
| 51 ["malformed error"] = true; | |
| 52 }; | |
| 53 | |
| 54 local function get_error_condition(stanza) | |
| 55 local _, condition = stanza:get_error(); | |
| 56 return condition or "malformed error"; | |
| 57 end | |
| 58 | |
| 59 local function is_kickable_error(stanza) | |
| 60 local cond = get_error_condition(stanza); | |
| 61 return kickable_error_conditions[cond] and cond; | |
| 62 end | |
| 63 ----------- | |
| 64 | 30 |
| 65 local room_mt = {}; | 31 local room_mt = {}; |
| 66 room_mt.__index = room_mt; | 32 room_mt.__index = room_mt; |
| 67 | 33 |
| 68 function room_mt:__tostring() | 34 function room_mt:__tostring() |
| 69 return "MUC room ("..self.jid..")"; | 35 return "MUC room ("..self.jid..")"; |
| 70 end | 36 end |
| 71 | 37 |
| 38 function room_mt:get_occupant_jid(real_jid) | |
| 39 return self._jid_nick[real_jid] | |
| 40 end | |
| 41 | |
| 72 function room_mt:get_default_role(affiliation) | 42 function room_mt:get_default_role(affiliation) |
| 73 if affiliation == "owner" or affiliation == "admin" then | 43 local role = module:fire_event("muc-get-default-role", { |
| 44 room = self; | |
| 45 affiliation = affiliation; | |
| 46 affiliation_rank = valid_affiliations[affiliation or "none"]; | |
| 47 }); | |
| 48 return role, valid_roles[role or "none"]; | |
| 49 end | |
| 50 module:hook("muc-get-default-role", function(event) | |
| 51 if event.affiliation_rank >= valid_affiliations.admin then | |
| 74 return "moderator"; | 52 return "moderator"; |
| 75 elseif affiliation == "member" then | 53 elseif event.affiliation_rank >= valid_affiliations.none then |
| 76 return "participant"; | 54 return "participant"; |
| 77 elseif not affiliation then | 55 end |
| 78 if not self:get_members_only() then | 56 end); |
| 79 return self:get_moderated() and "visitor" or "participant"; | 57 |
| 80 end | 58 --- Occupant functions |
| 81 end | 59 function room_mt:new_occupant(bare_real_jid, nick) |
| 82 end | 60 local occupant = occupant_lib.new(bare_real_jid, nick); |
| 83 | 61 local affiliation = self:get_affiliation(bare_real_jid); |
| 84 function room_mt:broadcast_presence(stanza, sid, code, nick) | 62 occupant.role = self:get_default_role(affiliation); |
| 85 stanza = get_filtered_presence(stanza); | 63 return occupant; |
| 86 local occupant = self._occupants[stanza.attr.from]; | 64 end |
| 87 stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) | 65 |
| 88 :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none", nick=nick}):up(); | 66 function room_mt:get_occupant_by_nick(nick) |
| 89 if code then | 67 local occupant = self._occupants[nick]; |
| 90 stanza:tag("status", {code=code}):up(); | 68 if occupant == nil then return nil end |
| 91 end | 69 return occupant_lib.copy(occupant); |
| 92 self:broadcast_except_nick(stanza, stanza.attr.from); | 70 end |
| 93 local me = self._occupants[stanza.attr.from]; | 71 |
| 94 if me then | 72 do |
| 95 stanza:tag("status", {code='110'}):up(); | 73 local function next_copied_occupant(occupants, occupant_jid) |
| 96 stanza.attr.to = sid; | 74 local next_occupant_jid, raw_occupant = next(occupants, occupant_jid); |
| 97 self:_route_stanza(stanza); | 75 if next_occupant_jid == nil then return nil end |
| 98 end | 76 return next_occupant_jid, occupant_lib.copy(raw_occupant); |
| 99 end | 77 end |
| 100 function room_mt:broadcast_message(stanza, historic) | 78 function room_mt:each_occupant(read_only) |
| 79 return next_copied_occupant, self._occupants, nil; | |
| 80 end | |
| 81 end | |
| 82 | |
| 83 function room_mt:has_occupant() | |
| 84 return next(self._occupants, nil) ~= nil | |
| 85 end | |
| 86 | |
| 87 function room_mt:get_occupant_by_real_jid(real_jid) | |
| 88 local occupant_jid = self:get_occupant_jid(real_jid); | |
| 89 if occupant_jid == nil then return nil end | |
| 90 return self:get_occupant_by_nick(occupant_jid); | |
| 91 end | |
| 92 | |
| 93 function room_mt:save_occupant(occupant) | |
| 94 occupant = occupant_lib.copy(occupant); -- So that occupant can be modified more | |
| 95 local id = occupant.nick | |
| 96 | |
| 97 -- Need to maintain _jid_nick secondary index | |
| 98 local old_occupant = self._occupants[id]; | |
| 99 if old_occupant then | |
| 100 for real_jid in old_occupant:each_session() do | |
| 101 self._jid_nick[real_jid] = nil; | |
| 102 end | |
| 103 end | |
| 104 | |
| 105 local has_live_session = false | |
| 106 if occupant.role ~= nil then | |
| 107 for real_jid, presence in occupant:each_session() do | |
| 108 if presence.attr.type == nil then | |
| 109 has_live_session = true | |
| 110 self._jid_nick[real_jid] = occupant.nick; | |
| 111 end | |
| 112 end | |
| 113 if not has_live_session then | |
| 114 -- Has no live sessions left; they have left the room. | |
| 115 occupant.role = nil | |
| 116 end | |
| 117 end | |
| 118 if not has_live_session then | |
| 119 occupant = nil | |
| 120 end | |
| 121 self._occupants[id] = occupant | |
| 122 end | |
| 123 | |
| 124 function room_mt:route_to_occupant(occupant, stanza) | |
| 101 local to = stanza.attr.to; | 125 local to = stanza.attr.to; |
| 102 for occupant, o_data in pairs(self._occupants) do | 126 for jid, pr in occupant:each_session() do |
| 103 for jid in pairs(o_data.sessions) do | 127 stanza.attr.to = jid; |
| 104 stanza.attr.to = jid; | 128 self:route_stanza(stanza); |
| 105 self:_route_stanza(stanza); | |
| 106 end | |
| 107 end | 129 end |
| 108 stanza.attr.to = to; | 130 stanza.attr.to = to; |
| 109 if historic then -- add to history | 131 end |
| 110 return self:save_to_history(stanza) | 132 |
| 111 end | 133 -- actor is the attribute table |
| 112 end | 134 local function add_item(x, affiliation, role, jid, nick, actor_nick, actor_jid, reason) |
| 113 function room_mt:save_to_history(stanza) | 135 x:tag("item", {affiliation = affiliation; role = role; jid = jid; nick = nick;}) |
| 114 local history = self._data['history']; | 136 if actor_nick or actor_jid then |
| 115 if not history then history = {}; self._data['history'] = history; end | 137 x:tag("actor", {nick = actor_nick; jid = actor_jid;}):up() |
| 116 stanza = st.clone(stanza); | 138 end |
| 117 stanza.attr.to = ""; | 139 if reason then |
| 118 local stamp = datetime.datetime(); | 140 x:tag("reason"):text(reason):up() |
| 119 stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203 | 141 end |
| 120 stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) | 142 x:up(); |
| 121 local entry = { stanza = stanza, stamp = stamp }; | 143 return x |
| 122 t_insert(history, entry); | 144 end |
| 123 while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end | 145 |
| 124 end | 146 -- actor is (real) jid |
| 125 function room_mt:broadcast_except_nick(stanza, nick) | 147 function room_mt:build_item_list(occupant, x, is_anonymous, nick, actor_nick, actor_jid, reason) |
| 126 for rnick, occupant in pairs(self._occupants) do | 148 local affiliation = self:get_affiliation(occupant.bare_jid) or "none"; |
| 127 if rnick ~= nick then | 149 local role = occupant.role or "none"; |
| 128 for jid in pairs(occupant.sessions) do | 150 if is_anonymous then |
| 129 stanza.attr.to = jid; | 151 add_item(x, affiliation, role, nil, nick, actor_nick, actor_jid, reason); |
| 130 self:_route_stanza(stanza); | 152 else |
| 131 end | 153 for real_jid, session in occupant:each_session() do |
| 132 end | 154 add_item(x, affiliation, role, real_jid, nick, actor_nick, actor_jid, reason); |
| 133 end | 155 end |
| 134 end | 156 end |
| 135 | 157 return x |
| 136 function room_mt:send_occupant_list(to) | 158 end |
| 137 local current_nick = self._jid_nick[to]; | 159 |
| 138 for occupant, o_data in pairs(self._occupants) do | 160 function room_mt:broadcast_message(stanza) |
| 139 if occupant ~= current_nick then | 161 if module:fire_event("muc-broadcast-message", {room = self, stanza = stanza}) then |
| 140 local pres = get_filtered_presence(o_data.sessions[o_data.jid]); | 162 return true; |
| 141 pres.attr.to, pres.attr.from = to, occupant; | 163 end |
| 142 pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) | 164 self:broadcast(stanza); |
| 143 :tag("item", {affiliation=o_data.affiliation or "none", role=o_data.role or "none"}):up(); | 165 return true; |
| 144 self:_route_stanza(pres); | 166 end |
| 145 end | 167 |
| 146 end | 168 -- Broadcast a stanza to all occupants in the room. |
| 147 end | 169 -- optionally checks conditional called with (nick, occupant) |
| 148 function room_mt:send_history(to, stanza) | 170 function room_mt:broadcast(stanza, cond_func) |
| 149 local history = self._data['history']; -- send discussion history | 171 for nick, occupant in self:each_occupant() do |
| 150 if history then | 172 if cond_func == nil or cond_func(nick, occupant) then |
| 151 local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc"); | 173 self:route_to_occupant(occupant, stanza) |
| 152 local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc"); | 174 end |
| 153 | 175 end |
| 154 local maxchars = history_tag and tonumber(history_tag.attr.maxchars); | 176 end |
| 155 if maxchars then maxchars = math.floor(maxchars); end | 177 |
| 156 | 178 local function can_see_real_jids(whois, occupant) |
| 157 local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history); | 179 if whois == "anyone" then |
| 158 if not history_tag then maxstanzas = 20; end | 180 return true; |
| 159 | 181 elseif whois == "moderators" then |
| 160 local seconds = history_tag and tonumber(history_tag.attr.seconds); | 182 return valid_roles[occupant.role or "none"] >= valid_roles.moderator; |
| 161 if seconds then seconds = datetime.datetime(os.time() - math.floor(seconds)); end | 183 end |
| 162 | 184 end |
| 163 local since = history_tag and history_tag.attr.since; | 185 |
| 164 if since then since = datetime.parse(since); since = since and datetime.datetime(since); end | 186 -- Broadcasts an occupant's presence to the whole room |
| 165 if seconds and (not since or since < seconds) then since = seconds; end | 187 -- Takes the x element that goes into the stanzas |
| 166 | 188 function room_mt:publicise_occupant_status(occupant, base_x, nick, actor, reason) |
| 167 local n = 0; | 189 -- Build real jid and (optionally) occupant jid template presences |
| 168 local charcount = 0; | 190 local base_presence; |
| 169 | 191 if occupant.role ~= nil then |
| 170 for i=#history,1,-1 do | 192 -- Try to use main jid's presence |
| 171 local entry = history[i]; | 193 local pr = occupant:get_presence(); |
| 172 if maxchars then | 194 if pr ~= nil then |
| 173 if not entry.chars then | 195 base_presence = st.clone(pr); |
| 174 entry.stanza.attr.to = ""; | 196 end |
| 175 entry.chars = #tostring(entry.stanza); | 197 end |
| 176 end | 198 base_presence = base_presence or st.presence {from = occupant.nick; type = "unavailable";}; |
| 177 charcount = charcount + entry.chars + #to; | 199 |
| 178 if charcount > maxchars then break; end | 200 -- Fire event (before full_p and anon_p are created) |
| 179 end | 201 local event = { |
| 180 if since and since > entry.stamp then break; end | 202 room = self; stanza = base_presence; x = base_x; |
| 181 if n + 1 > maxstanzas then break; end | 203 occupant = occupant; nick = nick; actor = actor; |
| 182 n = n + 1; | 204 reason = reason; |
| 183 end | 205 } |
| 184 for i=#history-n+1,#history do | 206 module:fire_event("muc-broadcast-presence", event); |
| 185 local msg = history[i].stanza; | 207 |
| 186 msg.attr.to = to; | 208 -- Allow muc-broadcast-presence listeners to change things |
| 187 self:_route_stanza(msg); | 209 nick = event.nick; |
| 188 end | 210 actor = event.actor; |
| 189 end | 211 reason = event.reason; |
| 190 end | 212 |
| 191 function room_mt:send_subject(to) | 213 local whois = self:get_whois(); |
| 192 if self._data['subject'] then | 214 |
| 193 self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject'])); | 215 local actor_nick; |
| 216 if actor then | |
| 217 actor_nick = select(3, jid_split(self:get_occupant_jid(actor))); | |
| 218 end | |
| 219 | |
| 220 local full_p, full_x; | |
| 221 local function get_full_p() | |
| 222 if full_p == nil then | |
| 223 full_x = st.clone(base_x); | |
| 224 self:build_item_list(occupant, full_x, false, nick, actor_nick, actor, reason); | |
| 225 full_p = st.clone(base_presence):add_child(full_x); | |
| 226 end | |
| 227 return full_p, full_x; | |
| 228 end | |
| 229 | |
| 230 local anon_p, anon_x; | |
| 231 local function get_anon_p() | |
| 232 if anon_p == nil then | |
| 233 anon_x = st.clone(base_x); | |
| 234 self:build_item_list(occupant, anon_x, true, nick, actor_nick, nil, reason); | |
| 235 anon_p = st.clone(base_presence):add_child(anon_x); | |
| 236 end | |
| 237 return anon_p, anon_x; | |
| 238 end | |
| 239 | |
| 240 local self_p, self_x; | |
| 241 if can_see_real_jids(whois, occupant) then | |
| 242 self_p, self_x = get_full_p(); | |
| 243 else | |
| 244 -- Can always see your own full jids | |
| 245 -- But not allowed to see actor's | |
| 246 self_x = st.clone(base_x); | |
| 247 self:build_item_list(occupant, self_x, false, nick, actor_nick, nil, reason); | |
| 248 self_p = st.clone(base_presence):add_child(self_x); | |
| 249 end | |
| 250 | |
| 251 -- General populance | |
| 252 for nick, n_occupant in self:each_occupant() do | |
| 253 if nick ~= occupant.nick then | |
| 254 local pr; | |
| 255 if can_see_real_jids(whois, n_occupant) then | |
| 256 pr = get_full_p(); | |
| 257 elseif occupant.bare_jid == n_occupant.bare_jid then | |
| 258 pr = self_p; | |
| 259 else | |
| 260 pr = get_anon_p(); | |
| 261 end | |
| 262 self:route_to_occupant(n_occupant, pr); | |
| 263 end | |
| 264 end | |
| 265 | |
| 266 -- Presences for occupant itself | |
| 267 self_x:tag("status", {code = "110";}):up(); | |
| 268 if occupant.role == nil then | |
| 269 -- They get an unavailable | |
| 270 self:route_to_occupant(occupant, self_p); | |
| 271 else | |
| 272 -- use their own presences as templates | |
| 273 for full_jid, pr in occupant:each_session() do | |
| 274 pr = st.clone(pr); | |
| 275 pr.attr.to = full_jid; | |
| 276 pr:add_child(self_x); | |
| 277 self:route_stanza(pr); | |
| 278 end | |
| 279 end | |
| 280 end | |
| 281 | |
| 282 function room_mt:send_occupant_list(to, filter) | |
| 283 local to_bare = jid_bare(to); | |
| 284 local is_anonymous = false; | |
| 285 local whois = self:get_whois(); | |
| 286 if whois ~= "anyone" then | |
| 287 local affiliation = self:get_affiliation(to); | |
| 288 if affiliation ~= "admin" and affiliation ~= "owner" then | |
| 289 local occupant = self:get_occupant_by_real_jid(to); | |
| 290 if not (occupant and can_see_real_jids(whois, occupant)) then | |
| 291 is_anonymous = true; | |
| 292 end | |
| 293 end | |
| 294 end | |
| 295 for occupant_jid, occupant in self:each_occupant() do | |
| 296 if filter == nil or filter(occupant_jid, occupant) then | |
| 297 local x = st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'}); | |
| 298 self:build_item_list(occupant, x, is_anonymous and to_bare ~= occupant.bare_jid); -- can always see your own jids | |
| 299 local pres = st.clone(occupant:get_presence()); | |
| 300 pres.attr.to = to; | |
| 301 pres:add_child(x); | |
| 302 self:route_stanza(pres); | |
| 303 end | |
| 194 end | 304 end |
| 195 end | 305 end |
| 196 | 306 |
| 197 function room_mt:get_disco_info(stanza) | 307 function room_mt:get_disco_info(stanza) |
| 198 local count = 0; for _ in pairs(self._occupants) do count = count + 1; end | 308 local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info"); |
| 199 return st.reply(stanza):query("http://jabber.org/protocol/disco#info") | 309 local form = dataform.new { |
| 200 :tag("identity", {category="conference", type="text", name=self:get_name()}):up() | 310 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }; |
| 201 :tag("feature", {var="http://jabber.org/protocol/muc"}):up() | 311 }; |
| 202 :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up() | 312 module:fire_event("muc-disco#info", {room = self; reply = reply; form = form;}); |
| 203 :tag("feature", {var=self:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up() | 313 reply:add_child(form:form(nil, "result")); |
| 204 :tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up() | 314 return reply; |
| 205 :tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up() | 315 end |
| 206 :tag("feature", {var=self:get_hidden() and "muc_hidden" or "muc_public"}):up() | 316 module:hook("muc-disco#info", function(event) |
| 207 :tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up() | 317 event.reply:tag("feature", {var = "http://jabber.org/protocol/muc"}):up(); |
| 208 :add_child(dataform.new({ | 318 end); |
| 209 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }, | 319 module:hook("muc-disco#info", function(event) |
| 210 { name = "muc#roominfo_description", label = "Description", value = "" }, | 320 local count = iterators.count(event.room:each_occupant()); |
| 211 { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) } | 321 table.insert(event.form, { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) }); |
| 212 }):form({["muc#roominfo_description"] = self:get_description()}, 'result')) | 322 end); |
| 213 ; | 323 |
| 214 end | |
| 215 function room_mt:get_disco_items(stanza) | 324 function room_mt:get_disco_items(stanza) |
| 216 local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); | 325 local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); |
| 217 for room_jid in pairs(self._occupants) do | 326 for room_jid in self:each_occupant() do |
| 218 reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up(); | 327 reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up(); |
| 219 end | 328 end |
| 220 return reply; | 329 return reply; |
| 221 end | 330 end |
| 222 function room_mt:set_subject(current_nick, subject) | 331 |
| 223 if subject == "" then subject = nil; end | 332 function room_mt:handle_kickable(origin, stanza) |
| 224 self._data['subject'] = subject; | 333 local real_jid = stanza.attr.from; |
| 225 self._data['subject_from'] = current_nick; | 334 local occupant = self:get_occupant_by_real_jid(real_jid); |
| 226 if self.save then self:save(); end | 335 if occupant == nil then return nil; end |
| 227 local msg = st.message({type='groupchat', from=current_nick}) | |
| 228 :tag('subject'):text(subject):up(); | |
| 229 self:broadcast_message(msg, false); | |
| 230 return true; | |
| 231 end | |
| 232 | |
| 233 local function build_unavailable_presence_from_error(stanza) | |
| 234 local type, condition, text = stanza:get_error(); | 336 local type, condition, text = stanza:get_error(); |
| 235 local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error"); | 337 local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error"); |
| 236 if text then | 338 if text then |
| 237 error_message = error_message..": "..text; | 339 error_message = error_message..": "..text; |
| 238 end | 340 end |
| 239 return st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to}) | 341 occupant:set_session(real_jid, st.presence({type="unavailable"}) |
| 240 :tag('status'):text(error_message); | 342 :tag('status'):text(error_message)); |
| 241 end | 343 self:save_occupant(occupant); |
| 242 | 344 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}) |
| 243 function room_mt:set_name(name) | 345 :tag("status", {code = "307"}) |
| 244 if name == "" or type(name) ~= "string" or name == (jid_split(self.jid)) then name = nil; end | 346 self:publicise_occupant_status(occupant, x); |
| 245 if self._data.name ~= name then | 347 if occupant.jid == real_jid then -- Was last session |
| 246 self._data.name = name; | 348 module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;}); |
| 247 if self.save then self:save(true); end | 349 end |
| 248 end | 350 return true; |
| 249 end | 351 end |
| 250 function room_mt:get_name() | 352 |
| 251 return self._data.name or jid_split(self.jid); | 353 -- Give the room creator owner affiliation |
| 252 end | 354 module:hook("muc-room-pre-create", function(event) |
| 253 function room_mt:set_description(description) | 355 event.room:set_affiliation(true, jid_bare(event.stanza.attr.from), "owner"); |
| 254 if description == "" or type(description) ~= "string" then description = nil; end | 356 end, -1); |
| 255 if self._data.description ~= description then | 357 |
| 256 self._data.description = description; | 358 -- check if user is banned |
| 257 if self.save then self:save(true); end | 359 module:hook("muc-occupant-pre-join", function(event) |
| 258 end | 360 local room, stanza = event.room, event.stanza; |
| 259 end | 361 local affiliation = room:get_affiliation(stanza.attr.from); |
| 260 function room_mt:get_description() | 362 if affiliation == "outcast" then |
| 261 return self._data.description; | 363 local reply = st.error_reply(stanza, "auth", "forbidden"):up(); |
| 262 end | 364 reply.tags[1].attr.code = "403"; |
| 263 function room_mt:set_password(password) | 365 event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); |
| 264 if password == "" or type(password) ~= "string" then password = nil; end | 366 return true; |
| 265 if self._data.password ~= password then | 367 end |
| 266 self._data.password = password; | 368 end, -10); |
| 267 if self.save then self:save(true); end | 369 |
| 268 end | 370 function room_mt:handle_presence_to_occupant(origin, stanza) |
| 269 end | 371 local type = stanza.attr.type; |
| 270 function room_mt:get_password() | 372 if type == "error" then -- error, kick em out! |
| 271 return self._data.password; | 373 return self:handle_kickable(origin, stanza) |
| 272 end | 374 elseif type == nil or type == "unavailable" then |
| 273 function room_mt:set_moderated(moderated) | 375 local real_jid = stanza.attr.from; |
| 274 moderated = moderated and true or nil; | 376 local bare_jid = jid_bare(real_jid); |
| 275 if self._data.moderated ~= moderated then | 377 local orig_occupant, dest_occupant; |
| 276 self._data.moderated = moderated; | 378 local is_new_room = next(self._affiliations) == nil; |
| 277 if self.save then self:save(true); end | 379 if is_new_room then |
| 278 end | 380 if type == "unavailable" then return true; end -- Unavailable from someone not in the room |
| 279 end | 381 if module:fire_event("muc-room-pre-create", { |
| 280 function room_mt:get_moderated() | 382 room = self; |
| 281 return self._data.moderated; | 383 origin = origin; |
| 282 end | 384 stanza = stanza; |
| 283 function room_mt:set_members_only(members_only) | 385 }) then return true; end |
| 284 members_only = members_only and true or nil; | 386 else |
| 285 if self._data.members_only ~= members_only then | 387 orig_occupant = self:get_occupant_by_real_jid(real_jid); |
| 286 self._data.members_only = members_only; | 388 if type == "unavailable" and orig_occupant == nil then return true; end -- Unavailable from someone not in the room |
| 287 if self.save then self:save(true); end | 389 end |
| 288 end | 390 local is_first_dest_session; |
| 289 end | 391 if type == "unavailable" then |
| 290 function room_mt:get_members_only() | 392 -- dest_occupant = nil |
| 291 return self._data.members_only; | 393 elseif orig_occupant and orig_occupant.nick == stanza.attr.to then -- Just a presence update |
| 292 end | 394 log("debug", "presence update for %s from session %s", orig_occupant.nick, real_jid); |
| 293 function room_mt:set_persistent(persistent) | 395 dest_occupant = orig_occupant; |
| 294 persistent = persistent and true or nil; | 396 else |
| 295 if self._data.persistent ~= persistent then | 397 local dest_jid = stanza.attr.to; |
| 296 self._data.persistent = persistent; | 398 dest_occupant = self:get_occupant_by_nick(dest_jid); |
| 297 if self.save then self:save(true); end | 399 if dest_occupant == nil then |
| 298 end | 400 log("debug", "no occupant found for %s; creating new occupant object for %s", dest_jid, real_jid); |
| 299 end | 401 is_first_dest_session = true; |
| 300 function room_mt:get_persistent() | 402 dest_occupant = self:new_occupant(bare_jid, dest_jid); |
| 301 return self._data.persistent; | 403 else |
| 302 end | 404 is_first_dest_session = false; |
| 303 function room_mt:set_hidden(hidden) | 405 end |
| 304 hidden = hidden and true or nil; | 406 end |
| 305 if self._data.hidden ~= hidden then | 407 local is_last_orig_session; |
| 306 self._data.hidden = hidden; | 408 if orig_occupant ~= nil then |
| 307 if self.save then self:save(true); end | 409 -- Is there are least 2 sessions? |
| 308 end | 410 local iter, ob, last = orig_occupant:each_session(); |
| 309 end | 411 is_last_orig_session = iter(ob, iter(ob, last)) == nil; |
| 310 function room_mt:get_hidden() | 412 end |
| 311 return self._data.hidden; | 413 |
| 312 end | 414 local event, event_name = { |
| 313 function room_mt:get_public() | 415 room = self; |
| 314 return not self:get_hidden(); | 416 origin = origin; |
| 315 end | 417 stanza = stanza; |
| 316 function room_mt:set_public(public) | 418 is_first_session = is_first_dest_session; |
| 317 return self:set_hidden(not public); | 419 is_last_session = is_last_orig_session; |
| 318 end | 420 }; |
| 319 function room_mt:set_changesubject(changesubject) | 421 if orig_occupant == nil then |
| 320 changesubject = changesubject and true or nil; | 422 event_name = "muc-occupant-pre-join"; |
| 321 if self._data.changesubject ~= changesubject then | 423 event.is_new_room = is_new_room; |
| 322 self._data.changesubject = changesubject; | 424 event.occupant = dest_occupant; |
| 323 if self.save then self:save(true); end | 425 elseif dest_occupant == nil then |
| 324 end | 426 event_name = "muc-occupant-pre-leave"; |
| 325 end | 427 event.occupant = orig_occupant; |
| 326 function room_mt:get_changesubject() | 428 else |
| 327 return self._data.changesubject; | 429 event_name = "muc-occupant-pre-change"; |
| 328 end | 430 event.orig_occupant = orig_occupant; |
| 329 function room_mt:get_historylength() | 431 event.dest_occupant = dest_occupant; |
| 330 return self._data.history_length or default_history_length; | 432 end |
| 331 end | 433 if module:fire_event(event_name, event) then return true; end |
| 332 function room_mt:set_historylength(length) | 434 |
| 333 length = math.min(tonumber(length) or default_history_length, max_history_length or math.huge); | 435 -- Check for nick conflicts |
| 334 if length == default_history_length then | 436 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 |
| 335 length = nil; | 437 log("debug", "%s couldn't join due to nick conflict: %s", real_jid, dest_occupant.nick); |
| 336 end | 438 local reply = st.error_reply(stanza, "cancel", "conflict"):up(); |
| 337 self._data.history_length = length; | 439 reply.tags[1].attr.code = "409"; |
| 338 end | 440 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); |
| 339 | 441 return true; |
| 340 | 442 end |
| 341 local valid_whois = { moderators = true, anyone = true }; | 443 |
| 342 | 444 -- Send presence stanza about original occupant |
| 343 function room_mt:set_whois(whois) | 445 if orig_occupant ~= nil and orig_occupant ~= dest_occupant then |
| 344 if valid_whois[whois] and self._data.whois ~= whois then | 446 local orig_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); |
| 345 self._data.whois = whois; | 447 local dest_nick; |
| 346 if self.save then self:save(true); end | 448 if dest_occupant == nil then -- Session is leaving |
| 347 end | 449 log("debug", "session %s is leaving occupant %s", real_jid, orig_occupant.nick); |
| 348 end | 450 if is_last_orig_session then |
| 349 | 451 orig_occupant.role = nil; |
| 350 function room_mt:get_whois() | 452 end |
| 351 return self._data.whois; | 453 orig_occupant:set_session(real_jid, stanza); |
| 352 end | 454 else |
| 353 | 455 log("debug", "session %s is changing from occupant %s to %s", real_jid, orig_occupant.nick, dest_occupant.nick); |
| 354 local function construct_stanza_id(room, stanza) | 456 local generated_unavail = st.presence {from = orig_occupant.nick, to = real_jid, type = "unavailable"}; |
| 355 local from_jid, to_nick = stanza.attr.from, stanza.attr.to; | 457 orig_occupant:set_session(real_jid, generated_unavail); |
| 356 local from_nick = room._jid_nick[from_jid]; | 458 dest_nick = select(3, jid_split(dest_occupant.nick)); |
| 357 local occupant = room._occupants[to_nick]; | 459 if not is_first_dest_session then -- User is swapping into another pre-existing session |
| 358 local to_jid = occupant.jid; | 460 log("debug", "session %s is swapping into multisession %s, showing it leave.", real_jid, dest_occupant.nick); |
| 359 | 461 -- Show the other session leaving |
| 360 return from_nick, to_jid, base64.encode(to_jid.."\0"..stanza.attr.id.."\0"..md5(from_jid)); | 462 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}) |
| 361 end | 463 :tag("status"):text("you are joining pre-existing session " .. dest_nick):up(); |
| 362 local function deconstruct_stanza_id(room, stanza) | 464 add_item(x, self:get_affiliation(bare_jid), "none"); |
| 363 local from_jid_possiblybare, to_nick = stanza.attr.from, stanza.attr.to; | 465 local pr = st.presence{from = dest_occupant.nick, to = real_jid, type = "unavailable"} |
| 364 local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$"); | 466 :add_child(x); |
| 365 local from_nick = room._jid_nick[from_jid]; | 467 self:route_stanza(pr); |
| 366 | 468 end |
| 367 if not(from_nick) then return; end | 469 if is_first_dest_session and is_last_orig_session then -- Normal nick change |
| 368 if not(from_jid_possiblybare == from_jid or from_jid_possiblybare == jid_bare(from_jid)) then return; end | 470 log("debug", "no sessions in %s left; publically marking as nick change", orig_occupant.nick); |
| 369 | 471 orig_x:tag("status", {code = "303";}):up(); |
| 370 local occupant = room._occupants[to_nick]; | 472 else -- The session itself always needs to see a nick change |
| 371 for to_jid in pairs(occupant and occupant.sessions or {}) do | 473 -- don't want to get our old nick's available presence, |
| 372 if md5(to_jid) == to_jid_hash then | 474 -- so remove our session from there, and manually generate an unavailable |
| 373 return from_nick, to_jid, id; | 475 orig_occupant:remove_session(real_jid); |
| 374 end | 476 log("debug", "generating nick change for %s", real_jid); |
| 375 end | 477 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); |
| 376 end | 478 -- self:build_item_list(orig_occupant, x, false, dest_nick); -- COMPAT: clients get confused if they see other items besides their own |
| 377 | 479 add_item(x, self:get_affiliation(bare_jid), orig_occupant.role, real_jid, dest_nick); |
| 378 | 480 x:tag("status", {code = "303";}):up(); |
| 379 function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc | 481 x:tag("status", {code = "110";}):up(); |
| 482 self:route_stanza(generated_unavail:add_child(x)); | |
| 483 dest_nick = nil; -- set dest_nick to nil; so general populance doesn't see it for whole orig_occupant | |
| 484 end | |
| 485 end | |
| 486 self:save_occupant(orig_occupant); | |
| 487 self:publicise_occupant_status(orig_occupant, orig_x, dest_nick); | |
| 488 | |
| 489 if is_last_orig_session then | |
| 490 module:fire_event("muc-occupant-left", {room = self; nick = orig_occupant.nick; occupant = orig_occupant;}); | |
| 491 end | |
| 492 end | |
| 493 | |
| 494 if dest_occupant ~= nil then | |
| 495 dest_occupant:set_session(real_jid, stanza); | |
| 496 local dest_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); | |
| 497 if is_new_room then | |
| 498 dest_x:tag("status", {code = "201"}):up(); | |
| 499 end | |
| 500 if orig_occupant == nil and self:get_whois() == "anyone" then | |
| 501 dest_x:tag("status", {code = "100"}):up(); | |
| 502 end | |
| 503 self:save_occupant(dest_occupant); | |
| 504 | |
| 505 if orig_occupant == nil then | |
| 506 -- Send occupant list to newly joined user | |
| 507 self:send_occupant_list(real_jid, function(nick, occupant) | |
| 508 -- Don't include self | |
| 509 return occupant:get_presence(real_jid) == nil; | |
| 510 end) | |
| 511 end | |
| 512 self:publicise_occupant_status(dest_occupant, dest_x); | |
| 513 | |
| 514 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 | |
| 515 log("debug", "session %s split nicks; showing %s rejoining", real_jid, orig_occupant.nick); | |
| 516 -- Show the original nick joining again | |
| 517 local pr = st.clone(orig_occupant:get_presence()); | |
| 518 pr.attr.to = real_jid; | |
| 519 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); | |
| 520 self:build_item_list(orig_occupant, x, false); | |
| 521 -- TODO: new status code to inform client this was the multi-session it left? | |
| 522 pr:add_child(x); | |
| 523 self:route_stanza(pr); | |
| 524 end | |
| 525 | |
| 526 if orig_occupant == nil then | |
| 527 if is_first_dest_session then | |
| 528 module:fire_event("muc-occupant-joined", {room = self; nick = dest_occupant.nick; occupant = dest_occupant;}); | |
| 529 end | |
| 530 module:fire_event("muc-occupant-session-new", {room = self; nick = dest_occupant.nick; occupant = dest_occupant; stanza = stanza; jid = real_jid;}); | |
| 531 end | |
| 532 end | |
| 533 elseif type ~= 'result' then -- bad type | |
| 534 if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences | |
| 535 origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error? | |
| 536 end | |
| 537 end | |
| 538 return true; | |
| 539 end | |
| 540 | |
| 541 function room_mt:handle_iq_to_occupant(origin, stanza) | |
| 380 local from, to = stanza.attr.from, stanza.attr.to; | 542 local from, to = stanza.attr.from, stanza.attr.to; |
| 381 local room = jid_bare(to); | |
| 382 local current_nick = self._jid_nick[from]; | |
| 383 local type = stanza.attr.type; | 543 local type = stanza.attr.type; |
| 384 log("debug", "room: %s, current_nick: %s, stanza: %s", room or "nil", current_nick or "nil", stanza:top_tag()); | 544 local id = stanza.attr.id; |
| 385 if (select(2, jid_split(from)) == muc_domain) then error("Presence from the MUC itself!!!"); end | 545 local occupant = self:get_occupant_by_nick(to); |
| 386 if stanza.name == "presence" then | 546 if (type == "error" or type == "result") then |
| 387 local pr = get_filtered_presence(stanza); | 547 do -- deconstruct_stanza_id |
| 388 pr.attr.from = current_nick; | 548 if not occupant then return nil; end |
| 389 if type == "error" then -- error, kick em out! | 549 local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$"); |
| 390 if current_nick then | 550 if not(from == from_jid or from == jid_bare(from_jid)) then return nil; end |
| 391 log("debug", "kicking %s from %s", current_nick, room); | 551 local from_occupant_jid = self:get_occupant_jid(from_jid); |
| 392 self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); | 552 if from_occupant_jid == nil then return nil; end |
| 393 end | 553 local session_jid |
| 394 elseif type == "unavailable" then -- unavailable | 554 for to_jid in occupant:each_session() do |
| 395 if current_nick then | 555 if md5(to_jid) == to_jid_hash then |
| 396 log("debug", "%s leaving %s", current_nick, room); | 556 session_jid = to_jid; |
| 397 self._jid_nick[from] = nil; | 557 break; |
| 398 local occupant = self._occupants[current_nick]; | |
| 399 local new_jid = next(occupant.sessions); | |
| 400 if new_jid == from then new_jid = next(occupant.sessions, new_jid); end | |
| 401 if new_jid then | |
| 402 local jid = occupant.jid; | |
| 403 occupant.jid = new_jid; | |
| 404 occupant.sessions[from] = nil; | |
| 405 pr.attr.to = from; | |
| 406 pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) | |
| 407 :tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up() | |
| 408 :tag("status", {code='110'}):up(); | |
| 409 self:_route_stanza(pr); | |
| 410 if jid ~= new_jid then | |
| 411 pr = st.clone(occupant.sessions[new_jid]) | |
| 412 :tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) | |
| 413 :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none"}); | |
| 414 pr.attr.from = current_nick; | |
| 415 self:broadcast_except_nick(pr, current_nick); | |
| 416 end | |
| 417 else | |
| 418 occupant.role = 'none'; | |
| 419 self:broadcast_presence(pr, from); | |
| 420 self._occupants[current_nick] = nil; | |
| 421 end | 558 end |
| 422 end | 559 end |
| 423 elseif not type then -- available | 560 if session_jid == nil then return nil; end |
| 424 if current_nick then | 561 stanza.attr.from, stanza.attr.to, stanza.attr.id = from_occupant_jid, session_jid, id; |
| 425 --if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence | 562 end |
| 426 if current_nick == to then -- simple presence | 563 log("debug", "%s sent private iq stanza to %s (%s)", from, to, stanza.attr.to); |
| 427 log("debug", "%s broadcasted presence", current_nick); | 564 self:route_stanza(stanza); |
| 428 self._occupants[current_nick].sessions[from] = pr; | 565 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; |
| 429 self:broadcast_presence(pr, from); | 566 return true; |
| 430 else -- change nick | 567 else -- Type is "get" or "set" |
| 431 local occupant = self._occupants[current_nick]; | 568 local current_nick = self:get_occupant_jid(from); |
| 432 local is_multisession = next(occupant.sessions, next(occupant.sessions)); | 569 if not current_nick then |
| 433 if self._occupants[to] or is_multisession then | |
| 434 log("debug", "%s couldn't change nick", current_nick); | |
| 435 local reply = st.error_reply(stanza, "cancel", "conflict"):up(); | |
| 436 reply.tags[1].attr.code = "409"; | |
| 437 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 438 else | |
| 439 local data = self._occupants[current_nick]; | |
| 440 local to_nick = select(3, jid_split(to)); | |
| 441 if to_nick then | |
| 442 log("debug", "%s (%s) changing nick to %s", current_nick, data.jid, to); | |
| 443 local p = st.presence({type='unavailable', from=current_nick}); | |
| 444 self:broadcast_presence(p, from, '303', to_nick); | |
| 445 self._occupants[current_nick] = nil; | |
| 446 self._occupants[to] = data; | |
| 447 self._jid_nick[from] = to; | |
| 448 pr.attr.from = to; | |
| 449 self._occupants[to].sessions[from] = pr; | |
| 450 self:broadcast_presence(pr, from); | |
| 451 else | |
| 452 --TODO malformed-jid | |
| 453 end | |
| 454 end | |
| 455 end | |
| 456 --else -- possible rejoin | |
| 457 -- log("debug", "%s had connection replaced", current_nick); | |
| 458 -- self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}) | |
| 459 -- :tag('status'):text('Replaced by new connection'):up()); -- send unavailable | |
| 460 -- self:handle_to_occupant(origin, stanza); -- resend available | |
| 461 --end | |
| 462 else -- enter room | |
| 463 local new_nick = to; | |
| 464 local is_merge; | |
| 465 if self._occupants[to] then | |
| 466 if jid_bare(from) ~= jid_bare(self._occupants[to].jid) then | |
| 467 new_nick = nil; | |
| 468 end | |
| 469 is_merge = true; | |
| 470 end | |
| 471 local password = stanza:get_child("x", "http://jabber.org/protocol/muc"); | |
| 472 password = password and password:get_child("password", "http://jabber.org/protocol/muc"); | |
| 473 password = password and password[1] ~= "" and password[1]; | |
| 474 if self:get_password() and self:get_password() ~= password then | |
| 475 log("debug", "%s couldn't join due to invalid password: %s", from, to); | |
| 476 local reply = st.error_reply(stanza, "auth", "not-authorized"):up(); | |
| 477 reply.tags[1].attr.code = "401"; | |
| 478 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 479 elseif not new_nick then | |
| 480 log("debug", "%s couldn't join due to nick conflict: %s", from, to); | |
| 481 local reply = st.error_reply(stanza, "cancel", "conflict"):up(); | |
| 482 reply.tags[1].attr.code = "409"; | |
| 483 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 484 else | |
| 485 log("debug", "%s joining as %s", from, to); | |
| 486 if not next(self._affiliations) then -- new room, no owners | |
| 487 self._affiliations[jid_bare(from)] = "owner"; | |
| 488 if self.locked and not stanza:get_child("x", "http://jabber.org/protocol/muc") then | |
| 489 self.locked = nil; -- Older groupchat protocol doesn't lock | |
| 490 end | |
| 491 elseif self.locked then -- Deny entry | |
| 492 origin.send(st.error_reply(stanza, "cancel", "item-not-found")); | |
| 493 return; | |
| 494 end | |
| 495 local affiliation = self:get_affiliation(from); | |
| 496 local role = self:get_default_role(affiliation) | |
| 497 if role then -- new occupant | |
| 498 if not is_merge then | |
| 499 self._occupants[to] = {affiliation=affiliation, role=role, jid=from, sessions={[from]=get_filtered_presence(stanza)}}; | |
| 500 else | |
| 501 self._occupants[to].sessions[from] = get_filtered_presence(stanza); | |
| 502 end | |
| 503 self._jid_nick[from] = to; | |
| 504 self:send_occupant_list(from); | |
| 505 pr.attr.from = to; | |
| 506 pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) | |
| 507 :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up(); | |
| 508 if not is_merge then | |
| 509 self:broadcast_except_nick(pr, to); | |
| 510 end | |
| 511 pr:tag("status", {code='110'}):up(); | |
| 512 if self._data.whois == 'anyone' then | |
| 513 pr:tag("status", {code='100'}):up(); | |
| 514 end | |
| 515 if self.locked then | |
| 516 pr:tag("status", {code='201'}):up(); | |
| 517 end | |
| 518 pr.attr.to = from; | |
| 519 self:_route_stanza(pr); | |
| 520 self:send_history(from, stanza); | |
| 521 self:send_subject(from); | |
| 522 elseif not affiliation then -- registration required for entering members-only room | |
| 523 local reply = st.error_reply(stanza, "auth", "registration-required"):up(); | |
| 524 reply.tags[1].attr.code = "407"; | |
| 525 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 526 else -- banned | |
| 527 local reply = st.error_reply(stanza, "auth", "forbidden"):up(); | |
| 528 reply.tags[1].attr.code = "403"; | |
| 529 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 530 end | |
| 531 end | |
| 532 end | |
| 533 elseif type ~= 'result' then -- bad type | |
| 534 if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences | |
| 535 origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error? | |
| 536 end | |
| 537 end | |
| 538 elseif not current_nick then -- not in room | |
| 539 if (type == "error" or type == "result") and stanza.name == "iq" then | |
| 540 local id = stanza.attr.id; | |
| 541 stanza.attr.from, stanza.attr.to, stanza.attr.id = deconstruct_stanza_id(self, stanza); | |
| 542 if stanza.attr.id then | |
| 543 self:_route_stanza(stanza); | |
| 544 end | |
| 545 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; | |
| 546 elseif type ~= "error" then | |
| 547 origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); | 570 origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); |
| 548 end | 571 return true; |
| 549 elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM | 572 end |
| 573 if not occupant then -- recipient not in room | |
| 574 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); | |
| 575 return true; | |
| 576 end | |
| 577 do -- construct_stanza_id | |
| 578 stanza.attr.id = base64.encode(occupant.jid.."\0"..stanza.attr.id.."\0"..md5(from)); | |
| 579 end | |
| 580 stanza.attr.from, stanza.attr.to = current_nick, occupant.jid; | |
| 581 log("debug", "%s sent private iq stanza to %s (%s)", from, to, occupant.jid); | |
| 582 if stanza.tags[1].attr.xmlns == 'vcard-temp' then | |
| 583 stanza.attr.to = jid_bare(stanza.attr.to); | |
| 584 end | |
| 585 self:route_stanza(stanza); | |
| 586 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; | |
| 587 return true; | |
| 588 end | |
| 589 end | |
| 590 | |
| 591 function room_mt:handle_message_to_occupant(origin, stanza) | |
| 592 local from, to = stanza.attr.from, stanza.attr.to; | |
| 593 local current_nick = self:get_occupant_jid(from); | |
| 594 local type = stanza.attr.type; | |
| 595 if not current_nick then -- not in room | |
| 596 if type ~= "error" then | |
| 597 origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); | |
| 598 end | |
| 599 return true; | |
| 600 end | |
| 601 if type == "groupchat" then -- groupchat messages not allowed in PM | |
| 550 origin.send(st.error_reply(stanza, "modify", "bad-request")); | 602 origin.send(st.error_reply(stanza, "modify", "bad-request")); |
| 551 elseif current_nick and stanza.name == "message" and type == "error" and is_kickable_error(stanza) then | 603 return true; |
| 604 elseif type == "error" and is_kickable_error(stanza) then | |
| 552 log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid); | 605 log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid); |
| 553 self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable | 606 return self:handle_kickable(origin, stanza); -- send unavailable |
| 554 else -- private stanza | 607 end |
| 555 local o_data = self._occupants[to]; | 608 |
| 556 if o_data then | 609 local o_data = self:get_occupant_by_nick(to); |
| 557 log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid); | 610 if not o_data then |
| 558 if stanza.name == "iq" then | 611 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); |
| 559 local id = stanza.attr.id; | 612 return true; |
| 560 if stanza.attr.type == "get" or stanza.attr.type == "set" then | 613 end |
| 561 stanza.attr.from, stanza.attr.to, stanza.attr.id = construct_stanza_id(self, stanza); | 614 log("debug", "%s sent private message stanza to %s (%s)", from, to, o_data.jid); |
| 562 else | 615 stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up(); |
| 563 stanza.attr.from, stanza.attr.to, stanza.attr.id = deconstruct_stanza_id(self, stanza); | 616 stanza.attr.from = current_nick; |
| 564 end | 617 self:route_to_occupant(o_data, stanza) |
| 565 if type == 'get' and stanza.tags[1].attr.xmlns == 'vcard-temp' then | 618 -- TODO: Remove x tag? |
| 566 stanza.attr.to = jid_bare(stanza.attr.to); | 619 stanza.attr.from = from; |
| 567 end | 620 return true; |
| 568 if stanza.attr.id then | |
| 569 self:_route_stanza(stanza); | |
| 570 end | |
| 571 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; | |
| 572 else -- message | |
| 573 stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up(); | |
| 574 stanza.attr.from = current_nick; | |
| 575 for jid in pairs(o_data.sessions) do | |
| 576 stanza.attr.to = jid; | |
| 577 self:_route_stanza(stanza); | |
| 578 end | |
| 579 stanza.attr.from, stanza.attr.to = from, to; | |
| 580 end | |
| 581 elseif type ~= "error" and type ~= "result" then -- recipient not in room | |
| 582 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); | |
| 583 end | |
| 584 end | |
| 585 end | 621 end |
| 586 | 622 |
| 587 function room_mt:send_form(origin, stanza) | 623 function room_mt:send_form(origin, stanza) |
| 588 origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") | 624 origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") |
| 589 :add_child(self:get_form_layout(stanza.attr.from):form()) | 625 :add_child(self:get_form_layout(stanza.attr.from):form()) |
| 596 instructions = "Complete and submit this form to configure the room.", | 632 instructions = "Complete and submit this form to configure the room.", |
| 597 { | 633 { |
| 598 name = 'FORM_TYPE', | 634 name = 'FORM_TYPE', |
| 599 type = 'hidden', | 635 type = 'hidden', |
| 600 value = 'http://jabber.org/protocol/muc#roomconfig' | 636 value = 'http://jabber.org/protocol/muc#roomconfig' |
| 601 }, | |
| 602 { | |
| 603 name = 'muc#roomconfig_roomname', | |
| 604 type = 'text-single', | |
| 605 label = 'Name', | |
| 606 value = self:get_name() or "", | |
| 607 }, | |
| 608 { | |
| 609 name = 'muc#roomconfig_roomdesc', | |
| 610 type = 'text-single', | |
| 611 label = 'Description', | |
| 612 value = self:get_description() or "", | |
| 613 }, | |
| 614 { | |
| 615 name = 'muc#roomconfig_persistentroom', | |
| 616 type = 'boolean', | |
| 617 label = 'Make Room Persistent?', | |
| 618 value = self:get_persistent() | |
| 619 }, | |
| 620 { | |
| 621 name = 'muc#roomconfig_publicroom', | |
| 622 type = 'boolean', | |
| 623 label = 'Make Room Publicly Searchable?', | |
| 624 value = not self:get_hidden() | |
| 625 }, | |
| 626 { | |
| 627 name = 'muc#roomconfig_changesubject', | |
| 628 type = 'boolean', | |
| 629 label = 'Allow Occupants to Change Subject?', | |
| 630 value = self:get_changesubject() | |
| 631 }, | |
| 632 { | |
| 633 name = 'muc#roomconfig_whois', | |
| 634 type = 'list-single', | |
| 635 label = 'Who May Discover Real JIDs?', | |
| 636 value = { | |
| 637 { value = 'moderators', label = 'Moderators Only', default = self._data.whois == 'moderators' }, | |
| 638 { value = 'anyone', label = 'Anyone', default = self._data.whois == 'anyone' } | |
| 639 } | |
| 640 }, | |
| 641 { | |
| 642 name = 'muc#roomconfig_roomsecret', | |
| 643 type = 'text-private', | |
| 644 label = 'Password', | |
| 645 value = self:get_password() or "", | |
| 646 }, | |
| 647 { | |
| 648 name = 'muc#roomconfig_moderatedroom', | |
| 649 type = 'boolean', | |
| 650 label = 'Make Room Moderated?', | |
| 651 value = self:get_moderated() | |
| 652 }, | |
| 653 { | |
| 654 name = 'muc#roomconfig_membersonly', | |
| 655 type = 'boolean', | |
| 656 label = 'Make Room Members-Only?', | |
| 657 value = self:get_members_only() | |
| 658 }, | |
| 659 { | |
| 660 name = 'muc#roomconfig_historylength', | |
| 661 type = 'text-single', | |
| 662 label = 'Maximum Number of History Messages Returned by Room', | |
| 663 value = tostring(self:get_historylength()) | |
| 664 } | 637 } |
| 665 }); | 638 }); |
| 666 return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form; | 639 return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form; |
| 667 end | 640 end |
| 668 | 641 |
| 669 function room_mt:process_form(origin, stanza) | 642 function room_mt:process_form(origin, stanza) |
| 670 local query = stanza.tags[1]; | 643 local form = stanza.tags[1]:get_child("x", "jabber:x:data"); |
| 671 local form; | 644 if form.attr.type == "cancel" then |
| 672 for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end | 645 origin.send(st.reply(stanza)); |
| 673 if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end | 646 elseif form.attr.type == "submit" then |
| 674 if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end | 647 local fields; |
| 675 if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; end | 648 if form.tags[1] == nil then -- Instant room |
| 676 | 649 fields = {}; |
| 677 local fields = self:get_form_layout(stanza.attr.from):data(form); | 650 else |
| 678 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 | 651 fields = self:get_form_layout(stanza.attr.from):data(form); |
| 679 | 652 if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then |
| 680 | 653 origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); |
| 681 local changed = {}; | 654 return true; |
| 682 | 655 end |
| 683 local function handle_option(name, field, allowed) | 656 end |
| 684 local new = fields[field]; | 657 |
| 685 if new == nil then return; end | 658 local event = {room = self; origin = origin; stanza = stanza; fields = fields; status_codes = {};}; |
| 686 if allowed and not allowed[new] then return; end | 659 function event.update_option(name, field, allowed) |
| 687 if new == self["get_"..name](self) then return; end | 660 local new = fields[field]; |
| 688 changed[name] = true; | 661 if new == nil then return; end |
| 689 self["set_"..name](self, new); | 662 if allowed and not allowed[new] then return; end |
| 690 end | 663 if new == self["get_"..name](self) then return; end |
| 691 | 664 event.status_codes["104"] = true; |
| 692 local event = { room = self, fields = fields, changed = changed, stanza = stanza, origin = origin, update_option = handle_option }; | 665 self["set_"..name](self, new); |
| 693 module:fire_event("muc-config-submitted", event); | 666 return true; |
| 694 | 667 end |
| 695 handle_option("name", "muc#roomconfig_roomname"); | 668 module:fire_event("muc-config-submitted", event); |
| 696 handle_option("description", "muc#roomconfig_roomdesc"); | 669 |
| 697 handle_option("persistent", "muc#roomconfig_persistentroom"); | 670 if self.save then self:save(true); end |
| 698 handle_option("moderated", "muc#roomconfig_moderatedroom"); | 671 origin.send(st.reply(stanza)); |
| 699 handle_option("members_only", "muc#roomconfig_membersonly"); | 672 |
| 700 handle_option("public", "muc#roomconfig_publicroom"); | 673 if next(event.status_codes) then |
| 701 handle_option("changesubject", "muc#roomconfig_changesubject"); | 674 local msg = st.message({type='groupchat', from=self.jid}) |
| 702 handle_option("historylength", "muc#roomconfig_historylength"); | 675 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) |
| 703 handle_option("whois", "muc#roomconfig_whois", valid_whois); | 676 for code in pairs(event.status_codes) do |
| 704 handle_option("password", "muc#roomconfig_roomsecret"); | 677 msg:tag("status", {code = code;}):up(); |
| 705 | 678 end |
| 706 if self.save then self:save(true); end | 679 msg:up(); |
| 707 if self.locked then | 680 self:broadcast_message(msg); |
| 708 module:fire_event("muc-room-unlocked", { room = self }); | 681 end |
| 709 self.locked = nil; | 682 else |
| 710 end | 683 origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); |
| 684 end | |
| 685 return true; | |
| 686 end | |
| 687 | |
| 688 -- Removes everyone from the room | |
| 689 function room_mt:clear(x) | |
| 690 x = x or st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'}); | |
| 691 local occupants_updated = {}; | |
| 692 for nick, occupant in self:each_occupant() do | |
| 693 occupant.role = nil; | |
| 694 self:save_occupant(occupant); | |
| 695 occupants_updated[occupant] = true; | |
| 696 end | |
| 697 for occupant in pairs(occupants_updated) do | |
| 698 self:publicise_occupant_status(occupant, x); | |
| 699 module:fire_event("muc-occupant-left", { room = self; nick = occupant.nick; occupant = occupant;}); | |
| 700 end | |
| 701 end | |
| 702 | |
| 703 function room_mt:destroy(newjid, reason, password) | |
| 704 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) | |
| 705 :tag("item", { affiliation='none', role='none' }):up() | |
| 706 :tag("destroy", {jid=newjid}); | |
| 707 if reason then x:tag("reason"):text(reason):up(); end | |
| 708 if password then x:tag("password"):text(password):up(); end | |
| 709 x:up(); | |
| 710 self:clear(x); | |
| 711 module:fire_event("muc-room-destroyed", { room = self }); | |
| 712 end | |
| 713 | |
| 714 function room_mt:handle_disco_info_get_query(origin, stanza) | |
| 715 origin.send(self:get_disco_info(stanza)); | |
| 716 return true; | |
| 717 end | |
| 718 | |
| 719 function room_mt:handle_disco_items_get_query(origin, stanza) | |
| 720 origin.send(self:get_disco_items(stanza)); | |
| 721 return true; | |
| 722 end | |
| 723 | |
| 724 function room_mt:handle_admin_query_set_command(origin, stanza) | |
| 725 local item = stanza.tags[1].tags[1]; | |
| 726 if item.attr.jid then -- Validate provided JID | |
| 727 item.attr.jid = jid_prep(item.attr.jid); | |
| 728 if not item.attr.jid then | |
| 729 origin.send(st.error_reply(stanza, "modify", "jid-malformed")); | |
| 730 return true; | |
| 731 end | |
| 732 end | |
| 733 if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation | |
| 734 local occupant = self:get_occupant_by_nick(self.jid.."/"..item.attr.nick); | |
| 735 if occupant then item.attr.jid = occupant.jid; end | |
| 736 elseif not item.attr.nick and item.attr.jid then | |
| 737 local nick = self:get_occupant_jid(item.attr.jid); | |
| 738 if nick then item.attr.nick = select(3, jid_split(nick)); end | |
| 739 end | |
| 740 local actor = stanza.attr.from; | |
| 741 local reason = item:get_child_text("reason"); | |
| 742 local success, errtype, err | |
| 743 if item.attr.affiliation and item.attr.jid and not item.attr.role then | |
| 744 success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, reason); | |
| 745 elseif item.attr.role and item.attr.nick and not item.attr.affiliation then | |
| 746 success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, reason); | |
| 747 else | |
| 748 success, errtype, err = nil, "cancel", "bad-request"; | |
| 749 end | |
| 750 if not success then origin.send(st.error_reply(stanza, errtype, err)); end | |
| 711 origin.send(st.reply(stanza)); | 751 origin.send(st.reply(stanza)); |
| 712 | 752 return true; |
| 713 if next(changed) then | 753 end |
| 714 local msg = st.message({type='groupchat', from=self.jid}) | 754 |
| 715 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up() | 755 function room_mt:handle_admin_query_get_command(origin, stanza) |
| 716 :tag('status', {code = '104'}):up(); | 756 local actor = stanza.attr.from; |
| 717 if changed.whois then | 757 local affiliation = self:get_affiliation(actor); |
| 718 local code = (self:get_whois() == 'moderators') and "173" or "172"; | 758 local item = stanza.tags[1].tags[1]; |
| 719 msg.tags[1]:tag('status', {code = code}):up(); | 759 local _aff = item.attr.affiliation; |
| 720 end | 760 local _aff_rank = valid_affiliations[_aff or "none"]; |
| 721 self:broadcast_message(msg, false) | 761 local _rol = item.attr.role; |
| 722 end | 762 if _aff and _aff_rank and not _rol then |
| 723 end | 763 -- You need to be at least an admin, and be requesting info about your affifiliation or lower |
| 724 | 764 -- e.g. an admin can't ask for a list of owners |
| 725 function room_mt:destroy(newjid, reason, password) | 765 local affiliation_rank = valid_affiliations[affiliation]; |
| 726 local pr = st.presence({type = "unavailable"}) | 766 if affiliation_rank >= valid_affiliations.admin and affiliation_rank >= _aff_rank then |
| 727 :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) | 767 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); |
| 728 :tag("item", { affiliation='none', role='none' }):up() | 768 for jid in self:each_affiliation(_aff or "none") do |
| 729 :tag("destroy", {jid=newjid}) | 769 reply:tag("item", {affiliation = _aff, jid = jid}):up(); |
| 730 if reason then pr:tag("reason"):text(reason):up(); end | 770 end |
| 731 if password then pr:tag("password"):text(password):up(); end | 771 origin.send(reply:up()); |
| 732 for nick, occupant in pairs(self._occupants) do | 772 return true; |
| 733 pr.attr.from = nick; | 773 else |
| 734 for jid in pairs(occupant.sessions) do | 774 origin.send(st.error_reply(stanza, "auth", "forbidden")); |
| 735 pr.attr.to = jid; | 775 return true; |
| 736 self:_route_stanza(pr); | 776 end |
| 737 self._jid_nick[jid] = nil; | 777 elseif _rol and valid_roles[_rol or "none"] and not _aff then |
| 738 end | 778 local role = self:get_role(self:get_occupant_jid(actor)) or self:get_default_role(affiliation); |
| 739 self._occupants[nick] = nil; | 779 if valid_roles[role or "none"] >= valid_roles.moderator then |
| 740 end | 780 if _rol == "none" then _rol = nil; end |
| 741 self:set_persistent(false); | 781 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); |
| 742 module:fire_event("muc-room-destroyed", { room = self }); | 782 -- TODO: whois check here? (though fully anonymous rooms are not supported) |
| 743 end | 783 for occupant_jid, occupant in self:each_occupant() do |
| 744 | 784 if occupant.role == _rol then |
| 745 function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc | 785 local nick = select(3,jid_split(occupant_jid)); |
| 786 self:build_item_list(occupant, reply, false, nick); | |
| 787 end | |
| 788 end | |
| 789 origin.send(reply:up()); | |
| 790 return true; | |
| 791 else | |
| 792 origin.send(st.error_reply(stanza, "auth", "forbidden")); | |
| 793 return true; | |
| 794 end | |
| 795 else | |
| 796 origin.send(st.error_reply(stanza, "cancel", "bad-request")); | |
| 797 return true; | |
| 798 end | |
| 799 end | |
| 800 | |
| 801 function room_mt:handle_owner_query_get_to_room(origin, stanza) | |
| 802 if self:get_affiliation(stanza.attr.from) ~= "owner" then | |
| 803 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms")); | |
| 804 return true; | |
| 805 end | |
| 806 | |
| 807 self:send_form(origin, stanza); | |
| 808 return true; | |
| 809 end | |
| 810 function room_mt:handle_owner_query_set_to_room(origin, stanza) | |
| 811 if self:get_affiliation(stanza.attr.from) ~= "owner" then | |
| 812 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms")); | |
| 813 return true; | |
| 814 end | |
| 815 | |
| 816 local child = stanza.tags[1].tags[1]; | |
| 817 if not child then | |
| 818 origin.send(st.error_reply(stanza, "modify", "bad-request")); | |
| 819 return true; | |
| 820 elseif child.name == "destroy" then | |
| 821 local newjid = child.attr.jid; | |
| 822 local reason = child:get_child_text("reason"); | |
| 823 local password = child:get_child_text("password"); | |
| 824 self:destroy(newjid, reason, password); | |
| 825 origin.send(st.reply(stanza)); | |
| 826 return true; | |
| 827 elseif child.name == "x" and child.attr.xmlns == "jabber:x:data" then | |
| 828 return self:process_form(origin, stanza); | |
| 829 else | |
| 830 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
| 831 return true; | |
| 832 end | |
| 833 end | |
| 834 | |
| 835 function room_mt:handle_groupchat_to_room(origin, stanza) | |
| 836 local from = stanza.attr.from; | |
| 837 local occupant = self:get_occupant_by_real_jid(from); | |
| 838 if module:fire_event("muc-occupant-groupchat", { | |
| 839 room = self; origin = origin; stanza = stanza; from = from; occupant = occupant; | |
| 840 }) then return true; end | |
| 841 stanza.attr.from = occupant.nick; | |
| 842 self:broadcast_message(stanza); | |
| 843 stanza.attr.from = from; | |
| 844 return true; | |
| 845 end | |
| 846 | |
| 847 -- Role check | |
| 848 module:hook("muc-occupant-groupchat", function(event) | |
| 849 local role_rank = valid_roles[event.occupant and event.occupant.role or "none"]; | |
| 850 if role_rank <= valid_roles.none then | |
| 851 event.origin.send(st.error_reply(event.stanza, "cancel", "not-acceptable")); | |
| 852 return true; | |
| 853 elseif role_rank <= valid_roles.visitor then | |
| 854 event.origin.send(st.error_reply(event.stanza, "auth", "forbidden")); | |
| 855 return true; | |
| 856 end | |
| 857 end, 50); | |
| 858 | |
| 859 -- hack - some buggy clients send presence updates to the room rather than their nick | |
| 860 function room_mt:handle_presence_to_room(origin, stanza) | |
| 861 local current_nick = self:get_occupant_jid(stanza.attr.from); | |
| 862 local handled | |
| 863 if current_nick then | |
| 864 local to = stanza.attr.to; | |
| 865 stanza.attr.to = current_nick; | |
| 866 handled = self:handle_presence_to_occupant(origin, stanza); | |
| 867 stanza.attr.to = to; | |
| 868 end | |
| 869 return handled; | |
| 870 end | |
| 871 | |
| 872 -- Need visitor role or higher to invite | |
| 873 module:hook("muc-pre-invite", function(event) | |
| 874 local room, stanza = event.room, event.stanza; | |
| 875 local _from, _to = stanza.attr.from, stanza.attr.to; | |
| 876 local inviter = room:get_occupant_by_real_jid(_from); | |
| 877 local role = inviter and inviter.role or room:get_default_role(room:get_affiliation(_from)); | |
| 878 if valid_roles[role or "none"] <= valid_roles.visitor then | |
| 879 event.origin.send(st.error_reply(stanza, "auth", "forbidden")); | |
| 880 return true; | |
| 881 end | |
| 882 end); | |
| 883 | |
| 884 function room_mt:handle_mediated_invite(origin, stanza) | |
| 885 local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite"); | |
| 886 local invitee = jid_prep(payload.attr.to); | |
| 887 if not invitee then | |
| 888 origin.send(st.error_reply(stanza, "cancel", "jid-malformed")); | |
| 889 return true; | |
| 890 elseif module:fire_event("muc-pre-invite", {room = self, origin = origin, stanza = stanza}) then | |
| 891 return true; | |
| 892 end | |
| 893 local invite = muc_util.filter_muc_x(st.clone(stanza)); | |
| 894 invite.attr.from = self.jid; | |
| 895 invite.attr.to = invitee; | |
| 896 invite:tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) | |
| 897 :tag('invite', {from = stanza.attr.from;}) | |
| 898 :tag('reason'):text(payload:get_child_text("reason")):up() | |
| 899 :up() | |
| 900 :up(); | |
| 901 if not module:fire_event("muc-invite", {room = self, stanza = invite, origin = origin, incoming = stanza}) then | |
| 902 self:route_stanza(invite); | |
| 903 end | |
| 904 return true; | |
| 905 end | |
| 906 | |
| 907 -- COMPAT: Some older clients expect this | |
| 908 module:hook("muc-invite", function(event) | |
| 909 local room, stanza = event.room, event.stanza; | |
| 910 local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite"); | |
| 911 local reason = invite:get_child_text("reason"); | |
| 912 stanza:tag('x', {xmlns = "jabber:x:conference"; jid = room.jid;}) | |
| 913 :text(reason or "") | |
| 914 :up(); | |
| 915 end); | |
| 916 | |
| 917 -- Add a plain message for clients which don't support invites | |
| 918 module:hook("muc-invite", function(event) | |
| 919 local room, stanza = event.room, event.stanza; | |
| 920 if not stanza:get_child("body") then | |
| 921 local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite"); | |
| 922 local reason = invite:get_child_text("reason") or ""; | |
| 923 stanza:tag("body") | |
| 924 :text(invite.attr.from.." invited you to the room "..room.jid..(reason == "" and (" ("..reason..")") or "")) | |
| 925 :up(); | |
| 926 end | |
| 927 end); | |
| 928 | |
| 929 function room_mt:handle_mediated_decline(origin, stanza) | |
| 930 local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline"); | |
| 931 local declinee = jid_prep(payload.attr.to); | |
| 932 if not declinee then | |
| 933 origin.send(st.error_reply(stanza, "cancel", "jid-malformed")); | |
| 934 return true; | |
| 935 elseif module:fire_event("muc-pre-decline", {room = self, origin = origin, stanza = stanza}) then | |
| 936 return true; | |
| 937 end | |
| 938 local decline = muc_util.filter_muc_x(st.clone(stanza)); | |
| 939 decline.attr.from = self.jid; | |
| 940 decline.attr.to = declinee; | |
| 941 decline:tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) | |
| 942 :tag("decline", {from = stanza.attr.from}) | |
| 943 :tag("reason"):text(payload:get_child_text("reason")):up() | |
| 944 :up() | |
| 945 :up(); | |
| 946 if not module:fire_event("muc-decline", {room = self, stanza = decline, origin = origin, incoming = stanza}) then | |
| 947 local declinee = decline.attr.to; -- re-fetch, in case event modified it | |
| 948 local occupant | |
| 949 if jid_bare(declinee) == self.jid then -- declinee jid is already an in-room jid | |
| 950 occupant = self:get_occupant_by_nick(declinee); | |
| 951 end | |
| 952 if occupant then | |
| 953 self:route_to_occupant(occupant, decline); | |
| 954 else | |
| 955 self:route_stanza(decline); | |
| 956 end | |
| 957 end | |
| 958 return true; | |
| 959 end | |
| 960 | |
| 961 -- Add a plain message for clients which don't support declines | |
| 962 module:hook("muc-decline", function(event) | |
| 963 local room, stanza = event.room, event.stanza; | |
| 964 if not stanza:get_child("body") then | |
| 965 local decline = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline"); | |
| 966 local reason = decline:get_child_text("reason") or ""; | |
| 967 stanza:tag("body") | |
| 968 :text(decline.attr.from.." declined your invite to the room "..room.jid..(reason == "" and (" ("..reason..")") or "")) | |
| 969 :up(); | |
| 970 end | |
| 971 end); | |
| 972 | |
| 973 function room_mt:handle_message_to_room(origin, stanza) | |
| 746 local type = stanza.attr.type; | 974 local type = stanza.attr.type; |
| 747 local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; | 975 if type == "groupchat" then |
| 748 if stanza.name == "iq" then | 976 return self:handle_groupchat_to_room(origin, stanza) |
| 749 if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" and not stanza.tags[1].attr.node then | 977 elseif type == "error" and is_kickable_error(stanza) then |
| 750 origin.send(self:get_disco_info(stanza)); | 978 return self:handle_kickable(origin, stanza) |
| 751 elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" and not stanza.tags[1].attr.node then | 979 elseif type == nil then |
| 752 origin.send(self:get_disco_items(stanza)); | 980 local x = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); |
| 753 elseif xmlns == "http://jabber.org/protocol/muc#admin" then | 981 if x then |
| 754 local actor = stanza.attr.from; | 982 local payload = x.tags[1]; |
| 755 local affiliation = self:get_affiliation(actor); | 983 if payload == nil then |
| 756 local current_nick = self._jid_nick[actor]; | 984 -- fallthrough |
| 757 local role = current_nick and self._occupants[current_nick].role or self:get_default_role(affiliation); | 985 elseif payload.name == "invite" and payload.attr.to then |
| 758 local item = stanza.tags[1].tags[1]; | 986 return self:handle_mediated_invite(origin, stanza) |
| 759 if item and item.name == "item" then | 987 elseif payload.name == "decline" and payload.attr.to then |
| 760 if type == "set" then | 988 return self:handle_mediated_decline(origin, stanza) |
| 761 local callback = function() origin.send(st.reply(stanza)); end | 989 end |
| 762 if item.attr.jid then -- Validate provided JID | |
| 763 item.attr.jid = jid_prep(item.attr.jid); | |
| 764 if not item.attr.jid then | |
| 765 origin.send(st.error_reply(stanza, "modify", "jid-malformed")); | |
| 766 return; | |
| 767 end | |
| 768 end | |
| 769 if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation | |
| 770 local occupant = self._occupants[self.jid.."/"..item.attr.nick]; | |
| 771 if occupant then item.attr.jid = occupant.jid; end | |
| 772 elseif not item.attr.nick and item.attr.jid then | |
| 773 local nick = self._jid_nick[item.attr.jid]; | |
| 774 if nick then item.attr.nick = select(3, jid_split(nick)); end | |
| 775 end | |
| 776 local reason = item.tags[1] and item.tags[1].name == "reason" and #item.tags[1] == 1 and item.tags[1][1]; | |
| 777 if item.attr.affiliation and item.attr.jid and not item.attr.role then | |
| 778 local success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, callback, reason); | |
| 779 if not success then origin.send(st.error_reply(stanza, errtype, err)); end | |
| 780 elseif item.attr.role and item.attr.nick and not item.attr.affiliation then | |
| 781 local success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, callback, reason); | |
| 782 if not success then origin.send(st.error_reply(stanza, errtype, err)); end | |
| 783 else | |
| 784 origin.send(st.error_reply(stanza, "cancel", "bad-request")); | |
| 785 end | |
| 786 elseif type == "get" then | |
| 787 local _aff = item.attr.affiliation; | |
| 788 local _rol = item.attr.role; | |
| 789 if _aff and not _rol then | |
| 790 if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then | |
| 791 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); | |
| 792 for jid, affiliation in pairs(self._affiliations) do | |
| 793 if affiliation == _aff then | |
| 794 reply:tag("item", {affiliation = _aff, jid = jid}):up(); | |
| 795 end | |
| 796 end | |
| 797 origin.send(reply); | |
| 798 else | |
| 799 origin.send(st.error_reply(stanza, "auth", "forbidden")); | |
| 800 end | |
| 801 elseif _rol and not _aff then | |
| 802 if role == "moderator" then | |
| 803 -- TODO allow admins and owners not in room? Provide read-only access to everyone who can see the participants anyway? | |
| 804 if _rol == "none" then _rol = nil; end | |
| 805 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); | |
| 806 for occupant_jid, occupant in pairs(self._occupants) do | |
| 807 if occupant.role == _rol then | |
| 808 reply:tag("item", { | |
| 809 nick = select(3, jid_split(occupant_jid)), | |
| 810 role = _rol or "none", | |
| 811 affiliation = occupant.affiliation or "none", | |
| 812 jid = occupant.jid | |
| 813 }):up(); | |
| 814 end | |
| 815 end | |
| 816 origin.send(reply); | |
| 817 else | |
| 818 origin.send(st.error_reply(stanza, "auth", "forbidden")); | |
| 819 end | |
| 820 else | |
| 821 origin.send(st.error_reply(stanza, "cancel", "bad-request")); | |
| 822 end | |
| 823 end | |
| 824 elseif type == "set" or type == "get" then | |
| 825 origin.send(st.error_reply(stanza, "cancel", "bad-request")); | |
| 826 end | |
| 827 elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then | |
| 828 if self:get_affiliation(stanza.attr.from) ~= "owner" then | |
| 829 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms")); | |
| 830 elseif stanza.attr.type == "get" then | |
| 831 self:send_form(origin, stanza); | |
| 832 elseif stanza.attr.type == "set" then | |
| 833 local child = stanza.tags[1].tags[1]; | |
| 834 if not child then | |
| 835 origin.send(st.error_reply(stanza, "modify", "bad-request")); | |
| 836 elseif child.name == "destroy" then | |
| 837 local newjid = child.attr.jid; | |
| 838 local reason, password; | |
| 839 for _,tag in ipairs(child.tags) do | |
| 840 if tag.name == "reason" then | |
| 841 reason = #tag.tags == 0 and tag[1]; | |
| 842 elseif tag.name == "password" then | |
| 843 password = #tag.tags == 0 and tag[1]; | |
| 844 end | |
| 845 end | |
| 846 self:destroy(newjid, reason, password); | |
| 847 origin.send(st.reply(stanza)); | |
| 848 else | |
| 849 self:process_form(origin, stanza); | |
| 850 end | |
| 851 end | |
| 852 elseif type == "set" or type == "get" then | |
| 853 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
| 854 end | |
| 855 elseif stanza.name == "message" and type == "groupchat" then | |
| 856 local from = stanza.attr.from; | |
| 857 local current_nick = self._jid_nick[from]; | |
| 858 local occupant = self._occupants[current_nick]; | |
| 859 if not occupant then -- not in room | |
| 860 origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); | |
| 861 elseif occupant.role == "visitor" then | |
| 862 origin.send(st.error_reply(stanza, "auth", "forbidden")); | |
| 863 else | |
| 864 local from = stanza.attr.from; | |
| 865 stanza.attr.from = current_nick; | |
| 866 local subject = stanza:get_child_text("subject"); | |
| 867 if subject then | |
| 868 if occupant.role == "moderator" or | |
| 869 ( self._data.changesubject and occupant.role == "participant" ) then -- and participant | |
| 870 self:set_subject(current_nick, subject); | |
| 871 else | |
| 872 stanza.attr.from = from; | |
| 873 origin.send(st.error_reply(stanza, "auth", "forbidden")); | |
| 874 end | |
| 875 else | |
| 876 self:broadcast_message(stanza, self:get_historylength() > 0 and stanza:get_child("body")); | |
| 877 end | |
| 878 stanza.attr.from = from; | |
| 879 end | |
| 880 elseif stanza.name == "message" and type == "error" and is_kickable_error(stanza) then | |
| 881 local current_nick = self._jid_nick[stanza.attr.from]; | |
| 882 log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid); | |
| 883 self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable | |
| 884 elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick | |
| 885 local to = stanza.attr.to; | |
| 886 local current_nick = self._jid_nick[stanza.attr.from]; | |
| 887 if current_nick then | |
| 888 stanza.attr.to = current_nick; | |
| 889 self:handle_to_occupant(origin, stanza); | |
| 890 stanza.attr.to = to; | |
| 891 elseif type ~= "error" and type ~= "result" then | |
| 892 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
| 893 end | |
| 894 elseif stanza.name == "message" and not(type == "chat" or type == "error" or type == "groupchat" or type == "headline") and #stanza.tags == 1 | |
| 895 and self._jid_nick[stanza.attr.from] and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" then | |
| 896 local x = stanza.tags[1]; | |
| 897 local payload = (#x.tags == 1 and x.tags[1]); | |
| 898 if payload and payload.name == "invite" and payload.attr.to then | |
| 899 local _from, _to = stanza.attr.from, stanza.attr.to; | |
| 900 local _invitee = jid_prep(payload.attr.to); | |
| 901 if _invitee then | |
| 902 local _reason = payload.tags[1] and payload.tags[1].name == 'reason' and #payload.tags[1].tags == 0 and payload.tags[1][1]; | |
| 903 local invite = st.message({from = _to, to = _invitee, id = stanza.attr.id}) | |
| 904 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) | |
| 905 :tag('invite', {from=_from}) | |
| 906 :tag('reason'):text(_reason or ""):up() | |
| 907 :up(); | |
| 908 if self:get_password() then | |
| 909 invite:tag("password"):text(self:get_password()):up(); | |
| 910 end | |
| 911 invite:up() | |
| 912 :tag('x', {xmlns="jabber:x:conference", jid=_to}) -- COMPAT: Some older clients expect this | |
| 913 :text(_reason or "") | |
| 914 :up() | |
| 915 :tag('body') -- Add a plain message for clients which don't support invites | |
| 916 :text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or "")) | |
| 917 :up(); | |
| 918 if self:get_members_only() and not self:get_affiliation(_invitee) then | |
| 919 log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to); | |
| 920 self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from]) | |
| 921 end | |
| 922 self:_route_stanza(invite); | |
| 923 else | |
| 924 origin.send(st.error_reply(stanza, "cancel", "jid-malformed")); | |
| 925 end | |
| 926 else | |
| 927 origin.send(st.error_reply(stanza, "cancel", "bad-request")); | 990 origin.send(st.error_reply(stanza, "cancel", "bad-request")); |
| 928 end | 991 return true; |
| 929 else | 992 end |
| 930 if type == "error" or type == "result" then return; end | 993 end |
| 931 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); | 994 end |
| 932 end | 995 |
| 933 end | 996 function room_mt:route_stanza(stanza) |
| 934 | 997 module:send(stanza); |
| 935 function room_mt:handle_stanza(origin, stanza) | 998 end |
| 936 local to_node, to_host, to_resource = jid_split(stanza.attr.to); | |
| 937 if to_resource then | |
| 938 self:handle_to_occupant(origin, stanza); | |
| 939 else | |
| 940 self:handle_to_room(origin, stanza); | |
| 941 end | |
| 942 end | |
| 943 | |
| 944 function room_mt:route_stanza(stanza) end -- Replace with a routing function, e.g., function(room, stanza) core_route_stanza(origin, stanza); end | |
| 945 | 999 |
| 946 function room_mt:get_affiliation(jid) | 1000 function room_mt:get_affiliation(jid) |
| 947 local node, host, resource = jid_split(jid); | 1001 local node, host, resource = jid_split(jid); |
| 948 local bare = node and node.."@"..host or host; | 1002 local bare = node and node.."@"..host or host; |
| 949 local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID. | 1003 local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID. |
| 950 if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned | 1004 if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned |
| 951 return result; | 1005 return result; |
| 952 end | 1006 end |
| 953 function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) | 1007 |
| 954 jid = jid_bare(jid); | 1008 -- Iterates over jid, affiliation pairs |
| 955 if affiliation == "none" then affiliation = nil; end | 1009 function room_mt:each_affiliation(with_affiliation) |
| 956 if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then | 1010 if not with_affiliation then |
| 1011 return pairs(self._affiliations); | |
| 1012 else | |
| 1013 return function(_affiliations, jid) | |
| 1014 local affiliation; | |
| 1015 repeat -- Iterate until we get a match | |
| 1016 jid, affiliation = next(_affiliations, jid); | |
| 1017 until jid == nil or affiliation == with_affiliation | |
| 1018 return jid, affiliation; | |
| 1019 end, self._affiliations, nil | |
| 1020 end | |
| 1021 end | |
| 1022 | |
| 1023 function room_mt:set_affiliation(actor, jid, affiliation, reason) | |
| 1024 if not actor then return nil, "modify", "not-acceptable"; end; | |
| 1025 | |
| 1026 local node, host, resource = jid_split(jid); | |
| 1027 if not host then return nil, "modify", "not-acceptable"; end | |
| 1028 jid = jid_join(node, host); -- Bare | |
| 1029 local is_host_only = node == nil; | |
| 1030 | |
| 1031 if valid_affiliations[affiliation or "none"] == nil then | |
| 957 return nil, "modify", "not-acceptable"; | 1032 return nil, "modify", "not-acceptable"; |
| 958 end | 1033 end |
| 959 if actor ~= true then | 1034 affiliation = affiliation ~= "none" and affiliation or nil; -- coerces `affiliation == false` to `nil` |
| 1035 | |
| 1036 local target_affiliation = self._affiliations[jid]; -- Raw; don't want to check against host | |
| 1037 local is_downgrade = valid_affiliations[target_affiliation or "none"] > valid_affiliations[affiliation or "none"]; | |
| 1038 | |
| 1039 if actor == true then | |
| 1040 actor = nil -- So we can pass it safely to 'publicise_occupant_status' below | |
| 1041 else | |
| 960 local actor_affiliation = self:get_affiliation(actor); | 1042 local actor_affiliation = self:get_affiliation(actor); |
| 961 local target_affiliation = self:get_affiliation(jid); | 1043 if actor_affiliation == "owner" then |
| 962 if target_affiliation == affiliation then -- no change, shortcut | 1044 if jid_bare(actor) == jid then -- self change |
| 963 if callback then callback(); end | 1045 -- need at least one owner |
| 964 return true; | 1046 local is_last = true; |
| 965 end | 1047 for j in self:each_affiliation("owner") do |
| 966 if actor_affiliation ~= "owner" then | 1048 if j ~= jid then is_last = false; break; end |
| 967 if affiliation == "owner" or affiliation == "admin" or actor_affiliation ~= "admin" or target_affiliation == "owner" or target_affiliation == "admin" then | 1049 end |
| 968 return nil, "cancel", "not-allowed"; | 1050 if is_last then |
| 969 end | 1051 return nil, "cancel", "conflict"; |
| 970 elseif target_affiliation == "owner" and jid_bare(actor) == jid then -- self change | 1052 end |
| 971 local is_last = true; | 1053 end |
| 972 for j, aff in pairs(self._affiliations) do if j ~= jid and aff == "owner" then is_last = false; break; end end | 1054 -- owners can do anything else |
| 973 if is_last then | 1055 elseif affiliation == "owner" or affiliation == "admin" |
| 974 return nil, "cancel", "conflict"; | 1056 or actor_affiliation ~= "admin" |
| 975 end | 1057 or target_affiliation == "owner" or target_affiliation == "admin" then |
| 976 end | 1058 -- Can't demote owners or other admins |
| 977 end | 1059 return nil, "cancel", "not-allowed"; |
| 1060 end | |
| 1061 end | |
| 1062 | |
| 1063 -- Set in 'database' | |
| 978 self._affiliations[jid] = affiliation; | 1064 self._affiliations[jid] = affiliation; |
| 1065 | |
| 1066 -- Update roles | |
| 979 local role = self:get_default_role(affiliation); | 1067 local role = self:get_default_role(affiliation); |
| 980 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) | 1068 local role_rank = valid_roles[role or "none"]; |
| 981 :tag("item", {affiliation=affiliation or "none", role=role or "none"}) | 1069 local occupants_updated = {}; -- Filled with old roles |
| 982 :tag("reason"):text(reason or ""):up() | 1070 for nick, occupant in self:each_occupant() do |
| 983 :up(); | 1071 if occupant.bare_jid == jid or ( |
| 984 local presence_type = nil; | 1072 -- Outcast can be by host. |
| 1073 is_host_only and affiliation == "outcast" and select(2, jid_split(occupant.bare_jid)) == host | |
| 1074 ) then | |
| 1075 -- need to publcize in all cases; as affiliation in <item/> has changed. | |
| 1076 occupants_updated[occupant] = occupant.role; | |
| 1077 if occupant.role ~= role and ( | |
| 1078 is_downgrade or | |
| 1079 valid_roles[occupant.role or "none"] < role_rank -- upgrade | |
| 1080 ) then | |
| 1081 occupant.role = role; | |
| 1082 self:save_occupant(occupant); | |
| 1083 end | |
| 1084 end | |
| 1085 end | |
| 1086 | |
| 1087 -- Tell the room of the new occupant affiliations+roles | |
| 1088 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}); | |
| 985 if not role then -- getting kicked | 1089 if not role then -- getting kicked |
| 986 presence_type = "unavailable"; | |
| 987 if affiliation == "outcast" then | 1090 if affiliation == "outcast" then |
| 988 x:tag("status", {code="301"}):up(); -- banned | 1091 x:tag("status", {code="301"}):up(); -- banned |
| 989 else | 1092 else |
| 990 x:tag("status", {code="321"}):up(); -- affiliation change | 1093 x:tag("status", {code="321"}):up(); -- affiliation change |
| 991 end | 1094 end |
| 992 end | 1095 end |
| 993 local modified_nicks = {}; | 1096 local is_semi_anonymous = self:get_whois() == "moderators"; |
| 994 for nick, occupant in pairs(self._occupants) do | 1097 for occupant, old_role in pairs(occupants_updated) do |
| 995 if jid_bare(occupant.jid) == jid then | 1098 self:publicise_occupant_status(occupant, x, nil, actor, reason); |
| 996 if not role then -- getting kicked | 1099 if occupant.role == nil then |
| 997 self._occupants[nick] = nil; | 1100 module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;}); |
| 998 else | 1101 elseif is_semi_anonymous and |
| 999 occupant.affiliation, occupant.role = affiliation, role; | 1102 (old_role == "moderator" and occupant.role ~= "moderator") or |
| 1000 end | 1103 (old_role ~= "moderator" and occupant.role == "moderator") then -- Has gained or lost moderator status |
| 1001 for jid,pres in pairs(occupant.sessions) do -- remove for all sessions of the nick | 1104 -- Send everyone else's presences (as jid visibility has changed) |
| 1002 if not role then self._jid_nick[jid] = nil; end | 1105 for real_jid in occupant:each_session() do |
| 1003 local p = st.clone(pres); | 1106 self:send_occupant_list(real_jid, function(occupant_jid, occupant) |
| 1004 p.attr.from = nick; | 1107 return occupant.bare_jid ~= jid; |
| 1005 p.attr.type = presence_type; | 1108 end); |
| 1006 p.attr.to = jid; | 1109 end |
| 1007 p:add_child(x); | 1110 end |
| 1008 self:_route_stanza(p); | 1111 end |
| 1009 if occupant.jid == jid then | 1112 |
| 1010 modified_nicks[nick] = p; | |
| 1011 end | |
| 1012 end | |
| 1013 end | |
| 1014 end | |
| 1015 if self.save then self:save(); end | 1113 if self.save then self:save(); end |
| 1016 if callback then callback(); end | 1114 |
| 1017 for nick,p in pairs(modified_nicks) do | 1115 module:fire_event("muc-set-affiliation", { |
| 1018 p.attr.from = nick; | 1116 room = self; |
| 1019 self:broadcast_except_nick(p, nick); | 1117 actor = actor; |
| 1020 end | 1118 jid = jid; |
| 1119 affiliation = affiliation or "none"; | |
| 1120 reason = reason; | |
| 1121 previous_affiliation = target_affiliation; | |
| 1122 in_room = next(occupants_updated) ~= nil; | |
| 1123 }); | |
| 1124 | |
| 1021 return true; | 1125 return true; |
| 1022 end | 1126 end |
| 1023 | 1127 |
| 1024 function room_mt:get_role(nick) | 1128 function room_mt:get_role(nick) |
| 1025 local session = self._occupants[nick]; | 1129 local occupant = self:get_occupant_by_nick(nick); |
| 1026 return session and session.role or nil; | 1130 return occupant and occupant.role or nil; |
| 1027 end | 1131 end |
| 1028 function room_mt:can_set_role(actor_jid, occupant_jid, role) | 1132 |
| 1029 local occupant = self._occupants[occupant_jid]; | 1133 function room_mt:set_role(actor, occupant_jid, role, reason) |
| 1030 if not occupant or not actor_jid then return nil, "modify", "not-acceptable"; end | 1134 if not actor then return nil, "modify", "not-acceptable"; end |
| 1031 | 1135 |
| 1032 if actor_jid == true then return true; end | 1136 local occupant = self:get_occupant_by_nick(occupant_jid); |
| 1033 | 1137 if not occupant then return nil, "modify", "not-acceptable"; end |
| 1034 local actor = self._occupants[self._jid_nick[actor_jid]]; | 1138 |
| 1035 if actor and actor.role == "moderator" then | 1139 if valid_roles[role or "none"] == nil then |
| 1036 if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then | 1140 return nil, "modify", "not-acceptable"; |
| 1037 if actor.affiliation == "owner" or actor.affiliation == "admin" then | 1141 end |
| 1038 return true; | 1142 role = role ~= "none" and role or nil; -- coerces `role == false` to `nil` |
| 1039 elseif occupant.role ~= "moderator" and role ~= "moderator" then | 1143 |
| 1040 return true; | 1144 if actor == true then |
| 1041 end | 1145 actor = nil -- So we can pass it safely to 'publicise_occupant_status' below |
| 1042 end | 1146 else |
| 1043 end | 1147 -- Can't do anything to other owners or admins |
| 1044 return nil, "cancel", "not-allowed"; | 1148 local occupant_affiliation = self:get_affiliation(occupant.bare_jid); |
| 1045 end | 1149 if occupant_affiliation == "owner" and occupant_affiliation == "admin" then |
| 1046 function room_mt:set_role(actor, occupant_jid, role, callback, reason) | 1150 return nil, "cancel", "not-allowed"; |
| 1047 if role == "none" then role = nil; end | 1151 end |
| 1048 if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end | 1152 |
| 1049 local allowed, err_type, err_condition = self:can_set_role(actor, occupant_jid, role); | 1153 -- If you are trying to give or take moderator role you need to be an owner or admin |
| 1050 if not allowed then return allowed, err_type, err_condition; end | 1154 if occupant.role == "moderator" or role == "moderator" then |
| 1051 local occupant = self._occupants[occupant_jid]; | 1155 local actor_affiliation = self:get_affiliation(actor); |
| 1052 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) | 1156 if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then |
| 1053 :tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"}) | 1157 return nil, "cancel", "not-allowed"; |
| 1054 :tag("reason"):text(reason or ""):up() | 1158 end |
| 1055 :up(); | 1159 end |
| 1056 local presence_type = nil; | 1160 |
| 1057 if not role then -- kick | 1161 -- Need to be in the room and a moderator |
| 1058 presence_type = "unavailable"; | 1162 local actor_occupant = self:get_occupant_by_real_jid(actor); |
| 1059 self._occupants[occupant_jid] = nil; | 1163 if not actor_occupant or actor_occupant.role ~= "moderator" then |
| 1060 for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick | 1164 return nil, "cancel", "not-allowed"; |
| 1061 self._jid_nick[jid] = nil; | 1165 end |
| 1062 end | 1166 end |
| 1167 | |
| 1168 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}); | |
| 1169 if not role then | |
| 1063 x:tag("status", {code = "307"}):up(); | 1170 x:tag("status", {code = "307"}):up(); |
| 1064 else | 1171 end |
| 1065 occupant.role = role; | 1172 occupant.role = role; |
| 1066 end | 1173 self:save_occupant(occupant); |
| 1067 local bp; | 1174 self:publicise_occupant_status(occupant, x, nil, actor, reason); |
| 1068 for jid,pres in pairs(occupant.sessions) do -- send to all sessions of the nick | 1175 if role == nil then |
| 1069 local p = st.clone(pres); | 1176 module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;}); |
| 1070 p.attr.from = occupant_jid; | 1177 end |
| 1071 p.attr.type = presence_type; | 1178 return true; |
| 1072 p.attr.to = jid; | 1179 end |
| 1073 p:add_child(x); | 1180 |
| 1074 self:_route_stanza(p); | 1181 local affiliation_notify = module:require "muc/affiliation_notify"; |
| 1075 if occupant.jid == jid then | 1182 |
| 1076 bp = p; | 1183 local name = module:require "muc/name"; |
| 1077 end | 1184 room_mt.get_name = name.get; |
| 1078 end | 1185 room_mt.set_name = name.set; |
| 1079 if callback then callback(); end | 1186 |
| 1080 if bp then | 1187 local description = module:require "muc/description"; |
| 1081 self:broadcast_except_nick(bp, occupant_jid); | 1188 room_mt.get_description = description.get; |
| 1082 end | 1189 room_mt.set_description = description.set; |
| 1083 return true; | 1190 |
| 1084 end | 1191 local hidden = module:require "muc/hidden"; |
| 1085 | 1192 room_mt.get_hidden = hidden.get; |
| 1086 function room_mt:_route_stanza(stanza) | 1193 room_mt.set_hidden = hidden.set; |
| 1087 local muc_child; | 1194 function room_mt:get_public() |
| 1088 local to_occupant = self._occupants[self._jid_nick[stanza.attr.to]]; | 1195 return not self:get_hidden(); |
| 1089 local from_occupant = self._occupants[stanza.attr.from]; | 1196 end |
| 1090 if stanza.name == "presence" then | 1197 function room_mt:set_public(public) |
| 1091 if to_occupant and from_occupant then | 1198 return self:set_hidden(not public); |
| 1092 if self._data.whois == 'anyone' then | 1199 end |
| 1093 muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); | 1200 |
| 1094 else | 1201 local password = module:require "muc/password"; |
| 1095 if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then | 1202 room_mt.get_password = password.get; |
| 1096 muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); | 1203 room_mt.set_password = password.set; |
| 1097 end | 1204 |
| 1098 end | 1205 local whois = module:require "muc/whois"; |
| 1099 end | 1206 room_mt.get_whois = whois.get; |
| 1100 end | 1207 room_mt.set_whois = whois.set; |
| 1101 if muc_child then | 1208 |
| 1102 for _, item in pairs(muc_child.tags) do | 1209 local members_only = module:require "muc/members_only"; |
| 1103 if item.name == "item" then | 1210 room_mt.get_members_only = members_only.get; |
| 1104 if from_occupant == to_occupant then | 1211 room_mt.set_members_only = members_only.set; |
| 1105 item.attr.jid = stanza.attr.to; | 1212 |
| 1106 else | 1213 local moderated = module:require "muc/moderated"; |
| 1107 item.attr.jid = from_occupant.jid; | 1214 room_mt.get_moderated = moderated.get; |
| 1108 end | 1215 room_mt.set_moderated = moderated.set; |
| 1109 end | 1216 |
| 1110 end | 1217 local persistent = module:require "muc/persistent"; |
| 1111 end | 1218 room_mt.get_persistent = persistent.get; |
| 1112 self:route_stanza(stanza); | 1219 room_mt.set_persistent = persistent.set; |
| 1113 if muc_child then | 1220 |
| 1114 for _, item in pairs(muc_child.tags) do | 1221 local subject = module:require "muc/subject"; |
| 1115 if item.name == "item" then | 1222 room_mt.get_changesubject = subject.get_changesubject; |
| 1116 item.attr.jid = nil; | 1223 room_mt.set_changesubject = subject.set_changesubject; |
| 1117 end | 1224 room_mt.get_subject = subject.get; |
| 1118 end | 1225 room_mt.set_subject = subject.set; |
| 1119 end | 1226 room_mt.send_subject = subject.send; |
| 1120 end | 1227 |
| 1228 local history = module:require "muc/history"; | |
| 1229 room_mt.send_history = history.send; | |
| 1230 room_mt.get_historylength = history.get_length; | |
| 1231 room_mt.set_historylength = history.set_length; | |
| 1121 | 1232 |
| 1122 local _M = {}; -- module "muc" | 1233 local _M = {}; -- module "muc" |
| 1123 | 1234 |
| 1124 function _M.new_room(jid, config) | 1235 function _M.new_room(jid, config) |
| 1125 return setmetatable({ | 1236 return setmetatable({ |
| 1126 jid = jid; | 1237 jid = jid; |
| 1127 _jid_nick = {}; | 1238 _jid_nick = {}; |
| 1128 _occupants = {}; | 1239 _occupants = {}; |
| 1129 _data = { | 1240 _data = { |
| 1130 whois = 'moderators'; | |
| 1131 history_length = math.min((config and config.history_length) | |
| 1132 or default_history_length, max_history_length); | |
| 1133 }; | 1241 }; |
| 1134 _affiliations = {}; | 1242 _affiliations = {}; |
| 1135 }, room_mt); | 1243 }, room_mt); |
| 1136 end | 1244 end |
| 1137 | 1245 |
| 1138 function _M.set_max_history_length(_max_history_length) | |
| 1139 max_history_length = _max_history_length or math.huge; | |
| 1140 end | |
| 1141 | |
| 1142 _M.room_mt = room_mt; | 1246 _M.room_mt = room_mt; |
| 1143 | 1247 |
| 1144 return _M; | 1248 return _M; |
