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