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)