Packages
ejabberd
24.7.0
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/mod_muc.erl
%%%----------------------------------------------------------------------
%%% 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-2024 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'}).
-protocol({xep, 249, '1.2'}).
-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_iq_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).
maybe_store_new_room(ServerHost, Host, Name, Opts) ->
case {proplists:get_bool(persistent, Opts), proplists:get_value(subscribers, Opts, [])} of
{false, []} ->
{atomic, ok};
{_, Subs} ->
Changes = [{add_subscription, JID, Nick, Nodes} || {JID, Nick, Nodes} <- Subs],
store_room(ServerHost, Host, Name, Opts, Changes)
end.
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, _} ->
maybe_store_new_room(ServerHost, Host, Room, NewOpts),
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, _} ->
maybe_store_new_room(ServerHost, Host, Room, NewOpts),
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) ->
case process_iq_register(IQ) of
{result, Result} ->
xmpp:make_iq_result(IQ, Result);
{error, Err} ->
xmpp:make_error(IQ, Err)
end.
-spec process_iq_register(iq()) -> {result, register()} | {error, stanza_error()}.
process_iq_register(#iq{type = Type, from = From, to = To, lang = Lang,
sub_els = [El = #register{}]}) ->
Host = To#jid.lserver,
RegisterDestination = jid:encode(To),
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 ->
{result, iq_get_register_info(ServerHost, RegisterDestination, From, Lang)};
set ->
process_iq_register_set(ServerHost, RegisterDestination, From, El, Lang)
end;
deny ->
ErrText = ?T("Access denied by service policy"),
Err = xmpp:err_forbidden(ErrText, Lang),
{error, 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,
OccupantIdFeatures = case gen_mod:is_loaded(ServerHost, mod_muc_occupantid) of
true -> [?NS_OCCUPANT_ID];
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 ++ OccupantIdFeatures],
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, {AuthorNick, AuthorJID}}) ->
{subject_author, {iolist_to_binary(AuthorNick), AuthorJID}};
({subject_author, AuthorNick}) -> % ejabberd 23.04 or older
{subject_author, {iolist_to_binary(AuthorNick), #jid{}}};
({allow_private_messages, Value}) -> % ejabberd 23.04 or older
Value2 = case Value of
true -> anyone;
false -> none;
_ -> Value
end,
{allowpm, Value2};
({AffOrRole, Affs}) when (AffOrRole == affiliation) or (AffOrRole == role) ->
{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(),
allowpm =>
econf:enum([anyone, participants, moderators, none]),
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},
{allowpm,anyone},
{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("It is also possible to register a nickname in a room, so "
"nobody else can use that nickname in that room. If a nick is "
"registered in the MUC service, that nick cannot be registered in "
"any room, and vice versa: a nick that is registered in a room "
"cannot be registered at 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"),
note => "improved in 23.10",
desc =>
?T("This option specifies who is allowed to register nickname "
"within the Multi-User Chat service and rooms. The default is 'all' for "
"backward compatibility, which means that any user is allowed "
"to register any free nick in the MUC service and in the rooms.")}},
{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 "
"overcrowded, 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 =>
["# This XML representation of vCard:",
"# <vCard xmlns='vcard-temp'>",
"# <FN>Conferences</FN>",
"# <ADR>",
"# <WORK/>",
"# <STREET>Elm Street</STREET>",
"# </ADR>",
"# </vCard>",
"# ",
"# is 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("Define the "
"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'.")}},
{allowpm,
#{value => "anyone | participants | moderators | none",
desc =>
?T("Who can send private messages. "
"The default value is 'anyone'.")}},
{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 "
"_`basic.md#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 "
"_`../../developer/xmpp-clients-bots/extensions/muc-sub.md|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"]}}]}]}.