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