diff options
Diffstat (limited to 'src/mod_muc_room.erl')
-rw-r--r-- | src/mod_muc_room.erl | 4461 |
1 files changed, 4461 insertions, 0 deletions
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl new file mode 100644 index 000000000..7c9bdd41b --- /dev/null +++ b/src/mod_muc_room.erl @@ -0,0 +1,4461 @@ +%%%---------------------------------------------------------------------- +%%% 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-2013 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 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(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), + ?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_integer(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)), + {next_state, normal_state, 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 = 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_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_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 = <<"query">>, + 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), + [ejabberd_router:route(FromNickJID, ToJID, Packet) + || 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}]}]}, + lists:foreach( + fun({_LJID, Info}) -> + ejabberd_router:route( + StateData#state.jid, + Info#user.jid, + MessagePkt) + end, + ?DICT:to_list(StateData#state.users)), + 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(_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(_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 -> + lists:foreach( + fun({_LJID, Info}) -> + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, + FromNick), + Info#user.jid, + Packet) + end, + ?DICT:to_list(StateData#state.users)), + NewStateData2 = add_message_to_history(FromNick, From, + Packet, + NewStateData1), + {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, + 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(StateData#state.jid)]), + add_to_log(room_existence, destroyed, StateData), + {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 == 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 = + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, min_message_interval, + fun(I) when is_integer(I), I>=0 -> I end, + 0), + MinPresenceInterval = + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, min_presence_interval, + fun(I) when is_integer(I), I>=0 -> I end, + 0), + 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 * 1000, + MinPresenceInterval * 1000]) + * 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)), + if not (NewState#state.config)#config.anonymous -> + WPacket = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}], + children = + [#xmlel{name = <<"body">>, + attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"This room is not anonymous">>)}]}, + #xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"status">>, + attrs = + [{<<"code">>, + <<"100">>}], + children = + []}]}]}, + ejabberd_router:route(StateData#state.jid, From, WPacket); + true -> ok + end, + 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, Lang, 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). + +send_update_presence(JID, StateData) -> + send_update_presence(JID, <<"">>, StateData). + +send_update_presence(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) -> + #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) -> + 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, + 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 = + []}]}]}, + Packet2 = xml:append_subtags(Presence, + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name + = + <<"item">>, + attrs + = + ItemAttrs2, + children + = + []}]}]), + 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 = calendar:now_to_universal_time(now()), + SenderJid = case + (StateData#state.config)#config.anonymous + of + true -> StateData#state.jid; + false -> FromJID + end, + TSPacket = xml:append_subtags(Packet, + [jlib:timestamp_to_xml(TimeStamp, utc, + SenderJid, <<"">>), + jlib:timestamp_to_xml(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, + 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, Lang, StateData) -> + case StateData#state.subject_author of + <<"">> -> ok; + Nick -> + Subject = StateData#state.subject, + Packet = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}], + children = + [#xmlel{name = <<"subject">>, attrs = [], + children = [{xmlcdata, Subject}]}, + #xmlel{name = <<"body">>, attrs = [], + children = + [{xmlcdata, + <<Nick/binary, + (translate:translate(Lang, + <<" has set the subject to: ">>))/binary, + Subject/binary>>}]}]}, + ejabberd_router:route(StateData#state.jid, JID, Packet) + end. + +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) -> + 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(fun (E, SD) -> + 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 + end, + 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. + +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}}; + 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), + {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_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. + +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)). |