Software / code / prosody
Comparison
plugins/muc/muc.lib.lua @ 6144:cb08bba0443a
Merge with daurnimator
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Thu, 17 Apr 2014 09:01:32 +0100 |
| parent | 6054:7a5ddbaf758d |
| parent | 6143:82b3a2155a55 |
| child | 6178:e12b13a46878 |
| child | 6232:d7dc71d9171d |
comparison
equal
deleted
inserted
replaced
| 6055:596539a30e9b | 6144:cb08bba0443a |
|---|---|
| 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 |
| 13 local gettime = os.time; | |
| 12 local datetime = require "util.datetime"; | 14 local datetime = require "util.datetime"; |
| 13 | 15 |
| 14 local dataform = require "util.dataforms"; | 16 local dataform = require "util.dataforms"; |
| 15 | 17 |
| 16 local jid_split = require "util.jid".split; | 18 local jid_split = require "util.jid".split; |
| 21 local t_insert, t_remove = table.insert, table.remove; | 23 local t_insert, t_remove = table.insert, table.remove; |
| 22 local setmetatable = setmetatable; | 24 local setmetatable = setmetatable; |
| 23 local base64 = require "util.encodings".base64; | 25 local base64 = require "util.encodings".base64; |
| 24 local md5 = require "util.hashes".md5; | 26 local md5 = require "util.hashes".md5; |
| 25 | 27 |
| 26 local muc_domain = nil; --module:get_host(); | |
| 27 local default_history_length, max_history_length = 20, math.huge; | 28 local default_history_length, max_history_length = 20, math.huge; |
| 28 | 29 |
| 29 ------------ | 30 local get_filtered_presence do |
| 30 local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; | 31 local presence_filters = { |
| 31 local function presence_filter(tag) | 32 ["http://jabber.org/protocol/muc"] = true; |
| 32 if presence_filters[tag.attr.xmlns] then | 33 ["http://jabber.org/protocol/muc#user"] = true; |
| 33 return nil; | 34 } |
| 34 end | 35 local function presence_filter(tag) |
| 35 return tag; | 36 if presence_filters[tag.attr.xmlns] then |
| 36 end | 37 return nil; |
| 37 | 38 end |
| 38 local function get_filtered_presence(stanza) | 39 return tag; |
| 39 return st.clone(stanza):maptags(presence_filter); | 40 end |
| 40 end | 41 function get_filtered_presence(stanza) |
| 41 local kickable_error_conditions = { | 42 return st.clone(stanza):maptags(presence_filter); |
| 42 ["gone"] = true; | 43 end |
| 43 ["internal-server-error"] = true; | 44 end |
| 44 ["item-not-found"] = true; | 45 |
| 45 ["jid-malformed"] = true; | 46 local is_kickable_error do |
| 46 ["recipient-unavailable"] = true; | 47 local kickable_error_conditions = { |
| 47 ["redirect"] = true; | 48 ["gone"] = true; |
| 48 ["remote-server-not-found"] = true; | 49 ["internal-server-error"] = true; |
| 49 ["remote-server-timeout"] = true; | 50 ["item-not-found"] = true; |
| 50 ["service-unavailable"] = true; | 51 ["jid-malformed"] = true; |
| 51 ["malformed error"] = true; | 52 ["recipient-unavailable"] = true; |
| 52 }; | 53 ["redirect"] = true; |
| 53 | 54 ["remote-server-not-found"] = true; |
| 54 local function get_error_condition(stanza) | 55 ["remote-server-timeout"] = true; |
| 55 local _, condition = stanza:get_error(); | 56 ["service-unavailable"] = true; |
| 56 return condition or "malformed error"; | 57 ["malformed error"] = true; |
| 57 end | 58 }; |
| 58 | 59 function is_kickable_error(stanza) |
| 59 local function is_kickable_error(stanza) | 60 local cond = select(2, stanza:get_error()) or "malformed error"; |
| 60 local cond = get_error_condition(stanza); | 61 return kickable_error_conditions[cond]; |
| 61 return kickable_error_conditions[cond] and cond; | 62 end |
| 62 end | 63 end |
| 63 ----------- | |
| 64 | 64 |
| 65 local room_mt = {}; | 65 local room_mt = {}; |
| 66 room_mt.__index = room_mt; | 66 room_mt.__index = room_mt; |
| 67 | 67 |
| 68 function room_mt:__tostring() | 68 function room_mt:__tostring() |
| 69 return "MUC room ("..self.jid..")"; | 69 return "MUC room ("..self.jid..")"; |
| 70 end | |
| 71 | |
| 72 function room_mt:get_occupant_jid(real_jid) | |
| 73 return self._jid_nick[real_jid] | |
| 70 end | 74 end |
| 71 | 75 |
| 72 function room_mt:get_default_role(affiliation) | 76 function room_mt:get_default_role(affiliation) |
| 73 if affiliation == "owner" or affiliation == "admin" then | 77 if affiliation == "owner" or affiliation == "admin" then |
| 74 return "moderator"; | 78 return "moderator"; |
| 79 return self:get_moderated() and "visitor" or "participant"; | 83 return self:get_moderated() and "visitor" or "participant"; |
| 80 end | 84 end |
| 81 end | 85 end |
| 82 end | 86 end |
| 83 | 87 |
| 88 function room_mt:lock() | |
| 89 self.locked = true | |
| 90 end | |
| 91 function room_mt:unlock() | |
| 92 module:fire_event("muc-room-unlocked", { room = self }); | |
| 93 self.locked = nil | |
| 94 end | |
| 95 function room_mt:is_locked() | |
| 96 return not not self.locked | |
| 97 end | |
| 98 | |
| 99 function room_mt:route_to_occupant(o_data, stanza) | |
| 100 local to = stanza.attr.to; | |
| 101 for jid in pairs(o_data.sessions) do | |
| 102 stanza.attr.to = jid; | |
| 103 self:_route_stanza(stanza); | |
| 104 end | |
| 105 stanza.attr.to = to; | |
| 106 end | |
| 107 | |
| 84 function room_mt:broadcast_presence(stanza, sid, code, nick) | 108 function room_mt:broadcast_presence(stanza, sid, code, nick) |
| 85 stanza = get_filtered_presence(stanza); | 109 stanza = get_filtered_presence(stanza); |
| 86 local occupant = self._occupants[stanza.attr.from]; | 110 local occupant = self._occupants[stanza.attr.from]; |
| 87 stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) | 111 stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) |
| 88 :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none", nick=nick}):up(); | 112 :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none", nick=nick}):up(); |
| 89 if code then | 113 if code then |
| 90 stanza:tag("status", {code=code}):up(); | 114 stanza:tag("status", {code=code}):up(); |
| 91 end | 115 end |
| 92 self:broadcast_except_nick(stanza, stanza.attr.from); | 116 self:broadcast_except_nick(stanza, stanza.attr.from); |
| 93 local me = self._occupants[stanza.attr.from]; | 117 stanza:tag("status", {code='110'}):up(); |
| 94 if me then | 118 stanza.attr.to = sid; |
| 95 stanza:tag("status", {code='110'}):up(); | 119 self:_route_stanza(stanza); |
| 96 stanza.attr.to = sid; | |
| 97 self:_route_stanza(stanza); | |
| 98 end | |
| 99 end | 120 end |
| 100 function room_mt:broadcast_message(stanza, historic) | 121 function room_mt:broadcast_message(stanza, historic) |
| 101 local to = stanza.attr.to; | 122 module:fire_event("muc-broadcast-message", {room = self, stanza = stanza, historic = historic}); |
| 102 for occupant, o_data in pairs(self._occupants) do | 123 self:broadcast(stanza); |
| 103 for jid in pairs(o_data.sessions) do | 124 end |
| 104 stanza.attr.to = jid; | 125 |
| 105 self:_route_stanza(stanza); | 126 -- add to history |
| 106 end | 127 module:hook("muc-broadcast-message", function(event) |
| 107 end | 128 if event.historic then |
| 108 stanza.attr.to = to; | 129 local room = event.room |
| 109 if historic then -- add to history | 130 local history = room._data['history']; |
| 110 return self:save_to_history(stanza) | 131 if not history then history = {}; room._data['history'] = history; end |
| 111 end | 132 local stanza = st.clone(event.stanza); |
| 112 end | 133 stanza.attr.to = ""; |
| 113 function room_mt:save_to_history(stanza) | 134 local ts = gettime(); |
| 114 local history = self._data['history']; | 135 local stamp = datetime.datetime(ts); |
| 115 if not history then history = {}; self._data['history'] = history; end | 136 stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = module.host, stamp = stamp}):up(); -- XEP-0203 |
| 116 stanza = st.clone(stanza); | 137 stanza:tag("x", {xmlns = "jabber:x:delay", from = module.host, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) |
| 117 stanza.attr.to = ""; | 138 local entry = { stanza = stanza, timestamp = ts }; |
| 118 local stamp = datetime.datetime(); | 139 t_insert(history, entry); |
| 119 stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203 | 140 while #history > room:get_historylength() do t_remove(history, 1) end |
| 120 stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) | 141 end |
| 121 local entry = { stanza = stanza, stamp = stamp }; | 142 end) |
| 122 t_insert(history, entry); | 143 |
| 123 while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end | |
| 124 end | |
| 125 function room_mt:broadcast_except_nick(stanza, nick) | 144 function room_mt:broadcast_except_nick(stanza, nick) |
| 126 for rnick, occupant in pairs(self._occupants) do | 145 return self:broadcast(stanza, function(rnick, occupant) return rnick ~= nick end) |
| 127 if rnick ~= nick then | 146 end |
| 128 for jid in pairs(occupant.sessions) do | 147 |
| 129 stanza.attr.to = jid; | 148 -- Broadcast a stanza to all occupants in the room. |
| 130 self:_route_stanza(stanza); | 149 -- optionally checks conditional called with nicl |
| 131 end | 150 function room_mt:broadcast(stanza, cond_func) |
| 151 for nick, occupant in pairs(self._occupants) do | |
| 152 if cond_func == nil or cond_func(nick, occupant) then | |
| 153 self:route_to_occupant(occupant, stanza) | |
| 132 end | 154 end |
| 133 end | 155 end |
| 134 end | 156 end |
| 135 | 157 |
| 136 function room_mt:send_occupant_list(to) | 158 function room_mt:send_occupant_list(to) |
| 137 local current_nick = self._jid_nick[to]; | 159 local current_nick = self:get_occupant_jid(to); |
| 138 for occupant, o_data in pairs(self._occupants) do | 160 for occupant, o_data in pairs(self._occupants) do |
| 139 if occupant ~= current_nick then | 161 if occupant ~= current_nick then |
| 140 local pres = get_filtered_presence(o_data.sessions[o_data.jid]); | 162 local pres = get_filtered_presence(o_data.sessions[o_data.jid]); |
| 141 pres.attr.to, pres.attr.from = to, occupant; | 163 pres.attr.to, pres.attr.from = to, occupant; |
| 142 pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) | 164 pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) |
| 143 :tag("item", {affiliation=o_data.affiliation or "none", role=o_data.role or "none"}):up(); | 165 :tag("item", {affiliation=o_data.affiliation or "none", role=o_data.role or "none"}):up(); |
| 144 self:_route_stanza(pres); | 166 self:_route_stanza(pres); |
| 145 end | 167 end |
| 146 end | 168 end |
| 147 end | 169 end |
| 148 function room_mt:send_history(to, stanza) | 170 |
| 149 local history = self._data['history']; -- send discussion history | 171 local function parse_history(stanza) |
| 150 if history then | 172 local x_tag = stanza:get_child("x", "http://jabber.org/protocol/muc"); |
| 151 local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc"); | 173 local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc"); |
| 152 local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc"); | 174 if not history_tag then |
| 153 | 175 return nil, 20, nil |
| 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 maxchars = tonumber(history_tag.attr.maxchars); |
| 157 local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history); | 179 |
| 158 if not history_tag then maxstanzas = 20; end | 180 local maxstanzas = tonumber(history_tag.attr.maxstanzas); |
| 159 | 181 |
| 160 local seconds = history_tag and tonumber(history_tag.attr.seconds); | 182 -- messages received since the UTC datetime specified |
| 161 if seconds then seconds = datetime.datetime(os.time() - math.floor(seconds)); end | 183 local since = history_tag.attr.since; |
| 162 | 184 if since then |
| 163 local since = history_tag and history_tag.attr.since; | 185 since = datetime.parse(since); |
| 164 if since then since = datetime.parse(since); since = since and datetime.datetime(since); end | 186 end |
| 165 if seconds and (not since or since < seconds) then since = seconds; end | 187 |
| 166 | 188 -- messages received in the last "X" seconds. |
| 167 local n = 0; | 189 local seconds = tonumber(history_tag.attr.seconds); |
| 168 local charcount = 0; | 190 if seconds then |
| 169 | 191 seconds = gettime() - seconds |
| 170 for i=#history,1,-1 do | 192 if since then |
| 171 local entry = history[i]; | 193 since = math.max(since, seconds); |
| 172 if maxchars then | 194 else |
| 173 if not entry.chars then | 195 since = seconds; |
| 174 entry.stanza.attr.to = ""; | 196 end |
| 175 entry.chars = #tostring(entry.stanza); | 197 end |
| 176 end | 198 |
| 177 charcount = charcount + entry.chars + #to; | 199 return maxchars, maxstanzas, since |
| 178 if charcount > maxchars then break; end | 200 end |
| 201 | |
| 202 module:hook("muc-get-history", function(event) | |
| 203 local room = event.room | |
| 204 local history = room._data['history']; -- send discussion history | |
| 205 if not history then return nil end | |
| 206 local history_len = #history | |
| 207 | |
| 208 local to = event.to | |
| 209 local maxchars = event.maxchars | |
| 210 local maxstanzas = event.maxstanzas or history_len | |
| 211 local since = event.since | |
| 212 local n = 0; | |
| 213 local charcount = 0; | |
| 214 for i=history_len,1,-1 do | |
| 215 local entry = history[i]; | |
| 216 if maxchars then | |
| 217 if not entry.chars then | |
| 218 entry.stanza.attr.to = ""; | |
| 219 entry.chars = #tostring(entry.stanza); | |
| 179 end | 220 end |
| 180 if since and since > entry.stamp then break; end | 221 charcount = charcount + entry.chars + #to; |
| 181 if n + 1 > maxstanzas then break; end | 222 if charcount > maxchars then break; end |
| 182 n = n + 1; | 223 end |
| 183 end | 224 if since and since > entry.timestamp then break; end |
| 184 for i=#history-n+1,#history do | 225 if n + 1 > maxstanzas then break; end |
| 185 local msg = history[i].stanza; | 226 n = n + 1; |
| 186 msg.attr.to = to; | 227 end |
| 187 self:_route_stanza(msg); | 228 |
| 188 end | 229 local i = history_len-n+1 |
| 189 end | 230 function event:next_stanza() |
| 190 end | 231 if i > history_len then return nil end |
| 191 function room_mt:send_subject(to) | 232 local entry = history[i] |
| 192 if self._data['subject'] then | 233 local msg = entry.stanza |
| 193 self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject'])); | 234 msg.attr.to = to; |
| 235 i = i + 1 | |
| 236 return msg | |
| 237 end | |
| 238 return true; | |
| 239 end) | |
| 240 | |
| 241 function room_mt:send_history(stanza) | |
| 242 local maxchars, maxstanzas, since = parse_history(stanza) | |
| 243 local event = { | |
| 244 room = self; | |
| 245 to = stanza.attr.from; -- `to` is required to calculate the character count for `maxchars` | |
| 246 maxchars = maxchars, maxstanzas = maxstanzas, since = since; | |
| 247 next_stanza = function() end; -- events should define this iterator | |
| 248 } | |
| 249 module:fire_event("muc-get-history", event) | |
| 250 for msg in event.next_stanza , event do | |
| 251 self:_route_stanza(msg); | |
| 194 end | 252 end |
| 195 end | 253 end |
| 196 | 254 |
| 197 function room_mt:get_disco_info(stanza) | 255 function room_mt:get_disco_info(stanza) |
| 198 local count = 0; for _ in pairs(self._occupants) do count = count + 1; end | 256 local count = 0; for _ in pairs(self._occupants) do count = count + 1; end |
| 202 :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up() | 260 :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up() |
| 203 :tag("feature", {var=self:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up() | 261 :tag("feature", {var=self:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up() |
| 204 :tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up() | 262 :tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up() |
| 205 :tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up() | 263 :tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up() |
| 206 :tag("feature", {var=self:get_hidden() and "muc_hidden" or "muc_public"}):up() | 264 :tag("feature", {var=self:get_hidden() and "muc_hidden" or "muc_public"}):up() |
| 207 :tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up() | 265 :tag("feature", {var=self:get_whois() ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up() |
| 208 :add_child(dataform.new({ | 266 :add_child(dataform.new({ |
| 209 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }, | 267 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }, |
| 210 { name = "muc#roominfo_description", label = "Description", value = "" }, | 268 { name = "muc#roominfo_description", label = "Description", value = "" }, |
| 211 { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) } | 269 { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) } |
| 212 }):form({["muc#roominfo_description"] = self:get_description()}, 'result')) | 270 }):form({["muc#roominfo_description"] = self:get_description()}, 'result')) |
| 217 for room_jid in pairs(self._occupants) do | 275 for room_jid in pairs(self._occupants) do |
| 218 reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up(); | 276 reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up(); |
| 219 end | 277 end |
| 220 return reply; | 278 return reply; |
| 221 end | 279 end |
| 280 | |
| 281 function room_mt:get_subject() | |
| 282 return self._data['subject'], self._data['subject_from'] | |
| 283 end | |
| 284 local function create_subject_message(subject) | |
| 285 return st.message({type='groupchat'}) | |
| 286 :tag('subject'):text(subject):up(); | |
| 287 end | |
| 288 function room_mt:send_subject(to) | |
| 289 local from, subject = self:get_subject() | |
| 290 if subject then | |
| 291 local msg = create_subject_message(subject) | |
| 292 msg.attr.from = from | |
| 293 msg.attr.to = to | |
| 294 self:_route_stanza(msg); | |
| 295 end | |
| 296 end | |
| 222 function room_mt:set_subject(current_nick, subject) | 297 function room_mt:set_subject(current_nick, subject) |
| 223 if subject == "" then subject = nil; end | 298 if subject == "" then subject = nil; end |
| 224 self._data['subject'] = subject; | 299 self._data['subject'] = subject; |
| 225 self._data['subject_from'] = current_nick; | 300 self._data['subject_from'] = current_nick; |
| 226 if self.save then self:save(); end | 301 if self.save then self:save(); end |
| 227 local msg = st.message({type='groupchat', from=current_nick}) | 302 local msg = create_subject_message(subject) |
| 228 :tag('subject'):text(subject):up(); | 303 msg.attr.from = current_nick |
| 229 self:broadcast_message(msg, false); | 304 self:broadcast_message(msg, false); |
| 230 return true; | 305 return true; |
| 231 end | 306 end |
| 232 | 307 |
| 233 local function build_unavailable_presence_from_error(stanza) | 308 function room_mt:handle_kickable(origin, stanza) |
| 234 local type, condition, text = stanza:get_error(); | 309 local type, condition, text = stanza:get_error(); |
| 235 local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error"); | 310 local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error"); |
| 236 if text then | 311 if text then |
| 237 error_message = error_message..": "..text; | 312 error_message = error_message..": "..text; |
| 238 end | 313 end |
| 239 return st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to}) | 314 local kick_stanza = st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to}) |
| 240 :tag('status'):text(error_message); | 315 :tag('status'):text(error_message); |
| 316 self:handle_unavailable_to_occupant(origin, kick_stanza); -- send unavailable | |
| 317 return true; | |
| 241 end | 318 end |
| 242 | 319 |
| 243 function room_mt:set_name(name) | 320 function room_mt:set_name(name) |
| 244 if name == "" or type(name) ~= "string" or name == (jid_split(self.jid)) then name = nil; end | 321 if name == "" or type(name) ~= "string" or name == (jid_split(self.jid)) then name = nil; end |
| 245 if self._data.name ~= name then | 322 if self._data.name ~= name then |
| 349 | 426 |
| 350 function room_mt:get_whois() | 427 function room_mt:get_whois() |
| 351 return self._data.whois; | 428 return self._data.whois; |
| 352 end | 429 end |
| 353 | 430 |
| 354 local function construct_stanza_id(room, stanza) | 431 function room_mt:handle_unavailable_to_occupant(origin, stanza) |
| 355 local from_jid, to_nick = stanza.attr.from, stanza.attr.to; | 432 local from = stanza.attr.from; |
| 356 local from_nick = room._jid_nick[from_jid]; | 433 local current_nick = self:get_occupant_jid(from); |
| 357 local occupant = room._occupants[to_nick]; | 434 if not current_nick then |
| 358 local to_jid = occupant.jid; | 435 return true; -- discard |
| 359 | 436 end |
| 360 return from_nick, to_jid, base64.encode(to_jid.."\0"..stanza.attr.id.."\0"..md5(from_jid)); | 437 local pr = get_filtered_presence(stanza); |
| 361 end | 438 pr.attr.from = current_nick; |
| 362 local function deconstruct_stanza_id(room, stanza) | 439 log("debug", "%s leaving %s", current_nick, self.jid); |
| 363 local from_jid_possiblybare, to_nick = stanza.attr.from, stanza.attr.to; | 440 self._jid_nick[from] = nil; |
| 364 local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$"); | 441 local occupant = self._occupants[current_nick]; |
| 365 local from_nick = room._jid_nick[from_jid]; | 442 local new_jid = next(occupant.sessions); |
| 366 | 443 if new_jid == from then new_jid = next(occupant.sessions, new_jid); end |
| 367 if not(from_nick) then return; end | 444 if new_jid then |
| 368 if not(from_jid_possiblybare == from_jid or from_jid_possiblybare == jid_bare(from_jid)) then return; end | 445 local jid = occupant.jid; |
| 369 | 446 occupant.jid = new_jid; |
| 370 local occupant = room._occupants[to_nick]; | 447 occupant.sessions[from] = nil; |
| 371 for to_jid in pairs(occupant and occupant.sessions or {}) do | 448 pr.attr.to = from; |
| 372 if md5(to_jid) == to_jid_hash then | 449 pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) |
| 373 return from_nick, to_jid, id; | 450 :tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up() |
| 374 end | 451 :tag("status", {code='110'}):up(); |
| 375 end | 452 self:_route_stanza(pr); |
| 376 end | 453 if jid ~= new_jid then |
| 377 | 454 pr = st.clone(occupant.sessions[new_jid]) |
| 378 | 455 :tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) |
| 379 function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc | 456 :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none"}); |
| 457 pr.attr.from = current_nick; | |
| 458 self:broadcast_except_nick(pr, current_nick); | |
| 459 end | |
| 460 else | |
| 461 occupant.role = 'none'; | |
| 462 self:broadcast_presence(pr, from); | |
| 463 self._occupants[current_nick] = nil; | |
| 464 module:fire_event("muc-occupant-left", { room = self; nick = current_nick; }); | |
| 465 end | |
| 466 return true; | |
| 467 end | |
| 468 | |
| 469 function room_mt:handle_occupant_presence(origin, stanza) | |
| 470 local from = stanza.attr.from; | |
| 471 local pr = get_filtered_presence(stanza); | |
| 472 local current_nick = stanza.attr.to | |
| 473 pr.attr.from = current_nick; | |
| 474 log("debug", "%s broadcasted presence", current_nick); | |
| 475 self._occupants[current_nick].sessions[from] = pr; | |
| 476 self:broadcast_presence(pr, from); | |
| 477 return true; | |
| 478 end | |
| 479 | |
| 480 function room_mt:handle_change_nick(origin, stanza, current_nick, to) | |
| 481 local from = stanza.attr.from; | |
| 482 local occupant = self._occupants[current_nick]; | |
| 483 local is_multisession = next(occupant.sessions, next(occupant.sessions)); | |
| 484 if self._occupants[to] or is_multisession then | |
| 485 log("debug", "%s couldn't change nick", current_nick); | |
| 486 local reply = st.error_reply(stanza, "cancel", "conflict"):up(); | |
| 487 reply.tags[1].attr.code = "409"; | |
| 488 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 489 return true; | |
| 490 else | |
| 491 local to_nick = select(3, jid_split(to)); | |
| 492 log("debug", "%s (%s) changing nick to %s", current_nick, occupant.jid, to); | |
| 493 local p = st.presence({type='unavailable', from=current_nick}); | |
| 494 self:broadcast_presence(p, from, '303', to_nick); | |
| 495 self._occupants[current_nick] = nil; | |
| 496 self._occupants[to] = occupant; | |
| 497 self._jid_nick[from] = to; | |
| 498 local pr = get_filtered_presence(stanza); | |
| 499 pr.attr.from = to; | |
| 500 self._occupants[to].sessions[from] = pr; | |
| 501 self:broadcast_presence(pr, from); | |
| 502 return true; | |
| 503 end | |
| 504 end | |
| 505 | |
| 506 module:hook("muc-occupant-pre-join", function(event) | |
| 507 return module:fire_event("muc-occupant-pre-join/affiliation", event) | |
| 508 or module:fire_event("muc-occupant-pre-join/password", event) | |
| 509 or module:fire_event("muc-occupant-pre-join/locked", event) | |
| 510 or module:fire_event("muc-occupant-pre-join/nick-conflict", event) | |
| 511 end, -1) | |
| 512 | |
| 513 module:hook("muc-occupant-pre-join/password", function(event) | |
| 514 local room, stanza = event.room, event.stanza; | |
| 380 local from, to = stanza.attr.from, stanza.attr.to; | 515 local from, to = stanza.attr.from, stanza.attr.to; |
| 381 local room = jid_bare(to); | 516 local password = stanza:get_child("x", "http://jabber.org/protocol/muc"); |
| 382 local current_nick = self._jid_nick[from]; | 517 password = password and password:get_child_text("password", "http://jabber.org/protocol/muc"); |
| 518 if not password or password == "" then password = nil; end | |
| 519 if room:get_password() ~= password then | |
| 520 local from, to = stanza.attr.from, stanza.attr.to; | |
| 521 log("debug", "%s couldn't join due to invalid password: %s", from, to); | |
| 522 local reply = st.error_reply(stanza, "auth", "not-authorized"):up(); | |
| 523 reply.tags[1].attr.code = "401"; | |
| 524 event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 525 return true; | |
| 526 end | |
| 527 end, -1) | |
| 528 | |
| 529 module:hook("muc-occupant-pre-join/nick-conflict", function(event) | |
| 530 local room, stanza = event.room, event.stanza; | |
| 531 local from, to = stanza.attr.from, stanza.attr.to; | |
| 532 local occupant = room._occupants[to] | |
| 533 if occupant -- occupant already exists | |
| 534 and jid_bare(from) ~= jid_bare(occupant.jid) then -- and has different bare real jid | |
| 535 log("debug", "%s couldn't join due to nick conflict: %s", from, to); | |
| 536 local reply = st.error_reply(stanza, "cancel", "conflict"):up(); | |
| 537 reply.tags[1].attr.code = "409"; | |
| 538 event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 539 return true; | |
| 540 end | |
| 541 end, -1) | |
| 542 | |
| 543 module:hook("muc-occupant-pre-join/locked", function(event) | |
| 544 if event.room:is_locked() then -- Deny entry | |
| 545 event.origin.send(st.error_reply(event.stanza, "cancel", "item-not-found")); | |
| 546 return true; | |
| 547 end | |
| 548 end, -1) | |
| 549 | |
| 550 function room_mt:handle_join(origin, stanza) | |
| 551 local from, to = stanza.attr.from, stanza.attr.to; | |
| 552 local affiliation = self:get_affiliation(from); | |
| 553 if affiliation == nil and next(self._affiliations) == nil then -- new room, no owners | |
| 554 affiliation = "owner"; | |
| 555 self._affiliations[jid_bare(from)] = affiliation; | |
| 556 if self:is_locked() and not stanza:get_child("x", "http://jabber.org/protocol/muc") then | |
| 557 self:unlock(); -- Older groupchat protocol doesn't lock | |
| 558 end | |
| 559 end | |
| 560 if module:fire_event("muc-occupant-pre-join", { | |
| 561 room = self; | |
| 562 origin = origin; | |
| 563 stanza = stanza; | |
| 564 affiliation = affiliation; | |
| 565 }) then return true; end | |
| 566 log("debug", "%s joining as %s", from, to); | |
| 567 | |
| 568 local role = self:get_default_role(affiliation) | |
| 569 if role then -- new occupant | |
| 570 local is_merge = not not self._occupants[to] | |
| 571 if not is_merge then | |
| 572 self._occupants[to] = {affiliation=affiliation, role=role, jid=from, sessions={[from]=get_filtered_presence(stanza)}}; | |
| 573 else | |
| 574 self._occupants[to].sessions[from] = get_filtered_presence(stanza); | |
| 575 end | |
| 576 self._jid_nick[from] = to; | |
| 577 self:send_occupant_list(from); | |
| 578 local pr = get_filtered_presence(stanza); | |
| 579 pr.attr.from = to; | |
| 580 pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) | |
| 581 :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up(); | |
| 582 if not is_merge then | |
| 583 self:broadcast_except_nick(pr, to); | |
| 584 end | |
| 585 pr:tag("status", {code='110'}):up(); | |
| 586 if self:get_whois() == 'anyone' then | |
| 587 pr:tag("status", {code='100'}):up(); | |
| 588 end | |
| 589 if self:is_locked() then | |
| 590 pr:tag("status", {code='201'}):up(); | |
| 591 end | |
| 592 pr.attr.to = from; | |
| 593 self:_route_stanza(pr); | |
| 594 self:send_history(from, stanza); | |
| 595 self:send_subject(from); | |
| 596 return true; | |
| 597 end | |
| 598 end | |
| 599 | |
| 600 -- registration required for entering members-only room | |
| 601 module:hook("muc-occupant-pre-join/affiliation", function(event) | |
| 602 if event.affiliation == nil and event.room:get_members_only() then | |
| 603 local reply = st.error_reply(event.stanza, "auth", "registration-required"):up(); | |
| 604 reply.tags[1].attr.code = "407"; | |
| 605 event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 606 return true; | |
| 607 end | |
| 608 end, -1) | |
| 609 | |
| 610 -- banned | |
| 611 module:hook("muc-occupant-pre-join/affiliation", function(event) | |
| 612 if event.affiliation == "outcast" then | |
| 613 local reply = st.error_reply(event.stanza, "auth", "forbidden"):up(); | |
| 614 reply.tags[1].attr.code = "403"; | |
| 615 event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); | |
| 616 return true; | |
| 617 end | |
| 618 end, -1) | |
| 619 | |
| 620 function room_mt:handle_available_to_occupant(origin, stanza) | |
| 621 local from, to = stanza.attr.from, stanza.attr.to; | |
| 622 local current_nick = self:get_occupant_jid(from); | |
| 623 if current_nick then | |
| 624 --if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence | |
| 625 if current_nick == to then -- simple presence | |
| 626 return self:handle_occupant_presence(origin, stanza) | |
| 627 else -- change nick | |
| 628 return self:handle_change_nick(origin, stanza, current_nick, to) | |
| 629 end | |
| 630 --else -- possible rejoin | |
| 631 -- log("debug", "%s had connection replaced", current_nick); | |
| 632 -- self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}) | |
| 633 -- :tag('status'):text('Replaced by new connection'):up()); -- send unavailable | |
| 634 -- self:handle_to_occupant(origin, stanza); -- resend available | |
| 635 --end | |
| 636 else -- enter room | |
| 637 return self:handle_join(origin, stanza) | |
| 638 end | |
| 639 end | |
| 640 | |
| 641 function room_mt:handle_presence_to_occupant(origin, stanza) | |
| 383 local type = stanza.attr.type; | 642 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()); | 643 if type == "error" then -- error, kick em out! |
| 385 if (select(2, jid_split(from)) == muc_domain) then error("Presence from the MUC itself!!!"); end | 644 return self:handle_kickable(origin, stanza) |
| 386 if stanza.name == "presence" then | 645 elseif type == "unavailable" then -- unavailable |
| 387 local pr = get_filtered_presence(stanza); | 646 return self:handle_unavailable_to_occupant(origin, stanza) |
| 388 pr.attr.from = current_nick; | 647 elseif not type then -- available |
| 389 if type == "error" then -- error, kick em out! | 648 return self:handle_available_to_occupant(origin, stanza) |
| 390 if current_nick then | 649 elseif type ~= 'result' then -- bad type |
| 391 log("debug", "kicking %s from %s", current_nick, room); | 650 if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences |
| 392 self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); | 651 origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error? |
| 393 end | 652 end |
| 394 elseif type == "unavailable" then -- unavailable | 653 end |
| 395 if current_nick then | 654 return true; |
| 396 log("debug", "%s leaving %s", current_nick, room); | 655 end |
| 397 self._jid_nick[from] = nil; | 656 |
| 398 local occupant = self._occupants[current_nick]; | 657 function room_mt:handle_iq_to_occupant(origin, stanza) |
| 399 local new_jid = next(occupant.sessions); | 658 local from, to = stanza.attr.from, stanza.attr.to; |
| 400 if new_jid == from then new_jid = next(occupant.sessions, new_jid); end | 659 local type = stanza.attr.type; |
| 401 if new_jid then | 660 local id = stanza.attr.id; |
| 402 local jid = occupant.jid; | 661 local current_nick = self:get_occupant_jid(from); |
| 403 occupant.jid = new_jid; | 662 local o_data = self._occupants[to]; |
| 404 occupant.sessions[from] = nil; | 663 if (type == "error" or type == "result") then |
| 405 pr.attr.to = from; | 664 do -- deconstruct_stanza_id |
| 406 pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) | 665 if not current_nick or not o_data then return nil; end |
| 407 :tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up() | 666 local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$"); |
| 408 :tag("status", {code='110'}):up(); | 667 if not(from == from_jid or from == jid_bare(from_jid)) then return nil; end |
| 409 self:_route_stanza(pr); | 668 local session_jid |
| 410 if jid ~= new_jid then | 669 for to_jid in pairs(o_data.sessions) do |
| 411 pr = st.clone(occupant.sessions[new_jid]) | 670 if md5(to_jid) == to_jid_hash then |
| 412 :tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) | 671 session_jid = to_jid; |
| 413 :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none"}); | 672 break; |
| 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 | 673 end |
| 422 end | 674 end |
| 423 elseif not type then -- available | 675 if session_jid == nil then return nil; end |
| 424 if current_nick then | 676 stanza.attr.from, stanza.attr.to, stanza.attr.id = current_nick, session_jid, id |
| 425 --if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence | 677 end |
| 426 if current_nick == to then -- simple presence | 678 log("debug", "%s sent private iq stanza to %s (%s)", from, to, stanza.attr.to); |
| 427 log("debug", "%s broadcasted presence", current_nick); | 679 self:_route_stanza(stanza); |
| 428 self._occupants[current_nick].sessions[from] = pr; | 680 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; |
| 429 self:broadcast_presence(pr, from); | 681 return true; |
| 430 else -- change nick | 682 else -- Type is "get" or "set" |
| 431 local occupant = self._occupants[current_nick]; | 683 if not current_nick then |
| 432 local is_multisession = next(occupant.sessions, next(occupant.sessions)); | |
| 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")); | 684 origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); |
| 548 end | 685 return true; |
| 549 elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM | 686 end |
| 687 if not o_data then -- recipient not in room | |
| 688 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); | |
| 689 return true; | |
| 690 end | |
| 691 do -- construct_stanza_id | |
| 692 stanza.attr.id = base64.encode(o_data.jid.."\0"..stanza.attr.id.."\0"..md5(from)); | |
| 693 end | |
| 694 stanza.attr.from, stanza.attr.to = current_nick, o_data.jid; | |
| 695 log("debug", "%s sent private iq stanza to %s (%s)", from, to, o_data.jid); | |
| 696 if stanza.tags[1].attr.xmlns == 'vcard-temp' then | |
| 697 stanza.attr.to = jid_bare(stanza.attr.to); | |
| 698 end | |
| 699 self:_route_stanza(stanza); | |
| 700 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; | |
| 701 return true; | |
| 702 end | |
| 703 end | |
| 704 | |
| 705 function room_mt:handle_message_to_occupant(origin, stanza) | |
| 706 local from, to = stanza.attr.from, stanza.attr.to; | |
| 707 local current_nick = self:get_occupant_jid(from); | |
| 708 local type = stanza.attr.type; | |
| 709 if not current_nick then -- not in room | |
| 710 if type ~= "error" then | |
| 711 origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); | |
| 712 end | |
| 713 return true; | |
| 714 end | |
| 715 if type == "groupchat" then -- groupchat messages not allowed in PM | |
| 550 origin.send(st.error_reply(stanza, "modify", "bad-request")); | 716 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 | 717 return true; |
| 718 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); | 719 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 | 720 return self:handle_kickable(origin, stanza); -- send unavailable |
| 554 else -- private stanza | 721 end |
| 555 local o_data = self._occupants[to]; | 722 |
| 556 if o_data then | 723 local o_data = self._occupants[to]; |
| 557 log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid); | 724 if not o_data then |
| 558 if stanza.name == "iq" then | 725 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); |
| 559 local id = stanza.attr.id; | 726 return true; |
| 560 if stanza.attr.type == "get" or stanza.attr.type == "set" then | 727 end |
| 561 stanza.attr.from, stanza.attr.to, stanza.attr.id = construct_stanza_id(self, stanza); | 728 log("debug", "%s sent private message stanza to %s (%s)", from, to, o_data.jid); |
| 562 else | 729 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); | 730 stanza.attr.from = current_nick; |
| 564 end | 731 self:route_to_occupant(o_data, stanza) |
| 565 if type == 'get' and stanza.tags[1].attr.xmlns == 'vcard-temp' then | 732 stanza.attr.from = from; |
| 566 stanza.attr.to = jid_bare(stanza.attr.to); | 733 return true; |
| 567 end | |
| 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 | 734 end |
| 586 | 735 |
| 587 function room_mt:send_form(origin, stanza) | 736 function room_mt:send_form(origin, stanza) |
| 588 origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") | 737 origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") |
| 589 :add_child(self:get_form_layout(stanza.attr.from):form()) | 738 :add_child(self:get_form_layout(stanza.attr.from):form()) |
| 590 ); | 739 ); |
| 591 end | 740 end |
| 592 | 741 |
| 593 function room_mt:get_form_layout(actor) | 742 function room_mt:get_form_layout(actor) |
| 743 local whois = self:get_whois() | |
| 594 local form = dataform.new({ | 744 local form = dataform.new({ |
| 595 title = "Configuration for "..self.jid, | 745 title = "Configuration for "..self.jid, |
| 596 instructions = "Complete and submit this form to configure the room.", | 746 instructions = "Complete and submit this form to configure the room.", |
| 597 { | 747 { |
| 598 name = 'FORM_TYPE', | 748 name = 'FORM_TYPE', |
| 632 { | 782 { |
| 633 name = 'muc#roomconfig_whois', | 783 name = 'muc#roomconfig_whois', |
| 634 type = 'list-single', | 784 type = 'list-single', |
| 635 label = 'Who May Discover Real JIDs?', | 785 label = 'Who May Discover Real JIDs?', |
| 636 value = { | 786 value = { |
| 637 { value = 'moderators', label = 'Moderators Only', default = self._data.whois == 'moderators' }, | 787 { value = 'moderators', label = 'Moderators Only', default = whois == 'moderators' }, |
| 638 { value = 'anyone', label = 'Anyone', default = self._data.whois == 'anyone' } | 788 { value = 'anyone', label = 'Anyone', default = whois == 'anyone' } |
| 639 } | 789 } |
| 640 }, | 790 }, |
| 641 { | 791 { |
| 642 name = 'muc#roomconfig_roomsecret', | 792 name = 'muc#roomconfig_roomsecret', |
| 643 type = 'text-private', | 793 type = 'text-private', |
| 666 return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form; | 816 return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form; |
| 667 end | 817 end |
| 668 | 818 |
| 669 function room_mt:process_form(origin, stanza) | 819 function room_mt:process_form(origin, stanza) |
| 670 local query = stanza.tags[1]; | 820 local query = stanza.tags[1]; |
| 671 local form; | 821 local form = query:get_child("x", "jabber:x:data") |
| 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 | |
| 673 if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end | 822 if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end |
| 674 if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end | 823 if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end |
| 675 if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; end | 824 if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; end |
| 676 | 825 |
| 677 local fields = self:get_form_layout(stanza.attr.from):data(form); | 826 local fields = self:get_form_layout(stanza.attr.from):data(form); |
| 702 handle_option("historylength", "muc#roomconfig_historylength"); | 851 handle_option("historylength", "muc#roomconfig_historylength"); |
| 703 handle_option("whois", "muc#roomconfig_whois", valid_whois); | 852 handle_option("whois", "muc#roomconfig_whois", valid_whois); |
| 704 handle_option("password", "muc#roomconfig_roomsecret"); | 853 handle_option("password", "muc#roomconfig_roomsecret"); |
| 705 | 854 |
| 706 if self.save then self:save(true); end | 855 if self.save then self:save(true); end |
| 707 if self.locked then | 856 if self:is_locked() then |
| 708 module:fire_event("muc-room-unlocked", { room = self }); | 857 self:unlock(); |
| 709 self.locked = nil; | |
| 710 end | 858 end |
| 711 origin.send(st.reply(stanza)); | 859 origin.send(st.reply(stanza)); |
| 712 | 860 |
| 713 if next(changed) then | 861 if next(changed) then |
| 714 local msg = st.message({type='groupchat', from=self.jid}) | 862 local msg = st.message({type='groupchat', from=self.jid}) |
| 735 pr.attr.to = jid; | 883 pr.attr.to = jid; |
| 736 self:_route_stanza(pr); | 884 self:_route_stanza(pr); |
| 737 self._jid_nick[jid] = nil; | 885 self._jid_nick[jid] = nil; |
| 738 end | 886 end |
| 739 self._occupants[nick] = nil; | 887 self._occupants[nick] = nil; |
| 888 module:fire_event("muc-occupant-left", { room = self; nick = nick; }); | |
| 740 end | 889 end |
| 741 self:set_persistent(false); | 890 self:set_persistent(false); |
| 742 module:fire_event("muc-room-destroyed", { room = self }); | 891 module:fire_event("muc-room-destroyed", { room = self }); |
| 743 end | 892 end |
| 744 | 893 |
| 745 function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc | 894 function room_mt:handle_disco_info_get_query(origin, stanza) |
| 746 local type = stanza.attr.type; | 895 origin.send(self:get_disco_info(stanza)); |
| 747 local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; | 896 return true; |
| 748 if stanza.name == "iq" then | 897 end |
| 749 if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" and not stanza.tags[1].attr.node then | 898 |
| 750 origin.send(self:get_disco_info(stanza)); | 899 function room_mt:handle_disco_items_get_query(origin, stanza) |
| 751 elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" and not stanza.tags[1].attr.node then | 900 origin.send(self:get_disco_items(stanza)); |
| 752 origin.send(self:get_disco_items(stanza)); | 901 return true; |
| 753 elseif xmlns == "http://jabber.org/protocol/muc#admin" then | 902 end |
| 754 local actor = stanza.attr.from; | 903 |
| 755 local affiliation = self:get_affiliation(actor); | 904 function room_mt:handle_admin_query_set_command(origin, stanza) |
| 756 local current_nick = self._jid_nick[actor]; | 905 local item = stanza.tags[1].tags[1]; |
| 757 local role = current_nick and self._occupants[current_nick].role or self:get_default_role(affiliation); | 906 if item.attr.jid then -- Validate provided JID |
| 758 local item = stanza.tags[1].tags[1]; | 907 item.attr.jid = jid_prep(item.attr.jid); |
| 759 if item and item.name == "item" then | 908 if not item.attr.jid then |
| 760 if type == "set" then | 909 origin.send(st.error_reply(stanza, "modify", "jid-malformed")); |
| 761 local callback = function() origin.send(st.reply(stanza)); end | 910 return true; |
| 762 if item.attr.jid then -- Validate provided JID | 911 end |
| 763 item.attr.jid = jid_prep(item.attr.jid); | 912 end |
| 764 if not item.attr.jid then | 913 if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation |
| 765 origin.send(st.error_reply(stanza, "modify", "jid-malformed")); | 914 local occupant = self._occupants[self.jid.."/"..item.attr.nick]; |
| 766 return; | 915 if occupant then item.attr.jid = occupant.jid; end |
| 767 end | 916 elseif not item.attr.nick and item.attr.jid then |
| 768 end | 917 local nick = self:get_occupant_jid(item.attr.jid); |
| 769 if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation | 918 if nick then item.attr.nick = select(3, jid_split(nick)); end |
| 770 local occupant = self._occupants[self.jid.."/"..item.attr.nick]; | 919 end |
| 771 if occupant then item.attr.jid = occupant.jid; end | 920 local actor = stanza.attr.from; |
| 772 elseif not item.attr.nick and item.attr.jid then | 921 local callback = function() origin.send(st.reply(stanza)); end |
| 773 local nick = self._jid_nick[item.attr.jid]; | 922 local reason = item:get_child_text("reason"); |
| 774 if nick then item.attr.nick = select(3, jid_split(nick)); end | 923 if item.attr.affiliation and item.attr.jid and not item.attr.role then |
| 775 end | 924 local success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, callback, reason); |
| 776 local reason = item.tags[1] and item.tags[1].name == "reason" and #item.tags[1] == 1 and item.tags[1][1]; | 925 if not success then origin.send(st.error_reply(stanza, errtype, err)); end |
| 777 if item.attr.affiliation and item.attr.jid and not item.attr.role then | 926 return true; |
| 778 local success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, callback, reason); | 927 elseif item.attr.role and item.attr.nick and not item.attr.affiliation then |
| 779 if not success then origin.send(st.error_reply(stanza, errtype, err)); end | 928 local success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, callback, reason); |
| 780 elseif item.attr.role and item.attr.nick and not item.attr.affiliation then | 929 if not success then origin.send(st.error_reply(stanza, errtype, err)); end |
| 781 local success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, callback, reason); | 930 return true; |
| 782 if not success then origin.send(st.error_reply(stanza, errtype, err)); end | 931 else |
| 783 else | 932 origin.send(st.error_reply(stanza, "cancel", "bad-request")); |
| 784 origin.send(st.error_reply(stanza, "cancel", "bad-request")); | 933 return true; |
| 785 end | 934 end |
| 786 elseif type == "get" then | 935 end |
| 787 local _aff = item.attr.affiliation; | 936 |
| 788 local _rol = item.attr.role; | 937 function room_mt:handle_admin_query_get_command(origin, stanza) |
| 789 if _aff and not _rol then | 938 local actor = stanza.attr.from; |
| 790 if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then | 939 local affiliation = self:get_affiliation(actor); |
| 791 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); | 940 local item = stanza.tags[1].tags[1]; |
| 792 for jid, affiliation in pairs(self._affiliations) do | 941 local _aff = item.attr.affiliation; |
| 793 if affiliation == _aff then | 942 local _rol = item.attr.role; |
| 794 reply:tag("item", {affiliation = _aff, jid = jid}):up(); | 943 if _aff and not _rol then |
| 795 end | 944 if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then |
| 796 end | 945 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); |
| 797 origin.send(reply); | 946 for jid, affiliation in pairs(self._affiliations) do |
| 798 else | 947 if affiliation == _aff then |
| 799 origin.send(st.error_reply(stanza, "auth", "forbidden")); | 948 reply:tag("item", {affiliation = _aff, jid = jid}):up(); |
| 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 | 949 end |
| 851 end | 950 end |
| 852 elseif type == "set" or type == "get" then | 951 origin.send(reply); |
| 853 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); | 952 return true; |
| 854 end | 953 else |
| 855 elseif stanza.name == "message" and type == "groupchat" then | 954 origin.send(st.error_reply(stanza, "auth", "forbidden")); |
| 955 return true; | |
| 956 end | |
| 957 elseif _rol and not _aff then | |
| 958 local role = self:get_role(self:get_occupant_jid(actor)) or self:get_default_role(affiliation); | |
| 959 if role == "moderator" then | |
| 960 if _rol == "none" then _rol = nil; end | |
| 961 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); | |
| 962 for occupant_jid, occupant in pairs(self._occupants) do | |
| 963 if occupant.role == _rol then | |
| 964 reply:tag("item", { | |
| 965 nick = select(3, jid_split(occupant_jid)), | |
| 966 role = _rol or "none", | |
| 967 affiliation = occupant.affiliation or "none", | |
| 968 jid = occupant.jid | |
| 969 }):up(); | |
| 970 end | |
| 971 end | |
| 972 origin.send(reply); | |
| 973 return true; | |
| 974 else | |
| 975 origin.send(st.error_reply(stanza, "auth", "forbidden")); | |
| 976 return true; | |
| 977 end | |
| 978 else | |
| 979 origin.send(st.error_reply(stanza, "cancel", "bad-request")); | |
| 980 return true; | |
| 981 end | |
| 982 end | |
| 983 | |
| 984 function room_mt:handle_owner_query_get_to_room(origin, stanza) | |
| 985 if self:get_affiliation(stanza.attr.from) ~= "owner" then | |
| 986 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms")); | |
| 987 return true; | |
| 988 end | |
| 989 | |
| 990 self:send_form(origin, stanza); | |
| 991 return true; | |
| 992 end | |
| 993 function room_mt:handle_owner_query_set_to_room(origin, stanza) | |
| 994 if self:get_affiliation(stanza.attr.from) ~= "owner" then | |
| 995 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms")); | |
| 996 return true; | |
| 997 end | |
| 998 | |
| 999 local child = stanza.tags[1].tags[1]; | |
| 1000 if not child then | |
| 1001 origin.send(st.error_reply(stanza, "modify", "bad-request")); | |
| 1002 return true; | |
| 1003 elseif child.name == "destroy" then | |
| 1004 local newjid = child.attr.jid; | |
| 1005 local reason = child:get_child_text("reason"); | |
| 1006 local password = child:get_child_text("password"); | |
| 1007 self:destroy(newjid, reason, password); | |
| 1008 origin.send(st.reply(stanza)); | |
| 1009 return true; | |
| 1010 else | |
| 1011 self:process_form(origin, stanza); | |
| 1012 return true; | |
| 1013 end | |
| 1014 end | |
| 1015 | |
| 1016 function room_mt:handle_groupchat_to_room(origin, stanza) | |
| 1017 local from = stanza.attr.from; | |
| 1018 local current_nick = self:get_occupant_jid(from); | |
| 1019 local occupant = self._occupants[current_nick]; | |
| 1020 if not occupant then -- not in room | |
| 1021 origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); | |
| 1022 return true; | |
| 1023 elseif occupant.role == "visitor" then | |
| 1024 origin.send(st.error_reply(stanza, "auth", "forbidden")); | |
| 1025 return true; | |
| 1026 else | |
| 856 local from = stanza.attr.from; | 1027 local from = stanza.attr.from; |
| 857 local current_nick = self._jid_nick[from]; | 1028 stanza.attr.from = current_nick; |
| 858 local occupant = self._occupants[current_nick]; | 1029 local subject = stanza:get_child_text("subject"); |
| 859 if not occupant then -- not in room | 1030 if subject then |
| 860 origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); | 1031 if occupant.role == "moderator" or |
| 861 elseif occupant.role == "visitor" then | 1032 ( self:get_changesubject() and occupant.role == "participant" ) then -- and participant |
| 862 origin.send(st.error_reply(stanza, "auth", "forbidden")); | 1033 self:set_subject(current_nick, subject); |
| 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 | 1034 else |
| 876 self:broadcast_message(stanza, self:get_historylength() > 0 and stanza:get_child("body")); | 1035 stanza.attr.from = from; |
| 877 end | 1036 origin.send(st.error_reply(stanza, "auth", "forbidden")); |
| 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 | 1037 end |
| 926 else | 1038 else |
| 1039 self:broadcast_message(stanza, self:get_historylength() > 0 and stanza:get_child("body")); | |
| 1040 end | |
| 1041 stanza.attr.from = from; | |
| 1042 return true; | |
| 1043 end | |
| 1044 end | |
| 1045 | |
| 1046 -- hack - some buggy clients send presence updates to the room rather than their nick | |
| 1047 function room_mt:handle_presence_to_room(origin, stanza) | |
| 1048 local current_nick = self:get_occupant_jid(stanza.attr.from); | |
| 1049 local handled | |
| 1050 if current_nick then | |
| 1051 local to = stanza.attr.to; | |
| 1052 stanza.attr.to = current_nick; | |
| 1053 handled = self:handle_presence_to_occupant(origin, stanza); | |
| 1054 stanza.attr.to = to; | |
| 1055 end | |
| 1056 return handled; | |
| 1057 end | |
| 1058 | |
| 1059 function room_mt:handle_mediated_invite(origin, stanza) | |
| 1060 local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite") | |
| 1061 local _from, _to = stanza.attr.from, stanza.attr.to; | |
| 1062 local current_nick = self:get_occupant_jid(_from) | |
| 1063 -- Need visitor role or higher to invite | |
| 1064 if not self:get_role(current_nick) or not self:get_default_role(self:get_affiliation(_from)) then | |
| 1065 origin.send(st.error_reply(stanza, "auth", "forbidden")); | |
| 1066 return true; | |
| 1067 end | |
| 1068 local _invitee = jid_prep(payload.attr.to); | |
| 1069 if _invitee then | |
| 1070 if self:get_whois() == "moderators" then | |
| 1071 _from = current_nick; | |
| 1072 end | |
| 1073 local _reason = payload:get_child_text("reason") | |
| 1074 local invite = st.message({from = _to, to = _invitee, id = stanza.attr.id}) | |
| 1075 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) | |
| 1076 :tag('invite', {from=_from}) | |
| 1077 :tag('reason'):text(_reason or ""):up() | |
| 1078 :up(); | |
| 1079 local password = self:get_password() | |
| 1080 if password then | |
| 1081 invite:tag("password"):text(password):up(); | |
| 1082 end | |
| 1083 invite:up() | |
| 1084 :tag('x', {xmlns="jabber:x:conference", jid=_to}) -- COMPAT: Some older clients expect this | |
| 1085 :text(_reason or "") | |
| 1086 :up() | |
| 1087 :tag('body') -- Add a plain message for clients which don't support invites | |
| 1088 :text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or "")) | |
| 1089 :up(); | |
| 1090 module:fire_event("muc-invite", { room = self, stanza = invite, origin = origin, incoming = stanza }); | |
| 1091 return true; | |
| 1092 else | |
| 1093 origin.send(st.error_reply(stanza, "cancel", "jid-malformed")); | |
| 1094 return true; | |
| 1095 end | |
| 1096 end | |
| 1097 | |
| 1098 module:hook("muc-invite", function(event) | |
| 1099 event.room:_route_stanza(event.stanza); | |
| 1100 return true; | |
| 1101 end, -1) | |
| 1102 | |
| 1103 -- When an invite is sent; add an affiliation for the invitee | |
| 1104 module:hook("muc-invite", function(event) | |
| 1105 local room, stanza = event.room, event.stanza | |
| 1106 local invitee = stanza.attr.to | |
| 1107 if room:get_members_only() and not room:get_affiliation(invitee) then | |
| 1108 local from = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite").attr.from | |
| 1109 local current_nick = room:get_occupant_jid(from) | |
| 1110 log("debug", "%s invited %s into members only room %s, granting membership", from, invitee, room.jid); | |
| 1111 room:set_affiliation(from, invitee, "member", nil, "Invited by " .. current_nick) | |
| 1112 end | |
| 1113 end) | |
| 1114 | |
| 1115 function room_mt:handle_mediated_decline(origin, stanza) | |
| 1116 local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline") | |
| 1117 local declinee = jid_prep(payload.attr.to); | |
| 1118 if declinee then | |
| 1119 local from, to = stanza.attr.from, stanza.attr.to; | |
| 1120 -- TODO: Validate declinee | |
| 1121 local reason = payload:get_child_text("reason") | |
| 1122 local decline = st.message({from = to, to = declinee, id = stanza.attr.id}) | |
| 1123 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) | |
| 1124 :tag('decline', {from=from}) | |
| 1125 :tag('reason'):text(reason or ""):up() | |
| 1126 :up() | |
| 1127 :up() | |
| 1128 :tag('body') -- Add a plain message for clients which don't support declines | |
| 1129 :text(from..' declined your invite to the room '..to..(reason and (' ('..reason..')') or "")) | |
| 1130 :up(); | |
| 1131 module:fire_event("muc-decline", { room = self, stanza = decline, origin = origin, incoming = stanza }); | |
| 1132 return true; | |
| 1133 else | |
| 1134 origin.send(st.error_reply(stanza, "cancel", "jid-malformed")); | |
| 1135 return true; | |
| 1136 end | |
| 1137 end | |
| 1138 | |
| 1139 module:hook("muc-decline", function(event) | |
| 1140 local room, stanza = event.room, event.stanza | |
| 1141 local occupant = room:get_occupant_by_real_jid(stanza.attr.to); | |
| 1142 if occupant then | |
| 1143 room:route_to_occupant(occupant, stanza) | |
| 1144 else | |
| 1145 room:route_stanza(stanza); | |
| 1146 end | |
| 1147 return true; | |
| 1148 end, -1) | |
| 1149 | |
| 1150 function room_mt:handle_message_to_room(origin, stanza) | |
| 1151 local type = stanza.attr.type; | |
| 1152 if type == "groupchat" then | |
| 1153 return self:handle_groupchat_to_room(origin, stanza) | |
| 1154 elseif type == "error" and is_kickable_error(stanza) then | |
| 1155 return self:handle_kickable(origin, stanza) | |
| 1156 elseif type == nil then | |
| 1157 local x = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); | |
| 1158 if x then | |
| 1159 local payload = x.tags[1]; | |
| 1160 if payload == nil then | |
| 1161 -- fallthrough | |
| 1162 elseif payload.name == "invite" and payload.attr.to then | |
| 1163 return self:handle_mediated_invite(origin, stanza) | |
| 1164 elseif payload.name == "decline" and payload.attr.to then | |
| 1165 return self:handle_mediated_decline(origin, stanza) | |
| 1166 end | |
| 927 origin.send(st.error_reply(stanza, "cancel", "bad-request")); | 1167 origin.send(st.error_reply(stanza, "cancel", "bad-request")); |
| 1168 return true; | |
| 928 end | 1169 end |
| 929 else | 1170 else |
| 930 if type == "error" or type == "result" then return; end | 1171 return nil; |
| 931 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); | 1172 end |
| 932 end | 1173 end |
| 933 end | 1174 |
| 934 | 1175 function room_mt:route_stanza(stanza) |
| 935 function room_mt:handle_stanza(origin, stanza) | 1176 module:send(stanza) |
| 936 local to_node, to_host, to_resource = jid_split(stanza.attr.to); | 1177 end |
| 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 | 1178 |
| 946 function room_mt:get_affiliation(jid) | 1179 function room_mt:get_affiliation(jid) |
| 947 local node, host, resource = jid_split(jid); | 1180 local node, host, resource = jid_split(jid); |
| 948 local bare = node and node.."@"..host or host; | 1181 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. | 1182 local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID. |
| 1029 local occupant = self._occupants[occupant_jid]; | 1262 local occupant = self._occupants[occupant_jid]; |
| 1030 if not occupant or not actor_jid then return nil, "modify", "not-acceptable"; end | 1263 if not occupant or not actor_jid then return nil, "modify", "not-acceptable"; end |
| 1031 | 1264 |
| 1032 if actor_jid == true then return true; end | 1265 if actor_jid == true then return true; end |
| 1033 | 1266 |
| 1034 local actor = self._occupants[self._jid_nick[actor_jid]]; | 1267 local actor = self._occupants[self:get_occupant_jid(actor_jid)]; |
| 1035 if actor and actor.role == "moderator" then | 1268 if actor and actor.role == "moderator" then |
| 1036 if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then | 1269 if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then |
| 1037 if actor.affiliation == "owner" or actor.affiliation == "admin" then | 1270 if actor.affiliation == "owner" or actor.affiliation == "admin" then |
| 1038 return true; | 1271 return true; |
| 1039 elseif occupant.role ~= "moderator" and role ~= "moderator" then | 1272 elseif occupant.role ~= "moderator" and role ~= "moderator" then |
| 1083 return true; | 1316 return true; |
| 1084 end | 1317 end |
| 1085 | 1318 |
| 1086 function room_mt:_route_stanza(stanza) | 1319 function room_mt:_route_stanza(stanza) |
| 1087 local muc_child; | 1320 local muc_child; |
| 1088 local to_occupant = self._occupants[self._jid_nick[stanza.attr.to]]; | |
| 1089 local from_occupant = self._occupants[stanza.attr.from]; | |
| 1090 if stanza.name == "presence" then | 1321 if stanza.name == "presence" then |
| 1322 local to_occupant = self._occupants[self:get_occupant_jid(stanza.attr.to)]; | |
| 1323 local from_occupant = self._occupants[stanza.attr.from]; | |
| 1091 if to_occupant and from_occupant then | 1324 if to_occupant and from_occupant then |
| 1092 if self._data.whois == 'anyone' then | 1325 if self:get_whois() == 'anyone' then |
| 1093 muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); | 1326 muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); |
| 1094 else | 1327 else |
| 1095 if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then | 1328 if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then |
| 1096 muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); | 1329 muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); |
| 1097 end | 1330 end |
| 1098 end | 1331 end |
| 1099 end | 1332 end |
| 1100 end | 1333 if muc_child then |
| 1101 if muc_child then | 1334 for item in muc_child:childtags("item") do |
| 1102 for _, item in pairs(muc_child.tags) do | |
| 1103 if item.name == "item" then | |
| 1104 if from_occupant == to_occupant then | 1335 if from_occupant == to_occupant then |
| 1105 item.attr.jid = stanza.attr.to; | 1336 item.attr.jid = stanza.attr.to; |
| 1106 else | 1337 else |
| 1107 item.attr.jid = from_occupant.jid; | 1338 item.attr.jid = from_occupant.jid; |
| 1108 end | 1339 end |
| 1109 end | 1340 end |
| 1110 end | 1341 end |
| 1111 end | 1342 end |
| 1112 self:route_stanza(stanza); | 1343 self:route_stanza(stanza); |
| 1113 if muc_child then | 1344 if muc_child then |
| 1114 for _, item in pairs(muc_child.tags) do | 1345 for item in muc_child:childtags("item") do |
| 1115 if item.name == "item" then | 1346 item.attr.jid = nil; |
| 1116 item.attr.jid = nil; | |
| 1117 end | |
| 1118 end | 1347 end |
| 1119 end | 1348 end |
| 1120 end | 1349 end |
| 1121 | 1350 |
| 1122 local _M = {}; -- module "muc" | 1351 local _M = {}; -- module "muc" |
| 1123 | 1352 |
| 1124 function _M.new_room(jid, config) | 1353 function _M.new_room(jid, config) |
| 1125 return setmetatable({ | 1354 return setmetatable({ |
| 1126 jid = jid; | 1355 jid = jid; |
| 1356 locked = nil; | |
| 1127 _jid_nick = {}; | 1357 _jid_nick = {}; |
| 1128 _occupants = {}; | 1358 _occupants = {}; |
| 1129 _data = { | 1359 _data = { |
| 1130 whois = 'moderators'; | 1360 whois = 'moderators'; |
| 1131 history_length = math.min((config and config.history_length) | 1361 history_length = math.min((config and config.history_length) |