mirror of
https://github.com/processone/ejabberd.git
synced 2024-12-22 17:28:25 +01:00
1821 lines
73 KiB
Erlang
1821 lines
73 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-2022 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,
|
|
store_changes/4,
|
|
restore_room/3,
|
|
forget_room/3,
|
|
create_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,
|
|
remove_user/2,
|
|
procname/2,
|
|
route/1, unhibernate_room/3]).
|
|
|
|
-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_lib("xmpp/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 store_changes(binary(), binary(), binary(), list()) -> {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 find_online_room_by_pid(binary(), pid()) -> {ok, binary(), binary()} | 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(), [binary()]}]} | {error, db_failure}.
|
|
|
|
-optional_callbacks([get_subscribed_rooms/3,
|
|
store_changes/4]).
|
|
|
|
%%====================================================================
|
|
%% API
|
|
%%====================================================================
|
|
start(Host, Opts) ->
|
|
case mod_muc_sup:start(Host) of
|
|
{ok, _} ->
|
|
ejabberd_hooks:add(remove_user, Host, ?MODULE,
|
|
remove_user, 50),
|
|
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) ->
|
|
ejabberd_hooks:delete(remove_user, Host, ?MODULE,
|
|
remove_user, 50),
|
|
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, misc: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 = misc: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}).
|
|
|
|
%% @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, Opts) ->
|
|
ServerHost = ejabberd_router:host_of_route(Host),
|
|
Proc = procname(ServerHost, {Name, Host}),
|
|
?GEN_SERVER:call(Proc, {create, Name, Host, 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).
|
|
|
|
store_changes(ServerHost, Host, Name, ChangesHints) ->
|
|
LServer = jid:nameprep(ServerHost),
|
|
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
|
Mod:store_changes(LServer, Host, Name, 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({unhibernate, Room, Host, ResetHibernationTime}, _From,
|
|
#{server_host := ServerHost} = State) ->
|
|
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
|
|
{reply, load_room(RMod, Host, ServerHost, Room, ResetHibernationTime), State};
|
|
handle_call({create, Room, Host, 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) of
|
|
{ok, _} ->
|
|
ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]),
|
|
{reply, ok, State};
|
|
Err ->
|
|
{reply, Err, State}
|
|
end;
|
|
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({'DOWN', _Ref, process, Pid, _Reason},
|
|
#{server_host := ServerHost} = State) ->
|
|
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
|
|
case RMod:find_online_room_by_pid(ServerHost, Pid) of
|
|
{ok, Room, Host} ->
|
|
handle_cast({room_destroyed, {Room, Host}, Pid}, State);
|
|
_ ->
|
|
{noreply, State}
|
|
end;
|
|
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 unhibernate_room(binary(), binary(), binary()) -> {ok, pid()} | error.
|
|
unhibernate_room(ServerHost, Host, Room) ->
|
|
unhibernate_room(ServerHost, Host, Room, true).
|
|
|
|
-spec unhibernate_room(binary(), binary(), binary(), boolean()) -> {ok, pid()} | error.
|
|
unhibernate_room(ServerHost, Host, Room, ResetHibernationTime) ->
|
|
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
|
|
case RMod:find_online_room(ServerHost, Room, Host) of
|
|
error ->
|
|
Proc = procname(ServerHost, {Room, Host}),
|
|
case ?GEN_SERVER:call(Proc, {unhibernate, Room, Host, ResetHibernationTime}, 20000) of
|
|
{ok, _} = R -> R;
|
|
_ -> error
|
|
end;
|
|
{ok, _} = R2 -> R2
|
|
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, true) 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, nick = Nick, events = Nodes}
|
|
|| {JID, Nick, 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 ->
|
|
lists:foreach(
|
|
fun(Host) ->
|
|
?DEBUG("Loading rooms at ~ts", [Host]),
|
|
lists:foreach(
|
|
fun(R) ->
|
|
{Room, _} = R#muc_room.name_host,
|
|
unhibernate_room(ServerHost, Host, Room, false)
|
|
end, get_rooms(ServerHost, Host))
|
|
end, Hosts);
|
|
false ->
|
|
ok
|
|
end.
|
|
|
|
-spec load_room(module(), binary(), binary(), binary(), boolean()) ->
|
|
{ok, pid()} | {error, notfound | term()}.
|
|
load_room(RMod, Host, ServerHost, Room, ResetHibernationTime) ->
|
|
case restore_room(ServerHost, Host, Room) of
|
|
error ->
|
|
{error, notfound};
|
|
Opts0 ->
|
|
Mod = gen_mod:db_mod(ServerHost, mod_muc),
|
|
case proplists:get_bool(persistent, Opts0) of
|
|
true ->
|
|
?DEBUG("Restore room: ~ts", [Room]),
|
|
Res2 = start_room(RMod, Host, ServerHost, Room, Opts0),
|
|
case {Res2, ResetHibernationTime} of
|
|
{{ok, _}, true} ->
|
|
NewOpts = lists:keyreplace(hibernation_time, 1, Opts0, {hibernation_time, undefined}),
|
|
store_room(ServerHost, Host, Room, NewOpts, []);
|
|
_ ->
|
|
ok
|
|
end,
|
|
Res2;
|
|
_ ->
|
|
?DEBUG("Restore hibernated non-persistent room: ~ts", [Room]),
|
|
Res = start_room(RMod, Host, ServerHost, Room, Opts0),
|
|
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} ->
|
|
erlang:monitor(process, 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} ->
|
|
erlang:monitor(process, 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(), [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(), [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, nick = Nick}} ->
|
|
[{jid:make(Name, Host), Nick, Nodes}];
|
|
_ ->
|
|
[]
|
|
end;
|
|
({Name, _, Pid}) ->
|
|
case mod_muc_room:is_subscribed(Pid, BareFrom) of
|
|
{true, Nick, Nodes} ->
|
|
[{jid:make(Name, Host), Nick, 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).
|
|
|
|
-spec remove_user(binary(), binary()) -> ok.
|
|
remove_user(User, Server) ->
|
|
LUser = jid:nodeprep(User),
|
|
LServer = jid:nameprep(Server),
|
|
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
|
case erlang:function_exported(Mod, remove_user, 2) of
|
|
true ->
|
|
Mod:remove_user(LUser, LServer);
|
|
false ->
|
|
ok
|
|
end,
|
|
JID = jid:make(User, Server),
|
|
lists:foreach(
|
|
fun(Host) ->
|
|
lists:foreach(
|
|
fun({_, _, Pid}) ->
|
|
mod_muc_room:change_item_async(
|
|
Pid, JID, affiliation, none, <<"User removed">>),
|
|
mod_muc_room:change_item_async(
|
|
Pid, JID, role, none, <<"User removed">>)
|
|
end,
|
|
get_online_rooms(LServer, Host))
|
|
end,
|
|
gen_mod:get_module_opt_hosts(LServer, mod_muc)),
|
|
ok.
|
|
|
|
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_password) ->
|
|
econf:pos_int(infinity);
|
|
mod_opt_type(max_captcha_whitelist) ->
|
|
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(cleanup_affiliations_on_start) ->
|
|
econf:bool();
|
|
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(),
|
|
allow_voice_requests => econf:bool(),
|
|
anonymous => econf:bool(),
|
|
captcha_protected => econf:bool(),
|
|
description => econf:binary(),
|
|
enable_hats => 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(),
|
|
pubsub => econf:binary(),
|
|
title => econf:binary(),
|
|
vcard => econf:vcard_temp(),
|
|
vcard_xupdate => econf:binary(),
|
|
voice_request_min_interval => econf:pos_int()});
|
|
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_password, infinity},
|
|
{max_captcha_whitelist, 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},
|
|
{cleanup_affiliations_on_start, false},
|
|
{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. "
|
|
"The default value is 'all', which means everyone is "
|
|
"allowed to create rooms.")}},
|
|
{access_persistent,
|
|
#{value => ?T("AccessName"),
|
|
desc =>
|
|
?T("To configure who is allowed to modify the 'persistent' room option. "
|
|
"The default value is 'all', which means everyone is allowed to "
|
|
"modify that option.")}},
|
|
{access_mam,
|
|
#{value => ?T("AccessName"),
|
|
desc =>
|
|
?T("To configure who is allowed to modify the 'mam' room option. "
|
|
"The default value is 'all', which means everyone 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("Same as top-level _`default_db`_ option, "
|
|
"but applied to this module only.")}},
|
|
{ram_db_type,
|
|
#{value => "mnesia | sql",
|
|
desc =>
|
|
?T("Same as top-level _`default_ram_db`_ option, "
|
|
"but applied to this module only.")}},
|
|
{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_password,
|
|
#{value => ?T("Number"),
|
|
note => "added in 21.01",
|
|
desc =>
|
|
?T("This option defines the maximum number of characters "
|
|
"that Password can have when configuring the room. "
|
|
"The default value is 'infinity'.")}},
|
|
{max_captcha_whitelist,
|
|
#{value => ?T("Number"),
|
|
note => "added in 21.01",
|
|
desc =>
|
|
?T("This option defines the maximum number of characters "
|
|
"that Captcha Whitelist 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. "
|
|
"The default value is '1000'.")}},
|
|
{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"]}]}},
|
|
{cleanup_affiliations_on_start,
|
|
#{value => "true | false",
|
|
note => "added in 22.05",
|
|
desc =>
|
|
?T("Remove affiliations for non-existing local users on startup. "
|
|
"The default value is 'false'.")}},
|
|
{default_room_options,
|
|
#{value => ?T("Options"),
|
|
note => "improved in 22.05",
|
|
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'.")}},
|
|
{allow_voice_requests,
|
|
#{value => "true | false",
|
|
desc =>
|
|
?T("Allow visitors in a moderated room to request voice. "
|
|
"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'.")}},
|
|
{description,
|
|
#{value => ?T("Room Description"),
|
|
desc =>
|
|
?T("Short description of the room. "
|
|
"The default value is an empty string.")}},
|
|
{enable_hats,
|
|
#{value => "true | false",
|
|
desc =>
|
|
?T("Allow extended roles as defined in XEP-0317 Hats. "
|
|
"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'.")}},
|
|
{pubsub,
|
|
#{value => ?T("PubSub Node"),
|
|
desc =>
|
|
?T("XMPP URI of associated Publish/Subscribe node. "
|
|
"The default value is an empty string.")}},
|
|
{vcard,
|
|
#{value => ?T("vCard"),
|
|
desc =>
|
|
?T("A custom vCard for the room. See the equivalent mod_muc option."
|
|
"The default value is an empty string.")}},
|
|
{voice_request_min_interval,
|
|
#{value => ?T("Number"),
|
|
desc =>
|
|
?T("Minimum interval between voice requests, in seconds. "
|
|
"The default value is '1800'.")}},
|
|
{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"]}}]}]}.
|