Packages
ejabberd
16.2.0
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/mod_muc.erl
%%%----------------------------------------------------------------------
%%% File : mod_muc.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : MUC support (XEP-0045)
%%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-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_iq_disco_items/4,
broadcast_service_message/2,
export/1,
import/1,
import/3,
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]).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("jlib.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).
%%====================================================================
%% 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}.
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),
store_room(LServer, Host, Name, Opts,
gen_mod:db_type(LServer, ?MODULE)).
store_room(_LServer, Host, Name, Opts, mnesia) ->
F = fun () ->
mnesia:write(#muc_room{name_host = {Name, Host},
opts = Opts})
end,
mnesia:transaction(F);
store_room(_LServer, Host, Name, Opts, riak) ->
{atomic, ejabberd_riak:put(#muc_room{name_host = {Name, Host},
opts = Opts},
muc_room_schema())};
store_room(LServer, Host, Name, Opts, odbc) ->
SName = ejabberd_odbc:escape(Name),
SHost = ejabberd_odbc:escape(Host),
SOpts = ejabberd_odbc:encode_term(Opts),
F = fun () ->
odbc_queries:update_t(<<"muc_room">>,
[<<"name">>, <<"host">>, <<"opts">>],
[SName, SHost, SOpts],
[<<"name='">>, SName, <<"' and host='">>,
SHost, <<"'">>])
end,
ejabberd_odbc:sql_transaction(LServer, F).
restore_room(ServerHost, Host, Name) ->
LServer = jid:nameprep(ServerHost),
restore_room(LServer, Host, Name,
gen_mod:db_type(LServer, ?MODULE)).
restore_room(_LServer, Host, Name, mnesia) ->
case catch mnesia:dirty_read(muc_room, {Name, Host}) of
[#muc_room{opts = Opts}] -> Opts;
_ -> error
end;
restore_room(_LServer, Host, Name, riak) ->
case ejabberd_riak:get(muc_room, muc_room_schema(), {Name, Host}) of
{ok, #muc_room{opts = Opts}} -> Opts;
_ -> error
end;
restore_room(LServer, Host, Name, odbc) ->
SName = ejabberd_odbc:escape(Name),
SHost = ejabberd_odbc:escape(Host),
case catch ejabberd_odbc:sql_query(LServer,
[<<"select opts from muc_room where name='">>,
SName, <<"' and host='">>, SHost,
<<"';">>])
of
{selected, [<<"opts">>], [[Opts]]} ->
opts_to_binary(ejabberd_odbc:decode_term(Opts));
_ -> error
end.
forget_room(ServerHost, Host, Name) ->
LServer = jid:nameprep(ServerHost),
forget_room(LServer, Host, Name,
gen_mod:db_type(LServer, ?MODULE)).
forget_room(LServer, Host, Name, mnesia) ->
remove_room_mam(LServer, Host, Name),
F = fun () -> mnesia:delete({muc_room, {Name, Host}})
end,
mnesia:transaction(F);
forget_room(LServer, Host, Name, riak) ->
remove_room_mam(LServer, Host, Name),
{atomic, ejabberd_riak:delete(muc_room, {Name, Host})};
forget_room(LServer, Host, Name, odbc) ->
remove_room_mam(LServer, Host, Name),
SName = ejabberd_odbc:escape(Name),
SHost = ejabberd_odbc:escape(Host),
F = fun () ->
ejabberd_odbc:sql_query_t([<<"delete from muc_room where name='">>,
SName, <<"' and host='">>, SHost,
<<"';">>])
end,
ejabberd_odbc:sql_transaction(LServer, F).
remove_room_mam(LServer, Host, Name) ->
case gen_mod:is_loaded(LServer, mod_mam) of
true ->
U = jid:nodeprep(Name),
S = jid:nameprep(Host),
DBType = gen_mod:db_type(LServer, mod_mam),
if DBType == odbc ->
mod_mam:remove_user(jid:to_string({U, S, <<>>}),
LServer, DBType);
true ->
mod_mam:remove_user(U, S, DBType)
end;
false ->
ok
end.
process_iq_disco_items(Host, From, To,
#iq{lang = Lang} = IQ) ->
Rsm = jlib:rsm_decode(IQ),
DiscoNode = fxml:get_tag_attr_s(<<"node">>, IQ#iq.sub_el),
Res = IQ#iq{type = result,
sub_el =
[#xmlel{name = <<"query">>,
attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}],
children = iq_disco_items(Host, From, Lang, DiscoNode, Rsm)}]},
ejabberd_router:route(To, From, jlib:iq_to_xml(Res)).
can_use_nick(_ServerHost, _Host, _JID, <<"">>) -> false;
can_use_nick(ServerHost, Host, JID, Nick) ->
LServer = jid:nameprep(ServerHost),
can_use_nick(LServer, Host, JID, Nick,
gen_mod:db_type(LServer, ?MODULE)).
can_use_nick(_LServer, Host, JID, Nick, mnesia) ->
{LUser, LServer, _} = jid:tolower(JID),
LUS = {LUser, LServer},
case catch mnesia:dirty_select(muc_registered,
[{#muc_registered{us_host = '$1',
nick = Nick, _ = '_'},
[{'==', {element, 2, '$1'}, Host}],
['$_']}])
of
{'EXIT', _Reason} -> true;
[] -> true;
[#muc_registered{us_host = {U, _Host}}] -> U == LUS
end;
can_use_nick(LServer, Host, JID, Nick, riak) ->
{LUser, LServer, _} = jid:tolower(JID),
LUS = {LUser, LServer},
case ejabberd_riak:get_by_index(muc_registered,
muc_registered_schema(),
<<"nick_host">>, {Nick, Host}) of
{ok, []} ->
true;
{ok, [#muc_registered{us_host = {U, _Host}}]} ->
U == LUS;
{error, _} ->
true
end;
can_use_nick(LServer, Host, JID, Nick, odbc) ->
SJID =
jid:to_string(jid:tolower(jid:remove_resource(JID))),
SNick = ejabberd_odbc:escape(Nick),
SHost = ejabberd_odbc:escape(Host),
case catch ejabberd_odbc:sql_query(LServer,
[<<"select jid from muc_registered ">>,
<<"where nick='">>, SNick,
<<"' and host='">>, SHost, <<"';">>])
of
{selected, [<<"jid">>], [[SJID1]]} -> SJID == SJID1;
_ -> true
end.
%%====================================================================
%% gen_server callbacks
%%====================================================================
init([Host, Opts]) ->
MyHost = gen_mod:get_opt_host(Host, Opts,
<<"conference.@HOST@">>),
case gen_mod:db_type(Host, Opts) of
mnesia ->
mnesia:create_table(muc_room,
[{disc_copies, [node()]},
{attributes,
record_info(fields, muc_room)}]),
mnesia:create_table(muc_registered,
[{disc_copies, [node()]},
{attributes,
record_info(fields, muc_registered)}]),
update_tables(MyHost),
mnesia:add_table_index(muc_registered, nick);
_ ->
ok
end,
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(A) when is_atom(A) -> A end, all),
AccessCreate = gen_mod:get_opt(access_create, Opts,
fun(A) when is_atom(A) -> A end, all),
AccessAdmin = gen_mod:get_opt(access_admin, Opts,
fun(A) when is_atom(A) -> A end,
none),
AccessPersistent = gen_mod:get_opt(access_persistent, Opts,
fun(A) when is_atom(A) -> A end,
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),
ejabberd_router:register_route(MyHost),
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) ->
ejabberd_router:unregister_route(State#state.host),
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);
_ ->
#xmlel{attrs = Attrs} = Packet,
Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs),
ErrText = <<"Access denied by service policy">>,
Err = jlib:make_error_reply(Packet,
?ERRT_FORBIDDEN(Lang, ErrText)),
ejabberd_router:route_error(To, From, Err, Packet)
end.
do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
From, To, Packet, DefRoomOpts) ->
{_AccessRoute, AccessCreate, AccessAdmin, _AccessPersistent} = Access,
{Room, _, Nick} = jid:tolower(To),
#xmlel{name = Name, attrs = Attrs} = Packet,
case Room of
<<"">> ->
case Nick of
<<"">> ->
case Name of
<<"iq">> ->
case jlib:iq_query_info(Packet) of
#iq{type = get, xmlns = (?NS_DISCO_INFO) = XMLNS,
sub_el = _SubEl, lang = Lang} =
IQ ->
Info = ejabberd_hooks:run_fold(disco_info,
ServerHost, [],
[ServerHost, ?MODULE,
<<"">>, <<"">>]),
Res = IQ#iq{type = result,
sub_el =
[#xmlel{name = <<"query">>,
attrs =
[{<<"xmlns">>, XMLNS}],
children =
iq_disco_info(
ServerHost, Lang) ++
Info}]},
ejabberd_router:route(To, From,
jlib:iq_to_xml(Res));
#iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ ->
spawn(?MODULE, process_iq_disco_items,
[Host, From, To, IQ]);
#iq{type = get, xmlns = (?NS_REGISTER) = XMLNS,
lang = Lang, sub_el = _SubEl} =
IQ ->
Res = IQ#iq{type = result,
sub_el =
[#xmlel{name = <<"query">>,
attrs =
[{<<"xmlns">>, XMLNS}],
children =
iq_get_register_info(ServerHost,
Host,
From,
Lang)}]},
ejabberd_router:route(To, From,
jlib:iq_to_xml(Res));
#iq{type = set, xmlns = (?NS_REGISTER) = XMLNS,
lang = Lang, sub_el = SubEl} =
IQ ->
case process_iq_register_set(ServerHost, Host, From,
SubEl, Lang)
of
{result, IQRes} ->
Res = IQ#iq{type = result,
sub_el =
[#xmlel{name = <<"query">>,
attrs =
[{<<"xmlns">>,
XMLNS}],
children = IQRes}]},
ejabberd_router:route(To, From,
jlib:iq_to_xml(Res));
{error, Error} ->
Err = jlib:make_error_reply(Packet, Error),
ejabberd_router:route(To, From, Err)
end;
#iq{type = get, xmlns = (?NS_VCARD) = XMLNS,
lang = Lang, sub_el = _SubEl} =
IQ ->
Res = IQ#iq{type = result,
sub_el =
[#xmlel{name = <<"vCard">>,
attrs =
[{<<"xmlns">>, XMLNS}],
children =
iq_get_vcard(Lang)}]},
ejabberd_router:route(To, From,
jlib:iq_to_xml(Res));
#iq{type = get, xmlns = ?NS_MUC_UNIQUE} = IQ ->
Res = IQ#iq{type = result,
sub_el =
[#xmlel{name = <<"unique">>,
attrs =
[{<<"xmlns">>,
?NS_MUC_UNIQUE}],
children =
[iq_get_unique(From)]}]},
ejabberd_router:route(To, From,
jlib:iq_to_xml(Res));
#iq{} ->
Err = jlib:make_error_reply(Packet,
?ERR_FEATURE_NOT_IMPLEMENTED),
ejabberd_router:route(To, From, Err);
_ -> ok
end;
<<"message">> ->
case fxml:get_attr_s(<<"type">>, Attrs) of
<<"error">> -> ok;
_ ->
case acl:match_rule(ServerHost, AccessAdmin, From)
of
allow ->
Msg = fxml:get_path_s(Packet,
[{elem, <<"body">>},
cdata]),
broadcast_service_message(Host, Msg);
_ ->
Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs),
ErrText =
<<"Only service administrators are allowed "
"to send service messages">>,
Err = jlib:make_error_reply(Packet,
?ERRT_FORBIDDEN(Lang,
ErrText)),
ejabberd_router:route(To, From, Err)
end
end;
<<"presence">> -> ok
end;
_ ->
case fxml:get_attr_s(<<"type">>, Attrs) of
<<"error">> -> ok;
<<"result">> -> ok;
_ ->
Err = jlib:make_error_reply(Packet,
?ERR_ITEM_NOT_FOUND),
ejabberd_router:route(To, From, Err)
end
end;
_ ->
case mnesia:dirty_read(muc_online_room, {Room, Host}) of
[] ->
Type = fxml:get_attr_s(<<"type">>, Attrs),
case {Name, Type} of
{<<"presence">>, <<"">>} ->
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 ->
Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs),
ErrText = <<"Room creation is denied by service policy">>,
Err = jlib:make_error_reply(
Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
ejabberd_router:route(To, From, Err)
end;
_ ->
Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs),
ErrText = <<"Conference room does not exist">>,
Err = jlib:make_error_reply(Packet,
?ERRT_ITEM_NOT_FOUND(Lang, ErrText)),
ejabberd_router:route(To, From, 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
end.
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),
get_rooms(LServer, Host,
gen_mod:db_type(LServer, ?MODULE)).
get_rooms(_LServer, Host, mnesia) ->
case catch mnesia:dirty_select(muc_room,
[{#muc_room{name_host = {'_', Host},
_ = '_'},
[], ['$_']}])
of
{'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), [];
Rs -> Rs
end;
get_rooms(_LServer, Host, riak) ->
case ejabberd_riak:get(muc_room, muc_room_schema()) of
{ok, Rs} ->
lists:filter(
fun(#muc_room{name_host = {_, H}}) ->
Host == H
end, Rs);
_Err ->
[]
end;
get_rooms(LServer, Host, odbc) ->
SHost = ejabberd_odbc:escape(Host),
case catch ejabberd_odbc:sql_query(LServer,
[<<"select name, opts from muc_room ">>,
<<"where host='">>, SHost, <<"';">>])
of
{selected, [<<"name">>, <<"opts">>], RoomOpts} ->
lists:map(fun ([Room, Opts]) ->
#muc_room{name_host = {Room, Host},
opts = opts_to_binary(
ejabberd_odbc:decode_term(Opts))}
end,
RoomOpts);
Err -> ?ERROR_MSG("failed to get rooms: ~p", [Err]), []
end.
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_info(ServerHost, Lang) ->
[#xmlel{name = <<"identity">>,
attrs =
[{<<"category">>, <<"conference">>},
{<<"type">>, <<"text">>},
{<<"name">>,
translate:translate(Lang, <<"Chatrooms">>)}],
children = []},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_DISCO_INFO}], children = []},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_DISCO_ITEMS}], children = []},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_MUC}], children = []},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_MUC_UNIQUE}], children = []},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_REGISTER}], children = []},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_RSM}], children = []},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_VCARD}], children = []}] ++
case gen_mod:is_loaded(ServerHost, mod_mam) of
true ->
[#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_MAM_TMP}]},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_MAM_0}]},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_MAM_1}]}];
false ->
[]
end.
iq_disco_items(Host, From, Lang, <<>>, none) ->
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">>, none)
end;
iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, none) ->
XmlEmpty = #xmlel{name = <<"item">>,
attrs =
[{<<"jid">>, <<"conference.localhost">>},
{<<"node">>, <<"emptyrooms">>},
{<<"name">>, translate:translate(Lang, <<"Empty Rooms">>)}],
children = []},
Query = {get_disco_item, only_non_empty, From, Lang},
[XmlEmpty | iq_disco_items_list(Host, get_vh_rooms(Host), Query)];
iq_disco_items(Host, From, Lang, <<"emptyrooms">>, none) ->
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,
#xmlel{name = <<"item">>,
attrs =
[{<<"jid">>,
jid:to_string({Name, Host,
<<"">>})},
{<<"name">>, Desc}],
children = []}};
_ -> false
end
end, Rooms).
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.
%% @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.
-define(XFIELD(Type, Label, Var, Val),
#xmlel{name = <<"field">>,
attrs =
[{<<"type">>, Type},
{<<"label">>, translate:translate(Lang, Label)},
{<<"var">>, Var}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children = [{xmlcdata, Val}]}]}).
iq_get_unique(From) ->
{xmlcdata,
p1_sha:sha(term_to_binary([From, p1_time_compat:timestamp(),
randoms:get_string()]))}.
get_nick(ServerHost, Host, From) ->
LServer = jid:nameprep(ServerHost),
get_nick(LServer, Host, From,
gen_mod:db_type(LServer, ?MODULE)).
get_nick(_LServer, Host, From, mnesia) ->
{LUser, LServer, _} = jid:tolower(From),
LUS = {LUser, LServer},
case catch mnesia:dirty_read(muc_registered,
{LUS, Host})
of
{'EXIT', _Reason} -> error;
[] -> error;
[#muc_registered{nick = Nick}] -> Nick
end;
get_nick(LServer, Host, From, riak) ->
{LUser, LServer, _} = jid:tolower(From),
US = {LUser, LServer},
case ejabberd_riak:get(muc_registered,
muc_registered_schema(),
{US, Host}) of
{ok, #muc_registered{nick = Nick}} -> Nick;
{error, _} -> error
end;
get_nick(LServer, Host, From, odbc) ->
SJID =
ejabberd_odbc:escape(jid:to_string(jid:tolower(jid:remove_resource(From)))),
SHost = ejabberd_odbc:escape(Host),
case catch ejabberd_odbc:sql_query(LServer,
[<<"select nick from muc_registered where "
"jid='">>,
SJID, <<"' and host='">>, SHost,
<<"';">>])
of
{selected, [<<"nick">>], [[Nick]]} -> Nick;
_ -> error
end.
iq_get_register_info(ServerHost, Host, From, Lang) ->
{Nick, Registered} = case get_nick(ServerHost, Host,
From)
of
error -> {<<"">>, []};
N ->
{N,
[#xmlel{name = <<"registered">>, attrs = [],
children = []}]}
end,
Registered ++
[#xmlel{name = <<"instructions">>, attrs = [],
children =
[{xmlcdata,
translate:translate(Lang,
<<"You need a client that supports x:data "
"to register the nickname">>)}]},
#xmlel{name = <<"x">>,
attrs = [{<<"xmlns">>, ?NS_XDATA},
{<<"type">>, <<"form">>}],
children =
[#xmlel{name = <<"title">>, attrs = [],
children =
[{xmlcdata,
<<(translate:translate(Lang,
<<"Nickname Registration at ">>))/binary,
Host/binary>>}]},
#xmlel{name = <<"instructions">>, attrs = [],
children =
[{xmlcdata,
translate:translate(Lang,
<<"Enter nickname you want to register">>)}]},
?XFIELD(<<"text-single">>, <<"Nickname">>, <<"nick">>,
Nick)]}].
set_nick(ServerHost, Host, From, Nick) ->
LServer = jid:nameprep(ServerHost),
set_nick(LServer, Host, From, Nick,
gen_mod:db_type(LServer, ?MODULE)).
set_nick(_LServer, Host, From, Nick, mnesia) ->
{LUser, LServer, _} = jid:tolower(From),
LUS = {LUser, LServer},
F = fun () ->
case Nick of
<<"">> ->
mnesia:delete({muc_registered, {LUS, Host}}), ok;
_ ->
Allow = case mnesia:select(muc_registered,
[{#muc_registered{us_host =
'$1',
nick = Nick,
_ = '_'},
[{'==', {element, 2, '$1'},
Host}],
['$_']}])
of
[] -> true;
[#muc_registered{us_host = {U, _Host}}] ->
U == LUS
end,
if Allow ->
mnesia:write(#muc_registered{us_host = {LUS, Host},
nick = Nick}),
ok;
true -> false
end
end
end,
mnesia:transaction(F);
set_nick(LServer, Host, From, Nick, riak) ->
{LUser, LServer, _} = jid:tolower(From),
LUS = {LUser, LServer},
{atomic,
case Nick of
<<"">> ->
ejabberd_riak:delete(muc_registered, {LUS, Host});
_ ->
Allow = case ejabberd_riak:get_by_index(
muc_registered,
muc_registered_schema(),
<<"nick_host">>, {Nick, Host}) of
{ok, []} ->
true;
{ok, [#muc_registered{us_host = {U, _Host}}]} ->
U == LUS;
{error, _} ->
false
end,
if Allow ->
ejabberd_riak:put(#muc_registered{us_host = {LUS, Host},
nick = Nick},
muc_registered_schema(),
[{'2i', [{<<"nick_host">>,
{Nick, Host}}]}]);
true ->
false
end
end};
set_nick(LServer, Host, From, Nick, odbc) ->
JID =
jid:to_string(jid:tolower(jid:remove_resource(From))),
SJID = ejabberd_odbc:escape(JID),
SNick = ejabberd_odbc:escape(Nick),
SHost = ejabberd_odbc:escape(Host),
F = fun () ->
case Nick of
<<"">> ->
ejabberd_odbc:sql_query_t([<<"delete from muc_registered where ">>,
<<"jid='">>, SJID,
<<"' and host='">>, Host,
<<"';">>]),
ok;
_ ->
Allow = case
ejabberd_odbc:sql_query_t([<<"select jid from muc_registered ">>,
<<"where nick='">>,
SNick,
<<"' and host='">>,
SHost, <<"';">>])
of
{selected, [<<"jid">>], [[J]]} -> J == JID;
_ -> true
end,
if Allow ->
odbc_queries:update_t(<<"muc_registered">>,
[<<"jid">>, <<"host">>,
<<"nick">>],
[SJID, SHost, SNick],
[<<"jid='">>, SJID,
<<"' and host='">>, SHost,
<<"'">>]),
ok;
true -> false
end
end
end,
ejabberd_odbc:sql_transaction(LServer, F).
iq_set_register_info(ServerHost, Host, From, Nick,
Lang) ->
case set_nick(ServerHost, Host, From, Nick) of
{atomic, ok} -> {result, []};
{atomic, false} ->
ErrText = <<"That nickname is registered by another "
"person">>,
{error, ?ERRT_CONFLICT(Lang, ErrText)};
_ -> {error, ?ERR_INTERNAL_SERVER_ERROR}
end.
process_iq_register_set(ServerHost, Host, From, SubEl,
Lang) ->
#xmlel{children = Els} = SubEl,
case fxml:get_subtag(SubEl, <<"remove">>) of
false ->
case fxml:remove_cdata(Els) of
[#xmlel{name = <<"x">>} = XEl] ->
case {fxml:get_tag_attr_s(<<"xmlns">>, XEl),
fxml:get_tag_attr_s(<<"type">>, XEl)}
of
{?NS_XDATA, <<"cancel">>} -> {result, []};
{?NS_XDATA, <<"submit">>} ->
XData = jlib:parse_xdata_submit(XEl),
case XData of
invalid -> {error, ?ERR_BAD_REQUEST};
_ ->
case lists:keysearch(<<"nick">>, 1, XData) of
{value, {_, [Nick]}} when Nick /= <<"">> ->
iq_set_register_info(ServerHost, Host, From,
Nick, Lang);
_ ->
ErrText =
<<"You must fill in field \"Nickname\" "
"in the form">>,
{error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}
end
end;
_ -> {error, ?ERR_BAD_REQUEST}
end;
_ -> {error, ?ERR_BAD_REQUEST}
end;
_ ->
iq_set_register_info(ServerHost, Host, From, <<"">>,
Lang)
end.
iq_get_vcard(Lang) ->
[#xmlel{name = <<"FN">>, attrs = [],
children = [{xmlcdata, <<"ejabberd/mod_muc">>}]},
#xmlel{name = <<"URL">>, attrs = [],
children = [{xmlcdata, ?EJABBERD_URI}]},
#xmlel{name = <<"DESC">>, attrs = [],
children =
[{xmlcdata,
<<(translate:translate(Lang,
<<"ejabberd MUC module">>))/binary,
"\nCopyright (c) 2003-2016 ProcessOne">>}]}].
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).
update_tables(Host) ->
update_muc_room_table(Host),
update_muc_registered_table(Host).
muc_room_schema() ->
{record_info(fields, muc_room), #muc_room{}}.
muc_registered_schema() ->
{record_info(fields, muc_registered), #muc_registered{}}.
update_muc_room_table(_Host) ->
Fields = record_info(fields, muc_room),
case mnesia:table_info(muc_room, attributes) of
Fields ->
ejabberd_config:convert_table_to_binary(
muc_room, Fields, set,
fun(#muc_room{name_host = {N, _}}) -> N end,
fun(#muc_room{name_host = {N, H},
opts = Opts} = R) ->
R#muc_room{name_host = {iolist_to_binary(N),
iolist_to_binary(H)},
opts = opts_to_binary(Opts)}
end);
_ ->
?INFO_MSG("Recreating muc_room table", []),
mnesia:transform_table(muc_room, ignore, Fields)
end.
update_muc_registered_table(_Host) ->
Fields = record_info(fields, muc_registered),
case mnesia:table_info(muc_registered, attributes) of
Fields ->
ejabberd_config:convert_table_to_binary(
muc_registered, Fields, set,
fun(#muc_registered{us_host = {_, H}}) -> H end,
fun(#muc_registered{us_host = {{U, S}, H},
nick = Nick} = R) ->
R#muc_registered{us_host = {{iolist_to_binary(U),
iolist_to_binary(S)},
iolist_to_binary(H)},
nick = iolist_to_binary(Nick)}
end);
_ ->
?INFO_MSG("Recreating muc_registered table", []),
mnesia:transform_table(muc_registered, ignore, Fields)
end.
export(_Server) ->
[{muc_room,
fun(Host, #muc_room{name_host = {Name, RoomHost}, opts = Opts}) ->
case str:suffix(Host, RoomHost) of
true ->
SName = ejabberd_odbc:escape(Name),
SRoomHost = ejabberd_odbc:escape(RoomHost),
SOpts = ejabberd_odbc:encode_term(Opts),
[[<<"delete from muc_room where name='">>, SName,
<<"' and host='">>, SRoomHost, <<"';">>],
[<<"insert into muc_room(name, host, opts) ",
"values (">>,
<<"'">>, SName, <<"', '">>, SRoomHost,
<<"', '">>, SOpts, <<"');">>]];
false ->
[]
end
end},
{muc_registered,
fun(Host, #muc_registered{us_host = {{U, S}, RoomHost},
nick = Nick}) ->
case str:suffix(Host, RoomHost) of
true ->
SJID = ejabberd_odbc:escape(
jid:to_string(
jid:make(U, S, <<"">>))),
SNick = ejabberd_odbc:escape(Nick),
SRoomHost = ejabberd_odbc:escape(RoomHost),
[[<<"delete from muc_registered where jid='">>,
SJID, <<"' and host='">>, SRoomHost, <<"';">>],
[<<"insert into muc_registered(jid, host, "
"nick) values ('">>,
SJID, <<"', '">>, SRoomHost, <<"', '">>, SNick,
<<"');">>]];
false ->
[]
end
end}].
import(_LServer) ->
[{<<"select name, host, opts from muc_room;">>,
fun([Name, RoomHost, SOpts]) ->
Opts = opts_to_binary(ejabberd_odbc:decode_term(SOpts)),
#muc_room{name_host = {Name, RoomHost}, opts = Opts}
end},
{<<"select jid, host, nick from muc_registered;">>,
fun([J, RoomHost, Nick]) ->
#jid{user = U, server = S} =
jid:from_string(J),
#muc_registered{us_host = {{U, S}, RoomHost},
nick = Nick}
end}].
import(_LServer, mnesia, #muc_room{} = R) ->
mnesia:dirty_write(R);
import(_LServer, mnesia, #muc_registered{} = R) ->
mnesia:dirty_write(R);
import(_LServer, riak, #muc_room{} = R) ->
ejabberd_riak:put(R, muc_room_schema());
import(_LServer, riak,
#muc_registered{us_host = {_, Host}, nick = Nick} = R) ->
ejabberd_riak:put(R, muc_registered_schema(),
[{'2i', [{<<"nick_host">>, {Nick, Host}}]}]);
import(_, _, _) ->
pass.
mod_opt_type(access) ->
fun (A) when is_atom(A) -> A end;
mod_opt_type(access_admin) ->
fun (A) when is_atom(A) -> A end;
mod_opt_type(access_create) ->
fun (A) when is_atom(A) -> A end;
mod_opt_type(access_persistent) ->
fun (A) when is_atom(A) -> A end;
mod_opt_type(db_type) -> fun gen_mod:v_db/1;
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].