24
1
mirror of https://github.com/processone/ejabberd.git synced 2024-06-08 21:43:07 +02:00
xmpp.chapril.org-ejabberd/src/mod_muc.erl
Paweł Chmielowski 8a645a2d3d Don't forget not-persistent rooms in load_permanent_rooms
Only non-persistent rooms that we are storing are those that were hibernated
but also have mucsub subscribers in them. I don't think it makes sense to
destroy those rooms on restart/reload if we didn't destroy them in first
place when last member did leave room, let just handle those rooms like
they are persistent, and kill them only when all user unsubscribe from them
or they are destroyed from api.
2020-08-06 11:44:44 +02:00

1663 lines
66 KiB
Erlang

%%%----------------------------------------------------------------------
%%% File : mod_muc.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : MUC support (XEP-0045)
%%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2020 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
-module(mod_muc).
-author('alexey@process-one.net').
-protocol({xep, 45, '1.25'}).
-ifndef(GEN_SERVER).
-define(GEN_SERVER, gen_server).
-endif.
-behaviour(?GEN_SERVER).
-behaviour(gen_mod).
%% API
-export([start/2,
stop/1,
start_link/2,
reload/3,
mod_doc/0,
room_destroyed/4,
store_room/4,
store_room/5,
restore_room/3,
forget_room/3,
create_room/5,
shutdown_rooms/1,
process_disco_info/1,
process_disco_items/1,
process_vcard/1,
process_register/1,
process_muc_unique/1,
process_mucsub/1,
broadcast_service_message/3,
export/1,
import_info/0,
import/5,
import_start/2,
opts_to_binary/1,
find_online_room/2,
register_online_room/3,
get_online_rooms/1,
count_online_rooms/1,
register_online_user/4,
unregister_online_user/4,
iq_set_register_info/5,
count_online_rooms_by_user/3,
get_online_rooms_by_user/3,
can_use_nick/4,
get_subscribed_rooms/2,
procname/2,
route/1]).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3,
mod_opt_type/1, mod_options/1, depends/2]).
-include("logger.hrl").
-include("xmpp.hrl").
-include("mod_muc.hrl").
-include("mod_muc_room.hrl").
-include("translate.hrl").
-include("ejabberd_stacktrace.hrl").
-type state() :: #{hosts := [binary()],
server_host := binary(),
worker := pos_integer()}.
-type access() :: {acl:acl(), acl:acl(), acl:acl(), acl:acl(), acl:acl()}.
-type muc_room_opts() :: [{atom(), any()}].
-export_type([access/0]).
-callback init(binary(), gen_mod:opts()) -> any().
-callback import(binary(), binary(), [binary()]) -> ok.
-callback store_room(binary(), binary(), binary(), list(), list()|undefined) -> {atomic, any()}.
-callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error.
-callback forget_room(binary(), binary(), binary()) -> {atomic, any()}.
-callback can_use_nick(binary(), binary(), jid(), binary()) -> boolean().
-callback get_rooms(binary(), binary()) -> [#muc_room{}].
-callback get_nick(binary(), binary(), jid()) -> binary() | error.
-callback set_nick(binary(), binary(), jid(), binary()) -> {atomic, ok | false}.
-callback register_online_room(binary(), binary(), binary(), pid()) -> any().
-callback unregister_online_room(binary(), binary(), binary(), pid()) -> any().
-callback find_online_room(binary(), binary(), binary()) -> {ok, pid()} | error.
-callback get_online_rooms(binary(), binary(), undefined | rsm_set()) -> [{binary(), binary(), pid()}].
-callback count_online_rooms(binary(), binary()) -> non_neg_integer().
-callback rsm_supported() -> boolean().
-callback register_online_user(binary(), ljid(), binary(), binary()) -> any().
-callback unregister_online_user(binary(), ljid(), binary(), binary()) -> any().
-callback count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer().
-callback get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}].
-callback get_subscribed_rooms(binary(), binary(), jid()) ->
{ok, [{jid(), [binary()]}]} | {error, db_failure}.
-optional_callbacks([get_subscribed_rooms/3]).
%%====================================================================
%% API
%%====================================================================
start(Host, Opts) ->
case mod_muc_sup:start(Host) of
{ok, _} ->
MyHosts = gen_mod:get_opt_hosts(Opts),
Mod = gen_mod:db_mod(Opts, ?MODULE),
RMod = gen_mod:ram_db_mod(Opts, ?MODULE),
Mod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)),
RMod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)),
load_permanent_rooms(MyHosts, Host, Opts);
Err ->
Err
end.
stop(Host) ->
Proc = mod_muc_sup:procname(Host),
supervisor:terminate_child(ejabberd_gen_mod_sup, Proc),
supervisor:delete_child(ejabberd_gen_mod_sup, Proc).
-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok.
reload(ServerHost, NewOpts, OldOpts) ->
NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
NewRMod = gen_mod:ram_db_mod(NewOpts, ?MODULE),
OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
OldRMod = gen_mod:ram_db_mod(OldOpts, ?MODULE),
NewHosts = gen_mod:get_opt_hosts(NewOpts),
OldHosts = gen_mod:get_opt_hosts(OldOpts),
AddHosts = NewHosts -- OldHosts,
DelHosts = OldHosts -- NewHosts,
if NewMod /= OldMod ->
NewMod:init(ServerHost, gen_mod:set_opt(hosts, NewHosts, NewOpts));
true ->
ok
end,
if NewRMod /= OldRMod ->
NewRMod:init(ServerHost, gen_mod:set_opt(hosts, NewHosts, NewOpts));
true ->
ok
end,
lists:foreach(
fun(I) ->
?GEN_SERVER:cast(procname(ServerHost, I),
{reload, AddHosts, DelHosts, NewHosts})
end, lists:seq(1, erlang:system_info(logical_processors))),
load_permanent_rooms(AddHosts, ServerHost, NewOpts),
shutdown_rooms(ServerHost, DelHosts, OldRMod),
lists:foreach(
fun(Host) ->
lists:foreach(
fun({_, _, Pid}) when node(Pid) == node() ->
mod_muc_room:config_reloaded(Pid);
(_) ->
ok
end, get_online_rooms(ServerHost, Host))
end, misc:intersection(NewHosts, OldHosts)).
depends(_Host, _Opts) ->
[{mod_mam, soft}].
start_link(Host, I) ->
Proc = procname(Host, I),
?GEN_SERVER:start_link({local, Proc}, ?MODULE, [Host, I],
ejabberd_config:fsm_limit_opts([])).
-spec procname(binary(), pos_integer() | {binary(), binary()}) -> atom().
procname(Host, I) when is_integer(I) ->
binary_to_atom(
<<(atom_to_binary(?MODULE, latin1))/binary, "_", Host/binary,
"_", (integer_to_binary(I))/binary>>, utf8);
procname(Host, RoomHost) ->
Cores = erlang:system_info(logical_processors),
I = erlang:phash2(RoomHost, Cores) + 1,
procname(Host, I).
-spec route(stanza()) -> ok.
route(Pkt) ->
To = xmpp:get_to(Pkt),
ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
route(Pkt, ServerHost).
-spec route(stanza(), binary()) -> ok.
route(Pkt, ServerHost) ->
From = xmpp:get_from(Pkt),
To = xmpp:get_to(Pkt),
Host = To#jid.lserver,
Access = mod_muc_opt:access(ServerHost),
case acl:match_rule(ServerHost, Access, From) of
allow ->
route(Pkt, Host, ServerHost);
deny ->
Lang = xmpp:get_lang(Pkt),
ErrText = ?T("Access denied by service policy"),
Err = xmpp:err_forbidden(ErrText, Lang),
ejabberd_router:route_error(Pkt, Err)
end.
-spec route(stanza(), binary(), binary()) -> ok.
route(#iq{to = #jid{luser = <<"">>, lresource = <<"">>}} = IQ, _, _) ->
ejabberd_router:process_iq(IQ);
route(#message{lang = Lang, body = Body, type = Type, from = From,
to = #jid{luser = <<"">>, lresource = <<"">>}} = Pkt,
Host, ServerHost) ->
if Type == error ->
ok;
true ->
AccessAdmin = mod_muc_opt:access_admin(ServerHost),
case acl:match_rule(ServerHost, AccessAdmin, From) of
allow ->
Msg = xmpp:get_text(Body),
broadcast_service_message(ServerHost, Host, Msg);
deny ->
ErrText = ?T("Only service administrators are allowed "
"to send service messages"),
Err = xmpp:err_forbidden(ErrText, Lang),
ejabberd_router:route_error(Pkt, Err)
end
end;
route(Pkt, Host, ServerHost) ->
{Room, _, _} = jid:tolower(xmpp:get_to(Pkt)),
case Room of
<<"">> ->
Txt = ?T("No module is handling this query"),
Err = xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)),
ejabberd_router:route_error(Pkt, Err);
_ ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
case RMod:find_online_room(ServerHost, Room, Host) of
error ->
Proc = procname(ServerHost, {Room, Host}),
case whereis(Proc) of
Pid when Pid == self() ->
route_to_room(Pkt, ServerHost);
Pid when is_pid(Pid) ->
?DEBUG("Routing to MUC worker ~p:~n~ts", [Proc, xmpp:pp(Pkt)]),
?GEN_SERVER:cast(Pid, {route_to_room, Pkt});
undefined ->
?DEBUG("MUC worker ~p is dead", [Proc]),
Err = xmpp:err_internal_server_error(),
ejabberd_router:route_error(Pkt, Err)
end;
{ok, Pid} ->
mod_muc_room:route(Pid, Pkt)
end
end.
-spec shutdown_rooms(binary()) -> [pid()].
shutdown_rooms(ServerHost) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
Hosts = gen_mod:get_module_opt_hosts(ServerHost, mod_muc),
shutdown_rooms(ServerHost, Hosts, RMod).
-spec shutdown_rooms(binary(), [binary()], module()) -> [pid()].
shutdown_rooms(ServerHost, Hosts, RMod) ->
Rooms = [RMod:get_online_rooms(ServerHost, Host, undefined)
|| Host <- Hosts],
lists:flatmap(
fun({_, _, Pid}) when node(Pid) == node() ->
mod_muc_room:shutdown(Pid),
[Pid];
(_) ->
[]
end, lists:flatten(Rooms)).
%% This function is called by a room in three situations:
%% A) The owner of the room destroyed it
%% B) The only participant of a temporary room leaves it
%% C) mod_muc:stop was called, and each room is being terminated
%% In this case, the mod_muc process died before the room processes
%% So the message sending must be caught
-spec room_destroyed(binary(), binary(), pid(), binary()) -> ok.
room_destroyed(Host, Room, Pid, ServerHost) ->
Proc = procname(ServerHost, {Room, Host}),
?GEN_SERVER:cast(Proc, {room_destroyed, {Room, Host}, Pid}).
%% @doc Create a room.
%% If Opts = default, the default room options are used.
%% Else use the passed options as defined in mod_muc_room.
create_room(Host, Name, From, Nick, Opts) ->
ServerHost = ejabberd_router:host_of_route(Host),
Proc = procname(ServerHost, {Name, Host}),
?GEN_SERVER:call(Proc, {create, Name, Host, From, Nick, Opts}).
store_room(ServerHost, Host, Name, Opts) ->
store_room(ServerHost, Host, Name, Opts, undefined).
store_room(ServerHost, Host, Name, Opts, ChangesHints) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:store_room(LServer, Host, Name, Opts, ChangesHints).
restore_room(ServerHost, Host, Name) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:restore_room(LServer, Host, Name).
forget_room(ServerHost, Host, Name) ->
LServer = jid:nameprep(ServerHost),
ejabberd_hooks:run(remove_room, LServer, [LServer, Name, Host]),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:forget_room(LServer, Host, Name).
can_use_nick(_ServerHost, _Host, _JID, <<"">>) -> false;
can_use_nick(ServerHost, Host, JID, Nick) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:can_use_nick(LServer, Host, JID, Nick).
-spec find_online_room(binary(), binary()) -> {ok, pid()} | error.
find_online_room(Room, Host) ->
ServerHost = ejabberd_router:host_of_route(Host),
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:find_online_room(ServerHost, Room, Host).
-spec register_online_room(binary(), binary(), pid()) -> any().
register_online_room(Room, Host, Pid) ->
ServerHost = ejabberd_router:host_of_route(Host),
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:register_online_room(ServerHost, Room, Host, Pid).
-spec get_online_rooms(binary()) -> [{binary(), binary(), pid()}].
get_online_rooms(Host) ->
ServerHost = ejabberd_router:host_of_route(Host),
get_online_rooms(ServerHost, Host).
-spec count_online_rooms(binary()) -> non_neg_integer().
count_online_rooms(Host) ->
ServerHost = ejabberd_router:host_of_route(Host),
count_online_rooms(ServerHost, Host).
-spec register_online_user(binary(), ljid(), binary(), binary()) -> any().
register_online_user(ServerHost, LJID, Name, Host) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:register_online_user(ServerHost, LJID, Name, Host).
-spec unregister_online_user(binary(), ljid(), binary(), binary()) -> any().
unregister_online_user(ServerHost, LJID, Name, Host) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:unregister_online_user(ServerHost, LJID, Name, Host).
-spec count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer().
count_online_rooms_by_user(ServerHost, LUser, LServer) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:count_online_rooms_by_user(ServerHost, LUser, LServer).
-spec get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}].
get_online_rooms_by_user(ServerHost, LUser, LServer) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:get_online_rooms_by_user(ServerHost, LUser, LServer).
%%====================================================================
%% gen_server callbacks
%%====================================================================
-spec init(list()) -> {ok, state()}.
init([Host, Worker]) ->
process_flag(trap_exit, true),
Opts = gen_mod:get_module_opts(Host, ?MODULE),
MyHosts = gen_mod:get_opt_hosts(Opts),
register_routes(Host, MyHosts, Worker),
register_iq_handlers(MyHosts, Worker),
{ok, #{server_host => Host, hosts => MyHosts, worker => Worker}}.
-spec handle_call(term(), {pid(), term()}, state()) ->
{reply, ok | {ok, pid()} | {error, any()}, state()} |
{stop, normal, ok, state()}.
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call({create, Room, Host, From, Nick, Opts}, _From,
#{server_host := ServerHost} = State) ->
?DEBUG("MUC: create new room '~ts'~n", [Room]),
NewOpts = case Opts of
default -> mod_muc_opt:default_room_options(ServerHost);
_ -> Opts
end,
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
case start_room(RMod, Host, ServerHost, Room, NewOpts, From, Nick) of
{ok, _} ->
ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]),
{reply, ok, State};
Err ->
{reply, Err, State}
end.
-spec handle_cast(term(), state()) -> {noreply, state()}.
handle_cast({route_to_room, Packet}, #{server_host := ServerHost} = State) ->
try route_to_room(Packet, ServerHost)
catch ?EX_RULE(Class, Reason, St) ->
StackTrace = ?EX_STACK(St),
?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts",
[xmpp:pp(Packet),
misc:format_exception(2, Class, Reason, StackTrace)])
end,
{noreply, State};
handle_cast({room_destroyed, {Room, Host}, Pid},
#{server_host := ServerHost} = State) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:unregister_online_room(ServerHost, Room, Host, Pid),
{noreply, State};
handle_cast({reload, AddHosts, DelHosts, NewHosts},
#{server_host := ServerHost, worker := Worker} = State) ->
register_routes(ServerHost, AddHosts, Worker),
register_iq_handlers(AddHosts, Worker),
unregister_routes(DelHosts, Worker),
unregister_iq_handlers(DelHosts, Worker),
{noreply, State#{hosts => NewHosts}};
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
-spec handle_info(term(), state()) -> {noreply, state()}.
handle_info({route, Packet}, #{server_host := ServerHost} = State) ->
%% We can only receive the packet here from other nodes
%% where mod_muc is not loaded. Such configuration
%% is *highly* discouraged
try route(Packet, ServerHost)
catch ?EX_RULE(Class, Reason, St) ->
StackTrace = ?EX_STACK(St),
?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts",
[xmpp:pp(Packet),
misc:format_exception(2, Class, Reason, StackTrace)])
end,
{noreply, State};
handle_info({room_destroyed, {Room, Host}, Pid}, State) ->
%% For backward compat
handle_cast({room_destroyed, {Room, Host}, Pid}, State);
handle_info(Info, State) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
-spec terminate(term(), state()) -> any().
terminate(_Reason, #{hosts := Hosts, worker := Worker}) ->
unregister_routes(Hosts, Worker),
unregister_iq_handlers(Hosts, Worker).
-spec code_change(term(), state(), term()) -> {ok, state()}.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
-spec register_iq_handlers([binary()], pos_integer()) -> ok.
register_iq_handlers(Hosts, 1) ->
%% Only register handlers on first worker
lists:foreach(
fun(Host) ->
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_REGISTER,
?MODULE, process_register),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD,
?MODULE, process_vcard),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MUCSUB,
?MODULE, process_mucsub),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MUC_UNIQUE,
?MODULE, process_muc_unique),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO,
?MODULE, process_disco_info),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS,
?MODULE, process_disco_items)
end, Hosts);
register_iq_handlers(_, _) ->
ok.
-spec unregister_iq_handlers([binary()], pos_integer()) -> ok.
unregister_iq_handlers(Hosts, 1) ->
%% Only unregister handlers on first worker
lists:foreach(
fun(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_REGISTER),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MUCSUB),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MUC_UNIQUE),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS)
end, Hosts);
unregister_iq_handlers(_, _) ->
ok.
-spec register_routes(binary(), [binary()], pos_integer()) -> ok.
register_routes(ServerHost, Hosts, 1) ->
%% Only register routes on first worker
lists:foreach(
fun(Host) ->
ejabberd_router:register_route(
Host, ServerHost, {apply, ?MODULE, route})
end, Hosts);
register_routes(_, _, _) ->
ok.
-spec unregister_routes([binary()], pos_integer()) -> ok.
unregister_routes(Hosts, 1) ->
%% Only unregister routes on first worker
lists:foreach(
fun(Host) ->
ejabberd_router:unregister_route(Host)
end, Hosts);
unregister_routes(_, _) ->
ok.
%% Function copied from mod_muc_room.erl
-spec extract_password(presence() | iq()) -> binary() | false.
extract_password(#presence{} = Pres) ->
case xmpp:get_subtag(Pres, #muc{}) of
#muc{password = Password} when is_binary(Password) ->
Password;
_ ->
false
end;
extract_password(#iq{} = IQ) ->
case xmpp:get_subtag(IQ, #muc_subscribe{}) of
#muc_subscribe{password = Password} when Password /= <<"">> ->
Password;
_ ->
false
end.
-spec route_to_room(stanza(), binary()) -> ok.
route_to_room(Packet, ServerHost) ->
From = xmpp:get_from(Packet),
To = xmpp:get_to(Packet),
{Room, Host, Nick} = jid:tolower(To),
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
case RMod:find_online_room(ServerHost, Room, Host) of
error ->
case should_start_room(Packet) of
false ->
Lang = xmpp:get_lang(Packet),
ErrText = ?T("Conference room does not exist"),
Err = xmpp:err_item_not_found(ErrText, Lang),
ejabberd_router:route_error(Packet, Err);
StartType ->
case load_room(RMod, Host, ServerHost, Room) of
{error, notfound} when StartType == start ->
case check_create_room(ServerHost, Host, Room, From) of
true ->
Pass = extract_password(Packet),
case start_new_room(RMod, Host, ServerHost, Room, Pass, From, Nick) of
{ok, Pid} ->
mod_muc_room:route(Pid, Packet);
_Err ->
Err = xmpp:err_internal_server_error(),
ejabberd_router:route_error(Packet, Err)
end;
false ->
Lang = xmpp:get_lang(Packet),
ErrText = ?T("Room creation is denied by service policy"),
Err = xmpp:err_forbidden(ErrText, Lang),
ejabberd_router:route_error(Packet, Err)
end;
{error, notfound} ->
Lang = xmpp:get_lang(Packet),
ErrText = ?T("Conference room does not exist"),
Err = xmpp:err_item_not_found(ErrText, Lang),
ejabberd_router:route_error(Packet, Err);
{error, _} ->
Err = xmpp:err_internal_server_error(),
ejabberd_router:route_error(Packet, Err);
{ok, Pid2} ->
mod_muc_room:route(Pid2, Packet)
end
end;
{ok, Pid} ->
mod_muc_room:route(Pid, Packet)
end.
-spec process_vcard(iq()) -> iq().
process_vcard(#iq{type = get, to = To, lang = Lang, sub_els = [#vcard_temp{}]} = IQ) ->
ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
VCard = case mod_muc_opt:vcard(ServerHost) of
undefined ->
#vcard_temp{fn = <<"ejabberd/mod_muc">>,
url = ejabberd_config:get_uri(),
desc = misc:get_descr(Lang, ?T("ejabberd MUC module"))};
V ->
V
end,
xmpp:make_iq_result(IQ, VCard);
process_vcard(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_vcard(#iq{lang = Lang} = IQ) ->
Txt = ?T("No module is handling this query"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
-spec process_register(iq()) -> iq().
process_register(#iq{type = Type, from = From, to = To, lang = Lang,
sub_els = [El = #register{}]} = IQ) ->
Host = To#jid.lserver,
ServerHost = ejabberd_router:host_of_route(Host),
AccessRegister = mod_muc_opt:access_register(ServerHost),
case acl:match_rule(ServerHost, AccessRegister, From) of
allow ->
case Type of
get ->
xmpp:make_iq_result(
IQ, iq_get_register_info(ServerHost, Host, From, Lang));
set ->
case process_iq_register_set(ServerHost, Host, From, El, Lang) of
{result, Result} ->
xmpp:make_iq_result(IQ, Result);
{error, Err} ->
xmpp:make_error(IQ, Err)
end
end;
deny ->
ErrText = ?T("Access denied by service policy"),
Err = xmpp:err_forbidden(ErrText, Lang),
xmpp:make_error(IQ, Err)
end.
-spec process_disco_info(iq()) -> iq().
process_disco_info(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_disco_info(#iq{type = get, from = From, to = To, lang = Lang,
sub_els = [#disco_info{node = <<"">>}]} = IQ) ->
ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
AccessRegister = mod_muc_opt:access_register(ServerHost),
X = ejabberd_hooks:run_fold(disco_info, ServerHost, [],
[ServerHost, ?MODULE, <<"">>, Lang]),
MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of
true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2];
false -> []
end,
RSMFeatures = case RMod:rsm_supported() of
true -> [?NS_RSM];
false -> []
end,
RegisterFeatures = case acl:match_rule(ServerHost, AccessRegister, From) of
allow -> [?NS_REGISTER];
deny -> []
end,
Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
?NS_MUC, ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE
| RegisterFeatures ++ RSMFeatures ++ MAMFeatures],
Name = mod_muc_opt:name(ServerHost),
Identity = #identity{category = <<"conference">>,
type = <<"text">>,
name = translate:translate(Lang, Name)},
xmpp:make_iq_result(
IQ, #disco_info{features = Features,
identities = [Identity],
xdata = X});
process_disco_info(#iq{type = get, lang = Lang,
sub_els = [#disco_info{}]} = IQ) ->
xmpp:make_error(IQ, xmpp:err_item_not_found(?T("Node not found"), Lang));
process_disco_info(#iq{lang = Lang} = IQ) ->
Txt = ?T("No module is handling this query"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
-spec process_disco_items(iq()) -> iq().
process_disco_items(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_disco_items(#iq{type = get, from = From, to = To, lang = Lang,
sub_els = [#disco_items{node = Node, rsm = RSM}]} = IQ) ->
Host = To#jid.lserver,
ServerHost = ejabberd_router:host_of_route(Host),
MaxRoomsDiscoItems = mod_muc_opt:max_rooms_discoitems(ServerHost),
case iq_disco_items(ServerHost, Host, From, Lang,
MaxRoomsDiscoItems, Node, RSM) of
{error, Err} ->
xmpp:make_error(IQ, Err);
{result, Result} ->
xmpp:make_iq_result(IQ, Result)
end;
process_disco_items(#iq{lang = Lang} = IQ) ->
Txt = ?T("No module is handling this query"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
-spec process_muc_unique(iq()) -> iq().
process_muc_unique(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_muc_unique(#iq{from = From, type = get,
sub_els = [#muc_unique{}]} = IQ) ->
Name = str:sha(term_to_binary([From, erlang:timestamp(),
p1_rand:get_string()])),
xmpp:make_iq_result(IQ, #muc_unique{name = Name}).
-spec process_mucsub(iq()) -> iq().
process_mucsub(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_mucsub(#iq{type = get, from = From, to = To, lang = Lang,
sub_els = [#muc_subscriptions{}]} = IQ) ->
Host = To#jid.lserver,
ServerHost = ejabberd_router:host_of_route(Host),
case get_subscribed_rooms(ServerHost, Host, From) of
{ok, Subs} ->
List = [#muc_subscription{jid = JID, events = Nodes}
|| {JID, Nodes} <- Subs],
xmpp:make_iq_result(IQ, #muc_subscriptions{list = List});
{error, _} ->
Txt = ?T("Database failure"),
xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
end;
process_mucsub(#iq{lang = Lang} = IQ) ->
Txt = ?T("No module is handling this query"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
-spec should_start_room(stanza()) -> start | load | false.
should_start_room(#presence{type = available}) ->
start;
should_start_room(#iq{type = T} = IQ) when T == get; T == set ->
case xmpp:has_subtag(IQ, #muc_subscribe{}) orelse
xmpp:has_subtag(IQ, #muc_owner{}) of
true ->
start;
_ ->
load
end;
should_start_room(#message{type = T, to = #jid{lresource = <<>>}})
when T == groupchat; T == normal->
load;
should_start_room(#message{type = T, to = #jid{lresource = Res}})
when Res /= <<>> andalso T /= groupchat andalso T /= error ->
load;
should_start_room(_) ->
false.
-spec check_create_room(binary(), binary(), binary(), jid()) -> boolean().
check_create_room(ServerHost, Host, Room, From) ->
AccessCreate = mod_muc_opt:access_create(ServerHost),
case acl:match_rule(ServerHost, AccessCreate, From) of
allow ->
case mod_muc_opt:max_room_id(ServerHost) of
Max when byte_size(Room) =< Max ->
Regexp = mod_muc_opt:regexp_room_id(ServerHost),
case re:run(Room, Regexp, [{capture, none}]) of
match ->
AccessAdmin = mod_muc_opt:access_admin(ServerHost),
case acl:match_rule(ServerHost, AccessAdmin, From) of
allow ->
true;
_ ->
ejabberd_hooks:run_fold(
check_create_room, ServerHost, true,
[ServerHost, Room, Host])
end;
_ ->
false
end;
_ ->
false
end;
_ ->
false
end.
-spec get_access(binary() | gen_mod:opts()) -> access().
get_access(ServerHost) ->
Access = mod_muc_opt:access(ServerHost),
AccessCreate = mod_muc_opt:access_create(ServerHost),
AccessAdmin = mod_muc_opt:access_admin(ServerHost),
AccessPersistent = mod_muc_opt:access_persistent(ServerHost),
AccessMam = mod_muc_opt:access_mam(ServerHost),
{Access, AccessCreate, AccessAdmin, AccessPersistent, AccessMam}.
-spec get_rooms(binary(), binary()) -> [#muc_room{}].
get_rooms(ServerHost, Host) ->
Mod = gen_mod:db_mod(ServerHost, ?MODULE),
Mod:get_rooms(ServerHost, Host).
-spec load_permanent_rooms([binary()], binary(), gen_mod:opts()) -> ok.
load_permanent_rooms(Hosts, ServerHost, Opts) ->
case mod_muc_opt:preload_rooms(Opts) of
true ->
Access = get_access(Opts),
HistorySize = mod_muc_opt:history_size(Opts),
QueueType = mod_muc_opt:queue_type(Opts),
RoomShaper = mod_muc_opt:room_shaper(Opts),
RMod = gen_mod:ram_db_mod(Opts, ?MODULE),
lists:foreach(
fun(Host) ->
?DEBUG("Loading rooms at ~ts", [Host]),
lists:foreach(
fun(R) ->
{Room, _} = R#muc_room.name_host,
case RMod:find_online_room(ServerHost, Room, Host) of
error ->
start_room(RMod, Host, ServerHost, Access,
Room, HistorySize, RoomShaper,
R#muc_room.opts, QueueType);
{ok, _} ->
ok
end
end, get_rooms(ServerHost, Host))
end, Hosts);
false ->
ok
end.
-spec load_room(module(), binary(), binary(), binary()) -> {ok, pid()} |
{error, notfound | term()}.
load_room(RMod, Host, ServerHost, Room) ->
case restore_room(ServerHost, Host, Room) of
error ->
{error, notfound};
Opts0 ->
case proplists:get_bool(persistent, Opts0) of
true ->
?DEBUG("Restore room: ~ts", [Room]),
start_room(RMod, Host, ServerHost, Room, Opts0);
_ ->
?DEBUG("Restore hibernated non-persistent room: ~ts", [Room]),
Res = start_room(RMod, Host, ServerHost, Room, Opts0),
Mod = gen_mod:db_mod(ServerHost, mod_muc),
case erlang:function_exported(Mod, get_subscribed_rooms, 3) of
true ->
ok;
_ ->
forget_room(ServerHost, Host, Room)
end,
Res
end
end.
start_new_room(RMod, Host, ServerHost, Room, Pass, From, Nick) ->
?DEBUG("Open new room: ~ts", [Room]),
DefRoomOpts = mod_muc_opt:default_room_options(ServerHost),
DefRoomOpts2 = add_password_options(Pass, DefRoomOpts),
start_room(RMod, Host, ServerHost, Room, DefRoomOpts2, From, Nick).
add_password_options(false, DefRoomOpts) ->
DefRoomOpts;
add_password_options(<<>>, DefRoomOpts) ->
DefRoomOpts;
add_password_options(Pass, DefRoomOpts) when is_binary(Pass) ->
O2 = lists:keystore(password, 1, DefRoomOpts, {password, Pass}),
lists:keystore(password_protected, 1, O2, {password_protected, true}).
start_room(Mod, Host, ServerHost, Room, DefOpts) ->
Access = get_access(ServerHost),
HistorySize = mod_muc_opt:history_size(ServerHost),
QueueType = mod_muc_opt:queue_type(ServerHost),
RoomShaper = mod_muc_opt:room_shaper(ServerHost),
start_room(Mod, Host, ServerHost, Access, Room, HistorySize,
RoomShaper, DefOpts, QueueType).
start_room(Mod, Host, ServerHost, Room, DefOpts, Creator, Nick) ->
Access = get_access(ServerHost),
HistorySize = mod_muc_opt:history_size(ServerHost),
QueueType = mod_muc_opt:queue_type(ServerHost),
RoomShaper = mod_muc_opt:room_shaper(ServerHost),
start_room(Mod, Host, ServerHost, Access, Room,
HistorySize, RoomShaper,
Creator, Nick, DefOpts, QueueType).
start_room(Mod, Host, ServerHost, Access, Room,
HistorySize, RoomShaper, DefOpts, QueueType) ->
case mod_muc_room:start(Host, ServerHost, Access, Room,
HistorySize, RoomShaper, DefOpts, QueueType) of
{ok, Pid} ->
Mod:register_online_room(ServerHost, Room, Host, Pid),
{ok, Pid};
Err ->
Err
end.
start_room(Mod, Host, ServerHost, Access, Room, HistorySize,
RoomShaper, Creator, Nick, DefOpts, QueueType) ->
case mod_muc_room:start(Host, ServerHost, Access, Room,
HistorySize, RoomShaper,
Creator, Nick, DefOpts, QueueType) of
{ok, Pid} ->
Mod:register_online_room(ServerHost, Room, Host, Pid),
{ok, Pid};
Err ->
Err
end.
-spec iq_disco_items(binary(), binary(), jid(), binary(), integer(), binary(),
rsm_set() | undefined) ->
{result, disco_items()} | {error, stanza_error()}.
iq_disco_items(ServerHost, Host, From, Lang, MaxRoomsDiscoItems, Node, RSM)
when Node == <<"">>; Node == <<"nonemptyrooms">>; Node == <<"emptyrooms">> ->
Count = count_online_rooms(ServerHost, Host),
Query = if Node == <<"">>, RSM == undefined, Count > MaxRoomsDiscoItems ->
{only_non_empty, From, Lang};
Node == <<"nonemptyrooms">> ->
{only_non_empty, From, Lang};
Node == <<"emptyrooms">> ->
{0, From, Lang};
true ->
{all, From, Lang}
end,
MaxItems = case RSM of
undefined ->
MaxRoomsDiscoItems;
#rsm_set{max = undefined} ->
MaxRoomsDiscoItems;
#rsm_set{max = Max} when Max > MaxRoomsDiscoItems ->
MaxRoomsDiscoItems;
#rsm_set{max = Max} ->
Max
end,
{Items, HitMax} = lists:foldr(
fun(_, {Acc, _}) when length(Acc) >= MaxItems ->
{Acc, true};
(R, {Acc, _}) ->
case get_room_disco_item(R, Query) of
{ok, Item} -> {[Item | Acc], false};
{error, _} -> {Acc, false}
end
end, {[], false}, get_online_rooms(ServerHost, Host, RSM)),
ResRSM = case Items of
[_|_] when RSM /= undefined; HitMax ->
#disco_item{jid = #jid{luser = First}} = hd(Items),
#disco_item{jid = #jid{luser = Last}} = lists:last(Items),
#rsm_set{first = #rsm_first{data = First},
last = Last,
count = Count};
[] when RSM /= undefined ->
#rsm_set{count = Count};
_ ->
undefined
end,
{result, #disco_items{node = Node, items = Items, rsm = ResRSM}};
iq_disco_items(_ServerHost, _Host, _From, Lang, _MaxRoomsDiscoItems, _Node, _RSM) ->
{error, xmpp:err_item_not_found(?T("Node not found"), Lang)}.
-spec get_room_disco_item({binary(), binary(), pid()},
{mod_muc_room:disco_item_filter(),
jid(), binary()}) -> {ok, disco_item()} |
{error, timeout | notfound}.
get_room_disco_item({Name, Host, Pid}, {Filter, JID, Lang}) ->
case mod_muc_room:get_disco_item(Pid, Filter, JID, Lang) of
{ok, Desc} ->
RoomJID = jid:make(Name, Host),
{ok, #disco_item{jid = RoomJID, name = Desc}};
{error, _} = Err ->
Err
end.
-spec get_subscribed_rooms(binary(), jid()) -> {ok, [{jid(), [binary()]}]} | {error, any()}.
get_subscribed_rooms(Host, User) ->
ServerHost = ejabberd_router:host_of_route(Host),
get_subscribed_rooms(ServerHost, Host, User).
-spec get_subscribed_rooms(binary(), binary(), jid()) ->
{ok, [{jid(), [binary()]}]} | {error, any()}.
get_subscribed_rooms(ServerHost, Host, From) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
BareFrom = jid:remove_resource(From),
case erlang:function_exported(Mod, get_subscribed_rooms, 3) of
false ->
Rooms = get_online_rooms(ServerHost, Host),
{ok, lists:flatmap(
fun({Name, _, Pid}) when Pid == self() ->
USR = jid:split(BareFrom),
case erlang:get(muc_subscribers) of
#{USR := #subscriber{nodes = Nodes}} ->
[{jid:make(Name, Host), Nodes}];
_ ->
[]
end;
({Name, _, Pid}) ->
case mod_muc_room:is_subscribed(Pid, BareFrom) of
{true, Nodes} ->
[{jid:make(Name, Host), Nodes}];
false -> []
end;
(_) ->
[]
end, Rooms)};
true ->
Mod:get_subscribed_rooms(LServer, Host, BareFrom)
end.
get_nick(ServerHost, Host, From) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:get_nick(LServer, Host, From).
iq_get_register_info(ServerHost, Host, From, Lang) ->
{Nick, Registered} = case get_nick(ServerHost, Host, From) of
error -> {<<"">>, false};
N -> {N, true}
end,
Title = <<(translate:translate(
Lang, ?T("Nickname Registration at ")))/binary, Host/binary>>,
Inst = translate:translate(Lang, ?T("Enter nickname you want to register")),
Fields = muc_register:encode([{roomnick, Nick}], Lang),
X = #xdata{type = form, title = Title,
instructions = [Inst], fields = Fields},
#register{nick = Nick,
registered = Registered,
instructions =
translate:translate(
Lang, ?T("You need a client that supports x:data "
"to register the nickname")),
xdata = X}.
set_nick(ServerHost, Host, From, Nick) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:set_nick(LServer, Host, From, Nick).
iq_set_register_info(ServerHost, Host, From, Nick,
Lang) ->
case set_nick(ServerHost, Host, From, Nick) of
{atomic, ok} -> {result, undefined};
{atomic, false} ->
ErrText = ?T("That nickname is registered by another person"),
{error, xmpp:err_conflict(ErrText, Lang)};
_ ->
Txt = ?T("Database failure"),
{error, xmpp:err_internal_server_error(Txt, Lang)}
end.
process_iq_register_set(ServerHost, Host, From,
#register{remove = true}, Lang) ->
iq_set_register_info(ServerHost, Host, From, <<"">>, Lang);
process_iq_register_set(_ServerHost, _Host, _From,
#register{xdata = #xdata{type = cancel}}, _Lang) ->
{result, undefined};
process_iq_register_set(ServerHost, Host, From,
#register{nick = Nick, xdata = XData}, Lang) ->
case XData of
#xdata{type = submit, fields = Fs} ->
try
Options = muc_register:decode(Fs),
N = proplists:get_value(roomnick, Options),
iq_set_register_info(ServerHost, Host, From, N, Lang)
catch _:{muc_register, Why} ->
ErrText = muc_register:format_error(Why),
{error, xmpp:err_bad_request(ErrText, Lang)}
end;
#xdata{} ->
Txt = ?T("Incorrect data form"),
{error, xmpp:err_bad_request(Txt, Lang)};
_ when is_binary(Nick), Nick /= <<"">> ->
iq_set_register_info(ServerHost, Host, From, Nick, Lang);
_ ->
ErrText = ?T("You must fill in field \"Nickname\" in the form"),
{error, xmpp:err_not_acceptable(ErrText, Lang)}
end.
-spec broadcast_service_message(binary(), binary(), binary()) -> ok.
broadcast_service_message(ServerHost, Host, Msg) ->
lists:foreach(
fun({_, _, Pid}) ->
mod_muc_room:service_message(Pid, Msg)
end, get_online_rooms(ServerHost, Host)).
-spec get_online_rooms(binary(), binary()) -> [{binary(), binary(), pid()}].
get_online_rooms(ServerHost, Host) ->
get_online_rooms(ServerHost, Host, undefined).
-spec get_online_rooms(binary(), binary(), undefined | rsm_set()) ->
[{binary(), binary(), pid()}].
get_online_rooms(ServerHost, Host, RSM) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:get_online_rooms(ServerHost, Host, RSM).
-spec count_online_rooms(binary(), binary()) -> non_neg_integer().
count_online_rooms(ServerHost, Host) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:count_online_rooms(ServerHost, Host).
opts_to_binary(Opts) ->
lists:map(
fun({title, Title}) ->
{title, iolist_to_binary(Title)};
({description, Desc}) ->
{description, iolist_to_binary(Desc)};
({password, Pass}) ->
{password, iolist_to_binary(Pass)};
({subject, [C|_] = Subj}) when is_integer(C), C >= 0, C =< 255 ->
{subject, iolist_to_binary(Subj)};
({subject_author, Author}) ->
{subject_author, iolist_to_binary(Author)};
({affiliations, Affs}) ->
{affiliations, lists:map(
fun({{U, S, R}, Aff}) ->
NewAff =
case Aff of
{A, Reason} ->
{A, iolist_to_binary(Reason)};
_ ->
Aff
end,
{{iolist_to_binary(U),
iolist_to_binary(S),
iolist_to_binary(R)},
NewAff}
end, Affs)};
({captcha_whitelist, CWList}) ->
{captcha_whitelist, lists:map(
fun({U, S, R}) ->
{iolist_to_binary(U),
iolist_to_binary(S),
iolist_to_binary(R)}
end, CWList)};
(Opt) ->
Opt
end, Opts).
export(LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:export(LServer).
import_info() ->
[{<<"muc_room">>, 4}, {<<"muc_registered">>, 4}].
import_start(LServer, DBType) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:init(LServer, []).
import(LServer, {sql, _}, DBType, Tab, L) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:import(LServer, Tab, L).
mod_opt_type(access) ->
econf:acl();
mod_opt_type(access_admin) ->
econf:acl();
mod_opt_type(access_create) ->
econf:acl();
mod_opt_type(access_persistent) ->
econf:acl();
mod_opt_type(access_mam) ->
econf:acl();
mod_opt_type(access_register) ->
econf:acl();
mod_opt_type(history_size) ->
econf:non_neg_int();
mod_opt_type(name) ->
econf:binary();
mod_opt_type(max_room_desc) ->
econf:pos_int(infinity);
mod_opt_type(max_room_id) ->
econf:pos_int(infinity);
mod_opt_type(max_rooms_discoitems) ->
econf:non_neg_int();
mod_opt_type(regexp_room_id) ->
econf:re([unicode]);
mod_opt_type(max_room_name) ->
econf:pos_int(infinity);
mod_opt_type(max_user_conferences) ->
econf:pos_int();
mod_opt_type(max_users) ->
econf:pos_int();
mod_opt_type(max_users_admin_threshold) ->
econf:pos_int();
mod_opt_type(max_users_presence) ->
econf:int();
mod_opt_type(min_message_interval) ->
econf:number(0);
mod_opt_type(min_presence_interval) ->
econf:number(0);
mod_opt_type(preload_rooms) ->
econf:bool();
mod_opt_type(room_shaper) ->
econf:atom();
mod_opt_type(user_message_shaper) ->
econf:atom();
mod_opt_type(user_presence_shaper) ->
econf:atom();
mod_opt_type(default_room_options) ->
econf:options(
#{allow_change_subj => econf:bool(),
allow_private_messages => econf:bool(),
allow_private_messages_from_visitors =>
econf:enum([anyone, moderators, nobody]),
allow_query_users => econf:bool(),
allow_subscription => econf:bool(),
allow_user_invites => econf:bool(),
allow_visitor_nickchange => econf:bool(),
allow_visitor_status => econf:bool(),
anonymous => econf:bool(),
captcha_protected => econf:bool(),
lang => econf:lang(),
logging => econf:bool(),
mam => econf:bool(),
max_users => econf:pos_int(),
members_by_default => econf:bool(),
members_only => econf:bool(),
moderated => econf:bool(),
password => econf:binary(),
password_protected => econf:bool(),
persistent => econf:bool(),
presence_broadcast =>
econf:list(
econf:enum([moderator, participant, visitor])),
public => econf:bool(),
public_list => econf:bool(),
title => econf:binary()});
mod_opt_type(db_type) ->
econf:db_type(?MODULE);
mod_opt_type(ram_db_type) ->
econf:db_type(?MODULE);
mod_opt_type(host) ->
econf:host();
mod_opt_type(hosts) ->
econf:hosts();
mod_opt_type(queue_type) ->
econf:queue_type();
mod_opt_type(hibernation_timeout) ->
econf:timeout(second, infinity);
mod_opt_type(vcard) ->
econf:vcard_temp().
mod_options(Host) ->
[{access, all},
{access_admin, none},
{access_create, all},
{access_persistent, all},
{access_mam, all},
{access_register, all},
{db_type, ejabberd_config:default_db(Host, ?MODULE)},
{ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)},
{history_size, 20},
{host, <<"conference.", Host/binary>>},
{hosts, []},
{name, ?T("Chatrooms")},
{max_room_desc, infinity},
{max_room_id, infinity},
{max_room_name, infinity},
{max_rooms_discoitems, 100},
{max_user_conferences, 100},
{max_users, 200},
{max_users_admin_threshold, 5},
{max_users_presence, 1000},
{min_message_interval, 0},
{min_presence_interval, 0},
{queue_type, ejabberd_option:queue_type(Host)},
{regexp_room_id, <<"">>},
{room_shaper, none},
{user_message_shaper, none},
{user_presence_shaper, none},
{preload_rooms, true},
{hibernation_timeout, infinity},
{vcard, undefined},
{default_room_options,
[{allow_change_subj,true},
{allow_private_messages,true},
{allow_query_users,true},
{allow_user_invites,false},
{allow_visitor_nickchange,true},
{allow_visitor_status,true},
{anonymous,true},
{captcha_protected,false},
{lang,<<>>},
{logging,false},
{members_by_default,true},
{members_only,false},
{moderated,true},
{password_protected,false},
{persistent,false},
{public,true},
{public_list,true},
{mam,false},
{allow_subscription,false},
{password,<<>>},
{title,<<>>},
{allow_private_messages_from_visitors,anyone},
{max_users,200},
{presence_broadcast,[moderator,participant,visitor]}]}].
mod_doc() ->
#{desc =>
[?T("This module provides support for https://xmpp.org/extensions/xep-0045.html"
"[XEP-0045: Multi-User Chat]. Users can discover existing rooms, "
"join or create them. Occupants of a room can chat in public or have private chats."), "",
?T("The MUC service allows any Jabber ID to register a nickname, so "
"nobody else can use that nickname in any room in the MUC "
"service. To register a nickname, open the Service Discovery in "
"your XMPP client and register in the MUC service."), "",
?T("This module supports clustering and load balancing. One module "
"can be started per cluster node. Rooms are distributed at "
"creation time on all available MUC module instances. The "
"multi-user chat module is clustered but the rooms themselves "
"are not clustered nor fault-tolerant: if the node managing a "
"set of rooms goes down, the rooms disappear and they will be "
"recreated on an available node on first connection attempt.")],
opts =>
[{access,
#{value => ?T("AccessName"),
desc =>
?T("You can specify who is allowed to use the Multi-User Chat service. "
"By default everyone is allowed to use it.")}},
{access_admin,
#{value => ?T("AccessName"),
desc =>
?T("This option specifies who is allowed to administrate "
"the Multi-User Chat service. The default value is 'none', "
"which means that only the room creator can administer "
"their room. The administrators can send a normal message "
"to the service JID, and it will be shown in all active "
"rooms as a service message. The administrators can send a "
"groupchat message to the JID of an active room, and the "
"message will be shown in the room as a service message.")}},
{access_create,
#{value => ?T("AccessName"),
desc =>
?T("To configure who is allowed to create new rooms at the "
"Multi-User Chat service, this option can be used. "
"By default any account in the local ejabberd server is "
"allowed to create rooms.")}},
{access_persistent,
#{value => ?T("AccessName"),
desc =>
?T("To configure who is allowed to modify the 'persistent' room option. "
"By default any account in the local ejabberd server is allowed to "
"modify that option.")}},
{access_mam,
#{value => ?T("AccessName"),
desc =>
?T("To configure who is allowed to modify the 'mam' room option. "
"By default any account in the local ejabberd server is allowed to "
"modify that option.")}},
{access_register,
#{value => ?T("AccessName"),
desc =>
?T("This option specifies who is allowed to register nickname "
"within the Multi-User Chat service. The default is 'all' for "
"backward compatibility, which means that any user is allowed "
"to register any free nick.")}},
{db_type,
#{value => "mnesia | sql",
desc =>
?T("Define the type of persistent storage where the module will "
"store room information. The default is the storage defined "
"by the global option 'default_db', or 'mnesia' if omitted.")}},
{ram_db_type,
#{value => "mnesia",
desc =>
?T("Define the type of volatile (in-memory) storage where the module "
"will store room information. The only available value for this "
"module is 'mnesia'.")}},
{hibernation_timeout,
#{value => "infinity | Seconds",
desc =>
?T("Timeout before hibernating the room process, expressed "
"in seconds. The default value is 'infinity'.")}},
{history_size,
#{value => ?T("Size"),
desc =>
?T("A small history of the current discussion is sent to users "
"when they enter the room. With this option you can define the "
"number of history messages to keep and send to users joining the room. "
"The value is a non-negative integer. Setting the value to 0 disables "
"the history feature and, as a result, nothing is kept in memory. "
"The default value is 20. This value affects all rooms on the service. "
"NOTE: modern XMPP clients rely on Message Archives (XEP-0313), so feel "
"free to disable the history feature if you're only using modern clients "
"and have 'mod_mam' module loaded.")}},
{host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}},
{hosts,
#{value => ?T("[Host, ...]"),
desc =>
?T("This option defines the Jabber IDs of the service. "
"If the 'hosts' option is not specified, the only Jabber ID will "
"be the hostname of the virtual host with the prefix \"conference.\". "
"The keyword '@HOST@' is replaced with the real virtual host name.")}},
{name,
#{value => "string()",
desc =>
?T("The value of the service name. This name is only visible in some "
"clients that support https://xmpp.org/extensions/xep-0030.html"
"[XEP-0030: Service Discovery]. The default is 'Chatrooms'.")}},
{max_room_desc,
#{value => ?T("Number"),
desc =>
?T("This option defines the maximum number of characters that "
"Room Description can have when configuring the room. "
"The default value is 'infinity'.")}},
{max_room_id,
#{value => ?T("Number"),
desc =>
?T("This option defines the maximum number of characters that "
"Room ID can have when creating a new room. "
"The default value is 'infinity'.")}},
{max_room_name,
#{value => ?T("Number"),
desc =>
?T("This option defines the maximum number of characters "
"that Room Name can have when configuring the room. "
"The default value is 'infinity'.")}},
{max_rooms_discoitems,
#{value => ?T("Number"),
desc =>
?T("When there are more rooms than this 'Number', "
"only the non-empty ones are returned in a Service Discovery query. "
"The default value is '100'.")}},
{max_user_conferences,
#{value => ?T("Number"),
desc =>
?T("This option defines the maximum number of rooms that any "
"given user can join. The default value is '100'. This option "
"is used to prevent possible abuses. Note that this is a soft "
"limit: some users can sometimes join more conferences in "
"cluster configurations.")}},
{max_users,
#{value => ?T("Number"),
desc =>
?T("This option defines at the service level, the maximum "
"number of users allowed per room. It can be lowered in "
"each room configuration but cannot be increased in "
"individual room configuration. The default value is '200'.")}},
{max_users_admin_threshold,
#{value => ?T("Number"),
desc =>
?T("This option defines the number of service admins or room "
"owners allowed to enter the room when the maximum number "
"of allowed occupants was reached. The default limit is '5'.")}},
{max_users_presence,
#{value => ?T("Number"),
desc =>
?T("This option defines after how many users in the room, "
"it is considered overcrowded. When a MUC room is considered "
"overcrowed, presence broadcasts are limited to reduce load, "
"traffic and excessive presence \"storm\" received by participants.")}},
{min_message_interval,
#{value => ?T("Number"),
desc =>
?T("This option defines the minimum interval between two "
"messages send by an occupant in seconds. This option "
"is global and valid for all rooms. A decimal value can be used. "
"When this option is not defined, message rate is not limited. "
"This feature can be used to protect a MUC service from occupant "
"abuses and limit number of messages that will be broadcasted by "
"the service. A good value for this minimum message interval is 0.4 second. "
"If an occupant tries to send messages faster, an error is send back "
"explaining that the message has been discarded and describing the "
"reason why the message is not acceptable.")}},
{min_presence_interval,
#{value => ?T("Number"),
desc =>
?T("This option defines the minimum of time between presence "
"changes coming from a given occupant in seconds. "
"This option is global and valid for all rooms. A decimal "
"value can be used. When this option is not defined, no "
"restriction is applied. This option can be used to protect "
"a MUC service for occupants abuses. If an occupant tries "
"to change its presence more often than the specified interval, "
"the presence is cached by ejabberd and only the last presence "
"is broadcasted to all occupants in the room after expiration "
"of the interval delay. Intermediate presence packets are "
"silently discarded. A good value for this option is 4 seconds.")}},
{queue_type,
#{value => "ram | file",
desc =>
?T("Same as top-level 'queue_type' option, but applied to this module only.")}},
{regexp_room_id,
#{value => "string()",
desc =>
?T("This option defines the regular expression that a Room ID "
"must satisfy to allow the room creation. The default value "
"is the empty string.")}},
{preload_rooms,
#{value => "true | false",
desc =>
?T("Whether to load all persistent rooms in memory on startup. "
"If disabled, the room is only loaded on first participant join. "
"The default is 'true'. It makes sense to disable room preloading "
"when the number of rooms is high: this will improve server startup "
"time and memory consumption.")}},
{room_shaper,
#{value => "none | ShaperName",
desc =>
?T("This option defines shaper for the MUC rooms. "
"The default value is 'none'.")}},
{user_message_shaper,
#{value => "none | ShaperName",
desc =>
?T("This option defines shaper for the users messages. "
"The default value is 'none'.")}},
{user_presence_shaper,
#{value => "none | ShaperName",
desc =>
?T("This option defines shaper for the users presences. "
"The default value is 'none'.")}},
{vcard,
#{value => ?T("vCard"),
desc =>
?T("A custom vCard of the service that will be displayed "
"by some XMPP clients in Service Discovery. The value of "
"'vCard' is a YAML map constructed from an XML representation "
"of vCard. Since the representation has no attributes, "
"the mapping is straightforward."),
example =>
[{?T("For example, the following XML representation of vCard:"),
["<vCard xmlns='vcard-temp'>",
" <FN>Conferences</FN>",
" <ADR>",
" <WORK/>",
" <STREET>Elm Street</STREET>",
" </ADR>",
"</vCard>"]},
{?T("will be translated to:"),
["vcard:",
" fn: Conferences",
" adr:",
" -",
" work: true",
" street: Elm Street"]}]}},
{default_room_options,
#{value => ?T("Options"),
desc =>
?T("This option allows to define the desired "
"default room options. Note that the creator of a room "
"can modify the options of his room at any time using an "
"XMPP client with MUC capability. The 'Options' are:")},
[{allow_change_subj,
#{value => "true | false",
desc =>
?T("Allow occupants to change the subject. "
"The default value is 'true'.")}},
{allow_private_messages,
#{value => "true | false",
desc =>
?T("Occupants can send private messages to other occupants. "
"The default value is 'true'.")}},
{allow_query_users,
#{value => "true | false",
desc =>
?T("Occupants can send IQ queries to other occupants. "
"The default value is 'true'.")}},
{allow_user_invites,
#{value => "true | false",
desc =>
?T("Allow occupants to send invitations. "
"The default value is 'false'.")}},
{allow_visitor_nickchange,
#{value => "true | false",
desc => ?T("Allow visitors to change nickname. "
"The default value is 'true'.")}},
{allow_visitor_status,
#{value => "true | false",
desc =>
?T("Allow visitors to send status text in presence updates. "
"If disallowed, the status text is stripped before broadcasting "
"the presence update to all the room occupants. "
"The default value is 'true'.")}},
{anonymous,
#{value => "true | false",
desc =>
?T("The room is anonymous: occupants don't see the real "
"JIDs of other occupants. Note that the room moderators "
"can always see the real JIDs of the occupants. "
"The default value is 'true'.")}},
{captcha_protected,
#{value => "true | false",
desc =>
?T("When a user tries to join a room where they have no "
"affiliation (not owner, admin or member), the room "
"requires them to fill a CAPTCHA challenge (see section "
"https://docs.ejabberd.im/admin/configuration/#captcha[CAPTCHA] "
"in order to accept their join in the room. "
"The default value is 'false'.")}},
{lang,
#{value => ?T("Language"),
desc =>
?T("Preferred language for the discussions in the room. "
"The language format should conform to RFC 5646. "
"There is no value by default.")}},
{logging,
#{value => "true | false",
desc =>
?T("The public messages are logged using 'mod_muc_log'. "
"The default value is 'false'.")}},
{members_by_default,
#{value => "true | false",
desc =>
?T("The occupants that enter the room are participants "
"by default, so they have \"voice\". "
"The default value is 'true'.")}},
{members_only,
#{value => "true | false",
desc =>
?T("Only members of the room can enter. "
"The default value is 'false'.")}},
{moderated,
#{value => "true | false",
desc =>
?T("Only occupants with \"voice\" can send public messages. "
"The default value is 'true'.")}},
{password_protected,
#{value => "true | false",
desc =>
?T("The password is required to enter the room. "
"The default value is 'false'.")}},
{password,
#{value => ?T("Password"),
desc =>
?T("Password of the room. Implies option 'password_protected' "
"set to 'true'. There is no default value.")}},
{persistent,
#{value => "true | false",
desc =>
?T("The room persists even if the last participant leaves. "
"The default value is 'false'.")}},
{public,
#{value => "true | false",
desc =>
?T("The room is public in the list of the MUC service, "
"so it can be discovered. MUC admins and room participants "
"will see private rooms in Service Discovery if their XMPP "
"client supports this feature. "
"The default value is 'true'.")}},
{public_list,
#{value => "true | false",
desc =>
?T("The list of participants is public, without requiring "
"to enter the room. The default value is 'true'.")}},
{mam,
#{value => "true | false",
desc =>
?T("Enable message archiving. Implies mod_mam is enabled. "
"The default value is 'false'.")}},
{allow_subscription,
#{value => "true | false",
desc =>
?T("Allow users to subscribe to room events as described in "
"https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/"
"[Multi-User Chat Subscriptions]. "
"The default value is 'false'.")}},
{title,
#{value => ?T("Room Title"),
desc =>
?T("A human-readable title of the room. "
"There is no default value")}},
{allow_private_messages_from_visitors,
#{value => "anyone | moderators | nobody",
desc =>
?T("Visitors can send private messages to other occupants. "
"The default value is 'anyone' which means visitors "
"can send private messages to any occupant.")}},
{max_users,
#{value => ?T("Number"),
desc =>
?T("Maximum number of occupants in the room. "
"The default value is '200'.")}},
{presence_broadcast,
#{value => "[moderator | participant | visitor, ...]",
desc =>
?T("List of roles for which presence is broadcasted. "
"The list can contain one or several of: 'moderator', "
"'participant', 'visitor'. The default value is shown "
"in the example below:"),
example =>
["presence_broadcast:",
" - moderator",
" - participant",
" - visitor"]}}]}]}.