mod_csi_simple: Consider messages encrypted payload as important (fixes part of ...
[prosody.git] / plugins / muc / muc.lib.lua
blobef2054b18d9237747198e76f35a6d13a0cd09ddf
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 --
10 local select = select;
11 local pairs = pairs;
12 local next = next;
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 jid_join = require "util.jid".join;
21 local jid_resource = require "util.jid".resource;
22 local resourceprep = require "util.encodings".stringprep.resourceprep;
23 local st = require "util.stanza";
24 local base64 = require "util.encodings".base64;
25 local md5 = require "util.hashes".md5;
27 local log = module._log;
29 local occupant_lib = module:require "muc/occupant"
30 local muc_util = module:require "muc/util";
31 local is_kickable_error = muc_util.is_kickable_error;
32 local valid_roles, valid_affiliations = muc_util.valid_roles, muc_util.valid_affiliations;
34 local room_mt = {};
35 room_mt.__index = room_mt;
37 function room_mt:__tostring()
38 return "MUC room ("..self.jid..")";
39 end
41 function room_mt.save()
42 -- overriden by mod_muc.lua
43 end
45 function room_mt:get_occupant_jid(real_jid)
46 return self._jid_nick[real_jid]
47 end
49 function room_mt:get_default_role(affiliation)
50 local role = module:fire_event("muc-get-default-role", {
51 room = self;
52 affiliation = affiliation;
53 affiliation_rank = valid_affiliations[affiliation or "none"];
54 });
55 role = role ~= "none" and role or nil; -- coerces `role == false` to `nil`
56 return role, valid_roles[role or "none"];
57 end
58 module:hook("muc-get-default-role", function(event)
59 if event.affiliation_rank >= valid_affiliations.admin then
60 return "moderator";
61 elseif event.affiliation_rank >= valid_affiliations.none then
62 return "participant";
63 end
64 end, -1);
66 --- Occupant functions
67 function room_mt:new_occupant(bare_real_jid, nick)
68 local occupant = occupant_lib.new(bare_real_jid, nick);
69 local affiliation = self:get_affiliation(bare_real_jid);
70 occupant.role = self:get_default_role(affiliation);
71 return occupant;
72 end
74 -- nick is in the form of an in-room JID
75 function room_mt:get_occupant_by_nick(nick)
76 local occupant = self._occupants[nick];
77 if occupant == nil then return nil end
78 return occupant_lib.copy(occupant);
79 end
82 local function next_copied_occupant(occupants, occupant_jid)
83 local next_occupant_jid, raw_occupant = next(occupants, occupant_jid);
84 if next_occupant_jid == nil then return nil end
85 return next_occupant_jid, occupant_lib.copy(raw_occupant);
86 end
87 -- FIXME Explain what 'read_only' is supposed to be
88 function room_mt:each_occupant(read_only) -- luacheck: ignore 212
89 return next_copied_occupant, self._occupants, nil;
90 end
91 end
93 function room_mt:has_occupant()
94 return next(self._occupants, nil) ~= nil
95 end
97 function room_mt:get_occupant_by_real_jid(real_jid)
98 local occupant_jid = self:get_occupant_jid(real_jid);
99 if occupant_jid == nil then return nil end
100 return self:get_occupant_by_nick(occupant_jid);
103 function room_mt:save_occupant(occupant)
104 occupant = occupant_lib.copy(occupant); -- So that occupant can be modified more
105 local id = occupant.nick
107 -- Need to maintain _jid_nick secondary index
108 local old_occupant = self._occupants[id];
109 if old_occupant then
110 for real_jid in old_occupant:each_session() do
111 self._jid_nick[real_jid] = nil;
115 local has_live_session = false
116 if occupant.role ~= nil then
117 for real_jid, presence in occupant:each_session() do
118 if presence.attr.type == nil then
119 has_live_session = true
120 self._jid_nick[real_jid] = occupant.nick;
123 if not has_live_session then
124 -- Has no live sessions left; they have left the room.
125 occupant.role = nil
128 if not has_live_session then
129 occupant = nil
131 self._occupants[id] = occupant
132 return occupant
135 function room_mt:route_to_occupant(occupant, stanza)
136 local to = stanza.attr.to;
137 for jid in occupant:each_session() do
138 stanza.attr.to = jid;
139 self:route_stanza(stanza);
141 stanza.attr.to = to;
144 -- actor is the attribute table
145 local function add_item(x, affiliation, role, jid, nick, actor_nick, actor_jid, reason)
146 x:tag("item", {affiliation = affiliation; role = role; jid = jid; nick = nick;})
147 if actor_nick or actor_jid then
148 x:tag("actor", {nick = actor_nick; jid = actor_jid;}):up()
150 if reason then
151 x:tag("reason"):text(reason):up()
153 x:up();
154 return x
157 -- actor is (real) jid
158 function room_mt:build_item_list(occupant, x, is_anonymous, nick, actor_nick, actor_jid, reason)
159 local affiliation = self:get_affiliation(occupant.bare_jid) or "none";
160 local role = occupant.role or "none";
161 if is_anonymous then
162 add_item(x, affiliation, role, nil, nick, actor_nick, actor_jid, reason);
163 else
164 for real_jid in occupant:each_session() do
165 add_item(x, affiliation, role, real_jid, nick, actor_nick, actor_jid, reason);
168 return x
171 function room_mt:broadcast_message(stanza)
172 if module:fire_event("muc-broadcast-message", {room = self, stanza = stanza}) then
173 return true;
175 self:broadcast(stanza);
176 return true;
179 -- Strip delay tags claiming to be from us
180 module:hook("muc-occupant-groupchat", function (event)
181 local stanza = event.stanza;
182 local room = event.room;
183 local room_jid = room.jid;
185 stanza:maptags(function (child)
186 if child.name == "delay" and child.attr["xmlns"] == "urn:xmpp:delay" then
187 if child.attr["from"] == room_jid then
188 return nil;
191 if child.name == "x" and child.attr["xmlns"] == "jabber:x:delay" then
192 if child.attr["from"] == room_jid then
193 return nil;
196 return child;
197 end)
198 end);
200 -- Broadcast a stanza to all occupants in the room.
201 -- optionally checks conditional called with (nick, occupant)
202 function room_mt:broadcast(stanza, cond_func)
203 for nick, occupant in self:each_occupant() do
204 if cond_func == nil or cond_func(nick, occupant) then
205 self:route_to_occupant(occupant, stanza)
210 local function can_see_real_jids(whois, occupant)
211 if whois == "anyone" then
212 return true;
213 elseif whois == "moderators" then
214 return valid_roles[occupant.role or "none"] >= valid_roles.moderator;
218 -- Broadcasts an occupant's presence to the whole room
219 -- Takes the x element that goes into the stanzas
220 function room_mt:publicise_occupant_status(occupant, x, nick, actor, reason)
221 local base_x = x.base or x;
222 -- Build real jid and (optionally) occupant jid template presences
223 local base_presence do
224 -- Try to use main jid's presence
225 local pr = occupant:get_presence();
226 if pr and (occupant.role ~= nil or pr.attr.type == "unavailable") then
227 base_presence = st.clone(pr);
228 else -- user is leaving but didn't send a leave presence. make one for them
229 base_presence = st.presence {from = occupant.nick; type = "unavailable";};
233 -- Fire event (before full_p and anon_p are created)
234 local event = {
235 room = self; stanza = base_presence; x = base_x;
236 occupant = occupant; nick = nick; actor = actor;
237 reason = reason;
239 module:fire_event("muc-broadcast-presence", event);
241 -- Allow muc-broadcast-presence listeners to change things
242 nick = event.nick;
243 actor = event.actor;
244 reason = event.reason;
246 local whois = self:get_whois();
248 local actor_nick;
249 if actor then
250 actor_nick = jid_resource(self:get_occupant_jid(actor));
253 local full_p, full_x;
254 local function get_full_p()
255 if full_p == nil then
256 full_x = st.clone(x.full or base_x);
257 self:build_item_list(occupant, full_x, false, nick, actor_nick, actor, reason);
258 full_p = st.clone(base_presence):add_child(full_x);
260 return full_p, full_x;
263 local anon_p, anon_x;
264 local function get_anon_p()
265 if anon_p == nil then
266 anon_x = st.clone(x.anon or base_x);
267 self:build_item_list(occupant, anon_x, true, nick, actor_nick, nil, reason);
268 anon_p = st.clone(base_presence):add_child(anon_x);
270 return anon_p, anon_x;
273 local self_p, self_x;
275 -- Can always see your own full jids
276 -- But not allowed to see actor's
277 self_x = st.clone(x.self or base_x);
278 self:build_item_list(occupant, self_x, false, nick, actor_nick, nil, reason);
279 self_p = st.clone(base_presence):add_child(self_x);
282 -- General populance
283 for occupant_nick, n_occupant in self:each_occupant() do
284 if occupant_nick ~= occupant.nick then
285 local pr;
286 if can_see_real_jids(whois, n_occupant) then
287 pr = get_full_p();
288 elseif occupant.bare_jid == n_occupant.bare_jid then
289 pr = self_p;
290 else
291 pr = get_anon_p();
293 self:route_to_occupant(n_occupant, pr);
297 -- Presences for occupant itself
298 self_x:tag("status", {code = "110";}):up();
299 if occupant.role == nil then
300 -- They get an unavailable
301 self:route_to_occupant(occupant, self_p);
302 else
303 -- use their own presences as templates
304 for full_jid, pr in occupant:each_session() do
305 pr = st.clone(pr);
306 pr.attr.to = full_jid;
307 pr:add_child(self_x);
308 self:route_stanza(pr);
313 function room_mt:send_occupant_list(to, filter)
314 local to_bare = jid_bare(to);
315 local is_anonymous = false;
316 local whois = self:get_whois();
317 if whois ~= "anyone" then
318 local affiliation = self:get_affiliation(to);
319 if affiliation ~= "admin" and affiliation ~= "owner" then
320 local occupant = self:get_occupant_by_real_jid(to);
321 if not (occupant and can_see_real_jids(whois, occupant)) then
322 is_anonymous = true;
326 for occupant_jid, occupant in self:each_occupant() do
327 if filter == nil or filter(occupant_jid, occupant) then
328 local x = st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'});
329 self:build_item_list(occupant, x, is_anonymous and to_bare ~= occupant.bare_jid); -- can always see your own jids
330 local pres = st.clone(occupant:get_presence());
331 pres.attr.to = to;
332 pres:add_child(x);
333 self:route_stanza(pres);
338 function room_mt:get_disco_info(stanza)
339 local node = stanza.tags[1].attr.node or "";
340 local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node });
341 local event_name = "muc-disco#info";
342 local event_data = { room = self, reply = reply, stanza = stanza };
344 if node ~= "" then
345 event_name = event_name.."/"..node;
346 else
347 event_data.form = dataform.new {
348 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" };
350 event_data.formdata = {};
352 module:fire_event(event_name, event_data);
353 if event_data.form then
354 reply:add_child(event_data.form:form(event_data.formdata, "result"));
356 return reply;
358 module:hook("muc-disco#info", function(event)
359 event.reply:tag("feature", {var = "http://jabber.org/protocol/muc"}):up();
360 event.reply:tag("feature", {var = "http://jabber.org/protocol/muc#stable_id"}):up();
361 end);
362 module:hook("muc-disco#info", function(event)
363 table.insert(event.form, { name = "muc#roominfo_occupants", label = "Number of occupants" });
364 event.formdata["muc#roominfo_occupants"] = tostring(iterators.count(event.room:each_occupant()));
365 end);
367 function room_mt:get_disco_items(stanza) -- luacheck: ignore 212
368 return st.reply(stanza):query("http://jabber.org/protocol/disco#items");
371 function room_mt:handle_kickable(origin, stanza) -- luacheck: ignore 212
372 local real_jid = stanza.attr.from;
373 local occupant = self:get_occupant_by_real_jid(real_jid);
374 if occupant == nil then return nil; end
375 local type, condition, text = stanza:get_error();
376 local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error");
377 if text and self:get_whois() == "anyone" then
378 error_message = error_message..": "..text;
380 occupant:set_session(real_jid, st.presence({type="unavailable"})
381 :tag('status'):text(error_message));
382 local is_last_session = occupant.jid == real_jid;
383 if is_last_session then
384 occupant.role = nil;
386 local new_occupant = self:save_occupant(occupant);
387 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
388 if is_last_session then
389 x:tag("status", {code = "333"});
391 self:publicise_occupant_status(new_occupant or occupant, x);
392 if is_last_session then
393 module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;});
395 return true;
398 -- Give the room creator owner affiliation
399 module:hook("muc-room-pre-create", function(event)
400 event.room:set_affiliation(true, jid_bare(event.stanza.attr.from), "owner");
401 end, -1);
403 -- check if user is banned
404 module:hook("muc-occupant-pre-join", function(event)
405 local room, stanza = event.room, event.stanza;
406 local affiliation = room:get_affiliation(stanza.attr.from);
407 if affiliation == "outcast" then
408 local reply = st.error_reply(stanza, "auth", "forbidden"):up();
409 reply.tags[1].attr.code = "403";
410 event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
411 return true;
413 end, -10);
415 module:hook("muc-occupant-pre-join", function(event)
416 local nick = jid_resource(event.occupant.nick);
417 if not nick:find("%S") then
418 event.origin.send(st.error_reply(event.stanza, "modify", "not-allowed", "Invisible Nicknames are forbidden"));
419 return true;
421 end, 1);
423 module:hook("muc-occupant-pre-change", function(event)
424 if not jid_resource(event.dest_occupant.nick):find("%S") then
425 event.origin.send(st.error_reply(event.stanza, "modify", "not-allowed", "Invisible Nicknames are forbidden"));
426 return true;
428 end, 1);
430 function room_mt:handle_first_presence(origin, stanza)
431 if not stanza:get_child("x", "http://jabber.org/protocol/muc") then
432 module:log("debug", "Room creation without <x>, possibly desynced");
434 origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
435 return true;
438 local real_jid = stanza.attr.from;
439 local dest_jid = stanza.attr.to;
440 local bare_jid = jid_bare(real_jid);
441 if module:fire_event("muc-room-pre-create", {
442 room = self;
443 origin = origin;
444 stanza = stanza;
445 }) then return true; end
446 local is_first_dest_session = true;
447 local dest_occupant = self:new_occupant(bare_jid, dest_jid);
449 local orig_nick = dest_occupant.nick;
450 if module:fire_event("muc-occupant-pre-join", {
451 room = self;
452 origin = origin;
453 stanza = stanza;
454 is_first_session = is_first_dest_session;
455 is_new_room = true;
456 occupant = dest_occupant;
457 }) then return true; end
458 local nick_changed = orig_nick ~= dest_occupant.nick;
460 dest_occupant:set_session(real_jid, stanza);
461 local dest_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
462 dest_x:tag("status", {code = "201"}):up();
463 if self:get_whois() == "anyone" then
464 dest_x:tag("status", {code = "100"}):up();
466 if nick_changed then
467 dest_x:tag("status", {code = "210"}):up();
469 self:save_occupant(dest_occupant);
471 self:publicise_occupant_status(dest_occupant, dest_x);
473 module:fire_event("muc-occupant-joined", {
474 room = self;
475 nick = dest_occupant.nick;
476 occupant = dest_occupant;
477 stanza = stanza;
478 origin = origin;
480 module:fire_event("muc-occupant-session-new", {
481 room = self;
482 nick = dest_occupant.nick;
483 occupant = dest_occupant;
484 stanza = stanza;
485 origin = origin;
486 jid = real_jid;
488 module:fire_event("muc-room-created", {
489 room = self;
490 creator = dest_occupant;
491 stanza = stanza;
492 origin = origin;
494 return true;
497 function room_mt:handle_normal_presence(origin, stanza)
498 local type = stanza.attr.type;
499 local real_jid = stanza.attr.from;
500 local bare_jid = jid_bare(real_jid);
501 local orig_occupant = self:get_occupant_by_real_jid(real_jid);
502 local muc_x = stanza:get_child("x", "http://jabber.org/protocol/muc");
504 if orig_occupant == nil and not muc_x and stanza.attr.type == nil then
505 module:log("debug", "Attempted join without <x>, possibly desynced");
506 origin.send(st.error_reply(stanza, "cancel", "item-not-found",
507 "You must join the room before sending presence updates"));
508 return true;
511 local is_first_dest_session;
512 local dest_occupant;
513 if type == "unavailable" then
514 if orig_occupant == nil then return true; end -- Unavailable from someone not in the room
515 -- dest_occupant = nil
516 elseif orig_occupant and orig_occupant.nick == stanza.attr.to then -- Just a presence update
517 log("debug", "presence update for %s from session %s", orig_occupant.nick, real_jid);
518 dest_occupant = orig_occupant;
519 else
520 local dest_jid = stanza.attr.to;
521 dest_occupant = self:get_occupant_by_nick(dest_jid);
522 if dest_occupant == nil then
523 log("debug", "no occupant found for %s; creating new occupant object for %s", dest_jid, real_jid);
524 is_first_dest_session = true;
525 dest_occupant = self:new_occupant(bare_jid, dest_jid);
526 else
527 is_first_dest_session = false;
530 local is_last_orig_session;
531 if orig_occupant ~= nil then
532 -- Is there are least 2 sessions?
533 local iter, ob, last = orig_occupant:each_session();
534 is_last_orig_session = iter(ob, iter(ob, last)) == nil;
537 local orig_nick = dest_occupant and dest_occupant.nick;
539 local event, event_name = {
540 room = self;
541 origin = origin;
542 stanza = stanza;
543 is_first_session = is_first_dest_session;
544 is_last_session = is_last_orig_session;
546 if orig_occupant == nil then
547 event_name = "muc-occupant-pre-join";
548 event.occupant = dest_occupant;
549 elseif dest_occupant == nil then
550 event_name = "muc-occupant-pre-leave";
551 event.occupant = orig_occupant;
552 else
553 event_name = "muc-occupant-pre-change";
554 event.orig_occupant = orig_occupant;
555 event.dest_occupant = dest_occupant;
557 if module:fire_event(event_name, event) then return true; end
559 local nick_changed = dest_occupant and orig_nick ~= dest_occupant.nick;
561 -- Check for nick conflicts
562 if dest_occupant ~= nil and not is_first_dest_session
563 and bare_jid ~= jid_bare(dest_occupant.bare_jid) then
564 -- new nick or has different bare real jid
565 log("debug", "%s couldn't join due to nick conflict: %s", real_jid, dest_occupant.nick);
566 local reply = st.error_reply(stanza, "cancel", "conflict"):up();
567 reply.tags[1].attr.code = "409";
568 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
569 return true;
572 -- Send presence stanza about original occupant
573 if orig_occupant ~= nil and orig_occupant ~= dest_occupant then
574 local orig_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
575 local dest_nick;
576 if dest_occupant == nil then -- Session is leaving
577 log("debug", "session %s is leaving occupant %s", real_jid, orig_occupant.nick);
578 if is_last_orig_session then
579 orig_occupant.role = nil;
581 orig_occupant:set_session(real_jid, stanza);
582 else
583 log("debug", "session %s is changing from occupant %s to %s", real_jid, orig_occupant.nick, dest_occupant.nick);
584 local generated_unavail = st.presence {from = orig_occupant.nick, to = real_jid, type = "unavailable"};
585 orig_occupant:set_session(real_jid, generated_unavail);
586 dest_nick = jid_resource(dest_occupant.nick);
587 if not is_first_dest_session then -- User is swapping into another pre-existing session
588 log("debug", "session %s is swapping into multisession %s, showing it leave.", real_jid, dest_occupant.nick);
589 -- Show the other session leaving
590 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
591 add_item(x, self:get_affiliation(bare_jid), "none");
592 local pr = st.presence{from = dest_occupant.nick, to = real_jid, type = "unavailable"}
593 :tag("status"):text("you are joining pre-existing session " .. dest_nick):up()
594 :add_child(x);
595 self:route_stanza(pr);
597 if is_first_dest_session and is_last_orig_session then -- Normal nick change
598 log("debug", "no sessions in %s left; publicly marking as nick change", orig_occupant.nick);
599 orig_x:tag("status", {code = "303";}):up();
600 else -- The session itself always needs to see a nick change
601 -- don't want to get our old nick's available presence,
602 -- so remove our session from there, and manually generate an unavailable
603 orig_occupant:remove_session(real_jid);
604 log("debug", "generating nick change for %s", real_jid);
605 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
606 -- COMPAT: clients get confused if they see other items besides their own
607 -- self:build_item_list(orig_occupant, x, false, dest_nick);
608 add_item(x, self:get_affiliation(bare_jid), orig_occupant.role, real_jid, dest_nick);
609 x:tag("status", {code = "303";}):up();
610 x:tag("status", {code = "110";}):up();
611 self:route_stanza(generated_unavail:add_child(x));
612 dest_nick = nil; -- set dest_nick to nil; so general populance doesn't see it for whole orig_occupant
616 self:save_occupant(orig_occupant);
617 self:publicise_occupant_status(orig_occupant, orig_x, dest_nick);
619 if is_last_orig_session then
620 module:fire_event("muc-occupant-left", {
621 room = self;
622 nick = orig_occupant.nick;
623 occupant = orig_occupant;
624 origin = origin;
625 stanza = stanza;
630 if dest_occupant ~= nil then
631 dest_occupant:set_session(real_jid, stanza);
632 self:save_occupant(dest_occupant);
634 if orig_occupant == nil or muc_x then
635 -- Send occupant list to newly joined or desynced user
636 self:send_occupant_list(real_jid, function(nick, occupant) -- luacheck: ignore 212
637 -- Don't include self
638 return occupant:get_presence(real_jid) == nil;
639 end)
641 local dest_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
642 local self_x = st.clone(dest_x);
643 if orig_occupant == nil and self:get_whois() == "anyone" then
644 self_x:tag("status", {code = "100"}):up();
646 if nick_changed then
647 self_x:tag("status", {code="210"}):up();
649 self:publicise_occupant_status(dest_occupant, {base=dest_x,self=self_x});
651 if orig_occupant ~= nil and orig_occupant ~= dest_occupant and not is_last_orig_session then
652 -- If user is swapping and wasn't last original session
653 log("debug", "session %s split nicks; showing %s rejoining", real_jid, orig_occupant.nick);
654 -- Show the original nick joining again
655 local pr = st.clone(orig_occupant:get_presence());
656 pr.attr.to = real_jid;
657 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
658 self:build_item_list(orig_occupant, x, false);
659 -- TODO: new status code to inform client this was the multi-session it left?
660 pr:add_child(x);
661 self:route_stanza(pr);
664 if orig_occupant == nil or muc_x then
665 if is_first_dest_session then
666 module:fire_event("muc-occupant-joined", {
667 room = self;
668 nick = dest_occupant.nick;
669 occupant = dest_occupant;
670 stanza = stanza;
671 origin = origin;
674 module:fire_event("muc-occupant-session-new", {
675 room = self;
676 nick = dest_occupant.nick;
677 occupant = dest_occupant;
678 stanza = stanza;
679 origin = origin;
680 jid = real_jid;
684 return true;
687 function room_mt:handle_presence_to_occupant(origin, stanza)
688 local type = stanza.attr.type;
689 if type == "error" then -- error, kick em out!
690 return self:handle_kickable(origin, stanza)
691 elseif type == nil or type == "unavailable" then
692 return self:handle_normal_presence(origin, stanza);
693 elseif type ~= 'result' then -- bad type
694 if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences
695 origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
698 return true;
701 function room_mt:handle_iq_to_occupant(origin, stanza)
702 local from, to = stanza.attr.from, stanza.attr.to;
703 local type = stanza.attr.type;
704 local id = stanza.attr.id;
705 local occupant = self:get_occupant_by_nick(to);
706 if (type == "error" or type == "result") then
707 do -- deconstruct_stanza_id
708 if not occupant then return nil; end
709 local from_jid, orig_id, to_jid_hash = (base64.decode(id) or ""):match("^(%Z+)%z(%Z*)%z(.+)$");
710 if not(from == from_jid or from == jid_bare(from_jid)) then return nil; end
711 local from_occupant_jid = self:get_occupant_jid(from_jid);
712 if from_occupant_jid == nil then return nil; end
713 local session_jid
714 for to_jid in occupant:each_session() do
715 if md5(to_jid) == to_jid_hash then
716 session_jid = to_jid;
717 break;
720 if session_jid == nil then return nil; end
721 stanza.attr.from, stanza.attr.to, stanza.attr.id = from_occupant_jid, session_jid, orig_id;
723 log("debug", "%s sent private iq stanza to %s (%s)", from, to, stanza.attr.to);
724 self:route_stanza(stanza);
725 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
726 return true;
727 else -- Type is "get" or "set"
728 local current_nick = self:get_occupant_jid(from);
729 if not current_nick then
730 origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
731 return true;
733 if not occupant then -- recipient not in room
734 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
735 return true;
737 -- XEP-0410 MUC Self-Ping #1220
738 if to == current_nick and stanza.attr.type == "get" and stanza:get_child("ping", "urn:xmpp:ping") then
739 self:route_stanza(st.reply(stanza));
740 return true;
742 do -- construct_stanza_id
743 stanza.attr.id = base64.encode(occupant.jid.."\0"..stanza.attr.id.."\0"..md5(from));
745 stanza.attr.from, stanza.attr.to = current_nick, occupant.jid;
746 log("debug", "%s sent private iq stanza to %s (%s)", from, to, occupant.jid);
747 local iq_ns = stanza.tags[1].attr.xmlns;
748 if iq_ns == 'vcard-temp' or iq_ns == "http://jabber.org/protocol/pubsub" or iq_ns == "urn:ietf:params:xml:ns:vcard-4.0" then
749 stanza.attr.to = jid_bare(stanza.attr.to);
751 self:route_stanza(stanza);
752 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
753 return true;
757 function room_mt:handle_message_to_occupant(origin, stanza)
758 local from, to = stanza.attr.from, stanza.attr.to;
759 local current_nick = self:get_occupant_jid(from);
760 local type = stanza.attr.type;
761 if not current_nick then -- not in room
762 if type ~= "error" then
763 origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
765 return true;
767 if type == "groupchat" then -- groupchat messages not allowed in PM
768 origin.send(st.error_reply(stanza, "modify", "bad-request"));
769 return true;
770 elseif type == "error" and is_kickable_error(stanza) then
771 log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
772 return self:handle_kickable(origin, stanza); -- send unavailable
775 local o_data = self:get_occupant_by_nick(to);
776 if not o_data then
777 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
778 return true;
780 log("debug", "%s sent private message stanza to %s (%s)", from, to, o_data.jid);
781 stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up();
782 stanza.attr.from = current_nick;
783 self:route_to_occupant(o_data, stanza)
784 -- TODO: Remove x tag?
785 stanza.attr.from = from;
786 return true;
789 function room_mt:send_form(origin, stanza)
790 origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
791 :add_child(self:get_form_layout(stanza.attr.from):form())
795 function room_mt:get_form_layout(actor)
796 local form = dataform.new({
797 title = "Configuration for "..self.jid,
798 instructions = "Complete and submit this form to configure the room.",
800 name = 'FORM_TYPE',
801 type = 'hidden',
802 value = 'http://jabber.org/protocol/muc#roomconfig'
805 return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form;
808 function room_mt:process_form(origin, stanza)
809 local form = stanza.tags[1]:get_child("x", "jabber:x:data");
810 if form.attr.type == "cancel" then
811 origin.send(st.reply(stanza));
812 elseif form.attr.type == "submit" then
813 local fields, errors, present;
814 if form.tags[1] == nil then -- Instant room
815 fields, present = {}, {};
816 else
817 fields, errors, present = self:get_form_layout(stanza.attr.from):data(form);
818 if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then
819 origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration"));
820 return true;
824 local event = {
825 room = self;
826 origin = origin;
827 stanza = stanza;
828 fields = fields;
829 status_codes = {};
830 actor = stanza.attr.from;
832 function event.update_option(name, field, allowed)
833 local new = fields[field];
834 if new == nil then return; end
835 if allowed and not allowed[new] then return; end
836 if new == self["get_"..name](self) then return; end
837 event.status_codes["104"] = true;
838 self["set_"..name](self, new);
839 return true;
841 module:fire_event("muc-config-submitted", event);
842 for submitted_field in pairs(present) do
843 event.field, event.value = submitted_field, fields[submitted_field];
844 module:fire_event("muc-config-submitted/"..submitted_field, event);
846 event.field, event.value = nil, nil;
848 self:save(true);
849 origin.send(st.reply(stanza));
851 if next(event.status_codes) then
852 local msg = st.message({type='groupchat', from=self.jid})
853 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
854 for code in pairs(event.status_codes) do
855 msg:tag("status", {code = code;}):up();
857 msg:up();
858 self:broadcast_message(msg);
860 else
861 origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form"));
863 return true;
866 -- Removes everyone from the room
867 function room_mt:clear(x)
868 x = x or st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'});
869 local occupants_updated = {};
870 for nick, occupant in self:each_occupant() do -- luacheck: ignore 213
871 occupant.role = nil;
872 self:save_occupant(occupant);
873 occupants_updated[occupant] = true;
875 for occupant in pairs(occupants_updated) do
876 self:publicise_occupant_status(occupant, x);
877 module:fire_event("muc-occupant-left", { room = self; nick = occupant.nick; occupant = occupant;});
881 function room_mt:destroy(newjid, reason, password)
882 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"})
883 :tag("destroy", {jid=newjid});
884 if reason then x:tag("reason"):text(reason):up(); end
885 if password then x:tag("password"):text(password):up(); end
886 x:up();
887 self.destroying = reason or true;
888 self:clear(x);
889 module:fire_event("muc-room-destroyed", { room = self, reason = reason, newjid = newjid, password = password });
890 return true;
893 function room_mt:handle_disco_info_get_query(origin, stanza)
894 origin.send(self:get_disco_info(stanza));
895 return true;
898 function room_mt:handle_disco_items_get_query(origin, stanza)
899 origin.send(self:get_disco_items(stanza));
900 return true;
903 function room_mt:handle_admin_query_set_command(origin, stanza)
904 local item = stanza.tags[1].tags[1];
905 if not item then
906 origin.send(st.error_reply(stanza, "cancel", "bad-request"));
907 return true;
909 if item.attr.jid then -- Validate provided JID
910 item.attr.jid = jid_prep(item.attr.jid);
911 if not item.attr.jid then
912 origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
913 return true;
916 if item.attr.nick then -- Validate provided nick
917 item.attr.nick = resourceprep(item.attr.nick);
918 if not item.attr.nick then
919 origin.send(st.error_reply(stanza, "modify", "jid-malformed", "invalid nickname"));
920 return true;
923 if not item.attr.jid and item.attr.nick then
924 -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation
925 local occupant = self:get_occupant_by_nick(self.jid.."/"..item.attr.nick);
926 if occupant then item.attr.jid = occupant.bare_jid; end
927 elseif item.attr.role and not item.attr.nick and item.attr.jid then
928 -- Role changes should use nick, but we have a JID so pull the nick from that
929 local nick = self:get_occupant_jid(item.attr.jid);
930 if nick then item.attr.nick = jid_resource(nick); end
932 local actor = stanza.attr.from;
933 local reason = item:get_child_text("reason");
934 local success, errtype, err
935 if item.attr.affiliation and item.attr.jid and not item.attr.role then
936 local registration_data;
937 if item.attr.nick then
938 local room_nick = self.jid.."/"..item.attr.nick;
939 local existing_occupant = self:get_occupant_by_nick(room_nick);
940 if existing_occupant and existing_occupant.bare_jid ~= item.attr.jid then
941 module:log("debug", "Existing occupant for %s: %s does not match %s", room_nick, existing_occupant.bare_jid, item.attr.jid);
942 self:set_role(true, room_nick, nil, "This nickname is reserved");
944 module:log("debug", "Reserving %s for %s (%s)", item.attr.nick, item.attr.jid, item.attr.affiliation);
945 registration_data = { reserved_nickname = item.attr.nick };
947 success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, reason, registration_data);
948 elseif item.attr.role and item.attr.nick and not item.attr.affiliation then
949 success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, reason);
950 else
951 success, errtype, err = nil, "cancel", "bad-request";
953 self:save(true);
954 if not success then
955 origin.send(st.error_reply(stanza, errtype, err));
956 else
957 origin.send(st.reply(stanza));
959 return true;
962 function room_mt:handle_admin_query_get_command(origin, stanza)
963 local actor = stanza.attr.from;
964 local affiliation = self:get_affiliation(actor);
965 local item = stanza.tags[1].tags[1];
966 local _aff = item.attr.affiliation;
967 local _aff_rank = valid_affiliations[_aff or "none"];
968 local _rol = item.attr.role;
969 if _aff and _aff_rank and not _rol then
970 -- You need to be at least an admin, and be requesting info about your affifiliation or lower
971 -- e.g. an admin can't ask for a list of owners
972 local affiliation_rank = valid_affiliations[affiliation or "none"];
973 if (affiliation_rank >= valid_affiliations.admin and affiliation_rank >= _aff_rank)
974 or (self:get_whois() == "anyone") then
975 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
976 for jid in self:each_affiliation(_aff or "none") do
977 local nick = self:get_registered_nick(jid);
978 reply:tag("item", {affiliation = _aff, jid = jid, nick = nick }):up();
980 origin.send(reply:up());
981 return true;
982 else
983 origin.send(st.error_reply(stanza, "auth", "forbidden"));
984 return true;
986 elseif _rol and valid_roles[_rol or "none"] and not _aff then
987 local role = self:get_role(self:get_occupant_jid(actor)) or self:get_default_role(affiliation);
988 if valid_roles[role or "none"] >= valid_roles.moderator then
989 if _rol == "none" then _rol = nil; end
990 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
991 -- TODO: whois check here? (though fully anonymous rooms are not supported)
992 for occupant_jid, occupant in self:each_occupant() do
993 if occupant.role == _rol then
994 local nick = jid_resource(occupant_jid);
995 self:build_item_list(occupant, reply, false, nick);
998 origin.send(reply:up());
999 return true;
1000 else
1001 origin.send(st.error_reply(stanza, "auth", "forbidden"));
1002 return true;
1004 else
1005 origin.send(st.error_reply(stanza, "cancel", "bad-request"));
1006 return true;
1010 function room_mt:handle_owner_query_get_to_room(origin, stanza)
1011 if self:get_affiliation(stanza.attr.from) ~= "owner" then
1012 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms"));
1013 return true;
1016 self:send_form(origin, stanza);
1017 return true;
1019 function room_mt:handle_owner_query_set_to_room(origin, stanza)
1020 if self:get_affiliation(stanza.attr.from) ~= "owner" then
1021 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms"));
1022 return true;
1025 local child = stanza.tags[1].tags[1];
1026 if not child then
1027 origin.send(st.error_reply(stanza, "modify", "bad-request"));
1028 return true;
1029 elseif child.name == "destroy" then
1030 local newjid = child.attr.jid;
1031 local reason = child:get_child_text("reason");
1032 local password = child:get_child_text("password");
1033 self:destroy(newjid, reason, password);
1034 origin.send(st.reply(stanza));
1035 return true;
1036 elseif child.name == "x" and child.attr.xmlns == "jabber:x:data" then
1037 return self:process_form(origin, stanza);
1038 else
1039 origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
1040 return true;
1044 function room_mt:handle_groupchat_to_room(origin, stanza)
1045 local from = stanza.attr.from;
1046 local occupant = self:get_occupant_by_real_jid(from);
1047 if module:fire_event("muc-occupant-groupchat", {
1048 room = self; origin = origin; stanza = stanza; from = from; occupant = occupant;
1049 }) then return true; end
1050 stanza.attr.from = occupant.nick;
1051 self:broadcast_message(stanza);
1052 stanza.attr.from = from;
1053 return true;
1056 -- Role check
1057 module:hook("muc-occupant-groupchat", function(event)
1058 local role_rank = valid_roles[event.occupant and event.occupant.role or "none"];
1059 if role_rank <= valid_roles.none then
1060 event.origin.send(st.error_reply(event.stanza, "cancel", "not-acceptable"));
1061 return true;
1062 elseif role_rank <= valid_roles.visitor then
1063 event.origin.send(st.error_reply(event.stanza, "auth", "forbidden"));
1064 return true;
1066 end, 50);
1068 -- hack - some buggy clients send presence updates to the room rather than their nick
1069 function room_mt:handle_presence_to_room(origin, stanza)
1070 local current_nick = self:get_occupant_jid(stanza.attr.from);
1071 local handled
1072 if current_nick then
1073 local to = stanza.attr.to;
1074 stanza.attr.to = current_nick;
1075 handled = self:handle_presence_to_occupant(origin, stanza);
1076 stanza.attr.to = to;
1078 return handled;
1081 -- Need visitor role or higher to invite
1082 module:hook("muc-pre-invite", function(event)
1083 local room, stanza = event.room, event.stanza;
1084 local _from = stanza.attr.from;
1085 local inviter = room:get_occupant_by_real_jid(_from);
1086 local role = inviter and inviter.role or room:get_default_role(room:get_affiliation(_from));
1087 if valid_roles[role or "none"] <= valid_roles.visitor then
1088 event.origin.send(st.error_reply(stanza, "auth", "forbidden"));
1089 return true;
1091 end);
1093 function room_mt:handle_mediated_invite(origin, stanza)
1094 local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite");
1095 local invitee = jid_prep(payload.attr.to);
1096 if not invitee then
1097 origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
1098 return true;
1099 elseif module:fire_event("muc-pre-invite", {room = self, origin = origin, stanza = stanza}) then
1100 return true;
1102 local invite = muc_util.filter_muc_x(st.clone(stanza));
1103 invite.attr.from = self.jid;
1104 invite.attr.to = invitee;
1105 invite:tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
1106 :tag('invite', {from = stanza.attr.from;})
1107 :tag('reason'):text(payload:get_child_text("reason")):up()
1108 :up()
1109 :up();
1110 if not module:fire_event("muc-invite", {room = self, stanza = invite, origin = origin, incoming = stanza}) then
1111 self:route_stanza(invite);
1113 return true;
1116 -- COMPAT: Some older clients expect this
1117 module:hook("muc-invite", function(event)
1118 local room, stanza = event.room, event.stanza;
1119 local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite");
1120 local reason = invite:get_child_text("reason");
1121 stanza:tag('x', {xmlns = "jabber:x:conference"; jid = room.jid;})
1122 :text(reason or "")
1123 :up();
1124 end);
1126 -- Add a plain message for clients which don't support invites
1127 module:hook("muc-invite", function(event)
1128 local room, stanza = event.room, event.stanza;
1129 if not stanza:get_child("body") then
1130 local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite");
1131 local reason = invite:get_child_text("reason") or "";
1132 stanza:tag("body")
1133 :text(invite.attr.from.." invited you to the room "..room.jid..(reason ~= "" and (" ("..reason..")") or ""))
1134 :up();
1136 end);
1138 function room_mt:handle_mediated_decline(origin, stanza)
1139 local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline");
1140 local declinee = jid_prep(payload.attr.to);
1141 if not declinee then
1142 origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
1143 return true;
1144 elseif module:fire_event("muc-pre-decline", {room = self, origin = origin, stanza = stanza}) then
1145 return true;
1147 local decline = muc_util.filter_muc_x(st.clone(stanza));
1148 decline.attr.from = self.jid;
1149 decline.attr.to = declinee;
1150 decline:tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
1151 :tag("decline", {from = stanza.attr.from})
1152 :tag("reason"):text(payload:get_child_text("reason")):up()
1153 :up()
1154 :up();
1155 if not module:fire_event("muc-decline", {room = self, stanza = decline, origin = origin, incoming = stanza}) then
1156 declinee = decline.attr.to; -- re-fetch, in case event modified it
1157 local occupant
1158 if jid_bare(declinee) == self.jid then -- declinee jid is already an in-room jid
1159 occupant = self:get_occupant_by_nick(declinee);
1161 if occupant then
1162 self:route_to_occupant(occupant, decline);
1163 else
1164 self:route_stanza(decline);
1167 return true;
1170 -- Add a plain message for clients which don't support declines
1171 module:hook("muc-decline", function(event)
1172 local room, stanza = event.room, event.stanza;
1173 if not stanza:get_child("body") then
1174 local decline = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline");
1175 local reason = decline:get_child_text("reason") or "";
1176 stanza:body(decline.attr.from.." declined your invite to the room "
1177 ..room.jid..(reason ~= "" and (" ("..reason..")") or ""));
1179 end);
1181 function room_mt:handle_message_to_room(origin, stanza)
1182 local type = stanza.attr.type;
1183 if type == "groupchat" then
1184 return self:handle_groupchat_to_room(origin, stanza)
1185 elseif type == "error" and is_kickable_error(stanza) then
1186 return self:handle_kickable(origin, stanza)
1187 elseif type == nil or type == "normal" then
1188 local x = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
1189 if x then
1190 local payload = x.tags[1];
1191 if payload == nil then --luacheck: ignore 542
1192 -- fallthrough
1193 elseif payload.name == "invite" and payload.attr.to then
1194 return self:handle_mediated_invite(origin, stanza)
1195 elseif payload.name == "decline" and payload.attr.to then
1196 return self:handle_mediated_decline(origin, stanza)
1198 origin.send(st.error_reply(stanza, "cancel", "bad-request"));
1199 return true;
1202 local form = stanza:get_child("x", "jabber:x:data");
1203 local form_type = dataform.get_type(form);
1204 if form_type == "http://jabber.org/protocol/muc#request" then
1205 self:handle_role_request(origin, stanza, form);
1206 return true;
1211 function room_mt:route_stanza(stanza) -- luacheck: ignore 212
1212 module:send(stanza);
1215 function room_mt:get_affiliation(jid)
1216 local node, host, resource = jid_split(jid);
1217 -- Affiliations are granted, revoked, and maintained based on the user's bare JID.
1218 local bare = node and node.."@"..host or host;
1219 local result = self._affiliations[bare];
1220 if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned
1221 return result;
1224 -- Iterates over jid, affiliation pairs
1225 function room_mt:each_affiliation(with_affiliation)
1226 local _affiliations, _affiliation_data = self._affiliations, self._affiliation_data;
1227 return function(_, jid)
1228 local affiliation;
1229 repeat -- Iterate until we get a match
1230 jid, affiliation = next(_affiliations, jid);
1231 until with_affiliation == nil or jid == nil or affiliation == with_affiliation
1232 return jid, affiliation, _affiliation_data[jid];
1233 end, nil, nil;
1236 function room_mt:set_affiliation(actor, jid, affiliation, reason, data)
1237 if not actor then return nil, "modify", "not-acceptable"; end;
1239 local node, host, resource = jid_split(jid);
1240 if not host then return nil, "modify", "not-acceptable"; end
1241 jid = jid_join(node, host); -- Bare
1242 local is_host_only = node == nil;
1244 if valid_affiliations[affiliation or "none"] == nil then
1245 return nil, "modify", "not-acceptable";
1247 affiliation = affiliation ~= "none" and affiliation or nil; -- coerces `affiliation == false` to `nil`
1249 local target_affiliation = self._affiliations[jid]; -- Raw; don't want to check against host
1250 local is_downgrade = valid_affiliations[target_affiliation or "none"] > valid_affiliations[affiliation or "none"];
1252 if actor == true then
1253 actor = nil -- So we can pass it safely to 'publicise_occupant_status' below
1254 else
1255 local actor_affiliation = self:get_affiliation(actor);
1256 if actor_affiliation == "owner" then
1257 if jid_bare(actor) == jid and is_downgrade then -- self change
1258 -- need at least one owner
1259 local is_last = true;
1260 for j in self:each_affiliation("owner") do
1261 if j ~= jid then is_last = false; break; end
1263 if is_last then
1264 return nil, "cancel", "conflict";
1267 -- owners can do anything else
1268 elseif affiliation == "owner" or affiliation == "admin"
1269 or actor_affiliation ~= "admin"
1270 or target_affiliation == "owner" or target_affiliation == "admin" then
1271 -- Can't demote owners or other admins
1272 return nil, "cancel", "not-allowed";
1276 -- Set in 'database'
1277 self._affiliations[jid] = affiliation;
1278 if not affiliation or data == false or (data ~= nil and next(data) == nil) then
1279 module:log("debug", "Clearing affiliation data for %s", jid);
1280 self._affiliation_data[jid] = nil;
1281 elseif data then
1282 module:log("debug", "Updating affiliation data for %s", jid);
1283 self._affiliation_data[jid] = data;
1286 -- Update roles
1287 local role = self:get_default_role(affiliation);
1288 local role_rank = valid_roles[role or "none"];
1289 local occupants_updated = {}; -- Filled with old roles
1290 for nick, occupant in self:each_occupant() do -- luacheck: ignore 213
1291 if occupant.bare_jid == jid or (
1292 -- Outcast can be by host.
1293 is_host_only and affiliation == "outcast" and select(2, jid_split(occupant.bare_jid)) == host
1294 ) then
1295 -- need to publcize in all cases; as affiliation in <item/> has changed.
1296 occupants_updated[occupant] = occupant.role;
1297 if occupant.role ~= role and (
1298 is_downgrade or
1299 valid_roles[occupant.role or "none"] < role_rank -- upgrade
1300 ) then
1301 occupant.role = role;
1302 self:save_occupant(occupant);
1307 -- Tell the room of the new occupant affiliations+roles
1308 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"});
1309 if not role then -- getting kicked
1310 if affiliation == "outcast" then
1311 x:tag("status", {code="301"}):up(); -- banned
1312 else
1313 x:tag("status", {code="321"}):up(); -- affiliation change
1316 local is_semi_anonymous = self:get_whois() == "moderators";
1318 if next(occupants_updated) ~= nil then
1319 for occupant, old_role in pairs(occupants_updated) do
1320 self:publicise_occupant_status(occupant, x, nil, actor, reason);
1321 if occupant.role == nil then
1322 module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;});
1323 elseif is_semi_anonymous and
1324 (old_role == "moderator" and occupant.role ~= "moderator") or
1325 (old_role ~= "moderator" and occupant.role == "moderator") then -- Has gained or lost moderator status
1326 -- Send everyone else's presences (as jid visibility has changed)
1327 for real_jid in occupant:each_session() do
1328 self:send_occupant_list(real_jid, function(occupant_jid, occupant) --luacheck: ignore 212 433
1329 return occupant.bare_jid ~= jid;
1330 end);
1334 else
1335 -- Announce affiliation change for a user that is not currently in the room,
1336 -- XEP-0045 (v1.31.2) example 195
1337 -- add_item(x, affiliation, role, jid, nick, actor_nick, actor_jid, reason)
1338 local announce_msg = st.message({ from = self.jid })
1339 :add_child(add_item(st.clone(x), affiliation, nil, jid, nil, nil, nil, reason));
1340 local min_role = is_semi_anonymous and "moderator" or "none";
1341 self:broadcast(announce_msg, muc_util.only_with_min_role(min_role));
1344 self:save(true);
1346 module:fire_event("muc-set-affiliation", {
1347 room = self;
1348 actor = actor;
1349 jid = jid;
1350 affiliation = affiliation or "none";
1351 reason = reason;
1352 previous_affiliation = target_affiliation;
1353 data = data and data or nil; -- coerce false to nil
1354 in_room = next(occupants_updated) ~= nil;
1357 return true;
1360 function room_mt:get_affiliation_data(jid, key)
1361 local data = self._affiliation_data[jid];
1362 if not data then return nil; end
1363 if key then
1364 return data[key];
1366 return data;
1369 function room_mt:get_role(nick)
1370 local occupant = self:get_occupant_by_nick(nick);
1371 return occupant and occupant.role or nil;
1374 function room_mt:set_role(actor, occupant_jid, role, reason)
1375 if not actor then return nil, "modify", "not-acceptable"; end
1377 local occupant = self:get_occupant_by_nick(occupant_jid);
1378 if not occupant then return nil, "modify", "item-not-found"; end
1380 if valid_roles[role or "none"] == nil then
1381 return nil, "modify", "not-acceptable";
1383 role = role ~= "none" and role or nil; -- coerces `role == false` to `nil`
1385 if actor == true then
1386 actor = nil -- So we can pass it safely to 'publicise_occupant_status' below
1387 else
1388 -- Can't do anything to other owners or admins
1389 local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
1390 if occupant_affiliation == "owner" or occupant_affiliation == "admin" then
1391 return nil, "cancel", "not-allowed";
1394 -- If you are trying to give or take moderator role you need to be an owner or admin
1395 if occupant.role == "moderator" or role == "moderator" then
1396 local actor_affiliation = self:get_affiliation(actor);
1397 if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
1398 return nil, "cancel", "not-allowed";
1402 -- Need to be in the room and a moderator
1403 local actor_occupant = self:get_occupant_by_real_jid(actor);
1404 if not actor_occupant or actor_occupant.role ~= "moderator" then
1405 return nil, "cancel", "not-allowed";
1409 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"});
1410 if not role then
1411 x:tag("status", {code = "307"}):up();
1413 occupant.role = role;
1414 self:save_occupant(occupant);
1415 self:publicise_occupant_status(occupant, x, nil, actor, reason);
1416 if role == nil then
1417 module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;});
1419 return true;
1422 local whois = module:require "muc/whois";
1423 room_mt.get_whois = whois.get;
1424 room_mt.set_whois = whois.set;
1426 local _M = {}; -- module "muc"
1428 function _M.new_room(jid, config)
1429 return setmetatable({
1430 jid = jid;
1431 _jid_nick = {};
1432 _occupants = {};
1433 _data = config or {};
1434 _affiliations = {};
1435 _affiliation_data = {};
1436 }, room_mt);
1439 local new_format = module:get_option_boolean("new_muc_storage_format", false);
1441 function room_mt:freeze(live)
1442 local frozen, state;
1443 if new_format then
1444 frozen = {
1445 _jid = self.jid;
1446 _data = self._data;
1448 for user, affiliation in pairs(self._affiliations) do
1449 frozen[user] = affiliation;
1451 else
1452 frozen = {
1453 jid = self.jid;
1454 _data = self._data;
1455 _affiliations = self._affiliations;
1456 _affiliation_data = self._affiliation_data;
1459 if live then
1460 state = {};
1461 for nick, occupant in self:each_occupant() do
1462 state[nick] = {
1463 bare_jid = occupant.bare_jid;
1464 role = occupant.role;
1465 jid = occupant.jid;
1467 for jid, presence in occupant:each_session() do
1468 state[jid] = st.preserialize(presence);
1471 local history = self._history;
1472 if history and history[1] ~= nil then
1473 state._last_message = st.preserialize(history[#history].stanza);
1474 state._last_message_at = history[#history].timestamp;
1477 return frozen, state;
1480 function _M.restore_room(frozen, state)
1481 local room_jid = frozen._jid or frozen.jid;
1482 local room = _M.new_room(room_jid, frozen._data);
1484 if state and state._last_message and state._last_message_at then
1485 room._history = {
1486 { stanza = st.deserialize(state._last_message),
1487 timestamp = state._last_message_at, },
1491 local occupants = {};
1492 local room_name, room_host = jid_split(room_jid);
1494 room._affiliation_data = frozen._affiliation_data or {};
1496 if frozen.jid and frozen._affiliations then
1497 -- Old storage format
1498 room._affiliations = frozen._affiliations;
1499 else
1500 -- New storage format
1501 for jid, data in pairs(frozen) do
1502 local node, host, resource = jid_split(jid);
1503 if host:sub(1,1) ~= "_" and not resource and type(data) == "string" then
1504 -- bare jid: affiliation
1505 room._affiliations[jid] = data;
1509 for jid, data in pairs(state or frozen) do
1510 local node, host, resource = jid_split(jid);
1511 if node or host:sub(1,1) ~= "_" then
1512 if host == room_host and node == room_name and resource and type(data) == "table" then
1513 -- full room jid: bare real jid and role
1514 local nick = jid;
1515 local occupant = occupants[nick] or occupant_lib.new(data.bare_jid, nick);
1516 occupant.bare_jid = data.bare_jid;
1517 occupant.role = data.role;
1518 occupant.jid = data.jid; -- Primary session JID
1519 occupants[nick] = occupant;
1520 elseif type(data) == "table" and data.name == "presence" then
1521 -- full user jid: presence
1522 local nick = data.attr.from;
1523 local occupant = occupants[nick] or occupant_lib.new(nil, nick);
1524 local presence = st.deserialize(data);
1525 occupant:set_session(jid, presence);
1526 occupants[nick] = occupant;
1531 for _, occupant in pairs(occupants) do
1532 room:save_occupant(occupant);
1535 return room;
1538 _M.room_mt = room_mt;
1540 return _M;