2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
11 -- create_room(jid) -> 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);
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();
47 function room_mt
:set_public(public
)
48 return self
:set_hidden(not public
);
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
);
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
)
152 module
:log("info", "Room limit of %d reached, no new rooms allowed", max_rooms
);
155 module
:log("debug", "Evicting room %s", jid
);
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
160 module
:log("error", "Failed to swap inactive room %s to disk: %s", jid
, err
);
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
});
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
;
181 -- Resource limit reached
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"));
191 local function restore_room(jid
)
192 local node
= jid_split(jid
);
193 local data
, err
= room_configs
:get(node
);
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
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
});
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
;
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
);
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);
243 function get_room_from_jid(room_jid
)
244 local room
= rooms
:get(room_jid
);
247 rooms
:set(room_jid
, room
); -- bump to top;
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
);
271 return nil, "room-exists";
273 local room
= muclib
.new_room(room_jid
, config
);
275 set_room_defaults(room
);
277 module
:fire_event("muc-room-created", {
280 return track_room(room
);
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
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
));
298 for room_jid
in pairs(all_persistent_rooms
) do
299 if not seen
[room_jid
] then
300 local room
= restore_room(room_jid
);
302 coroutine
.yield(room
);
304 module
:log("error", "Missing data for room '%s', omitting from iteration", room_jid
);
311 function live_rooms()
312 return rooms
:values();
315 function each_room(live_only
)
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();
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();
340 module
:hook("muc-room-pre-create", function (event
)
341 set_room_defaults(event
.room
, event
.stanza
.attr
["xml:lang"]);
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"));
352 module
:hook("muc-room-destroyed",function(event
)
353 local room
= event
.room
;
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
;
372 reason
= event
.reason
;
373 newjid
= event
.newjid
;
376 tombstone
.save
= room_save
;
377 tombstone
:set_persistent(true);
378 tombstone
:set_hidden(true);
379 tombstone
:save(true);
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
398 origin
.send(st
.error_reply(stanza
, "cancel", "not-allowed", "Room creation is restricted"));
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";
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" ;
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
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
);
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"));
465 elseif room
== false then -- Error loading room
466 origin
.send(st
.error_reply(stanza
, "wait", "resource-constraint"));
469 return room
[method
](room
, origin
, stanza
);
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
)
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") };
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
);