mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-22 16:20:52 +01:00
884 lines
32 KiB
Erlang
884 lines
32 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-2016 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_link/2,
|
|
start/2,
|
|
stop/1,
|
|
room_destroyed/4,
|
|
store_room/4,
|
|
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/2,
|
|
export/1,
|
|
import/1,
|
|
import/3,
|
|
opts_to_binary/1,
|
|
can_use_nick/4]).
|
|
|
|
-export([init/1, handle_call/3, handle_cast/2,
|
|
handle_info/2, terminate/2, code_change/3,
|
|
mod_opt_type/1, depends/2]).
|
|
|
|
-include("ejabberd.hrl").
|
|
-include("logger.hrl").
|
|
|
|
-include("xmpp.hrl").
|
|
-include("mod_muc.hrl").
|
|
|
|
-record(state,
|
|
{host = <<"">> :: binary(),
|
|
server_host = <<"">> :: binary(),
|
|
access = {none, none, none, none} :: {atom(), atom(), atom(), atom()},
|
|
history_size = 20 :: non_neg_integer(),
|
|
default_room_opts = [] :: list(),
|
|
room_shaper = none :: shaper:shaper()}).
|
|
|
|
-define(PROCNAME, ejabberd_mod_muc).
|
|
|
|
-define(MAX_ROOMS_DISCOITEMS, 100).
|
|
|
|
-type muc_room_opts() :: [{atom(), any()}].
|
|
-callback init(binary(), gen_mod:opts()) -> any().
|
|
-callback import(binary(), #muc_room{} | #muc_registered{}) -> ok | pass.
|
|
-callback store_room(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}.
|
|
|
|
%%====================================================================
|
|
%% API
|
|
%%====================================================================
|
|
start_link(Host, Opts) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
gen_server:start_link({local, Proc}, ?MODULE,
|
|
[Host, Opts], []).
|
|
|
|
start(Host, Opts) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
|
|
temporary, 1000, worker, [?MODULE]},
|
|
supervisor:start_child(ejabberd_sup, ChildSpec).
|
|
|
|
stop(Host) ->
|
|
Rooms = shutdown_rooms(Host),
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
gen_server:call(Proc, stop),
|
|
supervisor:delete_child(ejabberd_sup, Proc),
|
|
{wait, Rooms}.
|
|
|
|
depends(_Host, _Opts) ->
|
|
[{mod_mam, soft}].
|
|
|
|
shutdown_rooms(Host) ->
|
|
MyHost = gen_mod:get_module_opt_host(Host, mod_muc,
|
|
<<"conference.@HOST@">>),
|
|
Rooms = mnesia:dirty_select(muc_online_room,
|
|
[{#muc_online_room{name_host = '$1',
|
|
pid = '$2'},
|
|
[{'==', {element, 2, '$1'}, MyHost},
|
|
{'==', {node, '$2'}, node()}],
|
|
['$2']}]),
|
|
[Pid ! shutdown || Pid <- Rooms],
|
|
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, ?PROCNAME) !
|
|
{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) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
gen_server:call(Proc, {create, Name, From, Nick, Opts}).
|
|
|
|
store_room(ServerHost, Host, Name, Opts) ->
|
|
LServer = jid:nameprep(ServerHost),
|
|
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
|
Mod:store_room(LServer, Host, Name, Opts).
|
|
|
|
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).
|
|
|
|
%%====================================================================
|
|
%% gen_server callbacks
|
|
%%====================================================================
|
|
|
|
init([Host, Opts]) ->
|
|
IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1,
|
|
one_queue),
|
|
MyHost = gen_mod:get_opt_host(Host, Opts,
|
|
<<"conference.@HOST@">>),
|
|
Mod = gen_mod:db_mod(Host, Opts, ?MODULE),
|
|
Mod:init(Host, [{host, MyHost}|Opts]),
|
|
mnesia:create_table(muc_online_room,
|
|
[{ram_copies, [node()]},
|
|
{attributes, record_info(fields, muc_online_room)}]),
|
|
mnesia:add_table_copy(muc_online_room, node(), ram_copies),
|
|
catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]),
|
|
clean_table_from_bad_node(node(), MyHost),
|
|
mnesia:subscribe(system),
|
|
Access = gen_mod:get_opt(access, Opts,
|
|
fun acl:access_rules_validator/1, all),
|
|
AccessCreate = gen_mod:get_opt(access_create, Opts,
|
|
fun acl:access_rules_validator/1, all),
|
|
AccessAdmin = gen_mod:get_opt(access_admin, Opts,
|
|
fun acl:access_rules_validator/1,
|
|
none),
|
|
AccessPersistent = gen_mod:get_opt(access_persistent, Opts,
|
|
fun acl:access_rules_validator/1,
|
|
all),
|
|
HistorySize = gen_mod:get_opt(history_size, Opts,
|
|
fun(I) when is_integer(I), I>=0 -> I end,
|
|
20),
|
|
DefRoomOpts1 = gen_mod:get_opt(default_room_options, Opts,
|
|
fun(L) when is_list(L) -> L end,
|
|
[]),
|
|
DefRoomOpts =
|
|
lists:flatmap(
|
|
fun({Opt, Val}) ->
|
|
Bool = fun(B) when is_boolean(B) -> B end,
|
|
VFun = case Opt of
|
|
allow_change_subj -> Bool;
|
|
allow_private_messages -> Bool;
|
|
allow_query_users -> Bool;
|
|
allow_user_invites -> Bool;
|
|
allow_visitor_nickchange -> Bool;
|
|
allow_visitor_status -> Bool;
|
|
anonymous -> Bool;
|
|
captcha_protected -> Bool;
|
|
logging -> Bool;
|
|
members_by_default -> Bool;
|
|
members_only -> Bool;
|
|
moderated -> Bool;
|
|
password_protected -> Bool;
|
|
persistent -> Bool;
|
|
public -> Bool;
|
|
public_list -> Bool;
|
|
mam -> Bool;
|
|
password -> fun iolist_to_binary/1;
|
|
title -> fun iolist_to_binary/1;
|
|
allow_private_messages_from_visitors ->
|
|
fun(anyone) -> anyone;
|
|
(moderators) -> moderators;
|
|
(nobody) -> nobody
|
|
end;
|
|
max_users ->
|
|
fun(I) when is_integer(I), I > 0 -> I end;
|
|
presence_broadcast ->
|
|
fun(L) ->
|
|
lists:map(
|
|
fun(moderator) -> moderator;
|
|
(participant) -> participant;
|
|
(visitor) -> visitor
|
|
end, L)
|
|
end;
|
|
_ ->
|
|
?ERROR_MSG("unknown option ~p with value ~p",
|
|
[Opt, Val]),
|
|
fun(_) -> undefined end
|
|
end,
|
|
case gen_mod:get_opt(Opt, [{Opt, Val}], VFun) of
|
|
undefined -> [];
|
|
NewVal -> [{Opt, NewVal}]
|
|
end
|
|
end, DefRoomOpts1),
|
|
RoomShaper = gen_mod:get_opt(room_shaper, Opts,
|
|
fun(A) when is_atom(A) -> A end,
|
|
none),
|
|
gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_REGISTER,
|
|
?MODULE, process_register, IQDisc),
|
|
gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_VCARD,
|
|
?MODULE, process_vcard, IQDisc),
|
|
gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_MUCSUB,
|
|
?MODULE, process_mucsub, IQDisc),
|
|
gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_MUC_UNIQUE,
|
|
?MODULE, process_muc_unique, IQDisc),
|
|
gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO,
|
|
?MODULE, process_disco_info, IQDisc),
|
|
gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS,
|
|
?MODULE, process_disco_items, IQDisc),
|
|
ejabberd_router:register_route(MyHost, Host),
|
|
load_permanent_rooms(MyHost, Host,
|
|
{Access, AccessCreate, AccessAdmin, AccessPersistent},
|
|
HistorySize, RoomShaper),
|
|
{ok, #state{host = MyHost,
|
|
server_host = Host,
|
|
access = {Access, AccessCreate, AccessAdmin, AccessPersistent},
|
|
default_room_opts = DefRoomOpts,
|
|
history_size = HistorySize,
|
|
room_shaper = RoomShaper}}.
|
|
|
|
handle_call(stop, _From, State) ->
|
|
{stop, normal, ok, State};
|
|
handle_call({create, Room, From, Nick, Opts}, _From,
|
|
#state{host = Host, server_host = ServerHost,
|
|
access = Access, default_room_opts = DefOpts,
|
|
history_size = HistorySize,
|
|
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),
|
|
register_room(Host, Room, Pid),
|
|
{reply, ok, State}.
|
|
|
|
handle_cast(_Msg, State) -> {noreply, State}.
|
|
|
|
handle_info({route, From, To, Packet},
|
|
#state{host = Host, server_host = ServerHost,
|
|
access = Access, default_room_opts = DefRoomOpts,
|
|
history_size = HistorySize,
|
|
room_shaper = RoomShaper} = State) ->
|
|
case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
|
|
From, To, Packet, DefRoomOpts) of
|
|
{'EXIT', Reason} ->
|
|
?ERROR_MSG("~p", [Reason]);
|
|
_ ->
|
|
ok
|
|
end,
|
|
{noreply, State};
|
|
handle_info({room_destroyed, RoomHost, Pid}, State) ->
|
|
F = fun () ->
|
|
mnesia:delete_object(#muc_online_room{name_host =
|
|
RoomHost,
|
|
pid = Pid})
|
|
end,
|
|
mnesia:transaction(F),
|
|
{noreply, State};
|
|
handle_info({mnesia_system_event, {mnesia_down, Node}}, State) ->
|
|
clean_table_from_bad_node(Node),
|
|
{noreply, State};
|
|
handle_info(_Info, State) -> {noreply, State}.
|
|
|
|
terminate(_Reason, #state{host = MyHost}) ->
|
|
ejabberd_router:unregister_route(MyHost),
|
|
gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_REGISTER),
|
|
gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_VCARD),
|
|
gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_MUCSUB),
|
|
gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_MUC_UNIQUE),
|
|
gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO),
|
|
gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS),
|
|
ok.
|
|
|
|
code_change(_OldVsn, State, _Extra) -> {ok, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%%% Internal functions
|
|
%%--------------------------------------------------------------------
|
|
|
|
do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
|
|
From, To, Packet, DefRoomOpts) ->
|
|
{AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access,
|
|
case acl:match_rule(ServerHost, AccessRoute, From) of
|
|
allow ->
|
|
do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
|
|
From, To, Packet, DefRoomOpts);
|
|
deny ->
|
|
Lang = xmpp:get_lang(Packet),
|
|
ErrText = <<"Access denied by service policy">>,
|
|
Err = xmpp:err_forbidden(ErrText, Lang),
|
|
ejabberd_router:route_error(To, From, Packet, Err)
|
|
end.
|
|
|
|
do_route1(_Host, _ServerHost, _Access, _HistorySize, _RoomShaper,
|
|
From, #jid{luser = <<"">>, lresource = <<"">>} = To,
|
|
#iq{} = IQ, _DefRoomOpts) ->
|
|
ejabberd_local:process_iq(From, To, 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} = Access,
|
|
if Type == error ->
|
|
ok;
|
|
true ->
|
|
case acl:match_rule(ServerHost, AccessAdmin, From) of
|
|
allow ->
|
|
Msg = xmpp:get_text(Body),
|
|
broadcast_service_message(Host, Msg);
|
|
deny ->
|
|
ErrText = <<"Only service administrators are allowed "
|
|
"to send service messages">>,
|
|
Err = xmpp:make_error(
|
|
Packet, xmpp:err_forbidden(ErrText, Lang)),
|
|
ejabberd_router:route(To, From, Err)
|
|
end
|
|
end;
|
|
do_route1(_Host, _ServerHost, _Access, _HistorySize, _RoomShaper,
|
|
From, #jid{luser = <<"">>} = To, Packet, _DefRoomOpts) ->
|
|
Err = xmpp:err_item_not_found(),
|
|
ejabberd_router:route_error(To, From, Packet, Err);
|
|
do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
|
|
From, To, Packet, DefRoomOpts) ->
|
|
{_AccessRoute, AccessCreate, _AccessAdmin, _AccessPersistent} = Access,
|
|
{Room, _, Nick} = jid:tolower(To),
|
|
case mnesia:dirty_read(muc_online_room, {Room, Host}) of
|
|
[] ->
|
|
case Packet of
|
|
#presence{type = available, lang = Lang} ->
|
|
case check_user_can_create_room(
|
|
ServerHost, AccessCreate, From, Room) and
|
|
check_create_roomid(ServerHost, Room) of
|
|
true ->
|
|
{ok, Pid} = start_new_room(
|
|
Host, ServerHost, Access,
|
|
Room, HistorySize,
|
|
RoomShaper, From, Nick, DefRoomOpts),
|
|
register_room(Host, Room, Pid),
|
|
mod_muc_room:route(Pid, From, Nick, Packet),
|
|
ok;
|
|
false ->
|
|
ErrText = <<"Room creation is denied by service policy">>,
|
|
Err = xmpp:make_error(
|
|
Packet, xmpp:err_forbidden(ErrText, Lang)),
|
|
ejabberd_router:route(To, From, Err)
|
|
end;
|
|
_ ->
|
|
Lang = xmpp:get_lang(Packet),
|
|
ErrText = <<"Conference room does not exist">>,
|
|
Err = xmpp:err_item_not_found(ErrText, Lang),
|
|
ejabberd_router:route_error(To, From, Packet, Err)
|
|
end;
|
|
[R] ->
|
|
Pid = R#muc_online_room.pid,
|
|
?DEBUG("MUC: send to process ~p~n", [Pid]),
|
|
mod_muc_room:route(Pid, From, Nick, Packet),
|
|
ok
|
|
end.
|
|
|
|
-spec process_vcard(iq()) -> iq().
|
|
process_vcard(#iq{type = get, lang = Lang} = IQ) ->
|
|
Desc = translate:translate(Lang, <<"ejabberd MUC module">>),
|
|
Copyright = <<"Copyright (c) 2003-2016 ProcessOne">>,
|
|
xmpp:make_iq_result(
|
|
IQ, #vcard_temp{fn = <<"ejabberd/mod_muc">>,
|
|
url = ?EJABBERD_URI,
|
|
desc = <<Desc/binary, $\n, Copyright/binary>>});
|
|
process_vcard(#iq{type = set, lang = Lang} = IQ) ->
|
|
Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
|
|
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)).
|
|
|
|
-spec process_register(iq()) -> iq().
|
|
process_register(#iq{type = get, from = From, to = To, lang = Lang} = IQ) ->
|
|
Host = To#jid.lserver,
|
|
ServerHost = ejabberd_router:host_of_route(Host),
|
|
xmpp:make_iq_result(IQ, iq_get_register_info(ServerHost, Host, From, Lang));
|
|
process_register(#iq{type = set, from = From, to = To,
|
|
lang = Lang, sub_els = [El]} = IQ) ->
|
|
Host = To#jid.lserver,
|
|
ServerHost = ejabberd_router:host_of_route(Host),
|
|
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.
|
|
|
|
-spec process_disco_info(iq()) -> iq().
|
|
process_disco_info(#iq{type = set, lang = Lang} = IQ) ->
|
|
Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
|
|
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
|
|
process_disco_info(#iq{type = get, to = To, lang = Lang,
|
|
sub_els = [#disco_info{node = <<"">>}]} = IQ) ->
|
|
ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
|
|
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];
|
|
false -> []
|
|
end,
|
|
Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
|
|
?NS_REGISTER, ?NS_MUC, ?NS_RSM,
|
|
?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE | MAMFeatures],
|
|
Identity = #identity{category = <<"conference">>,
|
|
type = <<"text">>,
|
|
name = translate:translate(Lang, <<"Chatrooms">>)},
|
|
xmpp:make_iq_result(
|
|
IQ, #disco_info{features = Features,
|
|
identities = [Identity],
|
|
xdata = X});
|
|
process_disco_info(#iq{type = get, lang = Lang} = IQ) ->
|
|
xmpp:make_error(IQ, xmpp:err_item_not_found(<<"No info available">>, Lang)).
|
|
|
|
-spec process_disco_items(iq()) -> iq().
|
|
process_disco_items(#iq{type = set, lang = Lang} = IQ) ->
|
|
Txt = <<"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,
|
|
xmpp:make_iq_result(
|
|
IQ, #disco_items{node = Node,
|
|
items = iq_disco_items(Host, From, Lang, Node, RSM)}).
|
|
|
|
-spec process_muc_unique(iq()) -> iq().
|
|
process_muc_unique(#iq{type = set, lang = Lang} = IQ) ->
|
|
Txt = <<"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} = IQ) ->
|
|
Name = p1_sha:sha(term_to_binary([From, p1_time_compat:timestamp(),
|
|
randoms: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 = <<"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} = IQ) ->
|
|
Host = To#jid.lserver,
|
|
ServerHost = ejabberd_router:host_of_route(Host),
|
|
RoomJIDs = get_subscribed_rooms(ServerHost, Host, From),
|
|
xmpp:make_iq_result(IQ, #muc_subscriptions{list = RoomJIDs}).
|
|
|
|
check_user_can_create_room(ServerHost, AccessCreate,
|
|
From, _RoomID) ->
|
|
case acl:match_rule(ServerHost, AccessCreate, From) of
|
|
allow -> true;
|
|
_ -> false
|
|
end.
|
|
|
|
check_create_roomid(ServerHost, RoomID) ->
|
|
Max = gen_mod:get_module_opt(ServerHost, ?MODULE, max_room_id,
|
|
fun(infinity) -> infinity;
|
|
(I) when is_integer(I), I>0 -> I
|
|
end, infinity),
|
|
Regexp = gen_mod:get_module_opt(ServerHost, ?MODULE, regexp_room_id,
|
|
fun iolist_to_binary/1, ""),
|
|
(byte_size(RoomID) =< Max) and
|
|
(re:run(RoomID, Regexp, [unicode, {capture, none}]) == match).
|
|
|
|
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) ->
|
|
lists:foreach(
|
|
fun(R) ->
|
|
{Room, Host} = R#muc_room.name_host,
|
|
case mnesia:dirty_read(muc_online_room, {Room, Host}) of
|
|
[] ->
|
|
{ok, Pid} = mod_muc_room:start(Host,
|
|
ServerHost, Access, Room,
|
|
HistorySize, RoomShaper,
|
|
R#muc_room.opts),
|
|
register_room(Host, Room, Pid);
|
|
_ -> ok
|
|
end
|
|
end,
|
|
get_rooms(ServerHost, Host)).
|
|
|
|
start_new_room(Host, ServerHost, Access, Room,
|
|
HistorySize, RoomShaper, From,
|
|
Nick, DefRoomOpts) ->
|
|
case restore_room(ServerHost, Host, Room) of
|
|
error ->
|
|
?DEBUG("MUC: open new room '~s'~n", [Room]),
|
|
mod_muc_room:start(Host, ServerHost, Access, Room,
|
|
HistorySize, RoomShaper,
|
|
From, Nick, DefRoomOpts);
|
|
Opts ->
|
|
?DEBUG("MUC: restore room '~s'~n", [Room]),
|
|
mod_muc_room:start(Host, ServerHost, Access, Room,
|
|
HistorySize, RoomShaper, Opts)
|
|
end.
|
|
|
|
register_room(Host, Room, Pid) ->
|
|
F = fun() ->
|
|
mnesia:write(#muc_online_room{name_host = {Room, Host},
|
|
pid = Pid})
|
|
end,
|
|
mnesia:transaction(F).
|
|
|
|
iq_disco_items(Host, From, Lang, <<"">>, undefined) ->
|
|
Rooms = get_vh_rooms(Host),
|
|
case erlang:length(Rooms) < ?MAX_ROOMS_DISCOITEMS of
|
|
true ->
|
|
iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang});
|
|
false ->
|
|
iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, undefined)
|
|
end;
|
|
iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, undefined) ->
|
|
Empty = #disco_item{jid = jid:make(<<"conference.localhost">>),
|
|
node = <<"emptyrooms">>,
|
|
name = translate:translate(Lang, <<"Empty Rooms">>)},
|
|
Query = {get_disco_item, only_non_empty, From, Lang},
|
|
[Empty | iq_disco_items_list(Host, get_vh_rooms(Host), Query)];
|
|
iq_disco_items(Host, From, Lang, <<"emptyrooms">>, undefined) ->
|
|
iq_disco_items_list(Host, get_vh_rooms(Host), {get_disco_item, 0, From, Lang});
|
|
iq_disco_items(Host, From, Lang, _DiscoNode, Rsm) ->
|
|
{Rooms, RsmO} = get_vh_rooms(Host, Rsm),
|
|
RsmOut = jlib:rsm_encode(RsmO),
|
|
iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang}) ++ RsmOut.
|
|
|
|
iq_disco_items_list(Host, Rooms, Query) ->
|
|
lists:zf(
|
|
fun(#muc_online_room{name_host = {Name, _Host}, pid = Pid}) ->
|
|
case catch gen_fsm:sync_send_all_state_event(Pid, Query, 100) of
|
|
{item, Desc} ->
|
|
flush(),
|
|
{true, #disco_item{jid = jid:make(Name, Host),
|
|
name = Desc}};
|
|
_ ->
|
|
false
|
|
end
|
|
end, Rooms).
|
|
|
|
get_vh_rooms(_, _) ->
|
|
todo.
|
|
%% get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})->
|
|
%% AllRooms = lists:sort(get_vh_rooms(Host)),
|
|
%% Count = erlang:length(AllRooms),
|
|
%% Guard = case Direction of
|
|
%% _ when Index =/= undefined -> [{'==', {element, 2, '$1'}, Host}];
|
|
%% aft -> [{'==', {element, 2, '$1'}, Host}, {'>=',{element, 1, '$1'} ,I}];
|
|
%% before when I =/= []-> [{'==', {element, 2, '$1'}, Host}, {'=<',{element, 1, '$1'} ,I}];
|
|
%% _ -> [{'==', {element, 2, '$1'}, Host}]
|
|
%% end,
|
|
%% L = lists:sort(
|
|
%% mnesia:dirty_select(muc_online_room,
|
|
%% [{#muc_online_room{name_host = '$1', _ = '_'},
|
|
%% Guard,
|
|
%% ['$_']}])),
|
|
%% L2 = if
|
|
%% Index == undefined andalso Direction == before ->
|
|
%% lists:reverse(lists:sublist(lists:reverse(L), 1, M));
|
|
%% Index == undefined ->
|
|
%% lists:sublist(L, 1, M);
|
|
%% Index > Count orelse Index < 0 ->
|
|
%% [];
|
|
%% true ->
|
|
%% lists:sublist(L, Index+1, M)
|
|
%% end,
|
|
%% if L2 == [] -> {L2, #rsm_out{count = Count}};
|
|
%% true ->
|
|
%% H = hd(L2),
|
|
%% NewIndex = get_room_pos(H, AllRooms),
|
|
%% T = lists:last(L2),
|
|
%% {F, _} = H#muc_online_room.name_host,
|
|
%% {Last, _} = T#muc_online_room.name_host,
|
|
%% {L2,
|
|
%% #rsm_out{first = F, last = Last, count = Count,
|
|
%% index = NewIndex}}
|
|
%% end.
|
|
|
|
get_subscribed_rooms(ServerHost, Host, From) ->
|
|
Rooms = get_rooms(ServerHost, Host),
|
|
lists:flatmap(
|
|
fun(#muc_room{name_host = {Name, _}, opts = Opts}) ->
|
|
Subscribers = proplists:get_value(subscribers, Opts, []),
|
|
case lists:keymember(From, 1, Subscribers) of
|
|
true -> [jid:make(Name, Host, <<>>)];
|
|
false -> []
|
|
end;
|
|
(_) ->
|
|
[]
|
|
end, Rooms).
|
|
|
|
%% @doc Return the position of desired room in the list of rooms.
|
|
%% The room must exist in the list. The count starts in 0.
|
|
%% @spec (Desired::muc_online_room(), Rooms::[muc_online_room()]) -> integer()
|
|
get_room_pos(Desired, Rooms) ->
|
|
get_room_pos(Desired, Rooms, 0).
|
|
|
|
get_room_pos(Desired, [HeadRoom | _], HeadPosition)
|
|
when Desired#muc_online_room.name_host ==
|
|
HeadRoom#muc_online_room.name_host ->
|
|
HeadPosition;
|
|
get_room_pos(Desired, [_ | Rooms], HeadPosition) ->
|
|
get_room_pos(Desired, Rooms, HeadPosition + 1).
|
|
|
|
flush() -> receive _ -> flush() after 0 -> ok 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, <<"Nickname Registration at ">>))/binary, Host/binary>>,
|
|
Inst = translate:translate(Lang, <<"Enter nickname you want to register">>),
|
|
Field = #xdata_field{type = 'text-single',
|
|
label = translate:translate(Lang, <<"Nickname">>),
|
|
var = <<"nick">>,
|
|
values = [Nick]},
|
|
X = #xdata{type = form, title = Title,
|
|
instructions = [Inst], fields = [Field]},
|
|
#register{nick = Nick,
|
|
registered = Registered,
|
|
instructions =
|
|
translate:translate(
|
|
Lang, <<"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 = <<"That nickname is registered by another "
|
|
"person">>,
|
|
{error, xmpp:err_conflict(ErrText, Lang)};
|
|
_ ->
|
|
Txt = <<"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} ->
|
|
case lists:keyfind(<<"nick">>, #xdata_field.var, Fs) of
|
|
#xdata_field{values = [N]} ->
|
|
iq_set_register_info(ServerHost, Host, From, N, Lang);
|
|
_ ->
|
|
ErrText = <<"You must fill in field \"Nickname\" in the form">>,
|
|
{error, xmpp:err_not_acceptable(ErrText, Lang)}
|
|
end;
|
|
#xdata{} ->
|
|
Txt = <<"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 = <<"You must fill in field \"Nickname\" in the form">>,
|
|
{error, xmpp:err_not_acceptable(ErrText, Lang)}
|
|
end.
|
|
|
|
broadcast_service_message(Host, Msg) ->
|
|
lists:foreach(
|
|
fun(#muc_online_room{pid = Pid}) ->
|
|
gen_fsm:send_all_state_event(
|
|
Pid, {service_message, Msg})
|
|
end, get_vh_rooms(Host)).
|
|
|
|
|
|
get_vh_rooms(Host) ->
|
|
mnesia:dirty_select(muc_online_room,
|
|
[{#muc_online_room{name_host = '$1', _ = '_'},
|
|
[{'==', {element, 2, '$1'}, Host}],
|
|
['$_']}]).
|
|
|
|
|
|
clean_table_from_bad_node(Node) ->
|
|
F = fun() ->
|
|
Es = mnesia:select(
|
|
muc_online_room,
|
|
[{#muc_online_room{pid = '$1', _ = '_'},
|
|
[{'==', {node, '$1'}, Node}],
|
|
['$_']}]),
|
|
lists:foreach(fun(E) ->
|
|
mnesia:delete_object(E)
|
|
end, Es)
|
|
end,
|
|
mnesia:async_dirty(F).
|
|
|
|
clean_table_from_bad_node(Node, Host) ->
|
|
F = fun() ->
|
|
Es = mnesia:select(
|
|
muc_online_room,
|
|
[{#muc_online_room{pid = '$1',
|
|
name_host = {'_', Host},
|
|
_ = '_'},
|
|
[{'==', {node, '$1'}, Node}],
|
|
['$_']}]),
|
|
lists:foreach(fun(E) ->
|
|
mnesia:delete_object(E)
|
|
end, Es)
|
|
end,
|
|
mnesia:async_dirty(F).
|
|
|
|
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, Subj}) ->
|
|
{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(LServer) ->
|
|
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
|
Mod:import(LServer).
|
|
|
|
import(LServer, DBType, Data) ->
|
|
Mod = gen_mod:db_mod(DBType, ?MODULE),
|
|
Mod:import(LServer, Data).
|
|
|
|
mod_opt_type(access) ->
|
|
fun acl:access_rules_validator/1;
|
|
mod_opt_type(access_admin) ->
|
|
fun acl:access_rules_validator/1;
|
|
mod_opt_type(access_create) ->
|
|
fun acl:access_rules_validator/1;
|
|
mod_opt_type(access_persistent) ->
|
|
fun acl:access_rules_validator/1;
|
|
mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
|
|
mod_opt_type(default_room_options) ->
|
|
fun (L) when is_list(L) -> L end;
|
|
mod_opt_type(history_size) ->
|
|
fun (I) when is_integer(I), I >= 0 -> I end;
|
|
mod_opt_type(host) -> fun iolist_to_binary/1;
|
|
mod_opt_type(max_room_desc) ->
|
|
fun (infinity) -> infinity;
|
|
(I) when is_integer(I), I > 0 -> I
|
|
end;
|
|
mod_opt_type(max_room_id) ->
|
|
fun (infinity) -> infinity;
|
|
(I) when is_integer(I), I > 0 -> I
|
|
end;
|
|
mod_opt_type(regexp_room_id) ->
|
|
fun iolist_to_binary/1;
|
|
mod_opt_type(max_room_name) ->
|
|
fun (infinity) -> infinity;
|
|
(I) when is_integer(I), I > 0 -> I
|
|
end;
|
|
mod_opt_type(max_user_conferences) ->
|
|
fun (I) when is_integer(I), I > 0 -> I end;
|
|
mod_opt_type(max_users) ->
|
|
fun (I) when is_integer(I), I > 0 -> I end;
|
|
mod_opt_type(max_users_admin_threshold) ->
|
|
fun (I) when is_integer(I), I > 0 -> I end;
|
|
mod_opt_type(max_users_presence) ->
|
|
fun (MUP) when is_integer(MUP) -> MUP end;
|
|
mod_opt_type(min_message_interval) ->
|
|
fun (MMI) when is_number(MMI) -> MMI end;
|
|
mod_opt_type(min_presence_interval) ->
|
|
fun (I) when is_number(I), I >= 0 -> I end;
|
|
mod_opt_type(room_shaper) ->
|
|
fun (A) when is_atom(A) -> A end;
|
|
mod_opt_type(user_message_shaper) ->
|
|
fun (A) when is_atom(A) -> A end;
|
|
mod_opt_type(user_presence_shaper) ->
|
|
fun (A) when is_atom(A) -> A end;
|
|
mod_opt_type(_) ->
|
|
[access, access_admin, access_create, access_persistent,
|
|
db_type, default_room_options, history_size, host,
|
|
max_room_desc, max_room_id, max_room_name, regexp_room_id,
|
|
max_user_conferences, max_users,
|
|
max_users_admin_threshold, max_users_presence,
|
|
min_message_interval, min_presence_interval,
|
|
room_shaper, user_message_shaper, user_presence_shaper].
|