util.encodings: Spell out all IDNA 2008 options ICU has
[prosody.git] / plugins / muc / mod_muc.lua
blob89e677444d6fe84eb9fde16ae72ebf6a4405c346
1 -- Prosody IM
2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
4 --
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
7 --
9 -- Exposed functions:
11 -- create_room(jid) -> room
12 -- track_room(room)
13 -- delete_room(room)
14 -- forget_room(room)
15 -- get_room_from_jid(jid) -> room
16 -- each_room(live_only) -> () -> room [DEPRECATED]
17 -- all_rooms() -> room
18 -- live_rooms() -> room
19 -- shutdown_component()
21 if module:get_host_type() ~= "component" then
22 error("MUC should be loaded as a component, please see https://prosody.im/doc/components", 0);
23 end
25 local muclib = module:require "muc";
26 room_mt = muclib.room_mt; -- Yes, global.
27 new_room = muclib.new_room;
29 local name = module:require "muc/name";
30 room_mt.get_name = name.get;
31 room_mt.set_name = name.set;
33 local description = module:require "muc/description";
34 room_mt.get_description = description.get;
35 room_mt.set_description = description.set;
37 local language = module:require "muc/language";
38 room_mt.get_language = language.get;
39 room_mt.set_language = language.set;
41 local hidden = module:require "muc/hidden";
42 room_mt.get_hidden = hidden.get;
43 room_mt.set_hidden = hidden.set;
44 function room_mt:get_public()
45 return not self:get_hidden();
46 end
47 function room_mt:set_public(public)
48 return self:set_hidden(not public);
49 end
51 local password = module:require "muc/password";
52 room_mt.get_password = password.get;
53 room_mt.set_password = password.set;
55 local members_only = module:require "muc/members_only";
56 room_mt.get_members_only = members_only.get;
57 room_mt.set_members_only = members_only.set;
58 room_mt.get_allow_member_invites = members_only.get_allow_member_invites;
59 room_mt.set_allow_member_invites = members_only.set_allow_member_invites;
61 local moderated = module:require "muc/moderated";
62 room_mt.get_moderated = moderated.get;
63 room_mt.set_moderated = moderated.set;
65 local request = module:require "muc/request";
66 room_mt.handle_role_request = request.handle_request;
68 local persistent = module:require "muc/persistent";
69 room_mt.get_persistent = persistent.get;
70 room_mt.set_persistent = persistent.set;
72 local subject = module:require "muc/subject";
73 room_mt.get_changesubject = subject.get_changesubject;
74 room_mt.set_changesubject = subject.set_changesubject;
75 room_mt.get_subject = subject.get;
76 room_mt.set_subject = subject.set;
77 room_mt.send_subject = subject.send;
79 local history = module:require "muc/history";
80 room_mt.send_history = history.send;
81 room_mt.get_historylength = history.get_length;
82 room_mt.set_historylength = history.set_length;
84 local register = module:require "muc/register";
85 room_mt.get_registered_nick = register.get_registered_nick;
86 room_mt.get_registered_jid = register.get_registered_jid;
87 room_mt.handle_register_iq = register.handle_register_iq;
89 local jid_split = require "util.jid".split;
90 local jid_bare = require "util.jid".bare;
91 local st = require "util.stanza";
92 local cache = require "util.cache";
93 local um_is_admin = require "core.usermanager".is_admin;
95 module:require "muc/config_form_sections";
97 module:depends("disco");
98 module:add_identity("conference", "text", module:get_option_string("name", "Prosody Chatrooms"));
99 module:add_feature("http://jabber.org/protocol/muc");
100 module:depends "muc_unique"
101 module:require "muc/lock";
103 local function is_admin(jid)
104 return um_is_admin(jid, module.host);
107 do -- Monkey patch to make server admins room owners
108 local _get_affiliation = room_mt.get_affiliation;
109 function room_mt:get_affiliation(jid)
110 if is_admin(jid) then return "owner"; end
111 return _get_affiliation(self, jid);
114 local _set_affiliation = room_mt.set_affiliation;
115 function room_mt:set_affiliation(actor, jid, affiliation, reason, data)
116 if affiliation ~= "owner" and is_admin(jid) then return nil, "modify", "not-acceptable"; end
117 return _set_affiliation(self, actor, jid, affiliation, reason, data);
121 local persistent_rooms_storage = module:open_store("persistent");
122 local persistent_rooms = module:open_store("persistent", "map");
123 local room_configs = module:open_store("config");
124 local room_state = module:open_store("state");
126 local room_items_cache = {};
128 local function room_save(room, forced, savestate)
129 local node = jid_split(room.jid);
130 local is_persistent = persistent.get(room);
131 room_items_cache[room.jid] = room:get_public() and room:get_name() or nil;
132 if is_persistent or savestate then
133 persistent_rooms:set(nil, room.jid, true);
134 local data, state = room:freeze(savestate);
135 room_state:set(node, state);
136 return room_configs:set(node, data);
137 elseif forced then
138 persistent_rooms:set(nil, room.jid, nil);
139 room_state:set(node, nil);
140 return room_configs:set(node, nil);
144 local max_rooms = module:get_option_number("muc_max_rooms");
145 local max_live_rooms = module:get_option_number("muc_room_cache_size", 100);
147 local room_hit = module:measure("room_hit", "rate");
148 local room_miss = module:measure("room_miss", "rate")
149 local room_eviction = module:measure("room_eviction", "rate");
150 local rooms = cache.new(max_rooms or max_live_rooms, function (jid, room)
151 if max_rooms then
152 module:log("info", "Room limit of %d reached, no new rooms allowed", max_rooms);
153 return false;
155 module:log("debug", "Evicting room %s", jid);
156 room_eviction();
157 room_items_cache[room.jid] = room:get_public() and room:get_name() or nil;
158 local ok, err = room_save(room, nil, true); -- Force to disk
159 if not ok then
160 module:log("error", "Failed to swap inactive room %s to disk: %s", jid, err);
161 return false;
163 end);
165 -- Automatically destroy empty non-persistent rooms
166 module:hook("muc-occupant-left",function(event)
167 local room = event.room
168 if room.destroying then return end
169 if not room:has_occupant() and not persistent.get(room) then -- empty, non-persistent room
170 module:log("debug", "%q empty, destroying", room.jid);
171 module:fire_event("muc-room-destroyed", { room = room });
173 end, -1);
175 function track_room(room)
176 if rooms:set(room.jid, room) then
177 -- When room is created, over-ride 'save' method
178 room.save = room_save;
179 return room;
181 -- Resource limit reached
182 return false;
185 local function handle_broken_room(room, origin, stanza)
186 module:log("debug", "Returning error from broken room %s", room.jid);
187 origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
188 return true;
191 local function restore_room(jid)
192 local node = jid_split(jid);
193 local data, err = room_configs:get(node);
194 if data then
195 module:log("debug", "Restoring room %s from storage", jid);
196 if module:fire_event("muc-room-pre-restore", { jid = jid, data = data }) == false then
197 return false;
199 local state, s_err = room_state:get(node);
200 if not state and s_err then
201 module:log("debug", "Could not restore state of room %s: %s", jid, s_err);
203 local room = muclib.restore_room(data, state);
204 if track_room(room) then
205 room_state:set(node, nil);
206 module:fire_event("muc-room-restored", { jid = jid, room = room });
207 return room;
208 else
209 return false;
211 elseif err then
212 module:log("error", "Error restoring room %s from storage: %s", jid, err);
213 local room = muclib.new_room(jid, { locked = math.huge });
214 room.handle_normal_presence = handle_broken_room;
215 room.handle_first_presence = handle_broken_room;
216 return room;
220 -- Removes a room from memory, without saving it (save first if required)
221 function forget_room(room)
222 module:log("debug", "Forgetting %s", room.jid);
223 rooms.save = nil;
224 rooms:set(room.jid, nil);
227 -- Removes a room from the database (may remain in memory)
228 function delete_room(room)
229 module:log("debug", "Deleting %s", room.jid);
230 room_configs:set(jid_split(room.jid), nil);
231 room_state:set(jid_split(room.jid), nil);
232 persistent_rooms:set(nil, room.jid, nil);
233 room_items_cache[room.jid] = nil;
236 function module.unload()
237 for room in live_rooms() do
238 room:save(nil, true);
239 forget_room(room);
243 function get_room_from_jid(room_jid)
244 local room = rooms:get(room_jid);
245 if room then
246 room_hit();
247 rooms:set(room_jid, room); -- bump to top;
248 return room;
250 room_miss();
251 return restore_room(room_jid);
254 local function set_room_defaults(room, lang)
255 room:set_public(module:get_option_boolean("muc_room_default_public", false));
256 room:set_persistent(module:get_option_boolean("muc_room_default_persistent", room:get_persistent()));
257 room:set_members_only(module:get_option_boolean("muc_room_default_members_only", room:get_members_only()));
258 room:set_allow_member_invites(module:get_option_boolean("muc_room_default_allow_member_invites",
259 room:get_allow_member_invites()));
260 room:set_moderated(module:get_option_boolean("muc_room_default_moderated", room:get_moderated()));
261 room:set_whois(module:get_option_boolean("muc_room_default_public_jids",
262 room:get_whois() == "anyone") and "anyone" or "moderators");
263 room:set_changesubject(module:get_option_boolean("muc_room_default_change_subject", room:get_changesubject()));
264 room:set_historylength(module:get_option_number("muc_room_default_history_length", room:get_historylength()));
265 room:set_language(lang or module:get_option_string("muc_room_default_language"));
268 function create_room(room_jid, config)
269 local exists = get_room_from_jid(room_jid);
270 if exists then
271 return nil, "room-exists";
273 local room = muclib.new_room(room_jid, config);
274 if not config then
275 set_room_defaults(room);
277 module:fire_event("muc-room-created", {
278 room = room;
280 return track_room(room);
283 function all_rooms()
284 return coroutine.wrap(function ()
285 local seen = {}; -- Don't iterate over persistent rooms twice
286 for room in live_rooms() do
287 coroutine.yield(room);
288 seen[room.jid] = true;
290 local all_persistent_rooms, err = persistent_rooms_storage:get(nil);
291 if not all_persistent_rooms then
292 if err then
293 module:log("error", "Error loading list of persistent rooms, only rooms live in memory were iterated over");
294 module:log("debug", "%s", debug.traceback(err));
296 return nil;
298 for room_jid in pairs(all_persistent_rooms) do
299 if not seen[room_jid] then
300 local room = restore_room(room_jid);
301 if room then
302 coroutine.yield(room);
303 else
304 module:log("error", "Missing data for room '%s', omitting from iteration", room_jid);
308 end);
311 function live_rooms()
312 return rooms:values();
315 function each_room(live_only)
316 if live_only then
317 return live_rooms();
319 return all_rooms();
322 module:hook("host-disco-items", function(event)
323 local reply = event.reply;
324 module:log("debug", "host-disco-items called");
325 if next(room_items_cache) ~= nil then
326 for jid, room_name in pairs(room_items_cache) do
327 reply:tag("item", { jid = jid, name = room_name }):up();
329 else
330 for room in all_rooms() do
331 if not room:get_hidden() then
332 local jid, room_name = room.jid, room:get_name();
333 room_items_cache[jid] = room_name;
334 reply:tag("item", { jid = jid, name = room_name }):up();
338 end);
340 module:hook("muc-room-pre-create", function (event)
341 set_room_defaults(event.room, event.stanza.attr["xml:lang"]);
342 end, 1);
344 module:hook("muc-room-pre-create", function(event)
345 local origin, stanza = event.origin, event.stanza;
346 if not track_room(event.room) then
347 origin.send(st.error_reply(stanza, "wait", "resource-constraint"));
348 return true;
350 end, -1000);
352 module:hook("muc-room-destroyed",function(event)
353 local room = event.room;
354 forget_room(room);
355 delete_room(room);
356 end);
358 if module:get_option_boolean("muc_tombstones", true) then
360 local ttl = module:get_option_number("muc_tombstone_expiry", 86400 * 31);
362 module:hook("muc-room-destroyed",function(event)
363 local room = event.room;
364 if not room:get_persistent() then return end
365 if room._data.destroyed then
366 return -- Allow destruction of tombstone
369 local tombstone = new_room(room.jid, {
370 locked = os.time() + ttl;
371 destroyed = true;
372 reason = event.reason;
373 newjid = event.newjid;
374 -- password?
376 tombstone.save = room_save;
377 tombstone:set_persistent(true);
378 tombstone:set_hidden(true);
379 tombstone:save(true);
380 return true;
381 end, -10);
385 local restrict_room_creation = module:get_option("restrict_room_creation");
386 if restrict_room_creation == true then
387 restrict_room_creation = "admin";
389 if restrict_room_creation then
390 local host_suffix = module.host:gsub("^[^%.]+%.", "");
391 module:hook("muc-room-pre-create", function(event)
392 local origin, stanza = event.origin, event.stanza;
393 local user_jid = stanza.attr.from;
394 if not is_admin(user_jid) and not (
395 restrict_room_creation == "local" and
396 select(2, jid_split(user_jid)) == host_suffix
397 ) then
398 origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted"));
399 return true;
401 end);
405 for event_name, method in pairs {
406 -- Normal room interactions
407 ["iq-get/bare/http://jabber.org/protocol/disco#info:query"] = "handle_disco_info_get_query" ;
408 ["iq-get/bare/http://jabber.org/protocol/disco#items:query"] = "handle_disco_items_get_query" ;
409 ["iq-set/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
410 ["iq-get/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_get_command" ;
411 ["iq-set/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
412 ["iq-get/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_get_to_room" ;
413 ["message/bare"] = "handle_message_to_room" ;
414 ["presence/bare"] = "handle_presence_to_room" ;
415 ["iq/bare/jabber:iq:register:query"] = "handle_register_iq";
416 -- Host room
417 ["iq-get/host/http://jabber.org/protocol/disco#info:query"] = "handle_disco_info_get_query" ;
418 ["iq-get/host/http://jabber.org/protocol/disco#items:query"] = "handle_disco_items_get_query" ;
419 ["iq-set/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
420 ["iq-get/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_get_command" ;
421 ["iq-set/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
422 ["iq-get/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_get_to_room" ;
423 ["message/host"] = "handle_message_to_room" ;
424 ["presence/host"] = "handle_presence_to_room" ;
425 -- Direct to occupant (normal rooms and host room)
426 ["presence/full"] = "handle_presence_to_occupant" ;
427 ["iq/full"] = "handle_iq_to_occupant" ;
428 ["message/full"] = "handle_message_to_occupant" ;
429 } do
430 module:hook(event_name, function (event)
431 local origin, stanza = event.origin, event.stanza;
432 local room_jid = jid_bare(stanza.attr.to);
433 local room = get_room_from_jid(room_jid);
435 if room and room._data.destroyed then
436 if room._data.locked < os.time()
437 or (is_admin(stanza.attr.from) and stanza.name == "presence" and stanza.attr.type == nil) then
438 -- Allow the room to be recreated by admin or after time has passed
439 delete_room(room);
440 room = nil;
441 else
442 if stanza.attr.type ~= "error" then
443 local reply = st.error_reply(stanza, "cancel", "gone", room._data.reason)
444 if room._data.newjid then
445 local uri = "xmpp:"..room._data.newjid.."?join";
446 reply:get_child("error"):child_with_name("gone"):text(uri);
448 event.origin.send(reply);
450 return true;
454 if room == nil then
455 -- Watch presence to create rooms
456 if stanza.attr.type == nil and stanza.name == "presence" and stanza:get_child("x", "http://jabber.org/protocol/muc") then
457 room = muclib.new_room(room_jid);
458 return room:handle_first_presence(origin, stanza);
459 elseif stanza.attr.type ~= "error" then
460 origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
461 return true;
462 else
463 return;
465 elseif room == false then -- Error loading room
466 origin.send(st.error_reply(stanza, "wait", "resource-constraint"));
467 return true;
469 return room[method](room, origin, stanza);
470 end, -2)
473 function shutdown_component()
474 for room in live_rooms() do
475 room:save(nil, true);
478 module:hook_global("server-stopping", shutdown_component, -300);
480 do -- Ad-hoc commands
481 module:depends "adhoc";
482 local t_concat = table.concat;
483 local adhoc_new = module:require "adhoc".new;
484 local adhoc_initial = require "util.adhoc".new_initial_data_form;
485 local array = require "util.array";
486 local dataforms_new = require "util.dataforms".new;
488 local destroy_rooms_layout = dataforms_new {
489 title = "Destroy rooms";
490 instructions = "Select the rooms to destroy";
492 { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" };
493 { name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"};
496 local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function()
497 return { rooms = array.collect(all_rooms()):pluck("jid"):sort(); };
498 end, function(fields, errors)
499 if errors then
500 local errmsg = {};
501 for field, err in pairs(errors) do
502 errmsg[#errmsg + 1] = field .. ": " .. err;
504 return { status = "completed", error = { message = t_concat(errmsg, "\n") } };
506 for _, room in ipairs(fields.rooms) do
507 get_room_from_jid(room):destroy();
509 return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(fields.rooms, "\n") };
510 end);
511 local destroy_rooms_desc = adhoc_new("Destroy Rooms",
512 "http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin");
514 module:provides("adhoc", destroy_rooms_desc);