2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
4 -- Copyright (C) 2014 Daurnimator
6 -- This project is MIT/X11 licensed. Please see the
7 -- COPYING file in the source package for more information.
10 local select = select;
11 local pairs, ipairs = pairs, ipairs;
13 local setmetatable = setmetatable;
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;
25 local occupant_lib = module:require "muc/occupant"
26 local muc_util = module:require "muc/util";
27 local is_kickable_error = muc_util.is_kickable_error;
28 local valid_roles, valid_affiliations = muc_util.valid_roles, muc_util.valid_affiliations;
31 room_mt.__index = room_mt;
33 function room_mt:__tostring()
34 return "MUC room ("..self.jid..")";
37 function room_mt:get_occupant_jid(real_jid)
38 return self._jid_nick[real_jid]
41 function room_mt:get_default_role(affiliation)
42 if affiliation == "owner" or affiliation == "admin" then
44 elseif affiliation == "member" then
46 elseif not affiliation then
47 if not self:get_members_only() then
48 return self:get_moderated() and "visitor" or "participant";
53 --- Occupant functions
54 function room_mt:new_occupant(bare_real_jid, nick)
55 local occupant = occupant_lib.new(bare_real_jid, nick);
56 local affiliation = self:get_affiliation(bare_real_jid);
57 occupant.role = self:get_default_role(affiliation);
61 function room_mt:get_occupant_by_nick(nick)
62 local occupant = self._occupants[nick];
63 if occupant == nil then return nil end
64 return occupant_lib.copy(occupant);
68 local function next_copied_occupant(occupants, occupant_jid)
69 local next_occupant_jid, raw_occupant = next(occupants, occupant_jid);
70 if next_occupant_jid == nil then return nil end
71 return next_occupant_jid, occupant_lib.copy(raw_occupant);
73 function room_mt:each_occupant(read_only)
74 return next_copied_occupant, self._occupants, nil;
78 function room_mt:get_occupant_by_real_jid(real_jid)
79 local occupant_jid = self:get_occupant_jid(real_jid);
80 if occupant_jid == nil then return nil end
81 return self:get_occupant_by_nick(occupant_jid);
84 function room_mt:save_occupant(occupant)
85 occupant = occupant_lib.copy(occupant); -- So that occupant can be modified more
86 local id = occupant.nick
88 -- Need to maintain _jid_nick secondary index
89 local old_occupant = self._occupants[id];
91 for real_jid in pairs(old_occupant.sessions) do
92 self._jid_nick[real_jid] = nil;
95 if occupant.role ~= nil and next(occupant.sessions) then
96 for real_jid, presence in occupant:each_session() do
97 self._jid_nick[real_jid] = occupant.nick;
102 self._occupants[id] = occupant
105 function room_mt:route_to_occupant(occupant, stanza)
106 local to = stanza.attr.to;
107 for jid, pr in occupant:each_session() do
108 if pr.attr.type ~= "unavailable" then
109 stanza.attr.to = jid;
110 self:route_stanza(stanza);
116 -- actor is the attribute table
117 local function add_item(x, affiliation, role, jid, nick, actor, reason)
118 x:tag("item", {affiliation = affiliation; role = role; jid = jid; nick = nick;})
120 x:tag("actor", actor):up()
123 x:tag("reason"):text(reason):up()
129 -- actor is (real) jid
130 function room_mt:build_item_list(occupant, x, is_anonymous, nick, actor, reason)
131 local affiliation = self:get_affiliation(occupant.bare_jid) or "none";
132 local role = occupant.role or "none";
135 actor_attr = {nick = select(3,jid_split(self:get_occupant_jid(actor)))};
138 add_item(x, affiliation, role, nil, nick, actor_attr, reason);
141 actor_attr.jid = actor;
143 for real_jid, session in occupant:each_session() do
144 add_item(x, affiliation, role, real_jid, nick, actor_attr, reason);
150 function room_mt:broadcast_message(stanza)
151 module:fire_event("muc-broadcast-message", {room = self, stanza = stanza});
152 self:broadcast(stanza);
155 -- Broadcast a stanza to all occupants in the room.
156 -- optionally checks conditional called with (nick, occupant)
157 function room_mt:broadcast(stanza, cond_func)
158 for nick, occupant in self:each_occupant() do
159 if cond_func == nil or cond_func(nick, occupant) then
160 self:route_to_occupant(occupant, stanza)
165 local function can_see_real_jids(whois, occupant)
166 if whois == "anyone" then
168 elseif whois == "moderators" then
169 return valid_roles[occupant.role or "none"] >= valid_roles.moderator;
173 local function get_base_presence(occupant)
174 if occupant.role ~= nil then
175 -- Try to use main jid's presence
176 local pr = occupant:get_presence();
181 return st.presence {from = occupant.nick; type = "unavailable";};
184 -- Broadcasts an occupant's presence to the whole room
185 -- Takes the x element that goes into the stanzas
186 function room_mt:publicise_occupant_status(occupant, base_x, nick, actor, reason)
187 -- Build real jid and (optionally) occupant jid template presences
188 local function get_presence(is_anonymous)
189 local x = st.clone(base_x);
190 self:build_item_list(occupant, x, is_anonymous, nick, actor, reason);
191 return get_base_presence(occupant):add_child(x), x;
193 local full_p, full_x = get_presence(false);
194 local anon_p, anon_x;
195 local function get_anon_p()
196 if anon_p == nil then
197 anon_p, anon_x = get_presence(true);
199 return anon_p, anon_x;
202 local whois = self:get_whois();
205 for nick, n_occupant in self:each_occupant() do
206 if nick ~= occupant.nick then
208 if can_see_real_jids(whois, occupant) or occupant.bare_jid == n_occupant.bare_jid then
213 self:route_to_occupant(n_occupant, pr);
217 -- Presences for occupant itself
218 full_x:tag("status", {code = "110";}):up();
219 if occupant.role == nil then
220 -- They get an unavailable
221 self:route_to_occupant(occupant, full_p);
223 -- use their own presences as templates
224 for full_jid, pr in occupant:each_session() do
226 pr.attr.to = full_jid;
227 -- You can always see your own full jids
228 pr:add_child(full_x);
229 self:route_stanza(pr);
234 function room_mt:send_occupant_list(to, filter)
235 local to_bare = jid_bare(to);
236 local is_anonymous = true;
237 local whois = self:get_whois();
238 if whois ~= "anyone" then
239 local affiliation = self:get_affiliation(to);
240 if affiliation ~= "admin" and affiliation ~= "owner" then
241 local occupant = self:get_occupant_by_real_jid(to);
242 if not occupant or can_see_real_jids(whois, occupant) then
243 is_anonymous = false;
247 for occupant_jid, occupant in self:each_occupant() do
248 if filter == nil or filter(occupant_jid, occupant) then
249 local x = st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'});
250 self:build_item_list(occupant, x, is_anonymous and to_bare ~= occupant.bare_jid); -- can always see your own jids
251 local pres = st.clone(occupant:get_presence());
254 self:route_stanza(pres);
259 function room_mt:get_disco_info(stanza)
260 local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info");
261 local form = dataform.new {
262 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" };
264 module:fire_event("muc-disco#info", {room = self; reply = reply; form = form;});
265 reply:add_child(form:form(nil, "result"));
268 module:hook("muc-disco#info", function(event)
269 event.reply:tag("feature", {var = "http://jabber.org/protocol/muc"}):up();
271 module:hook("muc-disco#info", function(event)
272 event.reply:tag("feature", {var = event.room:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up();
274 module:hook("muc-disco#info", function(event)
275 event.reply:tag("feature", {var = event.room:get_members_only() and "muc_membersonly" or "muc_open"}):up();
277 module:hook("muc-disco#info", function(event)
278 event.reply:tag("feature", {var = event.room:get_persistent() and "muc_persistent" or "muc_temporary"}):up();
280 module:hook("muc-disco#info", function(event)
281 event.reply:tag("feature", {var = event.room:get_hidden() and "muc_hidden" or "muc_public"}):up();
283 module:hook("muc-disco#info", function(event)
284 local count = iterators.count(event.room:each_occupant());
285 table.insert(event.form, { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) });
288 function room_mt:get_disco_items(stanza)
289 local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
290 for room_jid in self:each_occupant() do
291 reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up();
296 function room_mt:get_subject()
297 return self._data['subject'], self._data['subject_from']
299 local function create_subject_message(from, subject)
300 return st.message({from = from; type = "groupchat"})
301 :tag('subject'):text(subject):up();
303 function room_mt:send_subject(to)
304 local msg = create_subject_message(self:get_subject());
306 self:route_stanza(msg);
308 function room_mt:set_subject(current_nick, subject)
309 if subject == "" then subject = nil; end
310 self._data['subject'] = subject;
311 self._data['subject_from'] = current_nick;
312 if self.save then self:save(); end
313 local msg = create_subject_message(current_nick, subject);
314 self:broadcast_message(msg);
318 function room_mt:handle_kickable(origin, stanza)
319 local real_jid = stanza.attr.from;
320 local occupant = self:get_occupant_by_real_jid(real_jid);
321 if occupant == nil then return nil; end
322 local type, condition, text = stanza:get_error();
323 local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error");
325 error_message = error_message..": "..text;
327 occupant:set_session(real_jid, st.presence({type="unavailable"})
328 :tag('status'):text(error_message));
329 self:save_occupant(occupant);
330 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";})
331 :tag("status", {code = "307"})
332 self:publicise_occupant_status(occupant, x);
336 function room_mt:set_moderated(moderated)
337 moderated = moderated and true or nil;
338 if self._data.moderated ~= moderated then
339 self._data.moderated = moderated;
340 if self.save then self:save(true); end
343 function room_mt:get_moderated()
344 return self._data.moderated;
346 function room_mt:set_members_only(members_only)
347 members_only = members_only and true or nil;
348 if self._data.members_only ~= members_only then
349 self._data.members_only = members_only;
350 if self.save then self:save(true); end
353 function room_mt:get_members_only()
354 return self._data.members_only;
356 function room_mt:set_persistent(persistent)
357 persistent = persistent and true or nil;
358 if self._data.persistent ~= persistent then
359 self._data.persistent = persistent;
360 if self.save then self:save(true); end
363 function room_mt:get_persistent()
364 return self._data.persistent;
366 function room_mt:set_hidden(hidden)
367 hidden = hidden and true or nil;
368 if self._data.hidden ~= hidden then
369 self._data.hidden = hidden;
370 if self.save then self:save(true); end
373 function room_mt:get_hidden()
374 return self._data.hidden;
376 function room_mt:get_public()
377 return not self:get_hidden();
379 function room_mt:set_public(public)
380 return self:set_hidden(not public);
382 function room_mt:set_changesubject(changesubject)
383 changesubject = changesubject and true or nil;
384 if self._data.changesubject ~= changesubject then
385 self._data.changesubject = changesubject;
386 if self.save then self:save(true); end
389 function room_mt:get_changesubject()
390 return self._data.changesubject;
393 -- Give the room creator owner affiliation
394 module:hook("muc-room-pre-create", function(event)
395 event.room:set_affiliation(true, jid_bare(event.stanza.attr.from), "owner");
398 -- registration required for entering members-only room
399 module:hook("muc-occupant-pre-join", function(event)
400 local room, stanza = event.room, event.stanza;
401 local affiliation = room:get_affiliation(stanza.attr.from);
402 if affiliation == nil and event.room:get_members_only() then
403 local reply = st.error_reply(stanza, "auth", "registration-required"):up();
404 reply.tags[1].attr.code = "407";
405 event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
410 -- check if user is banned
411 module:hook("muc-occupant-pre-join", function(event)
412 local room, stanza = event.room, event.stanza;
413 local affiliation = room:get_affiliation(stanza.attr.from);
414 if affiliation == "outcast" then
415 local reply = st.error_reply(stanza, "auth", "forbidden"):up();
416 reply.tags[1].attr.code = "403";
417 event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
422 -- Send occupant list to newly joined user
423 module:hook("muc-occupant-joined", function(event)
424 local real_jid = event.stanza.attr.from;
425 event.room:send_occupant_list(real_jid, function(nick, occupant)
426 -- Don't include self
427 return occupant:get_presence(real_jid) == nil;
431 -- Send subject to joining user
432 module:hook("muc-occupant-joined", function(event)
433 event.room:send_subject(event.stanza.attr.from);
436 function room_mt:handle_presence_to_occupant(origin, stanza)
437 local type = stanza.attr.type;
438 if type == "error" then -- error, kick em out!
439 return self:handle_kickable(origin, stanza)
440 elseif type == nil or type == "unavailable" then
441 local real_jid = stanza.attr.from;
442 local bare_jid = jid_bare(real_jid);
443 local orig_occupant, dest_occupant;
444 local is_new_room = next(self._affiliations) == nil;
446 if type == "unavailable" then return true; end -- Unavailable from someone not in the room
447 if module:fire_event("muc-room-pre-create", {
451 }) then return true; end
453 orig_occupant = self:get_occupant_by_real_jid(real_jid);
454 if type == "unavailable" and orig_occupant == nil then return true; end -- Unavailable from someone not in the room
456 local is_first_dest_session;
457 if type == "unavailable" then
458 -- dest_occupant = nil
459 elseif orig_occupant and orig_occupant.nick == stanza.attr.to then -- Just a presence update
460 log("debug", "presence update for %s from session %s", orig_occupant.nick, real_jid);
461 dest_occupant = orig_occupant;
463 local dest_jid = stanza.attr.to;
464 dest_occupant = self:get_occupant_by_nick(dest_jid);
465 if dest_occupant == nil then
466 log("debug", "no occupant found for %s; creating new occupant object for %s", dest_jid, real_jid);
467 is_first_dest_session = true;
468 dest_occupant = self:new_occupant(bare_jid, dest_jid);
470 is_first_dest_session = false;
473 local is_last_orig_session;
474 if orig_occupant ~= nil then
475 -- Is there are least 2 sessions?
476 local iter, ob, last = orig_occupant:each_session();
477 is_last_orig_session = iter(ob, iter(ob, last)) == nil;
480 local event, event_name = {
484 is_first_session = is_first_dest_session;
485 is_last_session = is_last_orig_session;
487 if orig_occupant == nil then
488 event_name = "muc-occupant-pre-join";
489 event.is_new_room = is_new_room;
490 elseif dest_occupant == nil then
491 event_name = "muc-occupant-pre-leave";
493 event_name = "muc-occupant-pre-change";
495 if module:fire_event(event_name, event) then return true; end
497 -- Check for nick conflicts
498 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
499 log("debug", "%s couldn't join due to nick conflict: %s", real_jid, dest_occupant.nick);
500 local reply = st.error_reply(stanza, "cancel", "conflict"):up();
501 reply.tags[1].attr.code = "409";
502 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
506 -- Send presence stanza about original occupant
507 if orig_occupant ~= nil and orig_occupant ~= dest_occupant then
508 local orig_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
510 if dest_occupant == nil then -- Session is leaving
511 log("debug", "session %s is leaving occupant %s", real_jid, orig_occupant.nick);
512 orig_occupant:set_session(real_jid, stanza);
514 log("debug", "session %s is changing from occupant %s to %s", real_jid, orig_occupant.nick, dest_occupant.nick);
515 local generated_unavail = st.presence {from = orig_occupant.nick, to = real_jid, type = "unavailable"};
516 orig_occupant:set_session(real_jid, generated_unavail);
517 dest_nick = select(3, jid_split(dest_occupant.nick));
518 if not is_first_dest_session then -- User is swapping into another pre-existing session
519 log("debug", "session %s is swapping into multisession %s, showing it leave.", real_jid, dest_occupant.nick);
520 -- Show the other session leaving
521 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";})
522 :tag("status"):text("you are joining pre-existing session " .. dest_nick):up();
523 add_item(x, self:get_affiliation(bare_jid), "none");
524 local pr = st.presence{from = dest_occupant.nick, to = real_jid, type = "unavailable"}
526 self:route_stanza(pr);
528 if is_first_dest_session and is_last_orig_session then -- Normal nick change
529 log("debug", "no sessions in %s left; publically marking as nick change", orig_occupant.nick);
530 orig_x:tag("status", {code = "303";}):up();
531 else -- The session itself always needs to see a nick change
532 -- don't want to get our old nick's available presence,
533 -- so remove our session from there, and manually generate an unavailable
534 orig_occupant:remove_session(real_jid);
535 log("debug", "generating nick change for %s", real_jid);
536 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
537 -- self:build_item_list(orig_occupant, x, false, dest_nick); -- COMPAT: clients get confused if they see other items besides their own
538 add_item(x, self:get_affiliation(bare_jid), orig_occupant.role, real_jid, dest_nick);
539 x:tag("status", {code = "303";}):up();
540 x:tag("status", {code = "110";}):up();
541 self:route_stanza(generated_unavail:add_child(x));
542 dest_nick = nil; -- set dest_nick to nil; so general populance doesn't see it for whole orig_occupant
545 self:save_occupant(orig_occupant);
546 self:publicise_occupant_status(orig_occupant, orig_x, dest_nick);
548 if is_last_orig_session then
549 module:fire_event("muc-occupant-left", {room = self; nick = orig_occupant.nick;});
553 if dest_occupant ~= nil then
554 dest_occupant:set_session(real_jid, stanza);
555 local dest_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
557 dest_x:tag("status", {code = "201"}):up();
559 if orig_occupant == nil and self:get_whois() == "anyone" then
560 dest_x:tag("status", {code = "100"}):up();
562 self:save_occupant(dest_occupant);
563 self:publicise_occupant_status(dest_occupant, dest_x);
565 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
566 log("debug", "session %s split nicks; showing %s rejoining", real_jid, orig_occupant.nick);
567 -- Show the original nick joining again
568 local pr = st.clone(orig_occupant:get_presence());
569 pr.attr.to = real_jid;
570 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
571 self:build_item_list(orig_occupant, x, false);
572 -- TODO: new status code to inform client this was the multi-session it left?
574 self:route_stanza(pr);
577 if orig_occupant == nil and is_first_dest_session then
578 module:fire_event("muc-occupant-joined", {room = self; nick = dest_occupant.nick; stanza = stanza;});
581 elseif type ~= 'result' then -- bad type
582 if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences
583 origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
589 function room_mt:handle_iq_to_occupant(origin, stanza)
590 local from, to = stanza.attr.from, stanza.attr.to;
591 local type = stanza.attr.type;
592 local id = stanza.attr.id;
593 local occupant = self:get_occupant_by_nick(to);
594 if (type == "error" or type == "result") then
595 do -- deconstruct_stanza_id
596 if not occupant then return nil; end
597 local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$");
598 if not(from == from_jid or from == jid_bare(from_jid)) then return nil; end
599 local from_occupant_jid = self:get_occupant_jid(from_jid);
600 if from_occupant_jid == nil then return nil; end
602 for to_jid in occupant:each_session() do
603 if md5(to_jid) == to_jid_hash then
604 session_jid = to_jid;
608 if session_jid == nil then return nil; end
609 stanza.attr.from, stanza.attr.to, stanza.attr.id = from_jid, session_jid, id;
611 log("debug", "%s sent private iq stanza to %s (%s)", from, to, stanza.attr.to);
612 self:route_stanza(stanza);
613 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
615 else -- Type is "get" or "set"
616 local current_nick = self:get_occupant_jid(from);
617 if not current_nick then
618 origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
621 if not occupant then -- recipient not in room
622 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
625 do -- construct_stanza_id
626 stanza.attr.id = base64.encode(occupant.jid.."\0"..stanza.attr.id.."\0"..md5(from));
628 stanza.attr.from, stanza.attr.to = current_nick, occupant.jid;
629 log("debug", "%s sent private iq stanza to %s (%s)", from, to, occupant.jid);
630 if stanza.tags[1].attr.xmlns == 'vcard-temp' then
631 stanza.attr.to = jid_bare(stanza.attr.to);
633 self:route_stanza(stanza);
634 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
639 function room_mt:handle_message_to_occupant(origin, stanza)
640 local from, to = stanza.attr.from, stanza.attr.to;
641 local current_nick = self:get_occupant_jid(from);
642 local type = stanza.attr.type;
643 if not current_nick then -- not in room
644 if type ~= "error" then
645 origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
649 if type == "groupchat" then -- groupchat messages not allowed in PM
650 origin.send(st.error_reply(stanza, "modify", "bad-request"));
652 elseif type == "error" and is_kickable_error(stanza) then
653 log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
654 return self:handle_kickable(origin, stanza); -- send unavailable
657 local o_data = self:get_occupant_by_nick(to);
659 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
662 log("debug", "%s sent private message stanza to %s (%s)", from, to, o_data.jid);
663 stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up();
664 stanza.attr.from = current_nick;
665 self:route_to_occupant(o_data, stanza)
666 -- TODO: Remove x tag?
667 stanza.attr.from = from;
671 function room_mt:send_form(origin, stanza)
672 origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
673 :add_child(self:get_form_layout(stanza.attr.from):form())
677 function room_mt:get_form_layout(actor)
678 local form = dataform.new({
679 title = "Configuration for "..self.jid,
680 instructions = "Complete and submit this form to configure the room.",
684 value = 'http://jabber.org/protocol/muc#roomconfig'
687 return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form;
689 module:hook("muc-config-form", function(event)
690 table.insert(event.form, {
691 name = 'muc#roomconfig_persistentroom',
693 label = 'Make Room Persistent?',
694 value = event.room:get_persistent()
697 module:hook("muc-config-form", function(event)
698 table.insert(event.form, {
699 name = 'muc#roomconfig_publicroom',
701 label = 'Make Room Publicly Searchable?',
702 value = not event.room:get_hidden()
705 module:hook("muc-config-form", function(event)
706 table.insert(event.form, {
707 name = 'muc#roomconfig_changesubject',
709 label = 'Allow Occupants to Change Subject?',
710 value = event.room:get_changesubject()
713 module:hook("muc-config-form", function(event)
714 table.insert(event.form, {
715 name = 'muc#roomconfig_moderatedroom',
717 label = 'Make Room Moderated?',
718 value = event.room:get_moderated()
721 module:hook("muc-config-form", function(event)
722 table.insert(event.form, {
723 name = 'muc#roomconfig_membersonly',
725 label = 'Make Room Members-Only?',
726 value = event.room:get_members_only()
730 function room_mt:process_form(origin, stanza)
731 local form = stanza.tags[1]:get_child("x", "jabber:x:data");
732 if form.attr.type == "cancel" then
733 origin.send(st.reply(stanza));
734 elseif form.attr.type == "submit" then
735 local fields = self:get_form_layout(stanza.attr.from):data(form);
736 if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then
737 origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration"));
741 local event = {room = self; origin = origin; stanza = stanza; fields = fields; status_codes = {};};
742 function event.update_option(name, field, allowed)
743 local new = fields[field];
744 if new == nil then return; end
745 if allowed and not allowed[new] then return; end
746 if new == self["get_"..name](self) then return; end
747 event.status_codes["104"] = true;
748 self["set_"..name](self, new);
751 module:fire_event("muc-config-submitted", event);
753 if self.save then self:save(true); end
754 origin.send(st.reply(stanza));
756 if next(event.status_codes) then
757 local msg = st.message({type='groupchat', from=self.jid})
758 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
759 for code in pairs(event.status_codes) do
760 msg:tag("status", {code = code;}):up();
763 self:broadcast_message(msg);
766 origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form"));
770 module:hook("muc-config-submitted", function(event)
771 event.update_option("persistent", "muc#roomconfig_persistentroom");
773 module:hook("muc-config-submitted", function(event)
774 event.update_option("moderated", "muc#roomconfig_moderatedroom");
776 module:hook("muc-config-submitted", function(event)
777 event.update_option("members_only", "muc#roomconfig_membersonly");
779 module:hook("muc-config-submitted", function(event)
780 event.update_option("public", "muc#roomconfig_publicroom");
782 module:hook("muc-config-submitted", function(event)
783 event.update_option("changesubject", "muc#roomconfig_changesubject");
786 -- Removes everyone from the room
787 function room_mt:clear(x)
788 x = x or st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'});
789 local occupants_updated = {};
790 for nick, occupant in self:each_occupant() do
792 self:save_occupant(occupant);
793 occupants_updated[occupant] = true;
795 for occupant in pairs(occupants_updated) do
796 self:publicise_occupant_status(occupant, x);
797 module:fire_event("muc-occupant-left", { room = self; nick = occupant.nick; });
801 function room_mt:destroy(newjid, reason, password)
802 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"})
803 :tag("item", { affiliation='none', role='none' }):up()
804 :tag("destroy", {jid=newjid});
805 if reason then x:tag("reason"):text(reason):up(); end
806 if password then x:tag("password"):text(password):up(); end
809 self:set_persistent(false);
810 module:fire_event("muc-room-destroyed", { room = self });
813 function room_mt:handle_disco_info_get_query(origin, stanza)
814 origin.send(self:get_disco_info(stanza));
818 function room_mt:handle_disco_items_get_query(origin, stanza)
819 origin.send(self:get_disco_items(stanza));
823 function room_mt:handle_admin_query_set_command(origin, stanza)
824 local item = stanza.tags[1].tags[1];
825 if item.attr.jid then -- Validate provided JID
826 item.attr.jid = jid_prep(item.attr.jid);
827 if not item.attr.jid then
828 origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
832 if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation
833 local occupant = self:get_occupant_by_nick(self.jid.."/"..item.attr.nick);
834 if occupant then item.attr.jid = occupant.jid; end
835 elseif not item.attr.nick and item.attr.jid then
836 local nick = self:get_occupant_jid(item.attr.jid);
837 if nick then item.attr.nick = select(3, jid_split(nick)); end
839 local actor = stanza.attr.from;
840 local reason = item:get_child_text("reason");
841 local success, errtype, err
842 if item.attr.affiliation and item.attr.jid and not item.attr.role then
843 success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, reason);
844 elseif item.attr.role and item.attr.nick and not item.attr.affiliation then
845 success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, reason);
847 success, errtype, err = nil, "cancel", "bad-request";
849 if not success then origin.send(st.error_reply(stanza, errtype, err)); end
850 origin.send(st.reply(stanza));
854 function room_mt:handle_admin_query_get_command(origin, stanza)
855 local actor = stanza.attr.from;
856 local affiliation = self:get_affiliation(actor);
857 local item = stanza.tags[1].tags[1];
858 local _aff = item.attr.affiliation;
859 local _rol = item.attr.role;
860 if _aff and not _rol then
861 if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then
862 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
863 for jid, affiliation in pairs(self._affiliations) do
864 if affiliation == _aff then
865 reply:tag("item", {affiliation = _aff, jid = jid}):up();
871 origin.send(st.error_reply(stanza, "auth", "forbidden"));
874 elseif _rol and not _aff then
875 local role = self:get_role(self:get_occupant_jid(actor)) or self:get_default_role(affiliation);
876 if valid_roles[role or "none"] >= valid_roles.moderator then
877 if _rol == "none" then _rol = nil; end
878 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
879 -- TODO: whois check here? (though fully anonymous rooms are not supported)
880 for occupant_jid, occupant in self:each_occupant() do
881 if occupant.role == _rol then
882 local nick = select(3,jid_split(occupant_jid));
883 self:build_item_list(occupant, reply, false, nick);
886 origin.send(reply:up());
889 origin.send(st.error_reply(stanza, "auth", "forbidden"));
893 origin.send(st.error_reply(stanza, "cancel", "bad-request"));
898 function room_mt:handle_owner_query_get_to_room(origin, stanza)
899 if self:get_affiliation(stanza.attr.from) ~= "owner" then
900 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms"));
904 self:send_form(origin, stanza);
907 function room_mt:handle_owner_query_set_to_room(origin, stanza)
908 if self:get_affiliation(stanza.attr.from) ~= "owner" then
909 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms"));
913 local child = stanza.tags[1].tags[1];
915 origin.send(st.error_reply(stanza, "modify", "bad-request"));
917 elseif child.name == "destroy" then
918 local newjid = child.attr.jid;
919 local reason = child:get_child_text("reason");
920 local password = child:get_child_text("password");
921 self:destroy(newjid, reason, password);
922 origin.send(st.reply(stanza));
924 elseif child.name == "x" and child.attr.xmlns == "jabber:x:data" then
925 return self:process_form(origin, stanza);
927 origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
932 function room_mt:handle_groupchat_to_room(origin, stanza)
933 local from = stanza.attr.from;
934 local occupant = self:get_occupant_by_real_jid(from);
935 if not occupant then -- not in room
936 origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
938 elseif occupant.role == "visitor" then
939 origin.send(st.error_reply(stanza, "auth", "forbidden"));
942 local from = stanza.attr.from;
943 stanza.attr.from = occupant.nick;
944 local subject = stanza:get_child_text("subject");
946 if occupant.role == "moderator" or
947 ( self:get_changesubject() and occupant.role == "participant" ) then -- and participant
948 self:set_subject(occupant.nick, subject);
950 stanza.attr.from = from;
951 origin.send(st.error_reply(stanza, "auth", "forbidden"));
954 self:broadcast_message(stanza);
956 stanza.attr.from = from;
961 -- hack - some buggy clients send presence updates to the room rather than their nick
962 function room_mt:handle_presence_to_room(origin, stanza)
963 local current_nick = self:get_occupant_jid(stanza.attr.from);
966 local to = stanza.attr.to;
967 stanza.attr.to = current_nick;
968 handled = self:handle_presence_to_occupant(origin, stanza);
974 -- Need visitor role or higher to invite
975 module:hook("muc-pre-invite", function(event)
976 local room, stanza = event.room, event.stanza;
977 local _from, _to = stanza.attr.from, stanza.attr.to;
978 local inviter = room:get_occupant_by_real_jid(_from);
979 local role = inviter and inviter.role or room:get_default_role(room:get_affiliation(_from));
980 if valid_roles[role or "none"] <= valid_roles.visitor then
981 event.origin.send(st.error_reply(stanza, "auth", "forbidden"));
986 -- Invitation privileges in members-only rooms SHOULD be restricted to room admins;
987 -- if a member without privileges to edit the member list attempts to invite another user
988 -- the service SHOULD return a <forbidden/> error to the occupant
989 module:hook("muc-pre-invite", function(event)
990 local room, stanza = event.room, event.stanza;
991 if room:get_members_only() and valid_affiliations[room:get_affiliation(stanza.attr.from) or "none"] < valid_affiliations.admin then
992 event.origin.send(st.error_reply(stanza, "auth", "forbidden"));
997 function room_mt:handle_mediated_invite(origin, stanza)
998 local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite");
999 local invitee = jid_prep(payload.attr.to);
1001 origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
1003 elseif not module:fire_event("muc-pre-invite", {room = self, origin = origin, stanza = stanza}) then
1006 local invite = st.message({from = self.jid, to = invitee, id = stanza.attr.id})
1007 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
1008 :tag('invite', {from = stanza.attr.from;})
1009 :tag('reason'):text(payload:get_child_text("reason")):up()
1012 if not module:fire_event("muc-invite", {room = self, stanza = invite, origin = origin, incoming = stanza}) then
1013 self:route_stanza(invite);
1018 -- COMPAT: Some older clients expect this
1019 module:hook("muc-invite", function(event)
1020 local room, stanza = event.room, event.stanza;
1021 local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite");
1022 local reason = invite:get_child_text("reason");
1023 stanza:tag('x', {xmlns = "jabber:x:conference"; jid = room.jid;})
1028 -- Add a plain message for clients which don't support invites
1029 module:hook("muc-invite", function(event)
1030 local room, stanza = event.room, event.stanza;
1031 local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite");
1032 local reason = invite:get_child_text("reason") or "";
1034 :text(invite.attr.from.." invited you to the room "..room.jid..(reason == "" and (" ("..reason..")") or ""))
1038 -- When an invite is sent; add an affiliation for the invitee
1039 module:hook("muc-invite", function(event)
1040 local room, stanza = event.room, event.stanza;
1041 local invitee = stanza.attr.to
1042 if room:get_members_only() and not room:get_affiliation(invitee) then
1043 local from = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite").attr.from
1044 log("debug", "%s invited %s into members only room %s, granting membership", from, invitee, room.jid);
1045 room:set_affiliation(from, invitee, "member", "Invited by " .. from); -- This might fail; ignore for now
1049 function room_mt:handle_mediated_decline(origin, stanza)
1050 local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline");
1051 local declinee = jid_prep(payload.attr.to);
1052 if not declinee then
1053 origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
1055 elseif not module:fire_event("muc-pre-decline", {room = self, origin = origin, stanza = stanza}) then
1058 local decline = st.message({from = self.jid, to = declinee, id = stanza.attr.id})
1059 :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
1060 :tag("decline", {from = stanza.attr.from})
1061 :tag("reason"):text(payload:get_child_text("reason")):up()
1064 if not module:fire_event("muc-decline", {room = self, stanza = decline, origin = origin, incoming = stanza}) then
1065 local occupant = self:get_occupant_by_real_jid(decline.attr.to);
1067 self:route_to_occupant(occupant, decline);
1069 self:route_stanza(decline);
1075 -- Add a plain message for clients which don't support declines
1076 module:hook("muc-decline", function(event)
1077 local room, stanza = event.room, event.stanza;
1078 local decline = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline");
1079 local reason = decline:get_child_text("reason") or "";
1081 :text(decline.attr.from.." declined your invite to the room "..room.jid..(reason == "" and (" ("..reason..")") or ""))
1085 function room_mt:handle_message_to_room(origin, stanza)
1086 local type = stanza.attr.type;
1087 if type == "groupchat" then
1088 return self:handle_groupchat_to_room(origin, stanza)
1089 elseif type == "error" and is_kickable_error(stanza) then
1090 return self:handle_kickable(origin, stanza)
1091 elseif type == nil then
1092 local x = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
1094 local payload = x.tags[1];
1095 if payload == nil then
1097 elseif payload.name == "invite" and payload.attr.to then
1098 return self:handle_mediated_invite(origin, stanza)
1099 elseif payload.name == "decline" and payload.attr.to then
1100 return self:handle_mediated_decline(origin, stanza)
1102 origin.send(st.error_reply(stanza, "cancel", "bad-request"));
1108 function room_mt:route_stanza(stanza)
1109 module:send(stanza);
1112 function room_mt:get_affiliation(jid)
1113 local node, host, resource = jid_split(jid);
1114 local bare = node and node.."@"..host or host;
1115 local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID.
1116 if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned
1120 function room_mt:set_affiliation(actor, jid, affiliation, reason)
1121 if not actor then return nil, "modify", "not-acceptable"; end;
1123 jid = jid_bare(jid);
1125 if valid_affiliations[affiliation or "none"] == nil then
1126 return nil, "modify", "not-acceptable";
1128 affiliation = affiliation ~= "none" and affiliation or nil; -- coerces `affiliation == false` to `nil`
1130 local target_affiliation = self._affiliations[jid]; -- Raw; don't want to check against host
1131 local is_downgrade = valid_affiliations[target_affiliation or "none"] > valid_affiliations[affiliation or "none"];
1133 if actor ~= true then
1134 local actor_bare = jid_bare(actor);
1135 local actor_affiliation = self._affiliations[actor_bare];
1136 if actor_affiliation == "owner" then
1137 if actor_bare == jid then -- self change
1138 -- need at least one owner
1139 local is_last = true;
1140 for j, aff in pairs(self._affiliations) do if j ~= jid and aff == "owner" then is_last = false; break; end end
1142 return nil, "cancel", "conflict";
1145 -- owners can do anything else
1146 elseif affiliation == "owner" or affiliation == "admin"
1147 or actor_affiliation ~= "admin"
1148 or target_affiliation == "owner" or target_affiliation == "admin" then
1149 -- Can't demote owners or other admins
1150 return nil, "cancel", "not-allowed";
1154 -- Set in 'database'
1155 self._affiliations[jid] = affiliation;
1158 local role = self:get_default_role(affiliation);
1159 local role_rank = valid_roles[role or "none"];
1160 local occupants_updated = {}; -- Filled with old roles
1161 for nick, occupant in self:each_occupant() do
1162 if occupant.bare_jid == jid then
1163 -- need to publcize in all cases; as affiliation in <item/> has changed.
1164 occupants_updated[occupant] = occupant.role;
1165 if occupant.role ~= role and (
1167 valid_roles[occupant.role or "none"] < role_rank -- upgrade
1169 occupant.role = role;
1170 self:save_occupant(occupant);
1175 -- Tell the room of the new occupant affiliations+roles
1176 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"});
1177 if not role then -- getting kicked
1178 if affiliation == "outcast" then
1179 x:tag("status", {code="301"}):up(); -- banned
1181 x:tag("status", {code="321"}):up(); -- affiliation change
1184 local is_semi_anonymous = self:get_whois() == "moderators";
1185 for occupant, old_role in pairs(occupants_updated) do
1186 self:publicise_occupant_status(occupant, x, nil, actor, reason);
1187 if is_semi_anonymous and
1188 (old_role == "moderator" and occupant.role ~= "moderator") or
1189 (old_role ~= "moderator" and occupant.role == "moderator") then -- Has gained or lost moderator status
1190 -- Send everyone else's presences (as jid visibility has changed)
1191 for real_jid in occupant:each_session() do
1192 self:send_occupant_list(real_jid, function(occupant_jid, occupant)
1193 return occupant.bare_jid ~= jid;
1199 if self.save then self:save(); end
1203 function room_mt:get_role(nick)
1204 local occupant = self:get_occupant_by_nick(nick);
1205 return occupant and occupant.role or nil;
1208 function room_mt:set_role(actor, occupant_jid, role, reason)
1209 if not actor then return nil, "modify", "not-acceptable"; end
1211 local occupant = self:get_occupant_by_nick(occupant_jid);
1212 if not occupant then return nil, "modify", "not-acceptable"; end
1214 if valid_roles[role or "none"] == nil then
1215 return nil, "modify", "not-acceptable";
1217 role = role ~= "none" and role or nil; -- coerces `role == false` to `nil`
1219 if actor ~= true then
1220 -- Can't do anything to other owners or admins
1221 local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
1222 if occupant_affiliation == "owner" and occupant_affiliation == "admin" then
1223 return nil, "cancel", "not-allowed";
1226 -- If you are trying to give or take moderator role you need to be an owner or admin
1227 if occupant.role == "moderator" or role == "moderator" then
1228 local actor_affiliation = self:get_affiliation(actor);
1229 if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
1230 return nil, "cancel", "not-allowed";
1234 -- Need to be in the room and a moderator
1235 local actor_occupant = self:get_occupant_by_real_jid(actor);
1236 if not actor_occupant or actor_occupant.role ~= "moderator" then
1237 return nil, "cancel", "not-allowed";
1241 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"});
1243 x:tag("status", {code = "307"}):up();
1245 occupant.role = role;
1246 self:save_occupant(occupant);
1247 self:publicise_occupant_status(occupant, x, nil, actor, reason);
1251 local name = module:require "muc/name";
1252 room_mt.get_name = name.get;
1253 room_mt.set_name = name.set;
1255 local description = module:require "muc/description";
1256 room_mt.get_description = description.get;
1257 room_mt.set_description = description.set;
1259 local password = module:require "muc/password";
1260 room_mt.get_password = password.get;
1261 room_mt.set_password = password.set;
1263 local whois = module:require "muc/whois";
1264 room_mt.get_whois = whois.get;
1265 room_mt.set_whois = whois.set;
1267 local history = module:require "muc/history";
1268 room_mt.send_history = history.send;
1269 room_mt.get_historylength = history.get_length;
1270 room_mt.set_historylength = history.set_length;
1272 local _M = {}; -- module "muc"
1274 _M.set_max_history_length = history.set_max_length;
1276 function _M.new_room(jid, config)
1277 return setmetatable({
1287 _M.room_mt = room_mt;