25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-12-22 17:28:25 +01:00
xmpp.chapril.org-ejabberd/src/mod_muc.erl
2019-06-24 20:32:34 +03:00

1081 lines
39 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-2019 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'}).
-behaviour(gen_server).
-behaviour(gen_mod).
%% API
-export([start/2,
stop/1,
reload/3,
room_destroyed/4,
store_room/4,
store_room/5,
restore_room/3,
forget_room/3,
create_room/5,
shutdown_rooms/1,
process_disco_info/1,
process_disco_items/1,
process_vcard/1,
process_register/1,
process_muc_unique/1,
process_mucsub/1,
broadcast_service_message/3,
export/1,
import_info/0,
import/5,
import_start/2,
opts_to_binary/1,
find_online_room/2,
register_online_room/3,
get_online_rooms/1,
count_online_rooms/1,
register_online_user/4,
unregister_online_user/4,
iq_set_register_info/5,
count_online_rooms_by_user/3,
get_online_rooms_by_user/3,
can_use_nick/4,
get_subscribed_rooms/2]).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3,
mod_opt_type/1, mod_options/1, depends/2]).
-include("logger.hrl").
-include("xmpp.hrl").
-include("mod_muc.hrl").
-include("translate.hrl").
-record(state,
{hosts = [] :: [binary()],
server_host = <<"">> :: binary(),
access = {none, none, none, none} :: {atom(), atom(), atom(), atom(), atom()},
history_size = 20 :: non_neg_integer(),
max_rooms_discoitems = 100 :: non_neg_integer(),
queue_type = ram :: ram | file,
default_room_opts = [] :: list(),
room_shaper = none :: ejabberd_shaper:shaper()}).
-type muc_room_opts() :: [{atom(), any()}].
-callback init(binary(), gen_mod:opts()) -> any().
-callback import(binary(), binary(), [binary()]) -> ok.
-callback store_room(binary(), binary(), binary(), list(), list()|undefined) -> {atomic, any()}.
-callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error.
-callback forget_room(binary(), binary(), binary()) -> {atomic, any()}.
-callback can_use_nick(binary(), binary(), jid(), binary()) -> boolean().
-callback get_rooms(binary(), binary()) -> [#muc_room{}].
-callback get_nick(binary(), binary(), jid()) -> binary() | error.
-callback set_nick(binary(), binary(), jid(), binary()) -> {atomic, ok | false}.
-callback register_online_room(binary(), binary(), binary(), pid()) -> any().
-callback unregister_online_room(binary(), binary(), binary(), pid()) -> any().
-callback find_online_room(binary(), binary(), binary()) -> {ok, pid()} | error.
-callback get_online_rooms(binary(), binary(), undefined | rsm_set()) -> [{binary(), binary(), pid()}].
-callback count_online_rooms(binary(), binary()) -> non_neg_integer().
-callback rsm_supported() -> boolean().
-callback register_online_user(binary(), ljid(), binary(), binary()) -> any().
-callback unregister_online_user(binary(), ljid(), binary(), binary()) -> any().
-callback count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer().
-callback get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}].
-callback get_subscribed_rooms(binary(), binary(), jid()) ->
{ok, [{jid(), [binary()]}]} | {error, db_failure}.
-optional_callbacks([get_subscribed_rooms/3]).
%%====================================================================
%% API
%%====================================================================
start(Host, Opts) ->
gen_mod:start_child(?MODULE, Host, Opts).
stop(Host) ->
Rooms = shutdown_rooms(Host),
gen_mod:stop_child(?MODULE, Host),
{wait, Rooms}.
reload(Host, NewOpts, OldOpts) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}).
depends(_Host, _Opts) ->
[{mod_mam, soft}].
shutdown_rooms(ServerHost) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
Hosts = gen_mod:get_module_opt_hosts(ServerHost, mod_muc),
Rooms = [RMod:get_online_rooms(ServerHost, Host, undefined)
|| Host <- Hosts],
lists:flatmap(
fun({_, _, Pid}) when node(Pid) == node() ->
Pid ! shutdown,
[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 catched
room_destroyed(Host, Room, Pid, ServerHost) ->
catch gen_mod:get_module_proc(ServerHost, ?MODULE) !
{room_destroyed, {Room, Host}, Pid},
ok.
%% @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 = gen_mod:get_module_proc(ServerHost, ?MODULE),
gen_server:call(Proc, {create, Name, Host, From, Nick, Opts}).
store_room(ServerHost, Host, Name, Opts) ->
store_room(ServerHost, Host, Name, Opts, undefined).
store_room(ServerHost, Host, Name, Opts, ChangesHints) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:store_room(LServer, Host, Name, Opts, ChangesHints).
restore_room(ServerHost, Host, Name) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:restore_room(LServer, Host, Name).
forget_room(ServerHost, Host, Name) ->
LServer = jid:nameprep(ServerHost),
ejabberd_hooks:run(remove_room, LServer, [LServer, Name, Host]),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:forget_room(LServer, Host, Name).
can_use_nick(_ServerHost, _Host, _JID, <<"">>) -> false;
can_use_nick(ServerHost, Host, JID, Nick) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:can_use_nick(LServer, Host, JID, Nick).
-spec find_online_room(binary(), binary()) -> {ok, pid()} | error.
find_online_room(Room, Host) ->
ServerHost = ejabberd_router:host_of_route(Host),
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:find_online_room(ServerHost, Room, Host).
-spec register_online_room(binary(), binary(), pid()) -> any().
register_online_room(Room, Host, Pid) ->
ServerHost = ejabberd_router:host_of_route(Host),
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:register_online_room(ServerHost, Room, Host, Pid).
-spec get_online_rooms(binary()) -> [{binary(), binary(), pid()}].
get_online_rooms(Host) ->
ServerHost = ejabberd_router:host_of_route(Host),
get_online_rooms(ServerHost, Host).
-spec count_online_rooms(binary()) -> non_neg_integer().
count_online_rooms(Host) ->
ServerHost = ejabberd_router:host_of_route(Host),
count_online_rooms(ServerHost, Host).
-spec register_online_user(binary(), ljid(), binary(), binary()) -> any().
register_online_user(ServerHost, LJID, Name, Host) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:register_online_user(ServerHost, LJID, Name, Host).
-spec unregister_online_user(binary(), ljid(), binary(), binary()) -> any().
unregister_online_user(ServerHost, LJID, Name, Host) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:unregister_online_user(ServerHost, LJID, Name, Host).
-spec count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer().
count_online_rooms_by_user(ServerHost, LUser, LServer) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:count_online_rooms_by_user(ServerHost, LUser, LServer).
-spec get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}].
get_online_rooms_by_user(ServerHost, LUser, LServer) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:get_online_rooms_by_user(ServerHost, LUser, LServer).
%%====================================================================
%% gen_server callbacks
%%====================================================================
init([Host, Opts]) ->
process_flag(trap_exit, true),
#state{access = Access, hosts = MyHosts,
history_size = HistorySize, queue_type = QueueType,
room_shaper = RoomShaper} = State = init_state(Host, 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)),
lists:foreach(
fun(MyHost) ->
register_iq_handlers(MyHost),
ejabberd_router:register_route(MyHost, Host),
load_permanent_rooms(MyHost, Host, Access, HistorySize,
RoomShaper, QueueType)
end, MyHosts),
{ok, State}.
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call({create, Room, Host, From, Nick, Opts}, _From,
#state{server_host = ServerHost,
access = Access, default_room_opts = DefOpts,
history_size = HistorySize, queue_type = QueueType,
room_shaper = RoomShaper} = State) ->
?DEBUG("MUC: create new room '~s'~n", [Room]),
NewOpts = case Opts of
default -> DefOpts;
_ -> Opts
end,
{ok, Pid} = mod_muc_room:start(
Host, ServerHost, Access,
Room, HistorySize,
RoomShaper, From,
Nick, NewOpts, QueueType),
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:register_online_room(ServerHost, Room, Host, Pid),
ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]),
{reply, ok, State}.
handle_cast({reload, ServerHost, NewOpts, OldOpts}, #state{hosts = OldHosts}) ->
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),
#state{hosts = NewHosts} = NewState = init_state(ServerHost, NewOpts),
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(NewHost) ->
ejabberd_router:register_route(NewHost, ServerHost),
register_iq_handlers(NewHost)
end, NewHosts -- OldHosts),
lists:foreach(
fun(OldHost) ->
ejabberd_router:unregister_route(OldHost),
unregister_iq_handlers(OldHost)
end, OldHosts -- NewHosts),
lists:foreach(
fun(Host) ->
lists:foreach(
fun({_, _, Pid}) when node(Pid) == node() ->
Pid ! config_reloaded;
(_) ->
ok
end, get_online_rooms(ServerHost, Host))
end, misc:intersection(NewHosts, OldHosts)),
{noreply, NewState};
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({route, Packet},
#state{server_host = ServerHost,
access = Access, default_room_opts = DefRoomOpts,
history_size = HistorySize, queue_type = QueueType,
max_rooms_discoitems = MaxRoomsDiscoItems,
room_shaper = RoomShaper} = State) ->
From = xmpp:get_from(Packet),
To = xmpp:get_to(Packet),
Host = To#jid.lserver,
case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems,
QueueType) of
{'EXIT', Reason} ->
?ERROR_MSG("~p", [Reason]);
_ ->
ok
end,
{noreply, State};
handle_info({room_destroyed, {Room, Host}, Pid}, State) ->
ServerHost = State#state.server_host,
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:unregister_online_room(ServerHost, Room, Host, Pid),
{noreply, State};
handle_info(Info, State) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #state{hosts = MyHosts}) ->
lists:foreach(
fun(MyHost) ->
ejabberd_router:unregister_route(MyHost),
unregister_iq_handlers(MyHost)
end, MyHosts).
code_change(_OldVsn, State, _Extra) -> {ok, State}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
init_state(Host, Opts) ->
MyHosts = gen_mod:get_opt_hosts(Opts),
Access = mod_muc_opt:access(Opts),
AccessCreate = mod_muc_opt:access_create(Opts),
AccessAdmin = mod_muc_opt:access_admin(Opts),
AccessPersistent = mod_muc_opt:access_persistent(Opts),
AccessMam = mod_muc_opt:access_mam(Opts),
HistorySize = mod_muc_opt:history_size(Opts),
MaxRoomsDiscoItems = mod_muc_opt:max_rooms_discoitems(Opts),
DefRoomOpts = mod_muc_opt:default_room_options(Opts),
QueueType = mod_muc_opt:queue_type(Opts),
RoomShaper = mod_muc_opt:room_shaper(Opts),
#state{hosts = MyHosts,
server_host = Host,
access = {Access, AccessCreate, AccessAdmin, AccessPersistent, AccessMam},
default_room_opts = DefRoomOpts,
queue_type = QueueType,
history_size = HistorySize,
max_rooms_discoitems = MaxRoomsDiscoItems,
room_shaper = RoomShaper}.
register_iq_handlers(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).
unregister_iq_handlers(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).
do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
From, To, Packet, DefRoomOpts, _MaxRoomsDiscoItems, QueueType) ->
{AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent, _AccessMam} = Access,
case acl:match_rule(ServerHost, AccessRoute, From) of
allow ->
do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
From, To, Packet, DefRoomOpts, QueueType);
deny ->
Lang = xmpp:get_lang(Packet),
ErrText = ?T("Access denied by service policy"),
Err = xmpp:err_forbidden(ErrText, Lang),
ejabberd_router:route_error(Packet, Err)
end.
do_route1(_Host, _ServerHost, _Access, _HistorySize, _RoomShaper,
_From, #jid{luser = <<"">>, lresource = <<"">>} = _To,
#iq{} = IQ, _DefRoomOpts, _QueueType) ->
ejabberd_router:process_iq(IQ);
do_route1(Host, ServerHost, Access, _HistorySize, _RoomShaper,
From, #jid{luser = <<"">>, lresource = <<"">>} = _To,
#message{lang = Lang, body = Body, type = Type} = Packet, _, _) ->
{_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent, _AccessMam} = Access,
if Type == error ->
ok;
true ->
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(Packet, Err)
end
end;
do_route1(_Host, _ServerHost, _Access, _HistorySize, _RoomShaper,
_From, #jid{luser = <<"">>} = _To, Packet, _DefRoomOpts, _) ->
Err = xmpp:err_service_unavailable(),
ejabberd_router:route_error(Packet, Err);
do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
From, To, Packet, DefRoomOpts, QueueType) ->
{Room, _, Nick} = jid:tolower(To),
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
case RMod:find_online_room(ServerHost, Room, Host) of
error ->
case is_create_request(Packet) of
true ->
case check_create_room(
ServerHost, Host, Room, From, Access) of
true ->
{ok, Pid} = start_new_room(
Host, ServerHost, Access,
Room, HistorySize,
RoomShaper, From, Nick, DefRoomOpts,
QueueType),
RMod:register_online_room(ServerHost, Room, Host, Pid),
mod_muc_room:route(Pid, Packet),
ok;
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;
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)
end;
{ok, Pid} ->
?DEBUG("MUC: send to process ~p~n", [Pid]),
mod_muc_room:route(Pid, Packet),
ok
end.
-spec process_vcard(iq()) -> iq().
process_vcard(#iq{type = get, lang = Lang, sub_els = [#vcard_temp{}]} = IQ) ->
xmpp:make_iq_result(
IQ, #vcard_temp{fn = <<"ejabberd/mod_muc">>,
url = ejabberd_config:get_uri(),
desc = misc:get_descr(Lang, ?T("ejabberd MUC module"))});
process_vcard(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_vcard(#iq{lang = Lang} = IQ) ->
Txt = ?T("No module is handling this query"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
-spec process_register(iq()) -> iq().
process_register(#iq{type = Type, from = From, to = To, lang = Lang,
sub_els = [El = #register{}]} = IQ) ->
Host = To#jid.lserver,
ServerHost = ejabberd_router:host_of_route(Host),
AccessRegister = mod_muc_opt:access_register(ServerHost),
case acl:match_rule(ServerHost, AccessRegister, From) of
allow ->
case Type of
get ->
xmpp:make_iq_result(
IQ, iq_get_register_info(ServerHost, Host, From, Lang));
set ->
case process_iq_register_set(ServerHost, Host, From, El, Lang) of
{result, Result} ->
xmpp:make_iq_result(IQ, Result);
{error, Err} ->
xmpp:make_error(IQ, Err)
end
end;
deny ->
ErrText = ?T("Access denied by service policy"),
Err = xmpp:err_forbidden(ErrText, Lang),
xmpp:make_error(IQ, Err)
end.
-spec process_disco_info(iq()) -> iq().
process_disco_info(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_disco_info(#iq{type = get, from = From, to = To, lang = Lang,
sub_els = [#disco_info{node = <<"">>}]} = IQ) ->
ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
AccessRegister = mod_muc_opt:access_register(ServerHost),
X = ejabberd_hooks:run_fold(disco_info, ServerHost, [],
[ServerHost, ?MODULE, <<"">>, Lang]),
MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of
true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2];
false -> []
end,
RSMFeatures = case RMod:rsm_supported() of
true -> [?NS_RSM];
false -> []
end,
RegisterFeatures = case acl:match_rule(ServerHost, AccessRegister, From) of
allow -> [?NS_REGISTER];
deny -> []
end,
Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
?NS_MUC, ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE
| RegisterFeatures ++ RSMFeatures ++ MAMFeatures],
Name = mod_muc_opt:name(ServerHost),
Identity = #identity{category = <<"conference">>,
type = <<"text">>,
name = translate:translate(Lang, Name)},
xmpp:make_iq_result(
IQ, #disco_info{features = Features,
identities = [Identity],
xdata = X});
process_disco_info(#iq{type = get, lang = Lang,
sub_els = [#disco_info{}]} = IQ) ->
xmpp:make_error(IQ, xmpp:err_item_not_found(?T("Node not found"), Lang));
process_disco_info(#iq{lang = Lang} = IQ) ->
Txt = ?T("No module is handling this query"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
-spec process_disco_items(iq()) -> iq().
process_disco_items(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_disco_items(#iq{type = get, from = From, to = To, lang = Lang,
sub_els = [#disco_items{node = Node, rsm = RSM}]} = IQ) ->
Host = To#jid.lserver,
ServerHost = ejabberd_router:host_of_route(Host),
MaxRoomsDiscoItems = mod_muc_opt:max_rooms_discoitems(ServerHost),
case iq_disco_items(ServerHost, Host, From, Lang,
MaxRoomsDiscoItems, Node, RSM) of
{error, Err} ->
xmpp:make_error(IQ, Err);
{result, Result} ->
xmpp:make_iq_result(IQ, Result)
end;
process_disco_items(#iq{lang = Lang} = IQ) ->
Txt = ?T("No module is handling this query"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
-spec process_muc_unique(iq()) -> iq().
process_muc_unique(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_muc_unique(#iq{from = From, type = get,
sub_els = [#muc_unique{}]} = IQ) ->
Name = str:sha(term_to_binary([From, erlang:timestamp(),
p1_rand:get_string()])),
xmpp:make_iq_result(IQ, #muc_unique{name = Name}).
-spec process_mucsub(iq()) -> iq().
process_mucsub(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_mucsub(#iq{type = get, from = From, to = To, lang = Lang,
sub_els = [#muc_subscriptions{}]} = IQ) ->
Host = To#jid.lserver,
ServerHost = ejabberd_router:host_of_route(Host),
case get_subscribed_rooms(ServerHost, Host, From) of
{ok, Subs} ->
List = [#muc_subscription{jid = JID, events = Nodes}
|| {JID, Nodes} <- Subs],
xmpp:make_iq_result(IQ, #muc_subscriptions{list = List});
{error, _} ->
Txt = ?T("Database failure"),
xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
end;
process_mucsub(#iq{lang = Lang} = IQ) ->
Txt = ?T("No module is handling this query"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
-spec is_create_request(stanza()) -> boolean().
is_create_request(#presence{type = available}) ->
true;
is_create_request(#iq{type = T} = IQ) when T == get; T == set ->
xmpp:has_subtag(IQ, #muc_subscribe{}) orelse
xmpp:has_subtag(IQ, #muc_owner{});
is_create_request(_) ->
false.
-spec check_create_room(binary(), binary(), binary(), jid(), tuple())
-> boolean().
check_create_room(ServerHost, Host, Room, From, Access) ->
{_AccessRoute, AccessCreate, AccessAdmin,
_AccessPersistent, _AccessMam} = Access,
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, [unicode, {capture, none}]) of
match ->
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.
get_rooms(ServerHost, Host) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:get_rooms(LServer, Host).
load_permanent_rooms(Host, ServerHost, Access,
HistorySize, RoomShaper, QueueType) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
lists:foreach(
fun(R) ->
{Room, Host} = R#muc_room.name_host,
case proplists:get_bool(persistent, R#muc_room.opts) of
true ->
case RMod:find_online_room(ServerHost, Room, Host) of
error ->
{ok, Pid} = mod_muc_room:start(Host,
ServerHost, Access, Room,
HistorySize, RoomShaper,
R#muc_room.opts, QueueType),
RMod:register_online_room(ServerHost, Room, Host, Pid);
{ok, _} ->
ok
end;
_ ->
forget_room(ServerHost, Host, Room)
end
end, get_rooms(ServerHost, Host)).
start_new_room(Host, ServerHost, Access, Room,
HistorySize, RoomShaper, From,
Nick, DefRoomOpts, QueueType) ->
Opts = case restore_room(ServerHost, Host, Room) of
error ->
error;
Opts0 ->
case proplists:get_bool(persistent, Opts0) of
true ->
Opts0;
_ ->
error
end
end,
case Opts of
error ->
?DEBUG("MUC: open new room '~s'~n", [Room]),
mod_muc_room:start(Host, ServerHost, Access, Room,
HistorySize, RoomShaper,
From, Nick, DefRoomOpts, QueueType);
_ ->
?DEBUG("MUC: restore room '~s'~n", [Room]),
mod_muc_room:start(Host, ServerHost, Access, Room,
HistorySize, RoomShaper, Opts, QueueType)
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 ->
{get_disco_item, only_non_empty, From, Lang};
Node == <<"nonemptyrooms">> ->
{get_disco_item, only_non_empty, From, Lang};
Node == <<"emptyrooms">> ->
{get_disco_item, 0, From, Lang};
true ->
{get_disco_item, all, From, Lang}
end,
Items = lists:flatmap(
fun(R) ->
case get_room_disco_item(R, Query) of
{ok, Item} -> [Item];
{error, _} -> []
end
end, get_online_rooms(ServerHost, Host, RSM)),
ResRSM = case Items of
[_|_] when RSM /= undefined ->
#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()},
term()) -> {ok, disco_item()} |
{error, timeout | notfound}.
get_room_disco_item({Name, Host, Pid},
{get_disco_item, Filter, JID, Lang}) ->
RoomJID = jid:make(Name, Host),
Timeout = 100,
Time = erlang:system_time(millisecond),
Query1 = {get_disco_item, Filter, JID, Lang, Time+Timeout},
try p1_fsm:sync_send_all_state_event(Pid, Query1, Timeout) of
{item, Desc} ->
{ok, #disco_item{jid = RoomJID, name = Desc}};
false ->
{error, notfound}
catch _:{timeout, {p1_fsm, _, _}} ->
{error, timeout};
_:{_, {p1_fsm, _, _}} ->
{error, notfound}
end.
-spec get_subscribed_rooms(binary(), jid()) -> {ok, [{jid(), [binary()]}]} | {error, any()}.
get_subscribed_rooms(Host, User) ->
ServerHost = ejabberd_router:host_of_route(Host),
get_subscribed_rooms(ServerHost, Host, User).
-record(subscriber, {jid :: jid(),
nick = <<>> :: binary(),
nodes = [] :: [binary()]}).
-spec get_subscribed_rooms(binary(), binary(), jid()) ->
{ok, [{jid(), [binary()]}]} | {error, any()}.
get_subscribed_rooms(ServerHost, Host, From) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
BareFrom = jid:remove_resource(From),
case erlang:function_exported(Mod, get_subscribed_rooms, 3) of
false ->
Rooms = get_online_rooms(ServerHost, Host),
{ok, lists:flatmap(
fun({Name, _, Pid}) when Pid == self() ->
USR = jid:split(BareFrom),
case erlang:get(muc_subscribers) of
#{USR := #subscriber{nodes = Nodes}} ->
[{jid:make(Name, Host), Nodes}];
_ ->
[]
end;
({Name, _, Pid}) ->
case p1_fsm:sync_send_all_state_event(
Pid, {is_subscribed, BareFrom}) of
{true, Nodes} ->
[{jid:make(Name, Host), Nodes}];
false -> []
end;
(_) ->
[]
end, Rooms)};
true ->
Mod:get_subscribed_rooms(LServer, Host, BareFrom)
end.
get_nick(ServerHost, Host, From) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:get_nick(LServer, Host, From).
iq_get_register_info(ServerHost, Host, From, Lang) ->
{Nick, Registered} = case get_nick(ServerHost, Host, From) of
error -> {<<"">>, false};
N -> {N, true}
end,
Title = <<(translate:translate(
Lang, ?T("Nickname Registration at ")))/binary, Host/binary>>,
Inst = translate:translate(Lang, ?T("Enter nickname you want to register")),
Fields = muc_register:encode([{roomnick, Nick}], Lang),
X = #xdata{type = form, title = Title,
instructions = [Inst], fields = Fields},
#register{nick = Nick,
registered = Registered,
instructions =
translate:translate(
Lang, ?T("You need a client that supports x:data "
"to register the nickname")),
xdata = X}.
set_nick(ServerHost, Host, From, Nick) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:set_nick(LServer, Host, From, Nick).
iq_set_register_info(ServerHost, Host, From, Nick,
Lang) ->
case set_nick(ServerHost, Host, From, Nick) of
{atomic, ok} -> {result, undefined};
{atomic, false} ->
ErrText = ?T("That nickname is registered by another person"),
{error, xmpp:err_conflict(ErrText, Lang)};
_ ->
Txt = ?T("Database failure"),
{error, xmpp:err_internal_server_error(Txt, Lang)}
end.
process_iq_register_set(ServerHost, Host, From,
#register{remove = true}, Lang) ->
iq_set_register_info(ServerHost, Host, From, <<"">>, Lang);
process_iq_register_set(_ServerHost, _Host, _From,
#register{xdata = #xdata{type = cancel}}, _Lang) ->
{result, undefined};
process_iq_register_set(ServerHost, Host, From,
#register{nick = Nick, xdata = XData}, Lang) ->
case XData of
#xdata{type = submit, fields = Fs} ->
try
Options = muc_register:decode(Fs),
N = proplists:get_value(roomnick, Options),
iq_set_register_info(ServerHost, Host, From, N, Lang)
catch _:{muc_register, Why} ->
ErrText = muc_register:format_error(Why),
{error, xmpp:err_bad_request(ErrText, Lang)}
end;
#xdata{} ->
Txt = ?T("Incorrect data form"),
{error, xmpp:err_bad_request(Txt, Lang)};
_ when is_binary(Nick), Nick /= <<"">> ->
iq_set_register_info(ServerHost, Host, From, Nick, Lang);
_ ->
ErrText = ?T("You must fill in field \"Nickname\" in the form"),
{error, xmpp:err_not_acceptable(ErrText, Lang)}
end.
-spec broadcast_service_message(binary(), binary(), binary()) -> ok.
broadcast_service_message(ServerHost, Host, Msg) ->
lists:foreach(
fun({_, _, Pid}) ->
p1_fsm:send_all_state_event(
Pid, {service_message, Msg})
end, get_online_rooms(ServerHost, Host)).
-spec get_online_rooms(binary(), binary()) -> [{binary(), binary(), pid()}].
get_online_rooms(ServerHost, Host) ->
get_online_rooms(ServerHost, Host, undefined).
-spec get_online_rooms(binary(), binary(), undefined | rsm_set()) ->
[{binary(), binary(), pid()}].
get_online_rooms(ServerHost, Host, RSM) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:get_online_rooms(ServerHost, Host, RSM).
-spec count_online_rooms(binary(), binary()) -> non_neg_integer().
count_online_rooms(ServerHost, Host) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:count_online_rooms(ServerHost, Host).
opts_to_binary(Opts) ->
lists:map(
fun({title, Title}) ->
{title, iolist_to_binary(Title)};
({description, Desc}) ->
{description, iolist_to_binary(Desc)};
({password, Pass}) ->
{password, iolist_to_binary(Pass)};
({subject, [C|_] = Subj}) when is_integer(C), C >= 0, C =< 255 ->
{subject, iolist_to_binary(Subj)};
({subject_author, Author}) ->
{subject_author, iolist_to_binary(Author)};
({affiliations, Affs}) ->
{affiliations, lists:map(
fun({{U, S, R}, Aff}) ->
NewAff =
case Aff of
{A, Reason} ->
{A, iolist_to_binary(Reason)};
_ ->
Aff
end,
{{iolist_to_binary(U),
iolist_to_binary(S),
iolist_to_binary(R)},
NewAff}
end, Affs)};
({captcha_whitelist, CWList}) ->
{captcha_whitelist, lists:map(
fun({U, S, R}) ->
{iolist_to_binary(U),
iolist_to_binary(S),
iolist_to_binary(R)}
end, CWList)};
(Opt) ->
Opt
end, Opts).
export(LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:export(LServer).
import_info() ->
[{<<"muc_room">>, 4}, {<<"muc_registered">>, 4}].
import_start(LServer, DBType) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:init(LServer, []).
import(LServer, {sql, _}, DBType, Tab, L) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:import(LServer, Tab, L).
mod_opt_type(access) ->
econf:acl();
mod_opt_type(access_admin) ->
econf:acl();
mod_opt_type(access_create) ->
econf:acl();
mod_opt_type(access_persistent) ->
econf:acl();
mod_opt_type(access_mam) ->
econf:acl();
mod_opt_type(access_register) ->
econf:acl();
mod_opt_type(history_size) ->
econf:non_neg_int();
mod_opt_type(name) ->
econf:binary();
mod_opt_type(max_room_desc) ->
econf:pos_int(infinity);
mod_opt_type(max_room_id) ->
econf:pos_int(infinity);
mod_opt_type(max_rooms_discoitems) ->
econf:non_neg_int();
mod_opt_type(regexp_room_id) ->
econf:binary();
mod_opt_type(max_room_name) ->
econf:pos_int(infinity);
mod_opt_type(max_user_conferences) ->
econf:pos_int();
mod_opt_type(max_users) ->
econf:pos_int();
mod_opt_type(max_users_admin_threshold) ->
econf:pos_int();
mod_opt_type(max_users_presence) ->
econf:int();
mod_opt_type(min_message_interval) ->
econf:number(0);
mod_opt_type(min_presence_interval) ->
econf:number(0);
mod_opt_type(room_shaper) ->
econf:atom();
mod_opt_type(user_message_shaper) ->
econf:atom();
mod_opt_type(user_presence_shaper) ->
econf:atom();
mod_opt_type(default_room_options) ->
econf:options(
#{allow_change_subj => econf:bool(),
allow_private_messages => econf:bool(),
allow_private_messages_from_visitors =>
econf:enum([anyone, moderators, nobody]),
allow_query_users => econf:bool(),
allow_subscription => econf:bool(),
allow_user_invites => econf:bool(),
allow_visitor_nickchange => econf:bool(),
allow_visitor_status => econf:bool(),
anonymous => econf:bool(),
captcha_protected => econf:bool(),
lang => econf:lang(),
logging => econf:bool(),
mam => econf:bool(),
max_users => econf:pos_int(),
members_by_default => econf:bool(),
members_only => econf:bool(),
moderated => econf:bool(),
password => econf:binary(),
password_protected => econf:bool(),
persistent => econf:bool(),
presence_broadcast =>
econf:list(
econf:enum([moderator, participant, visitor])),
public => econf:bool(),
public_list => econf:bool(),
title => econf:binary()});
mod_opt_type(db_type) ->
econf:db_type(?MODULE);
mod_opt_type(ram_db_type) ->
econf:db_type(?MODULE);
mod_opt_type(host) ->
econf:host();
mod_opt_type(hosts) ->
econf:hosts();
mod_opt_type(queue_type) ->
econf:queue_type().
mod_options(Host) ->
[{access, all},
{access_admin, none},
{access_create, all},
{access_persistent, all},
{access_mam, all},
{access_register, all},
{db_type, ejabberd_config:default_db(Host, ?MODULE)},
{ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)},
{history_size, 20},
{host, <<"conference.", Host/binary>>},
{hosts, []},
{name, ?T("Chatrooms")},
{max_room_desc, infinity},
{max_room_id, infinity},
{max_room_name, infinity},
{max_rooms_discoitems, 100},
{max_user_conferences, 100},
{max_users, 200},
{max_users_admin_threshold, 5},
{max_users_presence, 1000},
{min_message_interval, 0},
{min_presence_interval, 0},
{queue_type, ejabberd_option:queue_type(Host)},
{regexp_room_id, <<"">>},
{room_shaper, none},
{user_message_shaper, none},
{user_presence_shaper, none},
{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]}]}].