24
1
mirror of https://github.com/processone/ejabberd.git synced 2024-06-02 21:17:12 +02:00
xmpp.chapril.org-ejabberd/src/mod_muc_room.erl
Holger Weiss 7c7e51b6e8 Omit warning message regarding non-anonymous room
Previous versions of XEP-0045 suggested sending a warning message to new
occupants of a non-anonymous MUC room.  The current revision (1.25) says
that a status code of "100" must be returned with the user's initial
presence, instead.  We already do this (in addition to generating the
warning message).

Receiving the warning message each time the client joins the room can
become annoying, especially when reconnections occur frequently (e.g.,
on mobile devices).  So, we omit it, now.
2015-06-28 14:05:55 +02:00

4497 lines
142 KiB
Erlang

%%%----------------------------------------------------------------------
%%% File : mod_muc_room.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : MUC room stuff
%%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2015 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_room).
-author('alexey@process-one.net').
-behaviour(gen_fsm).
%% External exports
-export([start_link/9,
start_link/7,
start/9,
start/7,
route/4]).
%% gen_fsm callbacks
-export([init/1,
normal_state/2,
handle_event/3,
handle_sync_event/4,
handle_info/3,
terminate/3,
code_change/4]).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("jlib.hrl").
-include("mod_muc_room.hrl").
-define(MAX_USERS_DEFAULT_LIST,
[5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]).
-define(DEFAULT_MAX_USERS_PRESENCE,1000).
%-define(DBGFSM, true).
-ifdef(DBGFSM).
-define(FSMOPTS, [{debug, [trace]}]).
-else.
-define(FSMOPTS, []).
-endif.
%% Module start with or without supervisor:
-ifdef(NO_TRANSIENT_SUPERVISORS).
-define(SUPERVISOR_START,
gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
RoomShaper, Creator, Nick, DefRoomOpts],
?FSMOPTS)).
-else.
-define(SUPERVISOR_START,
Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup),
supervisor:start_child(
Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper,
Creator, Nick, DefRoomOpts])).
-endif.
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
start(Host, ServerHost, Access, Room, HistorySize, RoomShaper,
Creator, Nick, DefRoomOpts) ->
?SUPERVISOR_START.
start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) ->
Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup),
supervisor:start_child(
Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper,
Opts]).
start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper,
Creator, Nick, DefRoomOpts) ->
gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
RoomShaper, Creator, Nick, DefRoomOpts],
?FSMOPTS).
start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) ->
gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
RoomShaper, Opts],
?FSMOPTS).
%%%----------------------------------------------------------------------
%%% Callback functions from gen_fsm
%%%----------------------------------------------------------------------
%%----------------------------------------------------------------------
%% Func: init/1
%% Returns: {ok, StateName, StateData} |
%% {ok, StateName, StateData, Timeout} |
%% ignore |
%% {stop, StopReason}
%%----------------------------------------------------------------------
init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, DefRoomOpts]) ->
process_flag(trap_exit, true),
Shaper = shaper:new(RoomShaper),
State = set_affiliation(Creator, owner,
#state{host = Host, server_host = ServerHost,
access = Access, room = Room,
history = lqueue_new(HistorySize),
jid = jlib:make_jid(Room, Host, <<"">>),
just_created = true,
room_shaper = Shaper}),
State1 = set_opts(DefRoomOpts, State),
if (State1#state.config)#config.persistent ->
mod_muc:store_room(State1#state.server_host,
State1#state.host,
State1#state.room,
make_opts(State1));
true -> ok
end,
?INFO_MSG("Created MUC room ~s@~s by ~s",
[Room, Host, jlib:jid_to_string(Creator)]),
add_to_log(room_existence, created, State1),
add_to_log(room_existence, started, State1),
{ok, normal_state, State1};
init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) ->
process_flag(trap_exit, true),
Shaper = shaper:new(RoomShaper),
State = set_opts(Opts, #state{host = Host,
server_host = ServerHost,
access = Access,
room = Room,
history = lqueue_new(HistorySize),
jid = jlib:make_jid(Room, Host, <<"">>),
room_shaper = Shaper}),
add_to_log(room_existence, started, State),
{ok, normal_state, State}.
%%----------------------------------------------------------------------
%% Func: StateName/2
%% Returns: {next_state, NextStateName, NextStateData} |
%% {next_state, NextStateName, NextStateData, Timeout} |
%% {stop, Reason, NewStateData}
%%----------------------------------------------------------------------
normal_state({route, From, <<"">>,
#xmlel{name = <<"message">>, attrs = Attrs,
children = Els} =
Packet},
StateData) ->
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
case is_user_online(From, StateData) orelse
is_user_allowed_message_nonparticipant(From, StateData)
of
true ->
case xml:get_attr_s(<<"type">>, Attrs) of
<<"groupchat">> ->
Activity = get_user_activity(From, StateData),
Now = now_to_usec(now()),
MinMessageInterval =
trunc(gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, min_message_interval, fun(MMI) when is_number(MMI) -> MMI end, 0)
* 1000000),
Size = element_size(Packet),
{MessageShaper, MessageShaperInterval} =
shaper:update(Activity#activity.message_shaper, Size),
if Activity#activity.message /= undefined ->
ErrText = <<"Traffic rate limit is exceeded">>,
Err = jlib:make_error_reply(Packet,
?ERRT_RESOURCE_CONSTRAINT(Lang,
ErrText)),
ejabberd_router:route(StateData#state.jid, From, Err),
{next_state, normal_state, StateData};
Now >=
Activity#activity.message_time + MinMessageInterval,
MessageShaperInterval == 0 ->
{RoomShaper, RoomShaperInterval} =
shaper:update(StateData#state.room_shaper, Size),
RoomQueueEmpty =
queue:is_empty(StateData#state.room_queue),
if RoomShaperInterval == 0, RoomQueueEmpty ->
NewActivity = Activity#activity{message_time =
Now,
message_shaper =
MessageShaper},
StateData1 = store_user_activity(From,
NewActivity,
StateData),
StateData2 = StateData1#state{room_shaper =
RoomShaper},
process_groupchat_message(From, Packet,
StateData2);
true ->
StateData1 = if RoomQueueEmpty ->
erlang:send_after(RoomShaperInterval,
self(),
process_room_queue),
StateData#state{room_shaper =
RoomShaper};
true -> StateData
end,
NewActivity = Activity#activity{message_time =
Now,
message_shaper =
MessageShaper,
message = Packet},
RoomQueue = queue:in({message, From},
StateData#state.room_queue),
StateData2 = store_user_activity(From,
NewActivity,
StateData1),
StateData3 = StateData2#state{room_queue =
RoomQueue},
{next_state, normal_state, StateData3}
end;
true ->
MessageInterval = (Activity#activity.message_time +
MinMessageInterval
- Now)
div 1000,
Interval = lists:max([MessageInterval,
MessageShaperInterval]),
erlang:send_after(Interval, self(),
{process_user_message, From}),
NewActivity = Activity#activity{message = Packet,
message_shaper =
MessageShaper},
StateData1 = store_user_activity(From, NewActivity,
StateData),
{next_state, normal_state, StateData1}
end;
<<"error">> ->
case is_user_online(From, StateData) of
true ->
ErrorText = <<"This participant is kicked from the "
"room because he sent an error message">>,
NewState = expulse_participant(Packet, From, StateData,
translate:translate(Lang,
ErrorText)),
close_room_if_temporary_and_empty(NewState);
_ -> {next_state, normal_state, StateData}
end;
<<"chat">> ->
ErrText =
<<"It is not allowed to send private messages "
"to the conference">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_ACCEPTABLE(Lang,
ErrText)),
ejabberd_router:route(StateData#state.jid, From, Err),
{next_state, normal_state, StateData};
Type when (Type == <<"">>) or (Type == <<"normal">>) ->
IsInvitation = is_invitation(Els),
IsVoiceRequest = is_voice_request(Els) and
is_visitor(From, StateData),
IsVoiceApprovement = is_voice_approvement(Els) and
not is_visitor(From, StateData),
if IsInvitation ->
case catch check_invitation(From, Els, Lang, StateData)
of
{error, Error} ->
Err = jlib:make_error_reply(Packet, Error),
ejabberd_router:route(StateData#state.jid, From, Err),
{next_state, normal_state, StateData};
IJID ->
Config = StateData#state.config,
case Config#config.members_only of
true ->
case get_affiliation(IJID, StateData) of
none ->
NSD = set_affiliation(IJID, member,
StateData),
case
(NSD#state.config)#config.persistent
of
true ->
mod_muc:store_room(NSD#state.server_host,
NSD#state.host,
NSD#state.room,
make_opts(NSD));
_ -> ok
end,
{next_state, normal_state, NSD};
_ -> {next_state, normal_state, StateData}
end;
false -> {next_state, normal_state, StateData}
end
end;
IsVoiceRequest ->
NewStateData = case
(StateData#state.config)#config.allow_voice_requests
of
true ->
MinInterval =
(StateData#state.config)#config.voice_request_min_interval,
BareFrom =
jlib:jid_remove_resource(jlib:jid_tolower(From)),
NowPriority = -now_to_usec(now()),
CleanPriority = NowPriority +
MinInterval *
1000000,
Times =
clean_treap(StateData#state.last_voice_request_time,
CleanPriority),
case treap:lookup(BareFrom, Times)
of
error ->
Times1 =
treap:insert(BareFrom,
NowPriority,
true, Times),
NSD =
StateData#state{last_voice_request_time
=
Times1},
send_voice_request(From, NSD),
NSD;
{ok, _, _} ->
ErrText =
<<"Please, wait for a while before sending "
"new voice request">>,
Err =
jlib:make_error_reply(Packet,
?ERRT_NOT_ACCEPTABLE(Lang,
ErrText)),
ejabberd_router:route(StateData#state.jid,
From, Err),
StateData#state{last_voice_request_time
= Times}
end;
false ->
ErrText =
<<"Voice requests are disabled in this "
"conference">>,
Err = jlib:make_error_reply(Packet,
?ERRT_FORBIDDEN(Lang,
ErrText)),
ejabberd_router:route(StateData#state.jid,
From, Err),
StateData
end,
{next_state, normal_state, NewStateData};
IsVoiceApprovement ->
NewStateData = case is_moderator(From, StateData) of
true ->
case
extract_jid_from_voice_approvement(Els)
of
error ->
ErrText =
<<"Failed to extract JID from your voice "
"request approval">>,
Err =
jlib:make_error_reply(Packet,
?ERRT_BAD_REQUEST(Lang,
ErrText)),
ejabberd_router:route(StateData#state.jid,
From, Err),
StateData;
{ok, TargetJid} ->
case is_visitor(TargetJid,
StateData)
of
true ->
Reason = <<>>,
NSD =
set_role(TargetJid,
participant,
StateData),
catch
send_new_presence(TargetJid,
Reason,
NSD),
NSD;
_ -> StateData
end
end;
_ ->
ErrText =
<<"Only moderators can approve voice requests">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_ALLOWED(Lang,
ErrText)),
ejabberd_router:route(StateData#state.jid,
From, Err),
StateData
end,
{next_state, normal_state, NewStateData};
true -> {next_state, normal_state, StateData}
end;
_ ->
ErrText = <<"Improper message type">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_ACCEPTABLE(Lang,
ErrText)),
ejabberd_router:route(StateData#state.jid, From, Err),
{next_state, normal_state, StateData}
end;
_ ->
case xml:get_attr_s(<<"type">>, Attrs) of
<<"error">> -> ok;
_ ->
handle_roommessage_from_nonparticipant(Packet, Lang,
StateData, From)
end,
{next_state, normal_state, StateData}
end;
normal_state({route, From, <<"">>,
#xmlel{name = <<"iq">>} = Packet},
StateData) ->
case jlib:iq_query_info(Packet) of
#iq{type = Type, xmlns = XMLNS, lang = Lang,
sub_el = #xmlel{name = SubElName} = SubEl} =
IQ
when (XMLNS == (?NS_MUC_ADMIN)) or
(XMLNS == (?NS_MUC_OWNER))
or (XMLNS == (?NS_DISCO_INFO))
or (XMLNS == (?NS_DISCO_ITEMS))
or (XMLNS == (?NS_VCARD))
or (XMLNS == (?NS_CAPTCHA)) ->
Res1 = case XMLNS of
?NS_MUC_ADMIN ->
process_iq_admin(From, Type, Lang, SubEl, StateData);
?NS_MUC_OWNER ->
process_iq_owner(From, Type, Lang, SubEl, StateData);
?NS_DISCO_INFO ->
process_iq_disco_info(From, Type, Lang, StateData);
?NS_DISCO_ITEMS ->
process_iq_disco_items(From, Type, Lang, StateData);
?NS_VCARD ->
process_iq_vcard(From, Type, Lang, SubEl, StateData);
?NS_CAPTCHA ->
process_iq_captcha(From, Type, Lang, SubEl, StateData)
end,
{IQRes, NewStateData} = case Res1 of
{result, Res, SD} ->
{IQ#iq{type = result,
sub_el =
[#xmlel{name = SubElName,
attrs =
[{<<"xmlns">>,
XMLNS}],
children = Res}]},
SD};
{error, Error} ->
{IQ#iq{type = error,
sub_el = [SubEl, Error]},
StateData}
end,
ejabberd_router:route(StateData#state.jid, From,
jlib:iq_to_xml(IQRes)),
case NewStateData of
stop -> {stop, normal, StateData};
_ -> {next_state, normal_state, NewStateData}
end;
reply -> {next_state, normal_state, StateData};
_ ->
Err = jlib:make_error_reply(Packet,
?ERR_FEATURE_NOT_IMPLEMENTED),
ejabberd_router:route(StateData#state.jid, From, Err),
{next_state, normal_state, StateData}
end;
normal_state({route, From, Nick,
#xmlel{name = <<"presence">>} = Packet},
StateData) ->
Activity = get_user_activity(From, StateData),
Now = now_to_usec(now()),
MinPresenceInterval =
trunc(gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, min_presence_interval,
fun(I) when is_number(I), I>=0 ->
I
end, 0)
* 1000000),
if (Now >=
Activity#activity.presence_time + MinPresenceInterval)
and (Activity#activity.presence == undefined) ->
NewActivity = Activity#activity{presence_time = Now},
StateData1 = store_user_activity(From, NewActivity,
StateData),
process_presence(From, Nick, Packet, StateData1);
true ->
if Activity#activity.presence == undefined ->
Interval = (Activity#activity.presence_time +
MinPresenceInterval
- Now)
div 1000,
erlang:send_after(Interval, self(),
{process_user_presence, From});
true -> ok
end,
NewActivity = Activity#activity{presence =
{Nick, Packet}},
StateData1 = store_user_activity(From, NewActivity,
StateData),
{next_state, normal_state, StateData1}
end;
normal_state({route, From, ToNick,
#xmlel{name = <<"message">>, attrs = Attrs} = Packet},
StateData) ->
Type = xml:get_attr_s(<<"type">>, Attrs),
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
case decide_fate_message(Type, Packet, From, StateData)
of
{expulse_sender, Reason} ->
?DEBUG(Reason, []),
ErrorText = <<"This participant is kicked from the "
"room because he sent an error message "
"to another participant">>,
NewState = expulse_participant(Packet, From, StateData,
translate:translate(Lang, ErrorText)),
{next_state, normal_state, NewState};
forget_message -> {next_state, normal_state, StateData};
continue_delivery ->
case
{(StateData#state.config)#config.allow_private_messages,
is_user_online(From, StateData)}
of
{true, true} ->
case Type of
<<"groupchat">> ->
ErrText =
<<"It is not allowed to send private messages "
"of type \"groupchat\"">>,
Err = jlib:make_error_reply(Packet,
?ERRT_BAD_REQUEST(Lang,
ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
ToNick),
From, Err);
_ ->
case find_jids_by_nick(ToNick, StateData) of
false ->
ErrText =
<<"Recipient is not in the conference room">>,
Err = jlib:make_error_reply(Packet,
?ERRT_ITEM_NOT_FOUND(Lang,
ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
ToNick),
From, Err);
ToJIDs ->
SrcIsVisitor = is_visitor(From, StateData),
DstIsModerator = is_moderator(hd(ToJIDs),
StateData),
PmFromVisitors =
(StateData#state.config)#config.allow_private_messages_from_visitors,
if SrcIsVisitor == false;
PmFromVisitors == anyone;
(PmFromVisitors == moderators) and
DstIsModerator ->
{ok, #user{nick = FromNick}} =
(?DICT):find(jlib:jid_tolower(From),
StateData#state.users),
FromNickJID =
jlib:jid_replace_resource(StateData#state.jid,
FromNick),
X = #xmlel{name = <<"x">>,
attrs = [{<<"xmlns">>, ?NS_MUC_USER}]},
PrivMsg = xml:append_subtags(Packet, [X]),
[ejabberd_router:route(FromNickJID, ToJID, PrivMsg)
|| ToJID <- ToJIDs];
true ->
ErrText =
<<"It is not allowed to send private messages">>,
Err = jlib:make_error_reply(Packet,
?ERRT_FORBIDDEN(Lang,
ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
ToNick),
From, Err)
end
end
end;
{true, false} ->
ErrText =
<<"Only occupants are allowed to send messages "
"to the conference">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_ACCEPTABLE(Lang,
ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
ToNick),
From, Err);
{false, _} ->
ErrText =
<<"It is not allowed to send private messages">>,
Err = jlib:make_error_reply(Packet,
?ERRT_FORBIDDEN(Lang, ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
ToNick),
From, Err)
end,
{next_state, normal_state, StateData}
end;
normal_state({route, From, ToNick,
#xmlel{name = <<"iq">>, attrs = Attrs} = Packet},
StateData) ->
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
StanzaId = xml:get_attr_s(<<"id">>, Attrs),
case {(StateData#state.config)#config.allow_query_users,
is_user_online_iq(StanzaId, From, StateData)}
of
{true, {true, NewId, FromFull}} ->
case find_jid_by_nick(ToNick, StateData) of
false ->
case jlib:iq_query_info(Packet) of
reply -> ok;
_ ->
ErrText = <<"Recipient is not in the conference room">>,
Err = jlib:make_error_reply(Packet,
?ERRT_ITEM_NOT_FOUND(Lang,
ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
ToNick),
From, Err)
end;
ToJID ->
{ok, #user{nick = FromNick}} =
(?DICT):find(jlib:jid_tolower(FromFull),
StateData#state.users),
{ToJID2, Packet2} = handle_iq_vcard(FromFull, ToJID,
StanzaId, NewId, Packet),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
FromNick),
ToJID2, Packet2)
end;
{_, {false, _, _}} ->
case jlib:iq_query_info(Packet) of
reply -> ok;
_ ->
ErrText =
<<"Only occupants are allowed to send queries "
"to the conference">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_ACCEPTABLE(Lang,
ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
ToNick),
From, Err)
end;
_ ->
case jlib:iq_query_info(Packet) of
reply -> ok;
_ ->
ErrText = <<"Queries to the conference members are "
"not allowed in this room">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_ALLOWED(Lang, ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
ToNick),
From, Err)
end
end,
{next_state, normal_state, StateData};
normal_state(_Event, StateData) ->
{next_state, normal_state, StateData}.
%%----------------------------------------------------------------------
%% Func: handle_event/3
%% Returns: {next_state, NextStateName, NextStateData} |
%% {next_state, NextStateName, NextStateData, Timeout} |
%% {stop, Reason, NewStateData}
%%----------------------------------------------------------------------
handle_event({service_message, Msg}, _StateName,
StateData) ->
MessagePkt = #xmlel{name = <<"message">>,
attrs = [{<<"type">>, <<"groupchat">>}],
children =
[#xmlel{name = <<"body">>, attrs = [],
children = [{xmlcdata, Msg}]}]},
send_multiple(
StateData#state.jid,
StateData#state.server_host,
StateData#state.users,
MessagePkt),
NSD = add_message_to_history(<<"">>,
StateData#state.jid, MessagePkt, StateData),
{next_state, normal_state, NSD};
handle_event({destroy, Reason}, _StateName,
StateData) ->
{result, [], stop} = destroy_room(#xmlel{name =
<<"destroy">>,
attrs =
[{<<"xmlns">>, ?NS_MUC_OWNER}],
children =
case Reason of
none -> [];
_Else ->
[#xmlel{name =
<<"reason">>,
attrs = [],
children =
[{xmlcdata,
Reason}]}]
end},
StateData),
?INFO_MSG("Destroyed MUC room ~s with reason: ~p",
[jlib:jid_to_string(StateData#state.jid), Reason]),
add_to_log(room_existence, destroyed, StateData),
{stop, shutdown, StateData};
handle_event(destroy, StateName, StateData) ->
?INFO_MSG("Destroyed MUC room ~s",
[jlib:jid_to_string(StateData#state.jid)]),
handle_event({destroy, none}, StateName, StateData);
handle_event({set_affiliations, Affiliations},
StateName, StateData) ->
{next_state, StateName,
StateData#state{affiliations = Affiliations}};
handle_event(_Event, StateName, StateData) ->
{next_state, StateName, StateData}.
%%----------------------------------------------------------------------
%% Func: handle_sync_event/4
%% Returns: {next_state, NextStateName, NextStateData} |
%% {next_state, NextStateName, NextStateData, Timeout} |
%% {reply, Reply, NextStateName, NextStateData} |
%% {reply, Reply, NextStateName, NextStateData, Timeout} |
%% {stop, Reason, NewStateData} |
%% {stop, Reason, Reply, NewStateData}
%%----------------------------------------------------------------------
handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) ->
Reply = get_roomdesc_reply(JID, StateData,
get_roomdesc_tail(StateData, Lang)),
{reply, Reply, StateName, StateData};
handle_sync_event(get_config, _From, StateName,
StateData) ->
{reply, {ok, StateData#state.config}, StateName,
StateData};
handle_sync_event(get_state, _From, StateName,
StateData) ->
{reply, {ok, StateData}, StateName, StateData};
handle_sync_event({change_config, Config}, _From,
StateName, StateData) ->
{result, [], NSD} = change_config(Config, StateData),
{reply, {ok, NSD#state.config}, StateName, NSD};
handle_sync_event({change_state, NewStateData}, _From,
StateName, _StateData) ->
{reply, {ok, NewStateData}, StateName, NewStateData};
handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) ->
NSD = process_item_change(Item, StateData, UJID),
{reply, {ok, NSD}, StateName, NSD};
handle_sync_event(_Event, _From, StateName,
StateData) ->
Reply = ok, {reply, Reply, StateName, StateData}.
code_change(_OldVsn, StateName, StateData, _Extra) ->
{ok, StateName, StateData}.
%%----------------------------------------------------------------------
%% Func: handle_info/3
%% Returns: {next_state, NextStateName, NextStateData} |
%% {next_state, NextStateName, NextStateData, Timeout} |
%% {stop, Reason, NewStateData}
%%----------------------------------------------------------------------
handle_info({process_user_presence, From}, normal_state = _StateName, StateData) ->
RoomQueueEmpty = queue:is_empty(StateData#state.room_queue),
RoomQueue = queue:in({presence, From}, StateData#state.room_queue),
StateData1 = StateData#state{room_queue = RoomQueue},
if RoomQueueEmpty ->
StateData2 = prepare_room_queue(StateData1),
{next_state, normal_state, StateData2};
true -> {next_state, normal_state, StateData1}
end;
handle_info({process_user_message, From},
normal_state = _StateName, StateData) ->
RoomQueueEmpty =
queue:is_empty(StateData#state.room_queue),
RoomQueue = queue:in({message, From},
StateData#state.room_queue),
StateData1 = StateData#state{room_queue = RoomQueue},
if RoomQueueEmpty ->
StateData2 = prepare_room_queue(StateData1),
{next_state, normal_state, StateData2};
true -> {next_state, normal_state, StateData1}
end;
handle_info(process_room_queue,
normal_state = StateName, StateData) ->
case queue:out(StateData#state.room_queue) of
{{value, {message, From}}, RoomQueue} ->
Activity = get_user_activity(From, StateData),
Packet = Activity#activity.message,
NewActivity = Activity#activity{message = undefined},
StateData1 = store_user_activity(From, NewActivity,
StateData),
StateData2 = StateData1#state{room_queue = RoomQueue},
StateData3 = prepare_room_queue(StateData2),
process_groupchat_message(From, Packet, StateData3);
{{value, {presence, From}}, RoomQueue} ->
Activity = get_user_activity(From, StateData),
{Nick, Packet} = Activity#activity.presence,
NewActivity = Activity#activity{presence = undefined},
StateData1 = store_user_activity(From, NewActivity,
StateData),
StateData2 = StateData1#state{room_queue = RoomQueue},
StateData3 = prepare_room_queue(StateData2),
process_presence(From, Nick, Packet, StateData3);
{empty, _} -> {next_state, StateName, StateData}
end;
handle_info({captcha_succeed, From}, normal_state,
StateData) ->
NewState = case (?DICT):find(From,
StateData#state.robots)
of
{ok, {Nick, Packet}} ->
Robots = (?DICT):store(From, passed,
StateData#state.robots),
add_new_user(From, Nick, Packet,
StateData#state{robots = Robots});
_ -> StateData
end,
{next_state, normal_state, NewState};
handle_info({captcha_failed, From}, normal_state,
StateData) ->
NewState = case (?DICT):find(From,
StateData#state.robots)
of
{ok, {Nick, Packet}} ->
Robots = (?DICT):erase(From, StateData#state.robots),
Err = jlib:make_error_reply(Packet,
?ERR_NOT_AUTHORIZED),
ejabberd_router:route % TODO: s/Nick/""/
(jlib:jid_replace_resource(StateData#state.jid,
Nick),
From, Err),
StateData#state{robots = Robots};
_ -> StateData
end,
{next_state, normal_state, NewState};
handle_info(shutdown, _StateName, StateData) ->
{stop, shutdown, StateData};
handle_info(_Info, StateName, StateData) ->
{next_state, StateName, StateData}.
%%----------------------------------------------------------------------
%% Func: terminate/3
%% Purpose: Shutdown the fsm
%% Returns: any
%%----------------------------------------------------------------------
terminate(Reason, _StateName, StateData) ->
?INFO_MSG("Stopping MUC room ~s@~s",
[StateData#state.room, StateData#state.host]),
ReasonT = case Reason of
shutdown ->
<<"You are being removed from the room "
"because of a system shutdown">>;
_ -> <<"Room terminates">>
end,
ItemAttrs = [{<<"affiliation">>, <<"none">>},
{<<"role">>, <<"none">>}],
ReasonEl = #xmlel{name = <<"reason">>, attrs = [],
children = [{xmlcdata, ReasonT}]},
Packet = #xmlel{name = <<"presence">>,
attrs = [{<<"type">>, <<"unavailable">>}],
children =
[#xmlel{name = <<"x">>,
attrs = [{<<"xmlns">>, ?NS_MUC_USER}],
children =
[#xmlel{name = <<"item">>,
attrs = ItemAttrs,
children = [ReasonEl]},
#xmlel{name = <<"status">>,
attrs = [{<<"code">>, <<"332">>}],
children = []}]}]},
(?DICT):fold(fun (LJID, Info, _) ->
Nick = Info#user.nick,
case Reason of
shutdown ->
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick),
Info#user.jid, Packet);
_ -> ok
end,
tab_remove_online_user(LJID, StateData)
end,
[], StateData#state.users),
add_to_log(room_existence, stopped, StateData),
mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(),
StateData#state.server_host),
ok.
%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
route(Pid, From, ToNick, Packet) ->
gen_fsm:send_event(Pid, {route, From, ToNick, Packet}).
process_groupchat_message(From,
#xmlel{name = <<"message">>, attrs = Attrs} = Packet,
StateData) ->
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
case is_user_online(From, StateData) orelse
is_user_allowed_message_nonparticipant(From, StateData)
of
true ->
{FromNick, Role} = get_participant_data(From,
StateData),
if (Role == moderator) or (Role == participant) or
((StateData#state.config)#config.moderated == false) ->
{NewStateData1, IsAllowed} = case check_subject(Packet)
of
false -> {StateData, true};
Subject ->
case
can_change_subject(Role,
StateData)
of
true ->
NSD =
StateData#state{subject
=
Subject,
subject_author
=
FromNick},
case
(NSD#state.config)#config.persistent
of
true ->
mod_muc:store_room(NSD#state.server_host,
NSD#state.host,
NSD#state.room,
make_opts(NSD));
_ -> ok
end,
{NSD, true};
_ -> {StateData, false}
end
end,
case IsAllowed of
true ->
send_multiple(
jlib:jid_replace_resource(StateData#state.jid, FromNick),
StateData#state.server_host,
StateData#state.users,
Packet),
NewStateData2 = case has_body_or_subject(Packet) of
true ->
add_message_to_history(FromNick, From,
Packet,
NewStateData1);
false ->
NewStateData1
end,
{next_state, normal_state, NewStateData2};
_ ->
Err = case
(StateData#state.config)#config.allow_change_subj
of
true ->
?ERRT_FORBIDDEN(Lang,
<<"Only moderators and participants are "
"allowed to change the subject in this "
"room">>);
_ ->
?ERRT_FORBIDDEN(Lang,
<<"Only moderators are allowed to change "
"the subject in this room">>)
end,
ejabberd_router:route(StateData#state.jid, From,
jlib:make_error_reply(Packet, Err)),
{next_state, normal_state, StateData}
end;
true ->
ErrText = <<"Visitors are not allowed to send messages "
"to all occupants">>,
Err = jlib:make_error_reply(Packet,
?ERRT_FORBIDDEN(Lang, ErrText)),
ejabberd_router:route(StateData#state.jid, From, Err),
{next_state, normal_state, StateData}
end;
false ->
ErrText =
<<"Only occupants are allowed to send messages "
"to the conference">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
ejabberd_router:route(StateData#state.jid, From, Err),
{next_state, normal_state, StateData}
end.
%% @doc Check if this non participant can send message to room.
%%
%% XEP-0045 v1.23:
%% 7.9 Sending a Message to All Occupants
%% an implementation MAY allow users with certain privileges
%% (e.g., a room owner, room admin, or service-level admin)
%% to send messages to the room even if those users are not occupants.
is_user_allowed_message_nonparticipant(JID,
StateData) ->
case get_service_affiliation(JID, StateData) of
owner -> true;
_ -> false
end.
%% @doc Get information of this participant, or default values.
%% If the JID is not a participant, return values for a service message.
get_participant_data(From, StateData) ->
case (?DICT):find(jlib:jid_tolower(From),
StateData#state.users)
of
{ok, #user{nick = FromNick, role = Role}} ->
{FromNick, Role};
error -> {<<"">>, moderator}
end.
process_presence(From, Nick,
#xmlel{name = <<"presence">>, attrs = Attrs} = Packet,
StateData) ->
Type = xml:get_attr_s(<<"type">>, Attrs),
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
StateData1 = case Type of
<<"unavailable">> ->
case is_user_online(From, StateData) of
true ->
NewPacket = case
{(StateData#state.config)#config.allow_visitor_status,
is_visitor(From, StateData)}
of
{false, true} ->
strip_status(Packet);
_ -> Packet
end,
NewState = add_user_presence_un(From, NewPacket,
StateData),
case (?DICT):find(Nick, StateData#state.nicks) of
{ok, [_, _ | _]} -> ok;
_ -> send_new_presence(From, NewState)
end,
Reason = case xml:get_subtag(NewPacket,
<<"status">>)
of
false -> <<"">>;
Status_el ->
xml:get_tag_cdata(Status_el)
end,
remove_online_user(From, NewState, Reason);
_ -> StateData
end;
<<"error">> ->
case is_user_online(From, StateData) of
true ->
ErrorText =
<<"This participant is kicked from the "
"room because he sent an error presence">>,
expulse_participant(Packet, From, StateData,
translate:translate(Lang,
ErrorText));
_ -> StateData
end;
<<"">> ->
case is_user_online(From, StateData) of
true ->
case is_nick_change(From, Nick, StateData) of
true ->
case {nick_collision(From, Nick, StateData),
mod_muc:can_use_nick(StateData#state.server_host,
StateData#state.host,
From, Nick),
{(StateData#state.config)#config.allow_visitor_nickchange,
is_visitor(From, StateData)}}
of
{_, _, {false, true}} ->
ErrText =
<<"Visitors are not allowed to change their "
"nicknames in this room">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_ALLOWED(Lang,
ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick),
From, Err),
StateData;
{true, _, _} ->
Lang = xml:get_attr_s(<<"xml:lang">>,
Attrs),
ErrText =
<<"That nickname is already in use by another "
"occupant">>,
Err = jlib:make_error_reply(Packet,
?ERRT_CONFLICT(Lang,
ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick), % TODO: s/Nick/""/
From, Err),
StateData;
{_, false, _} ->
ErrText =
<<"That nickname is registered by another "
"person">>,
Err = jlib:make_error_reply(Packet,
?ERRT_CONFLICT(Lang,
ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick),
From, Err),
StateData;
_ -> change_nick(From, Nick, StateData)
end;
_NotNickChange ->
Stanza = case
{(StateData#state.config)#config.allow_visitor_status,
is_visitor(From, StateData)}
of
{false, true} ->
strip_status(Packet);
_Allowed -> Packet
end,
NewState = add_user_presence(From, Stanza,
StateData),
send_new_presence(From, NewState),
NewState
end;
_ -> add_new_user(From, Nick, Packet, StateData)
end;
_ -> StateData
end,
close_room_if_temporary_and_empty(StateData1).
close_room_if_temporary_and_empty(StateData1) ->
case not (StateData1#state.config)#config.persistent
andalso (?DICT):to_list(StateData1#state.users) == []
of
true ->
?INFO_MSG("Destroyed MUC room ~s because it's temporary "
"and empty",
[jlib:jid_to_string(StateData1#state.jid)]),
add_to_log(room_existence, destroyed, StateData1),
{stop, normal, StateData1};
_ -> {next_state, normal_state, StateData1}
end.
is_user_online(JID, StateData) ->
LJID = jlib:jid_tolower(JID),
(?DICT):is_key(LJID, StateData#state.users).
%% Check if the user is occupant of the room, or at least is an admin or owner.
is_occupant_or_admin(JID, StateData) ->
FAffiliation = get_affiliation(JID, StateData),
FRole = get_role(JID, StateData),
case FRole /= none orelse
FAffiliation == member orelse
FAffiliation == admin orelse FAffiliation == owner
of
true -> true;
_ -> false
end.
%%%
%%% Handle IQ queries of vCard
%%%
is_user_online_iq(StanzaId, JID, StateData)
when JID#jid.lresource /= <<"">> ->
{is_user_online(JID, StateData), StanzaId, JID};
is_user_online_iq(StanzaId, JID, StateData)
when JID#jid.lresource == <<"">> ->
try stanzaid_unpack(StanzaId) of
{OriginalId, Resource} ->
JIDWithResource = jlib:jid_replace_resource(JID,
Resource),
{is_user_online(JIDWithResource, StateData), OriginalId,
JIDWithResource}
catch
_:_ -> {is_user_online(JID, StateData), StanzaId, JID}
end.
handle_iq_vcard(FromFull, ToJID, StanzaId, NewId,
Packet) ->
ToBareJID = jlib:jid_remove_resource(ToJID),
IQ = jlib:iq_query_info(Packet),
handle_iq_vcard2(FromFull, ToJID, ToBareJID, StanzaId,
NewId, IQ, Packet).
handle_iq_vcard2(_FromFull, ToJID, ToBareJID, StanzaId,
_NewId, #iq{type = get, xmlns = ?NS_VCARD}, Packet)
when ToBareJID /= ToJID ->
{ToBareJID, change_stanzaid(StanzaId, ToJID, Packet)};
handle_iq_vcard2(_FromFull, ToJID, _ToBareJID,
_StanzaId, NewId, _IQ, Packet) ->
{ToJID, change_stanzaid(NewId, Packet)}.
stanzaid_pack(OriginalId, Resource) ->
<<"berd",
(jlib:encode_base64(<<"ejab\000",
OriginalId/binary, "\000",
Resource/binary>>))/binary>>.
stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) ->
StanzaId = jlib:decode_base64(StanzaIdBase64),
[<<"ejab">>, OriginalId, Resource] =
str:tokens(StanzaId, <<"\000">>),
{OriginalId, Resource}.
change_stanzaid(NewId, Packet) ->
#xmlel{name = Name, attrs = Attrs, children = Els} =
jlib:remove_attr(<<"id">>, Packet),
#xmlel{name = Name, attrs = [{<<"id">>, NewId} | Attrs],
children = Els}.
change_stanzaid(PreviousId, ToJID, Packet) ->
NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource),
change_stanzaid(NewId, Packet).
%%%
%%%
role_to_list(Role) ->
case Role of
moderator -> <<"moderator">>;
participant -> <<"participant">>;
visitor -> <<"visitor">>;
none -> <<"none">>
end.
affiliation_to_list(Affiliation) ->
case Affiliation of
owner -> <<"owner">>;
admin -> <<"admin">>;
member -> <<"member">>;
outcast -> <<"outcast">>;
none -> <<"none">>
end.
list_to_role(Role) ->
case Role of
<<"moderator">> -> moderator;
<<"participant">> -> participant;
<<"visitor">> -> visitor;
<<"none">> -> none
end.
list_to_affiliation(Affiliation) ->
case Affiliation of
<<"owner">> -> owner;
<<"admin">> -> admin;
<<"member">> -> member;
<<"outcast">> -> outcast;
<<"none">> -> none
end.
%% Decide the fate of the message and its sender
%% Returns: continue_delivery | forget_message | {expulse_sender, Reason}
decide_fate_message(<<"error">>, Packet, From,
StateData) ->
PD = case check_error_kick(Packet) of
%% If this is an error stanza and its condition matches a criteria
true ->
Reason =
io_lib:format("This participant is considered a ghost "
"and is expulsed: ~s",
[jlib:jid_to_string(From)]),
{expulse_sender, Reason};
false -> continue_delivery
end,
case PD of
{expulse_sender, R} ->
case is_user_online(From, StateData) of
true -> {expulse_sender, R};
false -> forget_message
end;
Other -> Other
end;
decide_fate_message(_, _, _, _) -> continue_delivery.
%% Check if the elements of this error stanza indicate
%% that the sender is a dead participant.
%% If so, return true to kick the participant.
check_error_kick(Packet) ->
case get_error_condition(Packet) of
<<"gone">> -> true;
<<"internal-server-error">> -> true;
<<"item-not-found">> -> true;
<<"jid-malformed">> -> true;
<<"recipient-unavailable">> -> true;
<<"redirect">> -> true;
<<"remote-server-not-found">> -> true;
<<"remote-server-timeout">> -> true;
<<"service-unavailable">> -> true;
_ -> false
end.
get_error_condition(Packet) ->
case catch get_error_condition2(Packet) of
{condition, ErrorCondition} -> ErrorCondition;
{'EXIT', _} -> <<"badformed error stanza">>
end.
get_error_condition2(Packet) ->
#xmlel{children = EEls} = xml:get_subtag(Packet,
<<"error">>),
[Condition] = [Name
|| #xmlel{name = Name,
attrs = [{<<"xmlns">>, ?NS_STANZAS}],
children = []}
<- EEls],
{condition, Condition}.
expulse_participant(Packet, From, StateData, Reason1) ->
ErrorCondition = get_error_condition(Packet),
Reason2 = iolist_to_binary(
io_lib:format(binary_to_list(Reason1) ++ ": " ++ "~s",
[ErrorCondition])),
NewState = add_user_presence_un(From,
#xmlel{name = <<"presence">>,
attrs =
[{<<"type">>,
<<"unavailable">>}],
children =
[#xmlel{name = <<"status">>,
attrs = [],
children =
[{xmlcdata,
Reason2}]}]},
StateData),
send_new_presence(From, NewState),
remove_online_user(From, NewState).
set_affiliation(JID, Affiliation, StateData) ->
set_affiliation(JID, Affiliation, StateData, <<"">>).
set_affiliation(JID, Affiliation, StateData, Reason) ->
LJID = jlib:jid_remove_resource(jlib:jid_tolower(JID)),
Affiliations = case Affiliation of
none ->
(?DICT):erase(LJID, StateData#state.affiliations);
_ ->
(?DICT):store(LJID, {Affiliation, Reason},
StateData#state.affiliations)
end,
StateData#state{affiliations = Affiliations}.
get_affiliation(JID, StateData) ->
{_AccessRoute, _AccessCreate, AccessAdmin,
_AccessPersistent} =
StateData#state.access,
Res = case acl:match_rule(StateData#state.server_host,
AccessAdmin, JID)
of
allow -> owner;
_ ->
LJID = jlib:jid_tolower(JID),
case (?DICT):find(LJID, StateData#state.affiliations) of
{ok, Affiliation} -> Affiliation;
_ ->
LJID1 = jlib:jid_remove_resource(LJID),
case (?DICT):find(LJID1, StateData#state.affiliations)
of
{ok, Affiliation} -> Affiliation;
_ ->
LJID2 = setelement(1, LJID, <<"">>),
case (?DICT):find(LJID2,
StateData#state.affiliations)
of
{ok, Affiliation} -> Affiliation;
_ ->
LJID3 = jlib:jid_remove_resource(LJID2),
case (?DICT):find(LJID3,
StateData#state.affiliations)
of
{ok, Affiliation} -> Affiliation;
_ -> none
end
end
end
end
end,
case Res of
{A, _Reason} -> A;
_ -> Res
end.
get_service_affiliation(JID, StateData) ->
{_AccessRoute, _AccessCreate, AccessAdmin,
_AccessPersistent} =
StateData#state.access,
case acl:match_rule(StateData#state.server_host,
AccessAdmin, JID)
of
allow -> owner;
_ -> none
end.
set_role(JID, Role, StateData) ->
LJID = jlib:jid_tolower(JID),
LJIDs = case LJID of
{U, S, <<"">>} ->
(?DICT):fold(fun (J, _, Js) ->
case J of
{U, S, _} -> [J | Js];
_ -> Js
end
end,
[], StateData#state.users);
_ ->
case (?DICT):is_key(LJID, StateData#state.users) of
true -> [LJID];
_ -> []
end
end,
{Users, Nicks} = case Role of
none ->
lists:foldl(fun (J, {Us, Ns}) ->
NewNs = case (?DICT):find(J, Us)
of
{ok,
#user{nick = Nick}} ->
(?DICT):erase(Nick,
Ns);
_ -> Ns
end,
{(?DICT):erase(J, Us), NewNs}
end,
{StateData#state.users,
StateData#state.nicks},
LJIDs);
_ ->
{lists:foldl(fun (J, Us) ->
{ok, User} = (?DICT):find(J,
Us),
(?DICT):store(J,
User#user{role =
Role},
Us)
end,
StateData#state.users, LJIDs),
StateData#state.nicks}
end,
StateData#state{users = Users, nicks = Nicks}.
get_role(JID, StateData) ->
LJID = jlib:jid_tolower(JID),
case (?DICT):find(LJID, StateData#state.users) of
{ok, #user{role = Role}} -> Role;
_ -> none
end.
get_default_role(Affiliation, StateData) ->
case Affiliation of
owner -> moderator;
admin -> moderator;
member -> participant;
outcast -> none;
none ->
case (StateData#state.config)#config.members_only of
true -> none;
_ ->
case (StateData#state.config)#config.members_by_default
of
true -> participant;
_ -> visitor
end
end
end.
is_visitor(Jid, StateData) ->
get_role(Jid, StateData) =:= visitor.
is_moderator(Jid, StateData) ->
get_role(Jid, StateData) =:= moderator.
get_max_users(StateData) ->
MaxUsers = (StateData#state.config)#config.max_users,
ServiceMaxUsers = get_service_max_users(StateData),
if MaxUsers =< ServiceMaxUsers -> MaxUsers;
true -> ServiceMaxUsers
end.
get_service_max_users(StateData) ->
gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, max_users,
fun(I) when is_integer(I), I>0 -> I end,
?MAX_USERS_DEFAULT).
get_max_users_admin_threshold(StateData) ->
gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, max_users_admin_threshold,
fun(I) when is_integer(I), I>0 -> I end,
5).
get_user_activity(JID, StateData) ->
case treap:lookup(jlib:jid_tolower(JID),
StateData#state.activity)
of
{ok, _P, A} -> A;
error ->
MessageShaper =
shaper:new(gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, user_message_shaper,
fun(A) when is_atom(A) -> A end,
none)),
PresenceShaper =
shaper:new(gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, user_presence_shaper,
fun(A) when is_atom(A) -> A end,
none)),
#activity{message_shaper = MessageShaper,
presence_shaper = PresenceShaper}
end.
store_user_activity(JID, UserActivity, StateData) ->
MinMessageInterval =
trunc(gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, min_message_interval,
fun(I) when is_number(I), I>=0 -> I end,
0)
* 1000),
MinPresenceInterval =
trunc(gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, min_presence_interval,
fun(I) when is_number(I), I>=0 -> I end,
0)
* 1000),
Key = jlib:jid_tolower(JID),
Now = now_to_usec(now()),
Activity1 = clean_treap(StateData#state.activity,
{1, -Now}),
Activity = case treap:lookup(Key, Activity1) of
{ok, _P, _A} -> treap:delete(Key, Activity1);
error -> Activity1
end,
StateData1 = case MinMessageInterval == 0 andalso
MinPresenceInterval == 0 andalso
UserActivity#activity.message_shaper == none andalso
UserActivity#activity.presence_shaper == none
andalso
UserActivity#activity.message == undefined andalso
UserActivity#activity.presence == undefined
of
true -> StateData#state{activity = Activity};
false ->
case UserActivity#activity.message == undefined andalso
UserActivity#activity.presence == undefined
of
true ->
{_, MessageShaperInterval} =
shaper:update(UserActivity#activity.message_shaper,
100000),
{_, PresenceShaperInterval} =
shaper:update(UserActivity#activity.presence_shaper,
100000),
Delay = lists:max([MessageShaperInterval,
PresenceShaperInterval,
MinMessageInterval,
MinPresenceInterval])
* 1000,
Priority = {1, -(Now + Delay)},
StateData#state{activity =
treap:insert(Key, Priority,
UserActivity,
Activity)};
false ->
Priority = {0, 0},
StateData#state{activity =
treap:insert(Key, Priority,
UserActivity,
Activity)}
end
end,
StateData1.
clean_treap(Treap, CleanPriority) ->
case treap:is_empty(Treap) of
true -> Treap;
false ->
{_Key, Priority, _Value} = treap:get_root(Treap),
if Priority > CleanPriority ->
clean_treap(treap:delete_root(Treap), CleanPriority);
true -> Treap
end
end.
prepare_room_queue(StateData) ->
case queue:out(StateData#state.room_queue) of
{{value, {message, From}}, _RoomQueue} ->
Activity = get_user_activity(From, StateData),
Packet = Activity#activity.message,
Size = element_size(Packet),
{RoomShaper, RoomShaperInterval} =
shaper:update(StateData#state.room_shaper, Size),
erlang:send_after(RoomShaperInterval, self(),
process_room_queue),
StateData#state{room_shaper = RoomShaper};
{{value, {presence, From}}, _RoomQueue} ->
Activity = get_user_activity(From, StateData),
{_Nick, Packet} = Activity#activity.presence,
Size = element_size(Packet),
{RoomShaper, RoomShaperInterval} =
shaper:update(StateData#state.room_shaper, Size),
erlang:send_after(RoomShaperInterval, self(),
process_room_queue),
StateData#state{room_shaper = RoomShaper};
{empty, _} -> StateData
end.
add_online_user(JID, Nick, Role, StateData) ->
LJID = jlib:jid_tolower(JID),
Users = (?DICT):store(LJID,
#user{jid = JID, nick = Nick, role = Role},
StateData#state.users),
add_to_log(join, Nick, StateData),
Nicks = (?DICT):update(Nick,
fun (Entry) ->
case lists:member(LJID, Entry) of
true -> Entry;
false -> [LJID | Entry]
end
end,
[LJID], StateData#state.nicks),
tab_add_online_user(JID, StateData),
StateData#state{users = Users, nicks = Nicks}.
remove_online_user(JID, StateData) ->
remove_online_user(JID, StateData, <<"">>).
remove_online_user(JID, StateData, Reason) ->
LJID = jlib:jid_tolower(JID),
{ok, #user{nick = Nick}} = (?DICT):find(LJID,
StateData#state.users),
add_to_log(leave, {Nick, Reason}, StateData),
tab_remove_online_user(JID, StateData),
Users = (?DICT):erase(LJID, StateData#state.users),
Nicks = case (?DICT):find(Nick, StateData#state.nicks)
of
{ok, [LJID]} ->
(?DICT):erase(Nick, StateData#state.nicks);
{ok, U} ->
(?DICT):store(Nick, U -- [LJID], StateData#state.nicks);
error -> StateData#state.nicks
end,
StateData#state{users = Users, nicks = Nicks}.
filter_presence(#xmlel{name = <<"presence">>,
attrs = Attrs, children = Els}) ->
FEls = lists:filter(fun (El) ->
case El of
{xmlcdata, _} -> false;
#xmlel{attrs = Attrs1} ->
XMLNS = xml:get_attr_s(<<"xmlns">>,
Attrs1),
NS_MUC = ?NS_MUC,
Size = byte_size(NS_MUC),
case XMLNS of
<<NS_MUC:Size/binary, _/binary>> ->
false;
_ ->
true
end
end
end,
Els),
#xmlel{name = <<"presence">>, attrs = Attrs,
children = FEls}.
strip_status(#xmlel{name = <<"presence">>,
attrs = Attrs, children = Els}) ->
FEls = lists:filter(fun (#xmlel{name = <<"status">>}) ->
false;
(_) -> true
end,
Els),
#xmlel{name = <<"presence">>, attrs = Attrs,
children = FEls}.
add_user_presence(JID, Presence, StateData) ->
LJID = jlib:jid_tolower(JID),
FPresence = filter_presence(Presence),
Users = (?DICT):update(LJID,
fun (#user{} = User) ->
User#user{last_presence = FPresence}
end,
StateData#state.users),
StateData#state{users = Users}.
add_user_presence_un(JID, Presence, StateData) ->
LJID = jlib:jid_tolower(JID),
FPresence = filter_presence(Presence),
Users = (?DICT):update(LJID,
fun (#user{} = User) ->
User#user{last_presence = FPresence,
role = none}
end,
StateData#state.users),
StateData#state{users = Users}.
%% Find and return a list of the full JIDs of the users of Nick.
%% Return jid record.
find_jids_by_nick(Nick, StateData) ->
case (?DICT):find(Nick, StateData#state.nicks) of
{ok, [User]} -> [jlib:make_jid(User)];
{ok, Users} -> [jlib:make_jid(LJID) || LJID <- Users];
error -> false
end.
%% Find and return the full JID of the user of Nick with
%% highest-priority presence. Return jid record.
find_jid_by_nick(Nick, StateData) ->
case (?DICT):find(Nick, StateData#state.nicks) of
{ok, [User]} -> jlib:make_jid(User);
{ok, [FirstUser | Users]} ->
#user{last_presence = FirstPresence} =
(?DICT):fetch(FirstUser, StateData#state.users),
{LJID, _} = lists:foldl(fun (Compare,
{HighestUser, HighestPresence}) ->
#user{last_presence = P1} =
(?DICT):fetch(Compare,
StateData#state.users),
case higher_presence(P1,
HighestPresence)
of
true -> {Compare, P1};
false ->
{HighestUser, HighestPresence}
end
end,
{FirstUser, FirstPresence}, Users),
jlib:make_jid(LJID);
error -> false
end.
higher_presence(Pres1, Pres2) ->
Pri1 = get_priority_from_presence(Pres1),
Pri2 = get_priority_from_presence(Pres2),
Pri1 > Pri2.
get_priority_from_presence(PresencePacket) ->
case xml:get_subtag(PresencePacket, <<"priority">>) of
false -> 0;
SubEl ->
case catch
jlib:binary_to_integer(xml:get_tag_cdata(SubEl))
of
P when is_integer(P) -> P;
_ -> 0
end
end.
find_nick_by_jid(Jid, StateData) ->
[{_, #user{nick = Nick}}] = lists:filter(fun ({_,
#user{jid = FJid}}) ->
FJid == Jid
end,
(?DICT):to_list(StateData#state.users)),
Nick.
is_nick_change(JID, Nick, StateData) ->
LJID = jlib:jid_tolower(JID),
case Nick of
<<"">> -> false;
_ ->
{ok, #user{nick = OldNick}} = (?DICT):find(LJID,
StateData#state.users),
Nick /= OldNick
end.
nick_collision(User, Nick, StateData) ->
UserOfNick = find_jid_by_nick(Nick, StateData),
UserOfNick /= false andalso
jlib:jid_remove_resource(jlib:jid_tolower(UserOfNick))
/= jlib:jid_remove_resource(jlib:jid_tolower(User)).
add_new_user(From, Nick,
#xmlel{attrs = Attrs, children = Els} = Packet,
StateData) ->
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
MaxUsers = get_max_users(StateData),
MaxAdminUsers = MaxUsers +
get_max_users_admin_threshold(StateData),
NUsers = dict:fold(fun (_, _, Acc) -> Acc + 1 end, 0,
StateData#state.users),
Affiliation = get_affiliation(From, StateData),
ServiceAffiliation = get_service_affiliation(From,
StateData),
NConferences = tab_count_user(From),
MaxConferences =
gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, max_user_conferences,
fun(I) when is_integer(I), I>0 -> I end,
10),
Collision = nick_collision(From, Nick, StateData),
case {(ServiceAffiliation == owner orelse
(Affiliation == admin orelse Affiliation == owner)
andalso NUsers < MaxAdminUsers
orelse NUsers < MaxUsers)
andalso NConferences < MaxConferences,
Collision,
mod_muc:can_use_nick(StateData#state.server_host,
StateData#state.host, From, Nick),
get_default_role(Affiliation, StateData)}
of
{false, _, _, _} ->
Err = jlib:make_error_reply(Packet,
?ERR_SERVICE_UNAVAILABLE),
ejabberd_router:route % TODO: s/Nick/""/
(jlib:jid_replace_resource(StateData#state.jid, Nick),
From, Err),
StateData;
{_, _, _, none} ->
Err = jlib:make_error_reply(Packet,
case Affiliation of
outcast ->
ErrText =
<<"You have been banned from this room">>,
?ERRT_FORBIDDEN(Lang, ErrText);
_ ->
ErrText =
<<"Membership is required to enter this room">>,
?ERRT_REGISTRATION_REQUIRED(Lang,
ErrText)
end),
ejabberd_router:route % TODO: s/Nick/""/
(jlib:jid_replace_resource(StateData#state.jid, Nick),
From, Err),
StateData;
{_, true, _, _} ->
ErrText = <<"That nickname is already in use by another occupant">>,
Err = jlib:make_error_reply(Packet,
?ERRT_CONFLICT(Lang, ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick),
From, Err),
StateData;
{_, _, false, _} ->
ErrText = <<"That nickname is registered by another person">>,
Err = jlib:make_error_reply(Packet,
?ERRT_CONFLICT(Lang, ErrText)),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick),
From, Err),
StateData;
{_, _, _, Role} ->
case check_password(ServiceAffiliation, Affiliation,
Els, From, StateData)
of
true ->
NewState = add_user_presence(From, Packet,
add_online_user(From, Nick, Role,
StateData)),
send_existing_presences(From, NewState),
send_new_presence(From, NewState),
Shift = count_stanza_shift(Nick, Els, NewState),
case send_history(From, Shift, NewState) of
true -> ok;
_ -> send_subject(From, StateData)
end,
case NewState#state.just_created of
true -> NewState#state{just_created = false};
false ->
Robots = (?DICT):erase(From, StateData#state.robots),
NewState#state{robots = Robots}
end;
nopass ->
ErrText = <<"A password is required to enter this room">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_AUTHORIZED(Lang,
ErrText)),
ejabberd_router:route % TODO: s/Nick/""/
(jlib:jid_replace_resource(StateData#state.jid,
Nick),
From, Err),
StateData;
captcha_required ->
SID = xml:get_attr_s(<<"id">>, Attrs),
RoomJID = StateData#state.jid,
To = jlib:jid_replace_resource(RoomJID, Nick),
Limiter = {From#jid.luser, From#jid.lserver},
case ejabberd_captcha:create_captcha(SID, RoomJID, To,
Lang, Limiter, From)
of
{ok, ID, CaptchaEls} ->
MsgPkt = #xmlel{name = <<"message">>,
attrs = [{<<"id">>, ID}],
children = CaptchaEls},
Robots = (?DICT):store(From, {Nick, Packet},
StateData#state.robots),
ejabberd_router:route(RoomJID, From, MsgPkt),
StateData#state{robots = Robots};
{error, limit} ->
ErrText = <<"Too many CAPTCHA requests">>,
Err = jlib:make_error_reply(Packet,
?ERRT_RESOURCE_CONSTRAINT(Lang,
ErrText)),
ejabberd_router:route % TODO: s/Nick/""/
(jlib:jid_replace_resource(StateData#state.jid,
Nick),
From, Err),
StateData;
_ ->
ErrText = <<"Unable to generate a CAPTCHA">>,
Err = jlib:make_error_reply(Packet,
?ERRT_INTERNAL_SERVER_ERROR(Lang,
ErrText)),
ejabberd_router:route % TODO: s/Nick/""/
(jlib:jid_replace_resource(StateData#state.jid,
Nick),
From, Err),
StateData
end;
_ ->
ErrText = <<"Incorrect password">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_AUTHORIZED(Lang,
ErrText)),
ejabberd_router:route % TODO: s/Nick/""/
(jlib:jid_replace_resource(StateData#state.jid,
Nick),
From, Err),
StateData
end
end.
check_password(owner, _Affiliation, _Els, _From,
_StateData) ->
%% Don't check pass if user is owner in MUC service (access_admin option)
true;
check_password(_ServiceAffiliation, Affiliation, Els,
From, StateData) ->
case (StateData#state.config)#config.password_protected
of
false -> check_captcha(Affiliation, From, StateData);
true ->
Pass = extract_password(Els),
case Pass of
false -> nopass;
_ ->
case (StateData#state.config)#config.password of
Pass -> true;
_ -> false
end
end
end.
check_captcha(Affiliation, From, StateData) ->
case (StateData#state.config)#config.captcha_protected
andalso ejabberd_captcha:is_feature_available()
of
true when Affiliation == none ->
case (?DICT):find(From, StateData#state.robots) of
{ok, passed} -> true;
_ ->
WList =
(StateData#state.config)#config.captcha_whitelist,
#jid{luser = U, lserver = S, lresource = R} = From,
case (?SETS):is_element({U, S, R}, WList) of
true -> true;
false ->
case (?SETS):is_element({U, S, <<"">>}, WList) of
true -> true;
false ->
case (?SETS):is_element({<<"">>, S, <<"">>}, WList)
of
true -> true;
false -> captcha_required
end
end
end
end;
_ -> true
end.
extract_password([]) -> false;
extract_password([#xmlel{attrs = Attrs} = El | Els]) ->
case xml:get_attr_s(<<"xmlns">>, Attrs) of
?NS_MUC ->
case xml:get_subtag(El, <<"password">>) of
false -> false;
SubEl -> xml:get_tag_cdata(SubEl)
end;
_ -> extract_password(Els)
end;
extract_password([_ | Els]) -> extract_password(Els).
count_stanza_shift(Nick, Els, StateData) ->
HL = lqueue_to_list(StateData#state.history),
Since = extract_history(Els, <<"since">>),
Shift0 = case Since of
false -> 0;
_ ->
Sin = calendar:datetime_to_gregorian_seconds(Since),
count_seconds_shift(Sin, HL)
end,
Seconds = extract_history(Els, <<"seconds">>),
Shift1 = case Seconds of
false -> 0;
_ ->
Sec =
calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(now()))
- Seconds,
count_seconds_shift(Sec, HL)
end,
MaxStanzas = extract_history(Els, <<"maxstanzas">>),
Shift2 = case MaxStanzas of
false -> 0;
_ -> count_maxstanzas_shift(MaxStanzas, HL)
end,
MaxChars = extract_history(Els, <<"maxchars">>),
Shift3 = case MaxChars of
false -> 0;
_ -> count_maxchars_shift(Nick, MaxChars, HL)
end,
lists:max([Shift0, Shift1, Shift2, Shift3]).
count_seconds_shift(Seconds, HistoryList) ->
lists:sum(lists:map(fun ({_Nick, _Packet, _HaveSubject,
TimeStamp, _Size}) ->
T =
calendar:datetime_to_gregorian_seconds(TimeStamp),
if T < Seconds -> 1;
true -> 0
end
end,
HistoryList)).
count_maxstanzas_shift(MaxStanzas, HistoryList) ->
S = length(HistoryList) - MaxStanzas,
if S =< 0 -> 0;
true -> S
end.
count_maxchars_shift(Nick, MaxSize, HistoryList) ->
NLen = byte_size(Nick) + 1,
Sizes = lists:map(fun ({_Nick, _Packet, _HaveSubject,
_TimeStamp, Size}) ->
Size + NLen
end,
HistoryList),
calc_shift(MaxSize, Sizes).
calc_shift(MaxSize, Sizes) ->
Total = lists:sum(Sizes),
calc_shift(MaxSize, Total, 0, Sizes).
calc_shift(_MaxSize, _Size, Shift, []) -> Shift;
calc_shift(MaxSize, Size, Shift, [S | TSizes]) ->
if MaxSize >= Size -> Shift;
true -> calc_shift(MaxSize, Size - S, Shift + 1, TSizes)
end.
extract_history([], _Type) -> false;
extract_history([#xmlel{attrs = Attrs} = El | Els],
Type) ->
case xml:get_attr_s(<<"xmlns">>, Attrs) of
?NS_MUC ->
AttrVal = xml:get_path_s(El,
[{elem, <<"history">>}, {attr, Type}]),
case Type of
<<"since">> ->
case jlib:datetime_string_to_timestamp(AttrVal) of
undefined -> false;
TS -> calendar:now_to_universal_time(TS)
end;
_ ->
case catch jlib:binary_to_integer(AttrVal) of
IntVal when is_integer(IntVal) and (IntVal >= 0) ->
IntVal;
_ -> false
end
end;
_ -> extract_history(Els, Type)
end;
extract_history([_ | Els], Type) ->
extract_history(Els, Type).
is_room_overcrowded(StateData) ->
MaxUsersPresence = gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, max_users_presence,
fun(MUP) when is_integer(MUP) -> MUP end,
?DEFAULT_MAX_USERS_PRESENCE),
(?DICT):size(StateData#state.users) > MaxUsersPresence.
send_update_presence(JID, StateData) ->
send_update_presence(JID, <<"">>, StateData).
send_update_presence(JID, Reason, StateData) ->
case is_room_overcrowded(StateData) of
true -> ok;
false -> send_update_presence1(JID, Reason, StateData)
end.
send_update_presence1(JID, Reason, StateData) ->
LJID = jlib:jid_tolower(JID),
LJIDs = case LJID of
{U, S, <<"">>} ->
(?DICT):fold(fun (J, _, Js) ->
case J of
{U, S, _} -> [J | Js];
_ -> Js
end
end,
[], StateData#state.users);
_ ->
case (?DICT):is_key(LJID, StateData#state.users) of
true -> [LJID];
_ -> []
end
end,
lists:foreach(fun (J) ->
send_new_presence(J, Reason, StateData)
end,
LJIDs).
send_new_presence(NJID, StateData) ->
send_new_presence(NJID, <<"">>, StateData).
send_new_presence(NJID, Reason, StateData) ->
case is_room_overcrowded(StateData) of
true -> ok;
false -> send_new_presence1(NJID, Reason, StateData)
end.
send_new_presence1(NJID, Reason, StateData) ->
#user{nick = Nick} =
(?DICT):fetch(jlib:jid_tolower(NJID),
StateData#state.users),
LJID = find_jid_by_nick(Nick, StateData),
{ok,
#user{jid = RealJID, role = Role,
last_presence = Presence}} =
(?DICT):find(jlib:jid_tolower(LJID),
StateData#state.users),
Affiliation = get_affiliation(LJID, StateData),
SAffiliation = affiliation_to_list(Affiliation),
SRole = role_to_list(Role),
lists:foreach(fun ({_LJID, Info}) ->
ItemAttrs = case Info#user.role == moderator orelse
(StateData#state.config)#config.anonymous
== false
of
true ->
[{<<"jid">>,
jlib:jid_to_string(RealJID)},
{<<"affiliation">>, SAffiliation},
{<<"role">>, SRole}];
_ ->
[{<<"affiliation">>, SAffiliation},
{<<"role">>, SRole}]
end,
ItemEls = case Reason of
<<"">> -> [];
_ ->
[#xmlel{name = <<"reason">>,
attrs = [],
children =
[{xmlcdata, Reason}]}]
end,
Status = case StateData#state.just_created of
true ->
[#xmlel{name = <<"status">>,
attrs =
[{<<"code">>, <<"201">>}],
children = []}];
false -> []
end,
Status2 = case
(StateData#state.config)#config.anonymous
== false
andalso NJID == Info#user.jid
of
true ->
[#xmlel{name = <<"status">>,
attrs =
[{<<"code">>, <<"100">>}],
children = []}
| Status];
false -> Status
end,
Status3 = case NJID == Info#user.jid of
true ->
[#xmlel{name = <<"status">>,
attrs =
[{<<"code">>, <<"110">>}],
children = []}
| Status2];
false -> Status2
end,
Packet = xml:append_subtags(Presence,
[#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>,
?NS_MUC_USER}],
children =
[#xmlel{name =
<<"item">>,
attrs
=
ItemAttrs,
children
=
ItemEls}
| Status3]}]),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick),
Info#user.jid, Packet)
end,
(?DICT):to_list(StateData#state.users)).
send_existing_presences(ToJID, StateData) ->
case is_room_overcrowded(StateData) of
true -> ok;
false -> send_existing_presences1(ToJID, StateData)
end.
send_existing_presences1(ToJID, StateData) ->
LToJID = jlib:jid_tolower(ToJID),
{ok, #user{jid = RealToJID, role = Role}} =
(?DICT):find(LToJID, StateData#state.users),
lists:foreach(fun ({FromNick, _Users}) ->
LJID = find_jid_by_nick(FromNick, StateData),
#user{jid = FromJID, role = FromRole,
last_presence = Presence} =
(?DICT):fetch(jlib:jid_tolower(LJID),
StateData#state.users),
case RealToJID of
FromJID -> ok;
_ ->
FromAffiliation = get_affiliation(LJID,
StateData),
ItemAttrs = case Role == moderator orelse
(StateData#state.config)#config.anonymous
== false
of
true ->
[{<<"jid">>,
jlib:jid_to_string(FromJID)},
{<<"affiliation">>,
affiliation_to_list(FromAffiliation)},
{<<"role">>,
role_to_list(FromRole)}];
_ ->
[{<<"affiliation">>,
affiliation_to_list(FromAffiliation)},
{<<"role">>,
role_to_list(FromRole)}]
end,
Packet = xml:append_subtags(Presence,
[#xmlel{name =
<<"x">>,
attrs =
[{<<"xmlns">>,
?NS_MUC_USER}],
children =
[#xmlel{name
=
<<"item">>,
attrs
=
ItemAttrs,
children
=
[]}]}]),
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
FromNick),
RealToJID, Packet)
end
end,
(?DICT):to_list(StateData#state.nicks)).
now_to_usec({MSec, Sec, USec}) ->
(MSec * 1000000 + Sec) * 1000000 + USec.
change_nick(JID, Nick, StateData) ->
LJID = jlib:jid_tolower(JID),
{ok, #user{nick = OldNick}} = (?DICT):find(LJID,
StateData#state.users),
Users = (?DICT):update(LJID,
fun (#user{} = User) -> User#user{nick = Nick} end,
StateData#state.users),
OldNickUsers = (?DICT):fetch(OldNick,
StateData#state.nicks),
NewNickUsers = case (?DICT):find(Nick,
StateData#state.nicks)
of
{ok, U} -> U;
error -> []
end,
SendOldUnavailable = length(OldNickUsers) == 1,
SendNewAvailable = SendOldUnavailable orelse
NewNickUsers == [],
Nicks = case OldNickUsers of
[LJID] ->
(?DICT):store(Nick, [LJID | NewNickUsers],
(?DICT):erase(OldNick, StateData#state.nicks));
[_ | _] ->
(?DICT):store(Nick, [LJID | NewNickUsers],
(?DICT):store(OldNick, OldNickUsers -- [LJID],
StateData#state.nicks))
end,
NewStateData = StateData#state{users = Users,
nicks = Nicks},
send_nick_changing(JID, OldNick, NewStateData,
SendOldUnavailable, SendNewAvailable),
add_to_log(nickchange, {OldNick, Nick}, StateData),
NewStateData.
send_nick_changing(JID, OldNick, StateData,
SendOldUnavailable, SendNewAvailable) ->
{ok,
#user{jid = RealJID, nick = Nick, role = Role,
last_presence = Presence}} =
(?DICT):find(jlib:jid_tolower(JID),
StateData#state.users),
Affiliation = get_affiliation(JID, StateData),
SAffiliation = affiliation_to_list(Affiliation),
SRole = role_to_list(Role),
lists:foreach(fun ({_LJID, Info}) ->
ItemAttrs1 = case Info#user.role == moderator orelse
(StateData#state.config)#config.anonymous
== false
of
true ->
[{<<"jid">>,
jlib:jid_to_string(RealJID)},
{<<"affiliation">>, SAffiliation},
{<<"role">>, SRole},
{<<"nick">>, Nick}];
_ ->
[{<<"affiliation">>, SAffiliation},
{<<"role">>, SRole},
{<<"nick">>, Nick}]
end,
ItemAttrs2 = case Info#user.role == moderator orelse
(StateData#state.config)#config.anonymous
== false
of
true ->
[{<<"jid">>,
jlib:jid_to_string(RealJID)},
{<<"affiliation">>, SAffiliation},
{<<"role">>, SRole}];
_ ->
[{<<"affiliation">>, SAffiliation},
{<<"role">>, SRole}]
end,
Status110 = case JID == Info#user.jid of
true ->
[#xmlel{name = <<"status">>,
attrs = [{<<"code">>, <<"110">>}]
}];
false ->
[]
end,
Packet1 = #xmlel{name = <<"presence">>,
attrs =
[{<<"type">>,
<<"unavailable">>}],
children =
[#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>,
?NS_MUC_USER}],
children =
[#xmlel{name =
<<"item">>,
attrs =
ItemAttrs1,
children =
[]},
#xmlel{name =
<<"status">>,
attrs =
[{<<"code">>,
<<"303">>}],
children =
[]}|Status110]}]},
Packet2 = xml:append_subtags(Presence,
[#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>,
?NS_MUC_USER}],
children =
[#xmlel{name
=
<<"item">>,
attrs
=
ItemAttrs2,
children
=
[]}|Status110]}]),
if SendOldUnavailable ->
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
OldNick),
Info#user.jid, Packet1);
true -> ok
end,
if SendNewAvailable ->
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick),
Info#user.jid, Packet2);
true -> ok
end
end,
(?DICT):to_list(StateData#state.users)).
lqueue_new(Max) ->
#lqueue{queue = queue:new(), len = 0, max = Max}.
%% If the message queue limit is set to 0, do not store messages.
lqueue_in(_Item, LQ = #lqueue{max = 0}) -> LQ;
%% Otherwise, rotate messages in the queue store.
lqueue_in(Item,
#lqueue{queue = Q1, len = Len, max = Max}) ->
Q2 = queue:in(Item, Q1),
if Len >= Max ->
Q3 = lqueue_cut(Q2, Len - Max + 1),
#lqueue{queue = Q3, len = Max, max = Max};
true -> #lqueue{queue = Q2, len = Len + 1, max = Max}
end.
lqueue_cut(Q, 0) -> Q;
lqueue_cut(Q, N) ->
{_, Q1} = queue:out(Q), lqueue_cut(Q1, N - 1).
lqueue_to_list(#lqueue{queue = Q1}) ->
queue:to_list(Q1).
add_message_to_history(FromNick, FromJID, Packet, StateData) ->
HaveSubject = case xml:get_subtag(Packet, <<"subject">>)
of
false -> false;
_ -> true
end,
TimeStamp = now(),
AddrPacket = case (StateData#state.config)#config.anonymous of
true -> Packet;
false ->
Address = #xmlel{name = <<"address">>,
attrs = [{<<"type">>, <<"ofrom">>},
{<<"jid">>,
jlib:jid_to_string(FromJID)}],
children = []},
Addresses = #xmlel{name = <<"addresses">>,
attrs = [{<<"xmlns">>, ?NS_ADDRESS}],
children = [Address]},
xml:append_subtags(Packet, [Addresses])
end,
TSPacket = jlib:add_delay_info(AddrPacket, StateData#state.jid, TimeStamp),
SPacket =
jlib:replace_from_to(jlib:jid_replace_resource(StateData#state.jid,
FromNick),
StateData#state.jid, TSPacket),
Size = element_size(SPacket),
Q1 = lqueue_in({FromNick, TSPacket, HaveSubject,
calendar:now_to_universal_time(TimeStamp), Size},
StateData#state.history),
add_to_log(text, {FromNick, Packet}, StateData),
StateData#state{history = Q1}.
send_history(JID, Shift, StateData) ->
lists:foldl(fun ({Nick, Packet, HaveSubject, _TimeStamp,
_Size},
B) ->
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick),
JID, Packet),
B or HaveSubject
end,
false,
lists:nthtail(Shift,
lqueue_to_list(StateData#state.history))).
send_subject(_JID, #state{subject_author = <<"">>}) -> ok;
send_subject(JID, StateData) ->
Subject = StateData#state.subject,
Packet = #xmlel{name = <<"message">>,
attrs = [{<<"type">>, <<"groupchat">>}],
children =
[#xmlel{name = <<"subject">>, attrs = [],
children = [{xmlcdata, Subject}]}]},
ejabberd_router:route(StateData#state.jid, JID, Packet).
check_subject(Packet) ->
case xml:get_subtag(Packet, <<"subject">>) of
false -> false;
SubjEl -> xml:get_tag_cdata(SubjEl)
end.
can_change_subject(Role, StateData) ->
case (StateData#state.config)#config.allow_change_subj
of
true -> Role == moderator orelse Role == participant;
_ -> Role == moderator
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Admin stuff
process_iq_admin(From, set, Lang, SubEl, StateData) ->
#xmlel{children = Items} = SubEl,
process_admin_items_set(From, Items, Lang, StateData);
process_iq_admin(From, get, Lang, SubEl, StateData) ->
case xml:get_subtag(SubEl, <<"item">>) of
false -> {error, ?ERR_BAD_REQUEST};
Item ->
FAffiliation = get_affiliation(From, StateData),
FRole = get_role(From, StateData),
case xml:get_tag_attr(<<"role">>, Item) of
false ->
case xml:get_tag_attr(<<"affiliation">>, Item) of
false -> {error, ?ERR_BAD_REQUEST};
{value, StrAffiliation} ->
case catch list_to_affiliation(StrAffiliation) of
{'EXIT', _} -> {error, ?ERR_BAD_REQUEST};
SAffiliation ->
if (FAffiliation == owner) or
(FAffiliation == admin) or
((FAffiliation == member) and (SAffiliation == member)) ->
Items = items_with_affiliation(SAffiliation,
StateData),
{result, Items, StateData};
true ->
ErrText =
<<"Administrator privileges required">>,
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
end
end
end;
{value, StrRole} ->
case catch list_to_role(StrRole) of
{'EXIT', _} -> {error, ?ERR_BAD_REQUEST};
SRole ->
if FRole == moderator ->
Items = items_with_role(SRole, StateData),
{result, Items, StateData};
true ->
ErrText = <<"Moderator privileges required">>,
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
end
end
end
end.
items_with_role(SRole, StateData) ->
lists:map(fun ({_, U}) -> user_to_item(U, StateData)
end,
search_role(SRole, StateData)).
items_with_affiliation(SAffiliation, StateData) ->
lists:map(fun ({JID, {Affiliation, Reason}}) ->
#xmlel{name = <<"item">>,
attrs =
[{<<"affiliation">>,
affiliation_to_list(Affiliation)},
{<<"jid">>, jlib:jid_to_string(JID)}],
children =
[#xmlel{name = <<"reason">>, attrs = [],
children = [{xmlcdata, Reason}]}]};
({JID, Affiliation}) ->
#xmlel{name = <<"item">>,
attrs =
[{<<"affiliation">>,
affiliation_to_list(Affiliation)},
{<<"jid">>, jlib:jid_to_string(JID)}],
children = []}
end,
search_affiliation(SAffiliation, StateData)).
user_to_item(#user{role = Role, nick = Nick, jid = JID},
StateData) ->
Affiliation = get_affiliation(JID, StateData),
#xmlel{name = <<"item">>,
attrs =
[{<<"role">>, role_to_list(Role)},
{<<"affiliation">>, affiliation_to_list(Affiliation)},
{<<"nick">>, Nick},
{<<"jid">>, jlib:jid_to_string(JID)}],
children = []}.
search_role(Role, StateData) ->
lists:filter(fun ({_, #user{role = R}}) -> Role == R
end,
(?DICT):to_list(StateData#state.users)).
search_affiliation(Affiliation, StateData) ->
lists:filter(fun ({_, A}) ->
case A of
{A1, _Reason} -> Affiliation == A1;
_ -> Affiliation == A
end
end,
(?DICT):to_list(StateData#state.affiliations)).
process_admin_items_set(UJID, Items, Lang, StateData) ->
UAffiliation = get_affiliation(UJID, StateData),
URole = get_role(UJID, StateData),
case find_changed_items(UJID, UAffiliation, URole,
Items, Lang, StateData, [])
of
{result, Res} ->
?INFO_MSG("Processing MUC admin query from ~s in "
"room ~s:~n ~p",
[jlib:jid_to_string(UJID),
jlib:jid_to_string(StateData#state.jid), Res]),
NSD = lists:foldl(process_item_change(UJID),
StateData, lists:flatten(Res)),
case (NSD#state.config)#config.persistent of
true ->
mod_muc:store_room(NSD#state.server_host,
NSD#state.host, NSD#state.room,
make_opts(NSD));
_ -> ok
end,
{result, [], NSD};
Err -> Err
end.
process_item_change(UJID) ->
fun(E, SD) ->
process_item_change(E, SD, UJID)
end.
process_item_change(E, SD, UJID) ->
case catch case E of
{JID, affiliation, owner, _} when JID#jid.luser == <<"">> ->
%% If the provided JID does not have username,
%% forget the affiliation completely
SD;
{JID, role, none, Reason} ->
catch
send_kickban_presence(UJID, JID,
Reason,
<<"307">>,
SD),
set_role(JID, none, SD);
{JID, affiliation, none, Reason} ->
case (SD#state.config)#config.members_only of
true ->
catch
send_kickban_presence(UJID, JID,
Reason,
<<"321">>,
none,
SD),
SD1 = set_affiliation(JID, none, SD),
set_role(JID, none, SD1);
_ ->
SD1 = set_affiliation(JID, none, SD),
send_update_presence(JID, SD1),
SD1
end;
{JID, affiliation, outcast, Reason} ->
catch
send_kickban_presence(UJID, JID,
Reason,
<<"301">>,
outcast,
SD),
set_affiliation(JID,
outcast,
set_role(JID, none, SD),
Reason);
{JID, affiliation, A, Reason}
when (A == admin) or (A == owner) ->
SD1 = set_affiliation(JID, A, SD, Reason),
SD2 = set_role(JID, moderator, SD1),
send_update_presence(JID, Reason, SD2),
SD2;
{JID, affiliation, member, Reason} ->
SD1 = set_affiliation(JID, member, SD, Reason),
SD2 = set_role(JID, participant, SD1),
send_update_presence(JID, Reason, SD2),
SD2;
{JID, role, Role, Reason} ->
SD1 = set_role(JID, Role, SD),
catch
send_new_presence(JID, Reason, SD1),
SD1;
{JID, affiliation, A, _Reason} ->
SD1 = set_affiliation(JID, A, SD),
send_update_presence(JID, SD1),
SD1
end
of
{'EXIT', ErrReason} ->
?ERROR_MSG("MUC ITEMS SET ERR: ~p~n", [ErrReason]),
SD;
NSD -> NSD
end.
find_changed_items(_UJID, _UAffiliation, _URole, [],
_Lang, _StateData, Res) ->
{result, Res};
find_changed_items(UJID, UAffiliation, URole,
[{xmlcdata, _} | Items], Lang, StateData, Res) ->
find_changed_items(UJID, UAffiliation, URole, Items,
Lang, StateData, Res);
find_changed_items(UJID, UAffiliation, URole,
[#xmlel{name = <<"item">>, attrs = Attrs} = Item
| Items],
Lang, StateData, Res) ->
TJID = case xml:get_attr(<<"jid">>, Attrs) of
{value, S} ->
case jlib:string_to_jid(S) of
error ->
ErrText = iolist_to_binary(
io_lib:format(translate:translate(
Lang,
<<"Jabber ID ~s is invalid">>),
[S])),
{error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
J -> {value, [J]}
end;
_ ->
case xml:get_attr(<<"nick">>, Attrs) of
{value, N} ->
case find_jids_by_nick(N, StateData) of
false ->
ErrText = iolist_to_binary(
io_lib:format(
translate:translate(
Lang,
<<"Nickname ~s does not exist in the room">>),
[N])),
{error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
J -> {value, J}
end;
_ -> {error, ?ERR_BAD_REQUEST}
end
end,
case TJID of
{value, [JID | _] = JIDs} ->
TAffiliation = get_affiliation(JID, StateData),
TRole = get_role(JID, StateData),
case xml:get_attr(<<"role">>, Attrs) of
false ->
case xml:get_attr(<<"affiliation">>, Attrs) of
false -> {error, ?ERR_BAD_REQUEST};
{value, StrAffiliation} ->
case catch list_to_affiliation(StrAffiliation) of
{'EXIT', _} ->
ErrText1 = iolist_to_binary(
io_lib:format(
translate:translate(
Lang,
<<"Invalid affiliation: ~s">>),
[StrAffiliation])),
{error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText1)};
SAffiliation ->
ServiceAf = get_service_affiliation(JID, StateData),
CanChangeRA = case can_change_ra(UAffiliation,
URole,
TAffiliation,
TRole, affiliation,
SAffiliation,
ServiceAf)
of
nothing -> nothing;
true -> true;
check_owner ->
case search_affiliation(owner,
StateData)
of
[{OJID, _}] ->
jlib:jid_remove_resource(OJID)
/=
jlib:jid_tolower(jlib:jid_remove_resource(UJID));
_ -> true
end;
_ -> false
end,
case CanChangeRA of
nothing ->
find_changed_items(UJID, UAffiliation, URole,
Items, Lang, StateData,
Res);
true ->
Reason = xml:get_path_s(Item,
[{elem, <<"reason">>},
cdata]),
MoreRes = [{jlib:jid_remove_resource(Jidx),
affiliation, SAffiliation, Reason}
|| Jidx <- JIDs],
find_changed_items(UJID, UAffiliation, URole,
Items, Lang, StateData,
[MoreRes | Res]);
false -> {error, ?ERR_NOT_ALLOWED}
end
end
end;
{value, StrRole} ->
case catch list_to_role(StrRole) of
{'EXIT', _} ->
ErrText1 = iolist_to_binary(
io_lib:format(translate:translate(
Lang,
<<"Invalid role: ~s">>),
[StrRole])),
{error, ?ERRT_BAD_REQUEST(Lang, ErrText1)};
SRole ->
ServiceAf = get_service_affiliation(JID, StateData),
CanChangeRA = case can_change_ra(UAffiliation, URole,
TAffiliation, TRole,
role, SRole, ServiceAf)
of
nothing -> nothing;
true -> true;
check_owner ->
case search_affiliation(owner,
StateData)
of
[{OJID, _}] ->
jlib:jid_remove_resource(OJID)
/=
jlib:jid_tolower(jlib:jid_remove_resource(UJID));
_ -> true
end;
_ -> false
end,
case CanChangeRA of
nothing ->
find_changed_items(UJID, UAffiliation, URole, Items,
Lang, StateData, Res);
true ->
Reason = xml:get_path_s(Item,
[{elem, <<"reason">>},
cdata]),
MoreRes = [{Jidx, role, SRole, Reason}
|| Jidx <- JIDs],
find_changed_items(UJID, UAffiliation, URole, Items,
Lang, StateData,
[MoreRes | Res]);
_ -> {error, ?ERR_NOT_ALLOWED}
end
end
end;
Err -> Err
end;
find_changed_items(_UJID, _UAffiliation, _URole, _Items,
_Lang, _StateData, _Res) ->
{error, ?ERR_BAD_REQUEST}.
can_change_ra(_FAffiliation, _FRole, owner, _TRole,
affiliation, owner, owner) ->
%% A room owner tries to add as persistent owner a
%% participant that is already owner because he is MUC admin
true;
can_change_ra(_FAffiliation, _FRole, _TAffiliation,
_TRole, _RoleorAffiliation, _Value, owner) ->
%% Nobody can decrease MUC admin's role/affiliation
false;
can_change_ra(_FAffiliation, _FRole, TAffiliation,
_TRole, affiliation, Value, _ServiceAf)
when TAffiliation == Value ->
nothing;
can_change_ra(_FAffiliation, _FRole, _TAffiliation,
TRole, role, Value, _ServiceAf)
when TRole == Value ->
nothing;
can_change_ra(FAffiliation, _FRole, outcast, _TRole,
affiliation, none, _ServiceAf)
when (FAffiliation == owner) or
(FAffiliation == admin) ->
true;
can_change_ra(FAffiliation, _FRole, outcast, _TRole,
affiliation, member, _ServiceAf)
when (FAffiliation == owner) or
(FAffiliation == admin) ->
true;
can_change_ra(owner, _FRole, outcast, _TRole,
affiliation, admin, _ServiceAf) ->
true;
can_change_ra(owner, _FRole, outcast, _TRole,
affiliation, owner, _ServiceAf) ->
true;
can_change_ra(FAffiliation, _FRole, none, _TRole,
affiliation, outcast, _ServiceAf)
when (FAffiliation == owner) or
(FAffiliation == admin) ->
true;
can_change_ra(FAffiliation, _FRole, none, _TRole,
affiliation, member, _ServiceAf)
when (FAffiliation == owner) or
(FAffiliation == admin) ->
true;
can_change_ra(owner, _FRole, none, _TRole, affiliation,
admin, _ServiceAf) ->
true;
can_change_ra(owner, _FRole, none, _TRole, affiliation,
owner, _ServiceAf) ->
true;
can_change_ra(FAffiliation, _FRole, member, _TRole,
affiliation, outcast, _ServiceAf)
when (FAffiliation == owner) or
(FAffiliation == admin) ->
true;
can_change_ra(FAffiliation, _FRole, member, _TRole,
affiliation, none, _ServiceAf)
when (FAffiliation == owner) or
(FAffiliation == admin) ->
true;
can_change_ra(owner, _FRole, member, _TRole,
affiliation, admin, _ServiceAf) ->
true;
can_change_ra(owner, _FRole, member, _TRole,
affiliation, owner, _ServiceAf) ->
true;
can_change_ra(owner, _FRole, admin, _TRole, affiliation,
_Affiliation, _ServiceAf) ->
true;
can_change_ra(owner, _FRole, owner, _TRole, affiliation,
_Affiliation, _ServiceAf) ->
check_owner;
can_change_ra(_FAffiliation, _FRole, _TAffiliation,
_TRole, affiliation, _Value, _ServiceAf) ->
false;
can_change_ra(_FAffiliation, moderator, _TAffiliation,
visitor, role, none, _ServiceAf) ->
true;
can_change_ra(_FAffiliation, moderator, _TAffiliation,
visitor, role, participant, _ServiceAf) ->
true;
can_change_ra(FAffiliation, _FRole, _TAffiliation,
visitor, role, moderator, _ServiceAf)
when (FAffiliation == owner) or
(FAffiliation == admin) ->
true;
can_change_ra(_FAffiliation, moderator, _TAffiliation,
participant, role, none, _ServiceAf) ->
true;
can_change_ra(_FAffiliation, moderator, _TAffiliation,
participant, role, visitor, _ServiceAf) ->
true;
can_change_ra(FAffiliation, _FRole, _TAffiliation,
participant, role, moderator, _ServiceAf)
when (FAffiliation == owner) or
(FAffiliation == admin) ->
true;
can_change_ra(_FAffiliation, _FRole, owner, moderator,
role, visitor, _ServiceAf) ->
false;
can_change_ra(owner, _FRole, _TAffiliation, moderator,
role, visitor, _ServiceAf) ->
true;
can_change_ra(_FAffiliation, _FRole, admin, moderator,
role, visitor, _ServiceAf) ->
false;
can_change_ra(admin, _FRole, _TAffiliation, moderator,
role, visitor, _ServiceAf) ->
true;
can_change_ra(_FAffiliation, _FRole, owner, moderator,
role, participant, _ServiceAf) ->
false;
can_change_ra(owner, _FRole, _TAffiliation, moderator,
role, participant, _ServiceAf) ->
true;
can_change_ra(_FAffiliation, _FRole, admin, moderator,
role, participant, _ServiceAf) ->
false;
can_change_ra(admin, _FRole, _TAffiliation, moderator,
role, participant, _ServiceAf) ->
true;
can_change_ra(_FAffiliation, _FRole, _TAffiliation,
_TRole, role, _Value, _ServiceAf) ->
false.
send_kickban_presence(UJID, JID, Reason, Code, StateData) ->
NewAffiliation = get_affiliation(JID, StateData),
send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation,
StateData).
send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation,
StateData) ->
LJID = jlib:jid_tolower(JID),
LJIDs = case LJID of
{U, S, <<"">>} ->
(?DICT):fold(fun (J, _, Js) ->
case J of
{U, S, _} -> [J | Js];
_ -> Js
end
end,
[], StateData#state.users);
_ ->
case (?DICT):is_key(LJID, StateData#state.users) of
true -> [LJID];
_ -> []
end
end,
lists:foreach(fun (J) ->
{ok, #user{nick = Nick}} = (?DICT):find(J,
StateData#state.users),
add_to_log(kickban, {Nick, Reason, Code}, StateData),
tab_remove_online_user(J, StateData),
send_kickban_presence1(UJID, J, Reason, Code,
NewAffiliation, StateData)
end,
LJIDs).
send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
StateData) ->
{ok, #user{jid = RealJID, nick = Nick}} =
(?DICT):find(jlib:jid_tolower(UJID),
StateData#state.users),
SAffiliation = affiliation_to_list(Affiliation),
BannedJIDString = jlib:jid_to_string(RealJID),
case MJID /= <<"">> of
true ->
{ok, #user{nick = ActorNick}} =
(?DICT):find(jlib:jid_tolower(MJID),
StateData#state.users);
false ->
ActorNick = <<"">>
end,
lists:foreach(fun ({_LJID, Info}) ->
JidAttrList = case Info#user.role == moderator orelse
(StateData#state.config)#config.anonymous
== false
of
true ->
[{<<"jid">>, BannedJIDString}];
false -> []
end,
ItemAttrs = [{<<"affiliation">>, SAffiliation},
{<<"role">>, <<"none">>}]
++ JidAttrList,
ItemEls = case Reason of
<<"">> -> [];
_ ->
[#xmlel{name = <<"reason">>,
attrs = [],
children =
[{xmlcdata, Reason}]}]
end,
ItemElsActor = case MJID of
<<"">> -> [];
_ -> [#xmlel{name = <<"actor">>,
attrs =
[{<<"nick">>, ActorNick}]}]
end,
Packet = #xmlel{name = <<"presence">>,
attrs =
[{<<"type">>, <<"unavailable">>}],
children =
[#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>,
?NS_MUC_USER}],
children =
[#xmlel{name =
<<"item">>,
attrs =
ItemAttrs,
children =
ItemElsActor ++ ItemEls},
#xmlel{name =
<<"status">>,
attrs =
[{<<"code">>,
Code}],
children =
[]}]}]},
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick),
Info#user.jid, Packet)
end,
(?DICT):to_list(StateData#state.users)).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Owner stuff
process_iq_owner(From, set, Lang, SubEl, StateData) ->
FAffiliation = get_affiliation(From, StateData),
case FAffiliation of
owner ->
#xmlel{children = Els} = SubEl,
case xml:remove_cdata(Els) of
[#xmlel{name = <<"x">>} = XEl] ->
case {xml:get_tag_attr_s(<<"xmlns">>, XEl),
xml:get_tag_attr_s(<<"type">>, XEl)}
of
{?NS_XDATA, <<"cancel">>} -> {result, [], StateData};
{?NS_XDATA, <<"submit">>} ->
case is_allowed_log_change(XEl, StateData, From) andalso
is_allowed_persistent_change(XEl, StateData, From)
andalso
is_allowed_room_name_desc_limits(XEl, StateData)
andalso
is_password_settings_correct(XEl, StateData)
of
true -> set_config(XEl, StateData);
false -> {error, ?ERR_NOT_ACCEPTABLE}
end;
_ -> {error, ?ERR_BAD_REQUEST}
end;
[#xmlel{name = <<"destroy">>} = SubEl1] ->
?INFO_MSG("Destroyed MUC room ~s by the owner ~s",
[jlib:jid_to_string(StateData#state.jid),
jlib:jid_to_string(From)]),
add_to_log(room_existence, destroyed, StateData),
destroy_room(SubEl1, StateData);
Items ->
process_admin_items_set(From, Items, Lang, StateData)
end;
_ ->
ErrText = <<"Owner privileges required">>,
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
end;
process_iq_owner(From, get, Lang, SubEl, StateData) ->
FAffiliation = get_affiliation(From, StateData),
case FAffiliation of
owner ->
#xmlel{children = Els} = SubEl,
case xml:remove_cdata(Els) of
[] -> get_config(Lang, StateData, From);
[Item] ->
case xml:get_tag_attr(<<"affiliation">>, Item) of
false -> {error, ?ERR_BAD_REQUEST};
{value, StrAffiliation} ->
case catch list_to_affiliation(StrAffiliation) of
{'EXIT', _} ->
ErrText = iolist_to_binary(
io_lib:format(
translate:translate(
Lang,
<<"Invalid affiliation: ~s">>),
[StrAffiliation])),
{error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
SAffiliation ->
Items = items_with_affiliation(SAffiliation,
StateData),
{result, Items, StateData}
end
end;
_ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED}
end;
_ ->
ErrText = <<"Owner privileges required">>,
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
end.
is_allowed_log_change(XEl, StateData, From) ->
case lists:keymember(<<"muc#roomconfig_enablelogging">>,
1, jlib:parse_xdata_submit(XEl))
of
false -> true;
true ->
allow ==
mod_muc_log:check_access_log(StateData#state.server_host,
From)
end.
is_allowed_persistent_change(XEl, StateData, From) ->
case
lists:keymember(<<"muc#roomconfig_persistentroom">>, 1,
jlib:parse_xdata_submit(XEl))
of
false -> true;
true ->
{_AccessRoute, _AccessCreate, _AccessAdmin,
AccessPersistent} =
StateData#state.access,
allow ==
acl:match_rule(StateData#state.server_host,
AccessPersistent, From)
end.
%% Check if the Room Name and Room Description defined in the Data Form
%% are conformant to the configured limits
is_allowed_room_name_desc_limits(XEl, StateData) ->
IsNameAccepted = case
lists:keysearch(<<"muc#roomconfig_roomname">>, 1,
jlib:parse_xdata_submit(XEl))
of
{value, {_, [N]}} ->
byte_size(N) =<
gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, max_room_name,
fun(infinity) -> infinity;
(I) when is_integer(I),
I>0 -> I
end, infinity);
_ -> true
end,
IsDescAccepted = case
lists:keysearch(<<"muc#roomconfig_roomdesc">>, 1,
jlib:parse_xdata_submit(XEl))
of
{value, {_, [D]}} ->
byte_size(D) =<
gen_mod:get_module_opt(StateData#state.server_host,
mod_muc, max_room_desc,
fun(infinity) -> infinity;
(I) when is_integer(I),
I>0 ->
I
end, infinity);
_ -> true
end,
IsNameAccepted and IsDescAccepted.
%% Return false if:
%% "the password for a password-protected room is blank"
is_password_settings_correct(XEl, StateData) ->
Config = StateData#state.config,
OldProtected = Config#config.password_protected,
OldPassword = Config#config.password,
NewProtected = case
lists:keysearch(<<"muc#roomconfig_passwordprotectedroom">>,
1, jlib:parse_xdata_submit(XEl))
of
{value, {_, [<<"1">>]}} -> true;
{value, {_, [<<"0">>]}} -> false;
_ -> undefined
end,
NewPassword = case
lists:keysearch(<<"muc#roomconfig_roomsecret">>, 1,
jlib:parse_xdata_submit(XEl))
of
{value, {_, [P]}} -> P;
_ -> undefined
end,
case {OldProtected, NewProtected, OldPassword,
NewPassword}
of
{true, undefined, <<"">>, undefined} -> false;
{true, undefined, _, <<"">>} -> false;
{_, true, <<"">>, undefined} -> false;
{_, true, _, <<"">>} -> false;
_ -> true
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}]}]}).
-define(BOOLXFIELD(Label, Var, Val),
?XFIELD(<<"boolean">>, Label, Var,
case Val of
true -> <<"1">>;
_ -> <<"0">>
end)).
-define(STRINGXFIELD(Label, Var, Val),
?XFIELD(<<"text-single">>, Label, Var, Val)).
-define(PRIVATEXFIELD(Label, Var, Val),
?XFIELD(<<"text-private">>, Label, Var, Val)).
-define(JIDMULTIXFIELD(Label, Var, JIDList),
#xmlel{name = <<"field">>,
attrs =
[{<<"type">>, <<"jid-multi">>},
{<<"label">>, translate:translate(Lang, Label)},
{<<"var">>, Var}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children = [{xmlcdata, jlib:jid_to_string(JID)}]}
|| JID <- JIDList]}).
get_default_room_maxusers(RoomState) ->
DefRoomOpts =
gen_mod:get_module_opt(RoomState#state.server_host,
mod_muc, default_room_options,
fun(L) when is_list(L) -> L end,
[]),
RoomState2 = set_opts(DefRoomOpts, RoomState),
(RoomState2#state.config)#config.max_users.
get_config(Lang, StateData, From) ->
{_AccessRoute, _AccessCreate, _AccessAdmin,
AccessPersistent} =
StateData#state.access,
ServiceMaxUsers = get_service_max_users(StateData),
DefaultRoomMaxUsers =
get_default_room_maxusers(StateData),
Config = StateData#state.config,
{MaxUsersRoomInteger, MaxUsersRoomString} = case
get_max_users(StateData)
of
N when is_integer(N) ->
{N,
jlib:integer_to_binary(N)};
_ -> {0, <<"none">>}
end,
Res = [#xmlel{name = <<"title">>, attrs = [],
children =
[{xmlcdata,
iolist_to_binary(
io_lib:format(
translate:translate(
Lang,
<<"Configuration of room ~s">>),
[jlib:jid_to_string(StateData#state.jid)]))}]},
#xmlel{name = <<"field">>,
attrs =
[{<<"type">>, <<"hidden">>},
{<<"var">>, <<"FORM_TYPE">>}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children =
[{xmlcdata,
<<"http://jabber.org/protocol/muc#roomconfig">>}]}]},
?STRINGXFIELD(<<"Room title">>,
<<"muc#roomconfig_roomname">>, (Config#config.title)),
?STRINGXFIELD(<<"Room description">>,
<<"muc#roomconfig_roomdesc">>,
(Config#config.description))]
++
case acl:match_rule(StateData#state.server_host,
AccessPersistent, From)
of
allow ->
[?BOOLXFIELD(<<"Make room persistent">>,
<<"muc#roomconfig_persistentroom">>,
(Config#config.persistent))];
_ -> []
end
++
[?BOOLXFIELD(<<"Make room public searchable">>,
<<"muc#roomconfig_publicroom">>,
(Config#config.public)),
?BOOLXFIELD(<<"Make participants list public">>,
<<"public_list">>, (Config#config.public_list)),
?BOOLXFIELD(<<"Make room password protected">>,
<<"muc#roomconfig_passwordprotectedroom">>,
(Config#config.password_protected)),
?PRIVATEXFIELD(<<"Password">>,
<<"muc#roomconfig_roomsecret">>,
case Config#config.password_protected of
true -> Config#config.password;
false -> <<"">>
end),
#xmlel{name = <<"field">>,
attrs =
[{<<"type">>, <<"list-single">>},
{<<"label">>,
translate:translate(Lang,
<<"Maximum Number of Occupants">>)},
{<<"var">>, <<"muc#roomconfig_maxusers">>}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children = [{xmlcdata, MaxUsersRoomString}]}]
++
if is_integer(ServiceMaxUsers) -> [];
true ->
[#xmlel{name = <<"option">>,
attrs =
[{<<"label">>,
translate:translate(Lang,
<<"No limit">>)}],
children =
[#xmlel{name = <<"value">>,
attrs = [],
children =
[{xmlcdata,
<<"none">>}]}]}]
end
++
[#xmlel{name = <<"option">>,
attrs =
[{<<"label">>,
jlib:integer_to_binary(N)}],
children =
[#xmlel{name = <<"value">>,
attrs = [],
children =
[{xmlcdata,
jlib:integer_to_binary(N)}]}]}
|| N
<- lists:usort([ServiceMaxUsers,
DefaultRoomMaxUsers,
MaxUsersRoomInteger
| ?MAX_USERS_DEFAULT_LIST]),
N =< ServiceMaxUsers]},
#xmlel{name = <<"field">>,
attrs =
[{<<"type">>, <<"list-single">>},
{<<"label">>,
translate:translate(Lang,
<<"Present real Jabber IDs to">>)},
{<<"var">>, <<"muc#roomconfig_whois">>}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children =
[{xmlcdata,
if Config#config.anonymous ->
<<"moderators">>;
true -> <<"anyone">>
end}]},
#xmlel{name = <<"option">>,
attrs =
[{<<"label">>,
translate:translate(Lang,
<<"moderators only">>)}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children =
[{xmlcdata,
<<"moderators">>}]}]},
#xmlel{name = <<"option">>,
attrs =
[{<<"label">>,
translate:translate(Lang,
<<"anyone">>)}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children =
[{xmlcdata,
<<"anyone">>}]}]}]},
?BOOLXFIELD(<<"Make room members-only">>,
<<"muc#roomconfig_membersonly">>,
(Config#config.members_only)),
?BOOLXFIELD(<<"Make room moderated">>,
<<"muc#roomconfig_moderatedroom">>,
(Config#config.moderated)),
?BOOLXFIELD(<<"Default users as participants">>,
<<"members_by_default">>,
(Config#config.members_by_default)),
?BOOLXFIELD(<<"Allow users to change the subject">>,
<<"muc#roomconfig_changesubject">>,
(Config#config.allow_change_subj)),
?BOOLXFIELD(<<"Allow users to send private messages">>,
<<"allow_private_messages">>,
(Config#config.allow_private_messages)),
#xmlel{name = <<"field">>,
attrs =
[{<<"type">>, <<"list-single">>},
{<<"label">>,
translate:translate(Lang,
<<"Allow visitors to send private messages to">>)},
{<<"var">>,
<<"allow_private_messages_from_visitors">>}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children =
[{xmlcdata,
case
Config#config.allow_private_messages_from_visitors
of
anyone -> <<"anyone">>;
moderators -> <<"moderators">>;
nobody -> <<"nobody">>
end}]},
#xmlel{name = <<"option">>,
attrs =
[{<<"label">>,
translate:translate(Lang,
<<"nobody">>)}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children =
[{xmlcdata, <<"nobody">>}]}]},
#xmlel{name = <<"option">>,
attrs =
[{<<"label">>,
translate:translate(Lang,
<<"moderators only">>)}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children =
[{xmlcdata,
<<"moderators">>}]}]},
#xmlel{name = <<"option">>,
attrs =
[{<<"label">>,
translate:translate(Lang,
<<"anyone">>)}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children =
[{xmlcdata,
<<"anyone">>}]}]}]},
?BOOLXFIELD(<<"Allow users to query other users">>,
<<"allow_query_users">>,
(Config#config.allow_query_users)),
?BOOLXFIELD(<<"Allow users to send invites">>,
<<"muc#roomconfig_allowinvites">>,
(Config#config.allow_user_invites)),
?BOOLXFIELD(<<"Allow visitors to send status text in "
"presence updates">>,
<<"muc#roomconfig_allowvisitorstatus">>,
(Config#config.allow_visitor_status)),
?BOOLXFIELD(<<"Allow visitors to change nickname">>,
<<"muc#roomconfig_allowvisitornickchange">>,
(Config#config.allow_visitor_nickchange)),
?BOOLXFIELD(<<"Allow visitors to send voice requests">>,
<<"muc#roomconfig_allowvoicerequests">>,
(Config#config.allow_voice_requests)),
?STRINGXFIELD(<<"Minimum interval between voice requests "
"(in seconds)">>,
<<"muc#roomconfig_voicerequestmininterval">>,
(jlib:integer_to_binary(Config#config.voice_request_min_interval)))]
++
case ejabberd_captcha:is_feature_available() of
true ->
[?BOOLXFIELD(<<"Make room CAPTCHA protected">>,
<<"captcha_protected">>,
(Config#config.captcha_protected))];
false -> []
end
++
[?JIDMULTIXFIELD(<<"Exclude Jabber IDs from CAPTCHA challenge">>,
<<"muc#roomconfig_captcha_whitelist">>,
((?SETS):to_list(Config#config.captcha_whitelist)))]
++
case
mod_muc_log:check_access_log(StateData#state.server_host,
From)
of
allow ->
[?BOOLXFIELD(<<"Enable logging">>,
<<"muc#roomconfig_enablelogging">>,
(Config#config.logging))];
_ -> []
end,
{result,
[#xmlel{name = <<"instructions">>, attrs = [],
children =
[{xmlcdata,
translate:translate(Lang,
<<"You need an x:data capable client to "
"configure room">>)}]},
#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}],
children = Res}],
StateData}.
set_config(XEl, StateData) ->
XData = jlib:parse_xdata_submit(XEl),
case XData of
invalid -> {error, ?ERR_BAD_REQUEST};
_ ->
case set_xoption(XData, StateData#state.config) of
#config{} = Config ->
Res = change_config(Config, StateData),
{result, _, NSD} = Res,
Type = case {(StateData#state.config)#config.logging,
Config#config.logging}
of
{true, false} -> roomconfig_change_disabledlogging;
{false, true} -> roomconfig_change_enabledlogging;
{_, _} -> roomconfig_change
end,
Users = [{U#user.jid, U#user.nick, U#user.role}
|| {_, U} <- (?DICT):to_list(StateData#state.users)],
add_to_log(Type, Users, NSD),
Res;
Err -> Err
end
end.
-define(SET_BOOL_XOPT(Opt, Val),
case Val of
<<"0">> ->
set_xoption(Opts, Config#config{Opt = false});
<<"false">> ->
set_xoption(Opts, Config#config{Opt = false});
<<"1">> -> set_xoption(Opts, Config#config{Opt = true});
<<"true">> ->
set_xoption(Opts, Config#config{Opt = true});
_ -> {error, ?ERR_BAD_REQUEST}
end).
-define(SET_NAT_XOPT(Opt, Val),
case catch jlib:binary_to_integer(Val) of
I when is_integer(I), I > 0 ->
set_xoption(Opts, Config#config{Opt = I});
_ -> {error, ?ERR_BAD_REQUEST}
end).
-define(SET_STRING_XOPT(Opt, Val),
set_xoption(Opts, Config#config{Opt = Val})).
-define(SET_JIDMULTI_XOPT(Opt, Vals),
begin
Set = lists:foldl(fun ({U, S, R}, Set1) ->
(?SETS):add_element({U, S, R}, Set1);
(#jid{luser = U, lserver = S, lresource = R},
Set1) ->
(?SETS):add_element({U, S, R}, Set1);
(_, Set1) -> Set1
end,
(?SETS):empty(), Vals),
set_xoption(Opts, Config#config{Opt = Set})
end).
set_xoption([], Config) -> Config;
set_xoption([{<<"muc#roomconfig_roomname">>, [Val]}
| Opts],
Config) ->
?SET_STRING_XOPT(title, Val);
set_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]}
| Opts],
Config) ->
?SET_STRING_XOPT(description, Val);
set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(allow_change_subj, Val);
set_xoption([{<<"allow_query_users">>, [Val]} | Opts],
Config) ->
?SET_BOOL_XOPT(allow_query_users, Val);
set_xoption([{<<"allow_private_messages">>, [Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(allow_private_messages, Val);
set_xoption([{<<"allow_private_messages_from_visitors">>,
[Val]}
| Opts],
Config) ->
case Val of
<<"anyone">> ->
?SET_STRING_XOPT(allow_private_messages_from_visitors,
anyone);
<<"moderators">> ->
?SET_STRING_XOPT(allow_private_messages_from_visitors,
moderators);
<<"nobody">> ->
?SET_STRING_XOPT(allow_private_messages_from_visitors,
nobody);
_ -> {error, ?ERR_BAD_REQUEST}
end;
set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>,
[Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(allow_visitor_status, Val);
set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>,
[Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(allow_visitor_nickchange, Val);
set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(public, Val);
set_xoption([{<<"public_list">>, [Val]} | Opts],
Config) ->
?SET_BOOL_XOPT(public_list, Val);
set_xoption([{<<"muc#roomconfig_persistentroom">>,
[Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(persistent, Val);
set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(moderated, Val);
set_xoption([{<<"members_by_default">>, [Val]} | Opts],
Config) ->
?SET_BOOL_XOPT(members_by_default, Val);
set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(members_only, Val);
set_xoption([{<<"captcha_protected">>, [Val]} | Opts],
Config) ->
?SET_BOOL_XOPT(captcha_protected, Val);
set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(allow_user_invites, Val);
set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>,
[Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(password_protected, Val);
set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]}
| Opts],
Config) ->
?SET_STRING_XOPT(password, Val);
set_xoption([{<<"anonymous">>, [Val]} | Opts],
Config) ->
?SET_BOOL_XOPT(anonymous, Val);
set_xoption([{<<"muc#roomconfig_allowvoicerequests">>,
[Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(allow_voice_requests, Val);
set_xoption([{<<"muc#roomconfig_voicerequestmininterval">>,
[Val]}
| Opts],
Config) ->
?SET_NAT_XOPT(voice_request_min_interval, Val);
set_xoption([{<<"muc#roomconfig_whois">>, [Val]}
| Opts],
Config) ->
case Val of
<<"moderators">> ->
?SET_BOOL_XOPT(anonymous,
(iolist_to_binary(integer_to_list(1))));
<<"anyone">> ->
?SET_BOOL_XOPT(anonymous,
(iolist_to_binary(integer_to_list(0))));
_ -> {error, ?ERR_BAD_REQUEST}
end;
set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]}
| Opts],
Config) ->
case Val of
<<"none">> -> ?SET_STRING_XOPT(max_users, none);
_ -> ?SET_NAT_XOPT(max_users, Val)
end;
set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]}
| Opts],
Config) ->
?SET_BOOL_XOPT(logging, Val);
set_xoption([{<<"muc#roomconfig_captcha_whitelist">>,
Vals}
| Opts],
Config) ->
JIDs = [jlib:string_to_jid(Val) || Val <- Vals],
?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs);
set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config) ->
set_xoption(Opts, Config);
set_xoption([_ | _Opts], _Config) ->
{error, ?ERR_BAD_REQUEST}.
change_config(Config, StateData) ->
NSD = StateData#state{config = Config},
case {(StateData#state.config)#config.persistent,
Config#config.persistent}
of
{_, true} ->
mod_muc:store_room(NSD#state.server_host,
NSD#state.host, NSD#state.room, make_opts(NSD));
{true, false} ->
mod_muc:forget_room(NSD#state.server_host,
NSD#state.host, NSD#state.room);
{false, false} -> ok
end,
case {(StateData#state.config)#config.members_only,
Config#config.members_only}
of
{false, true} ->
NSD1 = remove_nonmembers(NSD), {result, [], NSD1};
_ -> {result, [], NSD}
end.
remove_nonmembers(StateData) ->
lists:foldl(fun ({_LJID, #user{jid = JID}}, SD) ->
Affiliation = get_affiliation(JID, SD),
case Affiliation of
none ->
catch send_kickban_presence(<<"">>, JID, <<"">>,
<<"322">>, SD),
set_role(JID, none, SD);
_ -> SD
end
end,
StateData, (?DICT):to_list(StateData#state.users)).
set_opts([], StateData) -> StateData;
set_opts([{Opt, Val} | Opts], StateData) ->
NSD = case Opt of
title ->
StateData#state{config =
(StateData#state.config)#config{title =
Val}};
description ->
StateData#state{config =
(StateData#state.config)#config{description
= Val}};
allow_change_subj ->
StateData#state{config =
(StateData#state.config)#config{allow_change_subj
= Val}};
allow_query_users ->
StateData#state{config =
(StateData#state.config)#config{allow_query_users
= Val}};
allow_private_messages ->
StateData#state{config =
(StateData#state.config)#config{allow_private_messages
= Val}};
allow_private_messages_from_visitors ->
StateData#state{config =
(StateData#state.config)#config{allow_private_messages_from_visitors
= Val}};
allow_visitor_nickchange ->
StateData#state{config =
(StateData#state.config)#config{allow_visitor_nickchange
= Val}};
allow_visitor_status ->
StateData#state{config =
(StateData#state.config)#config{allow_visitor_status
= Val}};
public ->
StateData#state{config =
(StateData#state.config)#config{public =
Val}};
public_list ->
StateData#state{config =
(StateData#state.config)#config{public_list
= Val}};
persistent ->
StateData#state{config =
(StateData#state.config)#config{persistent =
Val}};
moderated ->
StateData#state{config =
(StateData#state.config)#config{moderated =
Val}};
members_by_default ->
StateData#state{config =
(StateData#state.config)#config{members_by_default
= Val}};
members_only ->
StateData#state{config =
(StateData#state.config)#config{members_only
= Val}};
allow_user_invites ->
StateData#state{config =
(StateData#state.config)#config{allow_user_invites
= Val}};
password_protected ->
StateData#state{config =
(StateData#state.config)#config{password_protected
= Val}};
captcha_protected ->
StateData#state{config =
(StateData#state.config)#config{captcha_protected
= Val}};
password ->
StateData#state{config =
(StateData#state.config)#config{password =
Val}};
anonymous ->
StateData#state{config =
(StateData#state.config)#config{anonymous =
Val}};
logging ->
StateData#state{config =
(StateData#state.config)#config{logging =
Val}};
captcha_whitelist ->
StateData#state{config =
(StateData#state.config)#config{captcha_whitelist
=
(?SETS):from_list(Val)}};
allow_voice_requests ->
StateData#state{config =
(StateData#state.config)#config{allow_voice_requests
= Val}};
voice_request_min_interval ->
StateData#state{config =
(StateData#state.config)#config{voice_request_min_interval
= Val}};
max_users ->
ServiceMaxUsers = get_service_max_users(StateData),
MaxUsers = if Val =< ServiceMaxUsers -> Val;
true -> ServiceMaxUsers
end,
StateData#state{config =
(StateData#state.config)#config{max_users =
MaxUsers}};
vcard ->
StateData#state{config =
(StateData#state.config)#config{vcard =
Val}};
affiliations ->
StateData#state{affiliations = (?DICT):from_list(Val)};
subject -> StateData#state{subject = Val};
subject_author -> StateData#state{subject_author = Val};
_ -> StateData
end,
set_opts(Opts, NSD).
-define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}).
make_opts(StateData) ->
Config = StateData#state.config,
[?MAKE_CONFIG_OPT(title), ?MAKE_CONFIG_OPT(description),
?MAKE_CONFIG_OPT(allow_change_subj),
?MAKE_CONFIG_OPT(allow_query_users),
?MAKE_CONFIG_OPT(allow_private_messages),
?MAKE_CONFIG_OPT(allow_private_messages_from_visitors),
?MAKE_CONFIG_OPT(allow_visitor_status),
?MAKE_CONFIG_OPT(allow_visitor_nickchange),
?MAKE_CONFIG_OPT(public), ?MAKE_CONFIG_OPT(public_list),
?MAKE_CONFIG_OPT(persistent),
?MAKE_CONFIG_OPT(moderated),
?MAKE_CONFIG_OPT(members_by_default),
?MAKE_CONFIG_OPT(members_only),
?MAKE_CONFIG_OPT(allow_user_invites),
?MAKE_CONFIG_OPT(password_protected),
?MAKE_CONFIG_OPT(captcha_protected),
?MAKE_CONFIG_OPT(password), ?MAKE_CONFIG_OPT(anonymous),
?MAKE_CONFIG_OPT(logging), ?MAKE_CONFIG_OPT(max_users),
?MAKE_CONFIG_OPT(allow_voice_requests),
?MAKE_CONFIG_OPT(voice_request_min_interval),
?MAKE_CONFIG_OPT(vcard),
{captcha_whitelist,
(?SETS):to_list((StateData#state.config)#config.captcha_whitelist)},
{affiliations,
(?DICT):to_list(StateData#state.affiliations)},
{subject, StateData#state.subject},
{subject_author, StateData#state.subject_author}].
destroy_room(DEl, StateData) ->
lists:foreach(fun ({_LJID, Info}) ->
Nick = Info#user.nick,
ItemAttrs = [{<<"affiliation">>, <<"none">>},
{<<"role">>, <<"none">>}],
Packet = #xmlel{name = <<"presence">>,
attrs =
[{<<"type">>, <<"unavailable">>}],
children =
[#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>,
?NS_MUC_USER}],
children =
[#xmlel{name =
<<"item">>,
attrs =
ItemAttrs,
children =
[]},
DEl]}]},
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
Nick),
Info#user.jid, Packet)
end,
(?DICT):to_list(StateData#state.users)),
case (StateData#state.config)#config.persistent of
true ->
mod_muc:forget_room(StateData#state.server_host,
StateData#state.host, StateData#state.room);
false -> ok
end,
{result, [], stop}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Disco
-define(FEATURE(Var),
#xmlel{name = <<"feature">>, attrs = [{<<"var">>, Var}],
children = []}).
-define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse),
case Opt of
true -> ?FEATURE(Fiftrue);
false -> ?FEATURE(Fiffalse)
end).
process_iq_disco_info(_From, set, _Lang, _StateData) ->
{error, ?ERR_NOT_ALLOWED};
process_iq_disco_info(_From, get, Lang, StateData) ->
Config = StateData#state.config,
{result,
[#xmlel{name = <<"identity">>,
attrs =
[{<<"category">>, <<"conference">>},
{<<"type">>, <<"text">>},
{<<"name">>, get_title(StateData)}],
children = []},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_VCARD}], children = []},
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_MUC}], children = []},
?CONFIG_OPT_TO_FEATURE((Config#config.public),
<<"muc_public">>, <<"muc_hidden">>),
?CONFIG_OPT_TO_FEATURE((Config#config.persistent),
<<"muc_persistent">>, <<"muc_temporary">>),
?CONFIG_OPT_TO_FEATURE((Config#config.members_only),
<<"muc_membersonly">>, <<"muc_open">>),
?CONFIG_OPT_TO_FEATURE((Config#config.anonymous),
<<"muc_semianonymous">>, <<"muc_nonanonymous">>),
?CONFIG_OPT_TO_FEATURE((Config#config.moderated),
<<"muc_moderated">>, <<"muc_unmoderated">>),
?CONFIG_OPT_TO_FEATURE((Config#config.password_protected),
<<"muc_passwordprotected">>, <<"muc_unsecured">>)]
++ iq_disco_info_extras(Lang, StateData),
StateData}.
-define(RFIELDT(Type, Var, Val),
#xmlel{name = <<"field">>,
attrs = [{<<"type">>, Type}, {<<"var">>, Var}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children = [{xmlcdata, Val}]}]}).
-define(RFIELD(Label, Var, Val),
#xmlel{name = <<"field">>,
attrs =
[{<<"label">>, translate:translate(Lang, Label)},
{<<"var">>, Var}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children = [{xmlcdata, Val}]}]}).
iq_disco_info_extras(Lang, StateData) ->
Len = (?DICT):size(StateData#state.users),
RoomDescription =
(StateData#state.config)#config.description,
[#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}],
children =
[?RFIELDT(<<"hidden">>, <<"FORM_TYPE">>,
<<"http://jabber.org/protocol/muc#roominfo">>),
?RFIELD(<<"Room description">>,
<<"muc#roominfo_description">>, RoomDescription),
?RFIELD(<<"Number of occupants">>,
<<"muc#roominfo_occupants">>,
(iolist_to_binary(integer_to_list(Len))))]}].
process_iq_disco_items(_From, set, _Lang, _StateData) ->
{error, ?ERR_NOT_ALLOWED};
process_iq_disco_items(From, get, _Lang, StateData) ->
case (StateData#state.config)#config.public_list of
true ->
{result, get_mucroom_disco_items(StateData), StateData};
_ ->
case is_occupant_or_admin(From, StateData) of
true ->
{result, get_mucroom_disco_items(StateData), StateData};
_ -> {error, ?ERR_FORBIDDEN}
end
end.
process_iq_captcha(_From, get, _Lang, _SubEl,
_StateData) ->
{error, ?ERR_NOT_ALLOWED};
process_iq_captcha(_From, set, _Lang, SubEl,
StateData) ->
case ejabberd_captcha:process_reply(SubEl) of
ok -> {result, [], StateData};
_ -> {error, ?ERR_NOT_ACCEPTABLE}
end.
process_iq_vcard(_From, get, _Lang, _SubEl, StateData) ->
#state{config = #config{vcard = VCardRaw}} = StateData,
case xml_stream:parse_element(VCardRaw) of
#xmlel{children = VCardEls} ->
{result, VCardEls, StateData};
{error, _} ->
{result, [], StateData}
end;
process_iq_vcard(From, set, Lang, SubEl, StateData) ->
case get_affiliation(From, StateData) of
owner ->
VCardRaw = xml:element_to_binary(SubEl),
Config = StateData#state.config,
NewConfig = Config#config{vcard = VCardRaw},
change_config(NewConfig, StateData);
_ ->
ErrText = <<"Owner privileges required">>,
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
end.
get_title(StateData) ->
case (StateData#state.config)#config.title of
<<"">> -> StateData#state.room;
Name -> Name
end.
get_roomdesc_reply(JID, StateData, Tail) ->
IsOccupantOrAdmin = is_occupant_or_admin(JID,
StateData),
if (StateData#state.config)#config.public or
IsOccupantOrAdmin ->
if (StateData#state.config)#config.public_list or
IsOccupantOrAdmin ->
{item, <<(get_title(StateData))/binary,Tail/binary>>};
true -> {item, get_title(StateData)}
end;
true -> false
end.
get_roomdesc_tail(StateData, Lang) ->
Desc = case (StateData#state.config)#config.public of
true -> <<"">>;
_ -> translate:translate(Lang, <<"private, ">>)
end,
Len = (?DICT):fold(fun (_, _, Acc) -> Acc + 1 end, 0,
StateData#state.users),
<<" (", Desc/binary,
(iolist_to_binary(integer_to_list(Len)))/binary, ")">>.
get_mucroom_disco_items(StateData) ->
lists:map(fun ({_LJID, Info}) ->
Nick = Info#user.nick,
#xmlel{name = <<"item">>,
attrs =
[{<<"jid">>,
jlib:jid_to_string({StateData#state.room,
StateData#state.host,
Nick})},
{<<"name">>, Nick}],
children = []}
end,
(?DICT):to_list(StateData#state.users)).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Voice request support
is_voice_request(Els) ->
lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} =
El,
false) ->
case xml:get_attr_s(<<"xmlns">>, Attrs) of
?NS_XDATA ->
case jlib:parse_xdata_submit(El) of
[_ | _] = Fields ->
case {lists:keysearch(<<"FORM_TYPE">>, 1,
Fields),
lists:keysearch(<<"muc#role">>, 1,
Fields)}
of
{{value,
{_,
[<<"http://jabber.org/protocol/muc#request">>]}},
{value, {_, [<<"participant">>]}}} ->
true;
_ -> false
end;
_ -> false
end;
_ -> false
end;
(_, Acc) -> Acc
end,
false, Els).
prepare_request_form(Requester, Nick, Lang) ->
#xmlel{name = <<"message">>,
attrs = [{<<"type">>, <<"normal">>}],
children =
[#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}],
children =
[#xmlel{name = <<"title">>, attrs = [],
children =
[{xmlcdata,
translate:translate(Lang,
<<"Voice request">>)}]},
#xmlel{name = <<"instructions">>, attrs = [],
children =
[{xmlcdata,
translate:translate(Lang,
<<"Either approve or decline the voice "
"request.">>)}]},
#xmlel{name = <<"field">>,
attrs =
[{<<"var">>, <<"FORM_TYPE">>},
{<<"type">>, <<"hidden">>}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children =
[{xmlcdata,
<<"http://jabber.org/protocol/muc#request">>}]}]},
#xmlel{name = <<"field">>,
attrs =
[{<<"var">>, <<"muc#role">>},
{<<"type">>, <<"hidden">>}],
children =
[#xmlel{name = <<"value">>, attrs = [],
children =
[{xmlcdata,
<<"participant">>}]}]},
?STRINGXFIELD(<<"User JID">>, <<"muc#jid">>,
(jlib:jid_to_string(Requester))),
?STRINGXFIELD(<<"Nickname">>, <<"muc#roomnick">>,
Nick),
?BOOLXFIELD(<<"Grant voice to this person?">>,
<<"muc#request_allow">>,
(jlib:binary_to_atom(<<"false">>)))]}]}.
send_voice_request(From, StateData) ->
Moderators = search_role(moderator, StateData),
FromNick = find_nick_by_jid(From, StateData),
lists:foreach(fun ({_, User}) ->
ejabberd_router:route(StateData#state.jid, User#user.jid,
prepare_request_form(From, FromNick,
<<"">>))
end,
Moderators).
is_voice_approvement(Els) ->
lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} =
El,
false) ->
case xml:get_attr_s(<<"xmlns">>, Attrs) of
?NS_XDATA ->
case jlib:parse_xdata_submit(El) of
[_ | _] = Fs ->
case {lists:keysearch(<<"FORM_TYPE">>, 1,
Fs),
lists:keysearch(<<"muc#role">>, 1,
Fs),
lists:keysearch(<<"muc#request_allow">>,
1, Fs)}
of
{{value,
{_,
[<<"http://jabber.org/protocol/muc#request">>]}},
{value, {_, [<<"participant">>]}},
{value, {_, [Flag]}}}
when Flag == <<"true">>;
Flag == <<"1">> ->
true;
_ -> false
end;
_ -> false
end;
_ -> false
end;
(_, Acc) -> Acc
end,
false, Els).
extract_jid_from_voice_approvement(Els) ->
lists:foldl(fun (#xmlel{name = <<"x">>} = El, error) ->
Fields = case jlib:parse_xdata_submit(El) of
invalid -> [];
Res -> Res
end,
lists:foldl(fun ({<<"muc#jid">>, [JIDStr]}, error) ->
case jlib:string_to_jid(JIDStr) of
error -> error;
J -> {ok, J}
end;
(_, Acc) -> Acc
end,
error, Fields);
(_, Acc) -> Acc
end,
error, Els).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Invitation support
is_invitation(Els) ->
lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} =
El,
false) ->
case xml:get_attr_s(<<"xmlns">>, Attrs) of
?NS_MUC_USER ->
case xml:get_subtag(El, <<"invite">>) of
false -> false;
_ -> true
end;
_ -> false
end;
(_, Acc) -> Acc
end,
false, Els).
check_invitation(From, Els, Lang, StateData) ->
FAffiliation = get_affiliation(From, StateData),
CanInvite =
(StateData#state.config)#config.allow_user_invites
orelse
FAffiliation == admin orelse FAffiliation == owner,
InviteEl = case xml:remove_cdata(Els) of
[#xmlel{name = <<"x">>, children = Els1} = XEl] ->
case xml:get_tag_attr_s(<<"xmlns">>, XEl) of
?NS_MUC_USER -> ok;
_ -> throw({error, ?ERR_BAD_REQUEST})
end,
case xml:remove_cdata(Els1) of
[#xmlel{name = <<"invite">>} = InviteEl1] -> InviteEl1;
_ -> throw({error, ?ERR_BAD_REQUEST})
end;
_ -> throw({error, ?ERR_BAD_REQUEST})
end,
JID = case
jlib:string_to_jid(xml:get_tag_attr_s(<<"to">>,
InviteEl))
of
error -> throw({error, ?ERR_JID_MALFORMED});
JID1 -> JID1
end,
case CanInvite of
false -> throw({error, ?ERR_NOT_ALLOWED});
true ->
Reason = xml:get_path_s(InviteEl,
[{elem, <<"reason">>}, cdata]),
ContinueEl = case xml:get_path_s(InviteEl,
[{elem, <<"continue">>}])
of
<<>> -> [];
Continue1 -> [Continue1]
end,
IEl = [#xmlel{name = <<"invite">>,
attrs = [{<<"from">>, jlib:jid_to_string(From)}],
children =
[#xmlel{name = <<"reason">>, attrs = [],
children = [{xmlcdata, Reason}]}]
++ ContinueEl}],
PasswdEl = case
(StateData#state.config)#config.password_protected
of
true ->
[#xmlel{name = <<"password">>, attrs = [],
children =
[{xmlcdata,
(StateData#state.config)#config.password}]}];
_ -> []
end,
Body = #xmlel{name = <<"body">>, attrs = [],
children =
[{xmlcdata,
iolist_to_binary(
[io_lib:format(
translate:translate(
Lang,
<<"~s invites you to the room ~s">>),
[jlib:jid_to_string(From),
jlib:jid_to_string({StateData#state.room,
StateData#state.host,
<<"">>})]),
case
(StateData#state.config)#config.password_protected
of
true ->
<<", ",
(translate:translate(Lang,
<<"the password is">>))/binary,
" '",
((StateData#state.config)#config.password)/binary,
"'">>;
_ -> <<"">>
end
,
case Reason of
<<"">> -> <<"">>;
_ -> <<" (", Reason/binary, ") ">>
end])}]},
Msg = #xmlel{name = <<"message">>,
attrs = [{<<"type">>, <<"normal">>}],
children =
[#xmlel{name = <<"x">>,
attrs = [{<<"xmlns">>, ?NS_MUC_USER}],
children = IEl ++ PasswdEl},
#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>, ?NS_XCONFERENCE},
{<<"jid">>,
jlib:jid_to_string({StateData#state.room,
StateData#state.host,
<<"">>})}],
children = [{xmlcdata, Reason}]},
Body]},
ejabberd_router:route(StateData#state.jid, JID, Msg),
JID
end.
%% Handle a message sent to the room by a non-participant.
%% If it is a decline, send to the inviter.
%% Otherwise, an error message is sent to the sender.
handle_roommessage_from_nonparticipant(Packet, Lang,
StateData, From) ->
case catch check_decline_invitation(Packet) of
{true, Decline_data} ->
send_decline_invitation(Decline_data,
StateData#state.jid, From);
_ ->
send_error_only_occupants(Packet, Lang,
StateData#state.jid, From)
end.
%% Check in the packet is a decline.
%% If so, also returns the splitted packet.
%% This function must be catched,
%% because it crashes when the packet is not a decline message.
check_decline_invitation(Packet) ->
#xmlel{name = <<"message">>} = Packet,
XEl = xml:get_subtag(Packet, <<"x">>),
(?NS_MUC_USER) = xml:get_tag_attr_s(<<"xmlns">>, XEl),
DEl = xml:get_subtag(XEl, <<"decline">>),
ToString = xml:get_tag_attr_s(<<"to">>, DEl),
ToJID = jlib:string_to_jid(ToString),
{true, {Packet, XEl, DEl, ToJID}}.
%% Send the decline to the inviter user.
%% The original stanza must be slightly modified.
send_decline_invitation({Packet, XEl, DEl, ToJID},
RoomJID, FromJID) ->
FromString =
jlib:jid_to_string(jlib:jid_remove_resource(FromJID)),
#xmlel{name = <<"decline">>, attrs = DAttrs,
children = DEls} =
DEl,
DAttrs2 = lists:keydelete(<<"to">>, 1, DAttrs),
DAttrs3 = [{<<"from">>, FromString} | DAttrs2],
DEl2 = #xmlel{name = <<"decline">>, attrs = DAttrs3,
children = DEls},
XEl2 = replace_subelement(XEl, DEl2),
Packet2 = replace_subelement(Packet, XEl2),
ejabberd_router:route(RoomJID, ToJID, Packet2).
%% Given an element and a new subelement,
%% replace the instance of the subelement in element with the new subelement.
replace_subelement(#xmlel{name = Name, attrs = Attrs,
children = SubEls},
NewSubEl) ->
{_, NameNewSubEl, _, _} = NewSubEl,
SubEls2 = lists:keyreplace(NameNewSubEl, 2, SubEls, NewSubEl),
#xmlel{name = Name, attrs = Attrs, children = SubEls2}.
send_error_only_occupants(Packet, Lang, RoomJID, From) ->
ErrText =
<<"Only occupants are allowed to send messages "
"to the conference">>,
Err = jlib:make_error_reply(Packet,
?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
ejabberd_router:route(RoomJID, From, Err).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Logging
add_to_log(Type, Data, StateData)
when Type == roomconfig_change_disabledlogging ->
mod_muc_log:add_to_log(StateData#state.server_host,
roomconfig_change, Data, StateData#state.jid,
make_opts(StateData));
add_to_log(Type, Data, StateData) ->
case (StateData#state.config)#config.logging of
true ->
mod_muc_log:add_to_log(StateData#state.server_host,
Type, Data, StateData#state.jid,
make_opts(StateData));
false -> ok
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Users number checking
tab_add_online_user(JID, StateData) ->
{LUser, LServer, LResource} = jlib:jid_tolower(JID),
US = {LUser, LServer},
Room = StateData#state.room,
Host = StateData#state.host,
catch ets:insert(muc_online_users,
#muc_online_users{us = US, resource = LResource,
room = Room, host = Host}).
tab_remove_online_user(JID, StateData) ->
{LUser, LServer, LResource} = jlib:jid_tolower(JID),
US = {LUser, LServer},
Room = StateData#state.room,
Host = StateData#state.host,
catch ets:delete_object(muc_online_users,
#muc_online_users{us = US, resource = LResource,
room = Room, host = Host}).
tab_count_user(JID) ->
{LUser, LServer, _} = jlib:jid_tolower(JID),
US = {LUser, LServer},
case catch ets:select(muc_online_users,
[{#muc_online_users{us = US, _ = '_'}, [], [[]]}])
of
Res when is_list(Res) -> length(Res);
_ -> 0
end.
element_size(El) ->
byte_size(xml:element_to_binary(El)).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Detect messange stanzas that don't have meaninful content
has_body_or_subject(Packet) ->
[] /= lists:dropwhile(fun
(#xmlel{name = <<"body">>}) -> false;
(#xmlel{name = <<"subject">>}) -> false;
(_) -> true
end, Packet#xmlel.children).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Multicast
send_multiple(From, Server, Users, Packet) ->
JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).