%%%---------------------------------------------------------------------- %%% File : mod_muc.erl %%% Author : Alexey Shchepin %%% Purpose : MUC support (XEP-0045) %%% Created : 19 Mar 2003 by Alexey Shchepin %%% %%% %%% 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]}]}].