%%%---------------------------------------------------------------------- %%% File : mod_muc_room.erl %%% Author : Alexey Shchepin %%% Purpose : MUC room stuff %%% Created : 19 Mar 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(mod_muc_room). -author('alexey@process-one.net'). -behaviour(gen_fsm). %% External exports -export([start_link/9, start_link/7, start/9, start/7, get_role/2, get_affiliation/2, is_occupant_or_admin/2, 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("xmpp.hrl"). -include("mod_muc_room.hrl"). -define(MAX_USERS_DEFAULT_LIST, [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). -define(DEFAULT_MAX_USERS_PRESENCE,1000). %-define(DBGFSM, true). -ifdef(DBGFSM). -define(FSMOPTS, [{debug, [trace]}]). -else. -define(FSMOPTS, []). -endif. -type state() :: #state{}. -type fsm_stop() :: {stop, normal, state()}. -type fsm_next() :: {next_state, normal_state, state()}. -type fsm_transition() :: fsm_stop() | fsm_next(). -type history_element() :: {binary(), %% nick message(), %% message itself boolean(), %% have subject erlang:timestamp(), non_neg_integer()}. -export_type([state/0]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts) -> gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts], ?FSMOPTS). start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts], ?FSMOPTS). 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 %%%---------------------------------------------------------------------- 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 = jid:make(Room, Host, <<"">>), just_created = true, room_shaper = Shaper}), State1 = set_opts(DefRoomOpts, State), store_room(State1), ?INFO_MSG("Created MUC room ~s@~s by ~s", [Room, Host, 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 = jid:make(Room, Host, <<"">>), room_shaper = Shaper}), add_to_log(room_existence, started, State), {ok, normal_state, State}. normal_state({route, From, <<"">>, #message{type = Type, lang = Lang} = Packet}, StateData) -> case is_user_online(From, StateData) orelse is_user_allowed_message_nonparticipant(From, StateData) of true when Type == groupchat -> Activity = get_user_activity(From, StateData), Now = p1_time_compat:system_time(micro_seconds), MinMessageInterval = trunc(gen_mod:get_module_opt( StateData#state.server_host, mod_muc, min_message_interval, fun(MMI) when is_number(MMI) -> MMI end, 0) * 1000000), Size = element_size(Packet), {MessageShaper, MessageShaperInterval} = shaper:update(Activity#activity.message_shaper, Size), if Activity#activity.message /= undefined -> ErrText = <<"Traffic rate limit is exceeded">>, Err = xmpp:make_error( Packet, xmpp:err_resource_constraint(ErrText, Lang)), 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; true when Type == error -> case is_user_online(From, StateData) of true -> ErrorText = <<"It is not allowed to send error messages to the" " room. The participant (~s) has sent an error " "message (~s) and got kicked from the room">>, NewState = expulse_participant(Packet, From, StateData, translate:translate(Lang, ErrorText)), close_room_if_temporary_and_empty(NewState); _ -> {next_state, normal_state, StateData} end; true when Type == chat -> ErrText = <<"It is not allowed to send private messages " "to the conference">>, Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error(StateData#state.jid, From, Packet, Err), {next_state, normal_state, StateData}; true when Type == normal -> {next_state, normal_state, try xmpp:decode_els(Packet) of Pkt -> process_normal_message(From, Pkt, StateData) catch _:{xmpp_codec, Why} -> Txt = xmpp:format_error(Why), Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error( StateData#state.jid, From, Packet, Err), StateData end}; true -> ErrText = <<"Improper message type">>, Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error(StateData#state.jid, From, Packet, Err), {next_state, normal_state, StateData}; false when Type /= error -> handle_roommessage_from_nonparticipant(Packet, StateData, From); false -> {next_state, normal_state, StateData} end; normal_state({route, From, <<"">>, #iq{type = Type, lang = Lang, sub_els = [_]} = IQ0}, StateData) when Type == get; Type == set -> try case ejabberd_hooks:run_fold( muc_process_iq, StateData#state.server_host, xmpp:set_from_to(xmpp:decode_els(IQ0), From, StateData#state.jid), [StateData]) of ignore -> {next_state, normal_state, StateData}; #iq{type = T} = IQRes when T == error; T == result -> ejabberd_router:route(StateData#state.jid, From, IQRes), {next_state, normal_state, StateData}; #iq{sub_els = [SubEl]} = IQ -> Res1 = case xmpp:get_ns(SubEl) of ?NS_MUC_ADMIN -> process_iq_admin(From, IQ, StateData); ?NS_MUC_OWNER -> process_iq_owner(From, IQ, StateData); ?NS_DISCO_INFO when SubEl#disco_info.node == <<>> -> process_iq_disco_info(From, IQ, StateData); ?NS_DISCO_INFO -> Txt = <<"Disco info is not available for this node">>, {error, xmpp:err_service_unavailable(Txt, Lang)}; ?NS_DISCO_ITEMS -> process_iq_disco_items(From, IQ, StateData); ?NS_VCARD -> process_iq_vcard(From, IQ, StateData); ?NS_MUCSUB -> process_iq_mucsub(From, IQ, StateData); ?NS_CAPTCHA -> process_iq_captcha(From, IQ, StateData); _ -> {error, xmpp:err_feature_not_implemented()} end, {IQRes, NewStateData} = case Res1 of {result, Res, SD} -> {xmpp:make_iq_result(IQ, Res), SD}; {result, Res} -> {xmpp:make_iq_result(IQ, Res), StateData}; {ignore, SD} -> {ignore, SD}; {error, Error, ResStateData} -> {xmpp:make_error(IQ0, Error), ResStateData}; {error, Error} -> {xmpp:make_error(IQ0, Error), StateData} end, if IQRes /= ignore -> ejabberd_router:route(StateData#state.jid, From, IQRes); true -> ok end, case NewStateData of stop -> {stop, normal, StateData}; _ -> {next_state, normal_state, NewStateData} end end catch _:{xmpp_codec, Why} -> ErrTxt = xmpp:format_error(Why), Err = xmpp:make_error(IQ0, xmpp:err_bad_request(ErrTxt, Lang)), ejabberd_router:route(StateData#state.jid, From, Err) end; normal_state({route, From, <<"">>, #iq{} = IQ}, StateData) -> Err = xmpp:err_bad_request(), ejabberd_router:route_error(StateData#state.jid, From, IQ, Err), {next_state, normal_state, StateData}; normal_state({route, From, Nick, #presence{} = Packet}, StateData) -> Activity = get_user_activity(From, StateData), Now = p1_time_compat:system_time(micro_seconds), 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, #message{type = Type, lang = Lang} = Packet}, StateData) -> case decide_fate_message(Packet, From, StateData) of {expulse_sender, Reason} -> ?DEBUG(Reason, []), ErrorText = <<"It is not allowed to send error messages to the" " room. The participant (~s) has sent an error " "message (~s) and got kicked from the room">>, 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} when Type == groupchat -> ErrText = <<"It is not allowed to send private messages " "of type \"groupchat\"">>, Err = xmpp:err_bad_request(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, ToNick), From, Packet, Err); {true, true} -> case find_jids_by_nick(ToNick, StateData) of [] -> ErrText = <<"Recipient is not in the conference room">>, Err = xmpp:err_item_not_found(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, ToNick), From, Packet, 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(jid:tolower(From), StateData#state.users), FromNickJID = jid:replace_resource(StateData#state.jid, FromNick), X = #muc_user{}, PrivMsg = xmpp:set_subtag(Packet, X), [ejabberd_router:route(FromNickJID, ToJID, PrivMsg) || ToJID <- ToJIDs]; true -> ErrText = <<"It is not allowed to send private messages">>, Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, ToNick), From, Packet, Err) end end; {true, false} -> ErrText = <<"Only occupants are allowed to send messages " "to the conference">>, Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, ToNick), From, Packet, Err); {false, _} -> ErrText = <<"It is not allowed to send private messages">>, Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, ToNick), From, Packet, Err) end, {next_state, normal_state, StateData} end; normal_state({route, From, ToNick, #iq{id = StanzaId, lang = Lang} = Packet}, StateData) -> 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 -> ErrText = <<"Recipient is not in the conference room">>, Err = xmpp:err_item_not_found(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, ToNick), From, Packet, Err); ToJID -> {ok, #user{nick = FromNick}} = (?DICT):find(jid:tolower(FromFull), StateData#state.users), {ToJID2, Packet2} = handle_iq_vcard(ToJID, NewId, Packet), ejabberd_router:route( jid:replace_resource(StateData#state.jid, FromNick), ToJID2, Packet2) end; {_, {false, _, _}} -> ErrText = <<"Only occupants are allowed to send queries " "to the conference">>, Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, ToNick), From, Packet, Err); _ -> ErrText = <<"Queries to the conference members are " "not allowed in this room">>, Err = xmpp:err_not_allowed(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, ToNick), From, Packet, Err) end, {next_state, normal_state, StateData}; normal_state(_Event, StateData) -> {next_state, normal_state, StateData}. handle_event({service_message, Msg}, _StateName, StateData) -> MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)}, send_wrapped_multiple( StateData#state.jid, StateData#state.users, MessagePkt, ?NS_MUCSUB_NODES_MESSAGES, StateData), NSD = add_message_to_history(<<"">>, StateData#state.jid, MessagePkt, StateData), {next_state, normal_state, NSD}; handle_event({destroy, Reason}, _StateName, StateData) -> {result, undefined, stop} = destroy_room(#muc_destroy{xmlns = ?NS_MUC_OWNER, reason = Reason}, StateData), ?INFO_MSG("Destroyed MUC room ~s with reason: ~p", [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", [jid:to_string(StateData#state.jid)]), handle_event({destroy, undefined}, 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}. handle_sync_event({get_disco_item, Filter, JID, Lang}, _From, StateName, StateData) -> Len = ?DICT:fold(fun(_, _, Acc) -> Acc + 1 end, 0, StateData#state.users), Reply = case (Filter == all) or (Filter == Len) or ((Filter /= 0) and (Len /= 0)) of true -> get_roomdesc_reply(JID, StateData, get_roomdesc_tail(StateData, Lang)); false -> false end, {reply, Reply, StateName, StateData}; %% This clause is only for backwards compatibility handle_sync_event({get_disco_item, JID, Lang}, From, StateName, StateData) -> handle_sync_event({get_disco_item, any, JID, Lang}, From, 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, undefined, NSD} = change_config(Config, StateData), {reply, {ok, NSD#state.config}, StateName, NSD}; handle_sync_event({change_state, NewStateData}, _From, StateName, _StateData) -> {reply, {ok, NewStateData}, StateName, NewStateData}; handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) -> NSD = process_item_change(Item, StateData, UJID), {reply, {ok, NSD}, StateName, NSD}; handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. 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), Txt = <<"The CAPTCHA verification has failed">>, Lang = xmpp:get_lang(Packet), Err = xmpp:err_not_authorized(Txt, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, Nick), From, Packet, Err), StateData#state{robots = Robots}; _ -> StateData end, {next_state, normal_state, NewState}; handle_info(shutdown, _StateName, StateData) -> {stop, shutdown, StateData}; handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. 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, Packet = #presence{ type = unavailable, sub_els = [#muc_user{items = [#muc_item{affiliation = none, reason = ReasonT, role = none}], status_codes = [332]}]}, (?DICT):fold(fun (LJID, Info, _) -> Nick = Info#user.nick, case Reason of shutdown -> send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, ?NS_MUCSUB_NODES_PARTICIPANTS, StateData); _ -> 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 %%%---------------------------------------------------------------------- -spec route(pid(), jid(), binary(), stanza()) -> ok. route(Pid, From, ToNick, Packet) -> gen_fsm:send_event(Pid, {route, From, ToNick, Packet}). -spec process_groupchat_message(jid(), message(), state()) -> fsm_next(). process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) -> case is_user_online(From, StateData) orelse is_user_allowed_message_nonparticipant(From, StateData) of true -> {FromNick, Role, IsSubscriber} = get_participant_data(From, StateData), if (Role == moderator) or (Role == participant) or IsSubscriber or ((StateData#state.config)#config.moderated == false) -> Subject = check_subject(Packet), {NewStateData1, IsAllowed} = case Subject of false -> {StateData, true}; _ -> case can_change_subject(Role, StateData) of true -> NSD = StateData#state{subject = Subject, subject_author = FromNick}, store_room(NSD), {NSD, true}; _ -> {StateData, false} end end, case IsAllowed of true -> case ejabberd_hooks:run_fold(muc_filter_message, StateData#state.server_host, Packet, [StateData, StateData#state.jid, From, FromNick]) of drop -> {next_state, normal_state, StateData}; NewPacket1 -> NewPacket = xmpp:remove_subtag(NewPacket1, #nick{}), Node = if Subject == false -> ?NS_MUCSUB_NODES_MESSAGES; true -> ?NS_MUCSUB_NODES_SUBJECT end, send_wrapped_multiple( jid:replace_resource(StateData#state.jid, FromNick), StateData#state.users, NewPacket, Node, NewStateData1), NewStateData2 = case has_body_or_subject(NewPacket) of true -> add_message_to_history(FromNick, From, NewPacket, NewStateData1); false -> NewStateData1 end, {next_state, normal_state, NewStateData2} end; _ -> Err = case (StateData#state.config)#config.allow_change_subj of true -> xmpp:err_forbidden( <<"Only moderators and participants are " "allowed to change the subject in this " "room">>, Lang); _ -> xmpp:err_forbidden( <<"Only moderators are allowed to change " "the subject in this room">>, Lang) end, ejabberd_router:route_error( StateData#state.jid, From, Packet, Err), {next_state, normal_state, StateData} end; true -> ErrText = <<"Visitors are not allowed to send messages " "to all occupants">>, Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error( StateData#state.jid, From, Packet, Err), {next_state, normal_state, StateData} end; false -> ErrText = <<"Only occupants are allowed to send messages " "to the conference">>, Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error(StateData#state.jid, From, Packet, Err), {next_state, normal_state, StateData} end. -spec process_normal_message(jid(), message(), state()) -> state(). process_normal_message(From, #message{lang = Lang} = Pkt, StateData) -> IsInvitation = is_invitation(Pkt), IsVoiceRequest = is_voice_request(Pkt) and is_visitor(From, StateData), IsVoiceApprovement = is_voice_approvement(Pkt) and not is_visitor(From, StateData), if IsInvitation -> case check_invitation(From, Pkt, StateData) of {error, Error} -> ejabberd_router:route_error(StateData#state.jid, From, Pkt, Error), 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), send_affiliation(IJID, member, StateData), store_room(NSD), NSD; _ -> StateData end; false -> StateData end end; IsVoiceRequest -> case (StateData#state.config)#config.allow_voice_requests of true -> MinInterval = (StateData#state.config)#config.voice_request_min_interval, BareFrom = jid:remove_resource(jid:tolower(From)), NowPriority = -p1_time_compat:system_time(micro_seconds), 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, Lang, NSD), NSD; {ok, _, _} -> ErrText = <<"Please, wait for a while before sending " "new voice request">>, Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error( StateData#state.jid, From, Pkt, Err), StateData#state{last_voice_request_time = Times} end; false -> ErrText = <<"Voice requests are disabled in this conference">>, Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error( StateData#state.jid, From, Pkt, Err), StateData end; IsVoiceApprovement -> case is_moderator(From, StateData) of true -> case extract_jid_from_voice_approvement(Pkt) of error -> ErrText = <<"Failed to extract JID from your voice " "request approval">>, Err = xmpp:err_bad_request(ErrText, Lang), ejabberd_router:route_error( StateData#state.jid, From, Pkt, Err), StateData; TargetJid -> case is_visitor(TargetJid, StateData) of true -> Reason = <<>>, NSD = set_role(TargetJid, participant, StateData), catch send_new_presence(TargetJid, Reason, NSD, StateData), NSD; _ -> StateData end end; _ -> ErrText = <<"Only moderators can approve voice requests">>, Err = xmpp:err_not_allowed(ErrText, Lang), ejabberd_router:route_error( StateData#state.jid, From, Pkt, Err), StateData end; true -> 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. -spec is_user_allowed_message_nonparticipant(jid(), state()) -> boolean(). 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. -spec get_participant_data(jid(), state()) -> {binary(), role(), boolean()}. get_participant_data(From, StateData) -> case (?DICT):find(jid:tolower(From), StateData#state.users) of {ok, #user{nick = FromNick, role = Role, is_subscriber = IsSubscriber}} -> {FromNick, Role, IsSubscriber}; error -> {<<"">>, moderator, false} end. -spec process_presence(jid(), binary(), presence(), state()) -> fsm_transition(). process_presence(From, Nick, #presence{type = Type0} = Packet0, StateData) -> IsOnline = is_user_online(From, StateData), if Type0 == available; IsOnline and ((Type0 == unavailable) or (Type0 == error)) -> case ejabberd_hooks:run_fold(muc_filter_presence, StateData#state.server_host, Packet0, [StateData, StateData#state.jid, From, Nick]) of drop -> {next_state, normal_state, StateData}; #presence{} = Packet -> close_room_if_temporary_and_empty( do_process_presence(From, Nick, Packet, StateData)) end; true -> {next_state, normal_state, StateData} end. -spec do_process_presence(jid(), binary(), presence(), state()) -> state(). do_process_presence(From, Nick, #presence{type = available, lang = Lang} = Packet, StateData) -> case is_user_online(From, StateData) of false -> add_new_user(From, Nick, Packet, StateData); 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 = xmpp:err_not_allowed(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, Nick), From, Packet, Err), StateData; {true, _, _} -> ErrText = <<"That nickname is already in use by another " "occupant">>, Err = xmpp:err_conflict(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, Nick), From, Packet, Err), StateData; {_, false, _} -> ErrText = <<"That nickname is registered by another " "person">>, Err = xmpp:err_conflict(ErrText, Lang), ejabberd_router:route_error( jid:replace_resource(StateData#state.jid, Nick), From, Packet, Err), StateData; _ -> case is_initial_presence(From, StateData) of true -> subscriber_becomes_available( From, Nick, Packet, StateData); false -> change_nick(From, Nick, StateData) end end; _NotNickChange -> case is_initial_presence(From, StateData) of true -> subscriber_becomes_available( From, Nick, Packet, StateData); false -> Stanza = maybe_strip_status_from_presence( From, Packet, StateData), NewState = add_user_presence(From, Stanza, StateData), send_new_presence(From, NewState, StateData), NewState end end end; do_process_presence(From, Nick, #presence{type = unavailable} = Packet, StateData) -> IsSubscriber = is_subscriber(From, StateData), 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, StateData) end, Reason = xmpp:get_text(NewPacket#presence.status), remove_online_user(From, NewState, IsSubscriber, Reason); do_process_presence(From, _Nick, #presence{type = error, lang = Lang} = Packet, StateData) -> ErrorText = <<"It is not allowed to send error messages to the" " room. The participant (~s) has sent an error " "message (~s) and got kicked from the room">>, expulse_participant(Packet, From, StateData, translate:translate(Lang, ErrorText)). -spec maybe_strip_status_from_presence(jid(), presence(), state()) -> presence(). maybe_strip_status_from_presence(From, Packet, StateData) -> case {(StateData#state.config)#config.allow_visitor_status, is_visitor(From, StateData)} of {false, true} -> strip_status(Packet); _Allowed -> Packet end. -spec subscriber_becomes_available(jid(), binary(), presence(), state()) -> state(). subscriber_becomes_available(From, Nick, Packet, StateData) -> Stanza = maybe_strip_status_from_presence(From, Packet, StateData), State1 = add_user_presence(From, Stanza, StateData), Aff = get_affiliation(From, State1), Role = get_default_role(Aff, State1), State2 = set_role(From, Role, State1), State3 = set_nick(From, Nick, State2), send_existing_presences(From, State3), send_initial_presence(From, State3, StateData), State3. -spec close_room_if_temporary_and_empty(state()) -> fsm_transition(). close_room_if_temporary_and_empty(StateData1) -> case not (StateData1#state.config)#config.persistent andalso (?DICT):size(StateData1#state.users) == 0 of true -> ?INFO_MSG("Destroyed MUC room ~s because it's temporary " "and empty", [jid:to_string(StateData1#state.jid)]), add_to_log(room_existence, destroyed, StateData1), {stop, normal, StateData1}; _ -> {next_state, normal_state, StateData1} end. -spec is_user_online(jid(), state()) -> boolean(). is_user_online(JID, StateData) -> LJID = jid:tolower(JID), (?DICT):is_key(LJID, StateData#state.users). -spec is_subscriber(jid(), state()) -> boolean(). is_subscriber(JID, StateData) -> LJID = jid:tolower(JID), case (?DICT):find(LJID, StateData#state.users) of {ok, #user{is_subscriber = IsSubscriber}} -> IsSubscriber; _ -> false end. %% Check if the user is occupant of the room, or at least is an admin or owner. -spec is_occupant_or_admin(jid(), state()) -> boolean(). is_occupant_or_admin(JID, StateData) -> FAffiliation = get_affiliation(JID, StateData), FRole = get_role(JID, StateData), case FRole /= none orelse FAffiliation == member orelse FAffiliation == admin orelse FAffiliation == owner of true -> true; _ -> false end. %%% %%% Handle IQ queries of vCard %%% -spec is_user_online_iq(binary(), jid(), state()) -> {boolean(), binary(), jid()}. 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 = jid:replace_resource(JID, Resource), {is_user_online(JIDWithResource, StateData), OriginalId, JIDWithResource} catch _:_ -> {is_user_online(JID, StateData), StanzaId, JID} end. -spec handle_iq_vcard(jid(), binary(), iq()) -> {jid(), iq()}. handle_iq_vcard(ToJID, NewId, #iq{type = Type, sub_els = SubEls} = IQ) -> ToBareJID = jid:remove_resource(ToJID), case SubEls of [SubEl] when Type == get, ToBareJID /= ToJID -> case xmpp:get_ns(SubEl) of ?NS_VCARD -> {ToBareJID, change_stanzaid(ToJID, IQ)}; _ -> {ToJID, xmpp:set_id(IQ, NewId)} end; _ -> {ToJID, xmpp:set_id(IQ, NewId)} end. -spec stanzaid_pack(binary(), binary()) -> binary(). stanzaid_pack(OriginalId, Resource) -> <<"berd", (jlib:encode_base64(<<"ejab\000", OriginalId/binary, "\000", Resource/binary>>))/binary>>. -spec stanzaid_unpack(binary()) -> {binary(), binary()}. stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) -> StanzaId = jlib:decode_base64(StanzaIdBase64), [<<"ejab">>, OriginalId, Resource] = str:tokens(StanzaId, <<"\000">>), {OriginalId, Resource}. -spec change_stanzaid(jid(), iq()) -> iq(). change_stanzaid(ToJID, #iq{id = PreviousId} = Packet) -> NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource), xmpp:set_id(Packet, NewId). %% Decide the fate of the message and its sender %% Returns: continue_delivery | forget_message | {expulse_sender, Reason} -spec decide_fate_message(message(), jid(), state()) -> continue_delivery | forget_message | {expulse_sender, binary()}. decide_fate_message(#message{type = error} = Msg, From, StateData) -> Err = xmpp:get_error(Msg), PD = case check_error_kick(Err) 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", [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. -spec check_error_kick(error()) -> boolean(). check_error_kick(#error{reason = Reason}) -> case Reason 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; check_error_kick(undefined) -> false. -spec get_error_condition(error()) -> string(). get_error_condition(#error{reason = Reason}) -> case Reason of #gone{} -> "gone"; #redirect{} -> "redirect"; Atom -> atom_to_list(Atom) end; get_error_condition(undefined) -> "undefined". -spec make_reason(stanza(), jid(), state(), binary()) -> binary(). make_reason(Packet, From, StateData, Reason1) -> {ok, #user{nick = FromNick}} = (?DICT):find(jid:tolower(From), StateData#state.users), Condition = get_error_condition(xmpp:get_error(Packet)), iolist_to_binary(io_lib:format(Reason1, [FromNick, Condition])). -spec expulse_participant(stanza(), jid(), state(), binary()) -> state(). expulse_participant(Packet, From, StateData, Reason1) -> IsSubscriber = is_subscriber(From, StateData), Reason2 = make_reason(Packet, From, StateData, Reason1), NewState = add_user_presence_un(From, #presence{type = unavailable, status = xmpp:mk_text(Reason2)}, StateData), send_new_presence(From, NewState, StateData), remove_online_user(From, NewState, IsSubscriber). -spec set_affiliation(jid(), affiliation(), state()) -> state(). set_affiliation(JID, Affiliation, StateData) -> set_affiliation(JID, Affiliation, StateData, <<"">>). -spec set_affiliation(jid(), affiliation(), state(), binary()) -> state(). set_affiliation(JID, Affiliation, StateData, Reason) -> LJID = jid:remove_resource(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}. -spec get_affiliation(jid(), state()) -> affiliation(). 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 = jid:tolower(JID), case (?DICT):find(LJID, StateData#state.affiliations) of {ok, Affiliation} -> Affiliation; _ -> LJID1 = 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 = 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. -spec get_service_affiliation(jid(), state()) -> owner | none. 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. -spec set_role(jid(), role(), state()) -> state(). set_role(JID, Role, StateData) -> LJID = 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), if User#user.last_presence == undefined -> Us; true -> (?DICT):store(J, User#user{role = Role}, Us) end end, StateData#state.users, LJIDs), StateData#state.nicks} end, StateData#state{users = Users, nicks = Nicks}. -spec get_role(jid(), state()) -> role(). get_role(JID, StateData) -> LJID = jid:tolower(JID), case (?DICT):find(LJID, StateData#state.users) of {ok, #user{role = Role}} -> Role; _ -> none end. -spec get_default_role(affiliation(), state()) -> role(). 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. -spec is_visitor(jid(), state()) -> boolean(). is_visitor(Jid, StateData) -> get_role(Jid, StateData) =:= visitor. -spec is_moderator(jid(), state()) -> boolean(). is_moderator(Jid, StateData) -> get_role(Jid, StateData) =:= moderator. -spec get_max_users(state()) -> non_neg_integer(). get_max_users(StateData) -> MaxUsers = (StateData#state.config)#config.max_users, ServiceMaxUsers = get_service_max_users(StateData), if MaxUsers =< ServiceMaxUsers -> MaxUsers; true -> ServiceMaxUsers end. -spec get_service_max_users(state()) -> pos_integer(). 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). -spec get_max_users_admin_threshold(state()) -> pos_integer(). 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). -spec get_user_activity(jid(), state()) -> #activity{}. get_user_activity(JID, StateData) -> case treap:lookup(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. -spec store_user_activity(jid(), #activity{}, state()) -> state(). store_user_activity(JID, UserActivity, StateData) -> MinMessageInterval = trunc(gen_mod:get_module_opt(StateData#state.server_host, mod_muc, min_message_interval, fun(I) when is_number(I), I>=0 -> I end, 0) * 1000), MinPresenceInterval = trunc(gen_mod:get_module_opt(StateData#state.server_host, mod_muc, min_presence_interval, fun(I) when is_number(I), I>=0 -> I end, 0) * 1000), Key = jid:tolower(JID), Now = p1_time_compat:system_time(micro_seconds), Activity1 = clean_treap(StateData#state.activity, {1, -Now}), Activity = case treap:lookup(Key, Activity1) of {ok, _P, _A} -> treap:delete(Key, Activity1); error -> Activity1 end, StateData1 = case MinMessageInterval == 0 andalso MinPresenceInterval == 0 andalso UserActivity#activity.message_shaper == none andalso UserActivity#activity.presence_shaper == none andalso UserActivity#activity.message == undefined andalso UserActivity#activity.presence == undefined of true -> StateData#state{activity = Activity}; false -> case UserActivity#activity.message == undefined andalso UserActivity#activity.presence == undefined of true -> {_, MessageShaperInterval} = shaper:update(UserActivity#activity.message_shaper, 100000), {_, PresenceShaperInterval} = shaper:update(UserActivity#activity.presence_shaper, 100000), Delay = lists:max([MessageShaperInterval, PresenceShaperInterval, MinMessageInterval, MinPresenceInterval]) * 1000, Priority = {1, -(Now + Delay)}, StateData#state{activity = treap:insert(Key, Priority, UserActivity, Activity)}; false -> Priority = {0, 0}, StateData#state{activity = treap:insert(Key, Priority, UserActivity, Activity)} end end, StateData1. -spec clean_treap(treap:treap(), integer()) -> treap:treap(). 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. -spec prepare_room_queue(state()) -> state(). 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. -spec update_online_user(jid(), #user{}, state()) -> state(). update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, is_subscriber = IsSubscriber} = User, StateData) -> LJID = jid:tolower(JID), Nicks1 = case (?DICT):find(LJID, StateData#state.users) of {ok, #user{nick = OldNick}} -> case lists:delete( LJID, ?DICT:fetch(OldNick, StateData#state.nicks)) of [] -> ?DICT:erase(OldNick, StateData#state.nicks); LJIDs -> ?DICT:store(OldNick, LJIDs, StateData#state.nicks) end; error -> StateData#state.nicks end, Nicks = (?DICT):update(Nick, fun (LJIDs) -> [LJID|LJIDs -- [LJID]] end, [LJID], Nicks1), Users = (?DICT):update(LJID, fun(U) -> U#user{nick = Nick, subscriptions = Nodes, is_subscriber = IsSubscriber} end, User, StateData#state.users), NewStateData = StateData#state{users = Users, nicks = Nicks}, case {?DICT:find(LJID, StateData#state.users), ?DICT:find(LJID, NewStateData#state.users)} of {{ok, #user{nick = Old}}, {ok, #user{nick = New}}} when Old /= New -> send_nick_changing(JID, Old, NewStateData, true, true); _ -> ok end, NewStateData. -spec add_online_user(jid(), binary(), role(), boolean(), [binary()], state()) -> state(). add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) -> tab_add_online_user(JID, StateData), User = #user{jid = JID, nick = Nick, role = Role, is_subscriber = IsSubscriber, subscriptions = Nodes}, StateData1 = update_online_user(JID, User, StateData), if IsSubscriber -> store_room(StateData1); true -> ok end, StateData1. -spec remove_online_user(jid(), state(), boolean()) -> state(). remove_online_user(JID, StateData, IsSubscriber) -> remove_online_user(JID, StateData, IsSubscriber, <<"">>). -spec remove_online_user(jid(), state(), boolean(), binary()) -> state(). remove_online_user(JID, StateData, _IsSubscriber = true, _Reason) -> LJID = jid:tolower(JID), Users = case (?DICT):find(LJID, StateData#state.users) of {ok, U} -> (?DICT):store(LJID, U#user{last_presence = undefined}, StateData#state.users); error -> StateData#state.users end, StateData#state{users = Users}; remove_online_user(JID, StateData, _IsSubscriber, Reason) -> LJID = 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}. -spec filter_presence(presence()) -> presence(). filter_presence(Presence) -> Els = lists:filter( fun(El) -> XMLNS = xmpp:get_ns(El), case catch binary:part(XMLNS, 0, size(?NS_MUC)) of ?NS_MUC -> false; _ -> true end end, xmpp:get_els(Presence)), xmpp:set_els(Presence, Els). -spec strip_status(presence()) -> presence(). strip_status(Presence) -> Presence#presence{status = []}. -spec add_user_presence(jid(), presence(), state()) -> state(). add_user_presence(JID, Presence, StateData) -> LJID = 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}. -spec add_user_presence_un(jid(), presence(), state()) -> state(). add_user_presence_un(JID, Presence, StateData) -> LJID = 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. -spec find_jids_by_nick(binary(), state()) -> [jid()]. find_jids_by_nick(Nick, StateData) -> case (?DICT):find(Nick, StateData#state.nicks) of {ok, [User]} -> [jid:make(User)]; {ok, Users} -> [jid:make(LJID) || LJID <- Users]; error -> [] end. %% Find and return the full JID of the user of Nick with %% highest-priority presence. Return jid record. -spec find_jid_by_nick(binary(), state()) -> jid() | false. find_jid_by_nick(Nick, StateData) -> case (?DICT):find(Nick, StateData#state.nicks) of {ok, [User]} -> jid:make(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), jid:make(LJID); error -> false end. -spec higher_presence(undefined | presence(), undefined | presence()) -> boolean(). higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined -> Pri1 = get_priority_from_presence(Pres1), Pri2 = get_priority_from_presence(Pres2), Pri1 > Pri2; higher_presence(Pres1, Pres2) -> Pres1 > Pres2. -spec get_priority_from_presence(presence()) -> integer(). get_priority_from_presence(#presence{priority = Prio}) -> case Prio of undefined -> 0; _ -> Prio end. -spec find_nick_by_jid(jid(), state()) -> binary(). find_nick_by_jid(JID, StateData) -> LJID = jid:tolower(JID), {ok, #user{nick = Nick}} = (?DICT):find(LJID, StateData#state.users), Nick. -spec is_nick_change(jid(), binary(), state()) -> boolean(). is_nick_change(JID, Nick, StateData) -> LJID = jid:tolower(JID), case Nick of <<"">> -> false; _ -> {ok, #user{nick = OldNick}} = (?DICT):find(LJID, StateData#state.users), Nick /= OldNick end. -spec nick_collision(jid(), binary(), state()) -> boolean(). nick_collision(User, Nick, StateData) -> UserOfNick = find_jid_by_nick(Nick, StateData), (UserOfNick /= false andalso jid:remove_resource(jid:tolower(UserOfNick)) /= jid:remove_resource(jid:tolower(User))). -spec add_new_user(jid(), binary(), presence() | iq(), state()) -> state() | {error, error()} | {ignore, state()} | {result, xmpp_element(), state()}. add_new_user(From, Nick, Packet, StateData) -> Lang = xmpp:get_lang(Packet), UserRoomJID = jid:replace_resource(StateData#state.jid, Nick), 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), IsSubscribeRequest = not is_record(Packet, presence), 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, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> Txt = <<"Too many users in this conference">>, Err = xmpp:err_resource_constraint(Txt, Lang), ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> {error, Err} end; {false, _, _, _} when NConferences >= MaxConferences -> Txt = <<"You have joined too many conferences">>, Err = xmpp:err_resource_constraint(Txt, Lang), ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> {error, Err} end; {false, _, _, _} -> Err = xmpp:err_service_unavailable(), ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> {error, Err} end; {_, _, _, none} -> Err = case Affiliation of outcast -> ErrText = <<"You have been banned from this room">>, xmpp:err_forbidden(ErrText, Lang); _ -> ErrText = <<"Membership is required to enter this room">>, xmpp:err_registration_required(ErrText, Lang) end, ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> {error, Err} end; {_, true, _, _} -> ErrText = <<"That nickname is already in use by another occupant">>, Err = xmpp:err_conflict(ErrText, Lang), ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> {error, Err} end; {_, _, false, _} -> ErrText = <<"That nickname is registered by another person">>, Err = xmpp:err_conflict(ErrText, Lang), ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> {error, Err} end; {_, _, _, Role} -> case check_password(ServiceAffiliation, Affiliation, Packet, From, StateData) of true -> Nodes = get_subscription_nodes(Packet), NewStateData = if not IsSubscribeRequest -> NewState = add_user_presence( From, Packet, add_online_user(From, Nick, Role, IsSubscribeRequest, Nodes, StateData)), send_existing_presences(From, NewState), send_initial_presence(From, NewState, StateData), Shift = count_stanza_shift(Nick, Packet, NewState), case send_history(From, Shift, NewState) of true -> ok; _ -> send_subject(From, StateData) end, NewState; true -> add_online_user(From, Nick, none, IsSubscribeRequest, Nodes, StateData) end, ResultState = case NewStateData#state.just_created of true -> NewStateData#state{just_created = false}; false -> Robots = (?DICT):erase(From, StateData#state.robots), NewStateData#state{robots = Robots} end, if not IsSubscribeRequest -> ResultState; true -> {result, subscribe_result(Packet), ResultState} end; nopass -> ErrText = <<"A password is required to enter this room">>, Err = xmpp:err_not_authorized(ErrText, Lang), ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> {error, Err} end; captcha_required -> SID = xmpp:get_id(Packet), RoomJID = StateData#state.jid, To = 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, Body, CaptchaEls} -> MsgPkt = #message{id = ID, body = Body, sub_els = CaptchaEls}, Robots = (?DICT):store(From, {Nick, Packet}, StateData#state.robots), ejabberd_router:route(RoomJID, From, MsgPkt), NewState = StateData#state{robots = Robots}, if not IsSubscribeRequest -> NewState; true -> {ignore, NewState} end; {error, limit} -> ErrText = <<"Too many CAPTCHA requests">>, Err = xmpp:err_resource_constraint(ErrText, Lang), ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> {error, Err} end; _ -> ErrText = <<"Unable to generate a CAPTCHA">>, Err = xmpp:err_internal_server_error(ErrText, Lang), ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> {error, Err} end end; _ -> ErrText = <<"Incorrect password">>, Err = xmpp:err_not_authorized(ErrText, Lang), ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> {error, Err} end end end. -spec check_password(affiliation(), affiliation(), stanza(), jid(), state()) -> boolean() | nopass. check_password(owner, _Affiliation, _Packet, _From, _StateData) -> %% Don't check pass if user is owner in MUC service (access_admin option) true; check_password(_ServiceAffiliation, Affiliation, Packet, From, StateData) -> case (StateData#state.config)#config.password_protected of false -> check_captcha(Affiliation, From, StateData); true -> Pass = extract_password(Packet), case Pass of false -> nopass; _ -> case (StateData#state.config)#config.password of Pass -> true; _ -> false end end end. -spec check_captcha(affiliation(), jid(), state()) -> true | captcha_required. 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. -spec extract_password(stanza()) -> binary() | false. extract_password(Packet) -> case xmpp:get_subtag(Packet, #muc{}) of #muc{password = Password} when is_binary(Password) -> Password; _ -> false end. -spec count_stanza_shift(binary(), stanza(), state()) -> non_neg_integer(). count_stanza_shift(Nick, Packet, StateData) -> case xmpp:get_subtag(Packet, #muc_history{}) of #muc_history{since = Since, seconds = Seconds, maxstanzas = MaxStanzas, maxchars = MaxChars} -> HL = lqueue_to_list(StateData#state.history), Shift0 = case Since of undefined -> 0; _ -> Sin = calendar:datetime_to_gregorian_seconds( calendar:now_to_datetime(Since)), count_seconds_shift(Sin, HL) end, Shift1 = case Seconds of undefined -> 0; _ -> Sec = calendar:datetime_to_gregorian_seconds( calendar:universal_time()) - Seconds, count_seconds_shift(Sec, HL) end, Shift2 = case MaxStanzas of undefined -> 0; _ -> count_maxstanzas_shift(MaxStanzas, HL) end, Shift3 = case MaxChars of undefined -> 0; _ -> count_maxchars_shift(Nick, MaxChars, HL) end, lists:max([Shift0, Shift1, Shift2, Shift3]); false -> 0 end. -spec count_seconds_shift(non_neg_integer(), [history_element()]) -> non_neg_integer(). 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)). -spec count_maxstanzas_shift(non_neg_integer(), [history_element()]) -> non_neg_integer(). count_maxstanzas_shift(MaxStanzas, HistoryList) -> S = length(HistoryList) - MaxStanzas, if S =< 0 -> 0; true -> S end. -spec count_maxchars_shift(binary(), non_neg_integer(), [history_element()]) -> integer(). 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). -spec calc_shift(non_neg_integer(), [non_neg_integer()]) -> integer(). calc_shift(MaxSize, Sizes) -> Total = lists:sum(Sizes), calc_shift(MaxSize, Total, 0, Sizes). -spec calc_shift(non_neg_integer(), integer(), integer(), [non_neg_integer()]) -> integer(). 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. -spec is_room_overcrowded(state()) -> boolean(). is_room_overcrowded(StateData) -> MaxUsersPresence = gen_mod:get_module_opt(StateData#state.server_host, mod_muc, max_users_presence, fun(MUP) when is_integer(MUP) -> MUP end, ?DEFAULT_MAX_USERS_PRESENCE), (?DICT):size(StateData#state.users) > MaxUsersPresence. -spec presence_broadcast_allowed(jid(), state()) -> boolean(). presence_broadcast_allowed(JID, StateData) -> Role = get_role(JID, StateData), lists:member(Role, (StateData#state.config)#config.presence_broadcast). -spec is_initial_presence(jid(), state()) -> boolean(). is_initial_presence(From, StateData) -> LJID = jid:tolower(From), case (?DICT):find(LJID, StateData#state.users) of {ok, #user{last_presence = Pres}} when Pres /= undefined -> false; _ -> true end. -spec send_initial_presence(jid(), state(), state()) -> ok. send_initial_presence(NJID, StateData, OldStateData) -> send_new_presence1(NJID, <<"">>, true, StateData, OldStateData). -spec send_update_presence(jid(), state(), state()) -> ok. send_update_presence(JID, StateData, OldStateData) -> send_update_presence(JID, <<"">>, StateData, OldStateData). -spec send_update_presence(jid(), binary(), state(), state()) -> ok. send_update_presence(JID, Reason, StateData, OldStateData) -> case is_room_overcrowded(StateData) of true -> ok; false -> send_update_presence1(JID, Reason, StateData, OldStateData) end. -spec send_update_presence1(jid(), binary(), state(), state()) -> ok. send_update_presence1(JID, Reason, StateData, OldStateData) -> LJID = 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_presence1(J, Reason, false, StateData, OldStateData) end, LJIDs). -spec send_new_presence(jid(), state(), state()) -> ok. send_new_presence(NJID, StateData, OldStateData) -> send_new_presence(NJID, <<"">>, false, StateData, OldStateData). -spec send_new_presence(jid(), binary(), state(), state()) -> ok. send_new_presence(NJID, Reason, StateData, OldStateData) -> send_new_presence(NJID, Reason, false, StateData, OldStateData). -spec send_new_presence(jid(), binary(), boolean(), state(), state()) -> ok. send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> case is_room_overcrowded(StateData) of true -> ok; false -> send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) end. -spec is_ra_changed(jid() | ljid(), boolean(), state(), state()) -> boolean(). is_ra_changed(_, _IsInitialPresence = true, _, _) -> false; is_ra_changed(LJID, _IsInitialPresence = false, NewStateData, OldStateData) -> JID = case LJID of #jid{} -> LJID; _ -> jid:make(LJID) end, NewRole = get_role(LJID, NewStateData), NewAff = get_affiliation(JID, NewStateData), OldRole = get_role(LJID, OldStateData), OldAff = get_affiliation(JID, OldStateData), if (NewRole == none) and (NewAff == OldAff) -> %% A user is leaving the room; false; true -> (NewRole /= OldRole) or (NewAff /= OldAff) end. -spec send_new_presence1(jid(), binary(), boolean(), state(), state()) -> ok. send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> LNJID = jid:tolower(NJID), #user{nick = Nick} = (?DICT):fetch(LNJID, StateData#state.users), LJID = find_jid_by_nick(Nick, StateData), {ok, #user{jid = RealJID, role = Role0, last_presence = Presence0} = UserInfo} = (?DICT):find(jid:tolower(LJID), StateData#state.users), {Role1, Presence1} = case presence_broadcast_allowed(NJID, StateData) of true -> {Role0, Presence0}; false -> {none, #presence{type = unavailable}} end, Affiliation = get_affiliation(LJID, StateData), UserList = case not (presence_broadcast_allowed(NJID, StateData) orelse presence_broadcast_allowed(NJID, OldStateData)) of true -> [{LNJID, UserInfo}]; false -> (?DICT):to_list(StateData#state.users) end, lists:foreach( fun({LUJID, Info}) -> {Role, Presence} = if LNJID == LUJID -> {Role0, Presence0}; true -> {Role1, Presence1} end, Item0 = #muc_item{affiliation = Affiliation, role = Role}, Item1 = case Info#user.role == moderator orelse (StateData#state.config)#config.anonymous == false of true -> Item0#muc_item{jid = RealJID}; false -> Item0 end, Item = Item1#muc_item{reason = Reason}, StatusCodes = status_codes(IsInitialPresence, NJID, Info, StateData), Pres = if Presence == undefined -> #presence{}; true -> Presence end, Packet = xmpp:set_subtag( Pres, #muc_user{items = [Item], status_codes = StatusCodes}), Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of true -> ?NS_MUCSUB_NODES_AFFILIATIONS; false -> ?NS_MUCSUB_NODES_PRESENCE end, send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, Node1, StateData), Type = xmpp:get_type(Packet), IsSubscriber = Info#user.is_subscriber, IsOccupant = Info#user.last_presence /= undefined, if (IsSubscriber and not IsOccupant) and (IsInitialPresence or (Type == unavailable)) -> Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS, send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, Node2, StateData); true -> ok end end, UserList). -spec send_existing_presences(jid(), state()) -> ok. send_existing_presences(ToJID, StateData) -> case is_room_overcrowded(StateData) of true -> ok; false -> send_existing_presences1(ToJID, StateData) end. -spec send_existing_presences1(jid(), state()) -> ok. send_existing_presences1(ToJID, StateData) -> LToJID = 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(jid:tolower(LJID), StateData#state.users), PresenceBroadcast = lists:member( FromRole, (StateData#state.config)#config.presence_broadcast), case {RealToJID, PresenceBroadcast} of {FromJID, _} -> ok; {_, false} -> ok; _ -> FromAffiliation = get_affiliation(LJID, StateData), Item0 = #muc_item{affiliation = FromAffiliation, role = FromRole}, Item = case Role == moderator orelse (StateData#state.config)#config.anonymous == false of true -> Item0#muc_item{jid = FromJID}; false -> Item0 end, Packet = xmpp:set_subtag( Presence, #muc_user{items = [Item]}), send_wrapped(jid:replace_resource(StateData#state.jid, FromNick), RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData) end end, (?DICT):to_list(StateData#state.nicks)). -spec set_nick(jid(), binary(), state()) -> state(). set_nick(JID, Nick, State) -> LJID = jid:tolower(JID), {ok, #user{nick = OldNick}} = (?DICT):find(LJID, State#state.users), Users = (?DICT):update(LJID, fun (#user{} = User) -> User#user{nick = Nick} end, State#state.users), OldNickUsers = (?DICT):fetch(OldNick, State#state.nicks), NewNickUsers = case (?DICT):find(Nick, State#state.nicks) of {ok, U} -> U; error -> [] end, Nicks = case OldNickUsers of [LJID] -> (?DICT):store(Nick, [LJID | NewNickUsers -- [LJID]], (?DICT):erase(OldNick, State#state.nicks)); [_ | _] -> (?DICT):store(Nick, [LJID | NewNickUsers -- [LJID]], (?DICT):store(OldNick, OldNickUsers -- [LJID], State#state.nicks)) end, State#state{users = Users, nicks = Nicks}. -spec change_nick(jid(), binary(), state()) -> state(). change_nick(JID, Nick, StateData) -> LJID = jid:tolower(JID), {ok, #user{nick = OldNick}} = (?DICT):find(LJID, 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 == [], NewStateData = set_nick(JID, Nick, StateData), case presence_broadcast_allowed(JID, NewStateData) of true -> send_nick_changing(JID, OldNick, NewStateData, SendOldUnavailable, SendNewAvailable); false -> ok end, add_to_log(nickchange, {OldNick, Nick}, StateData), NewStateData. -spec send_nick_changing(jid(), binary(), state(), boolean(), boolean()) -> ok. send_nick_changing(JID, OldNick, StateData, SendOldUnavailable, SendNewAvailable) -> {ok, #user{jid = RealJID, nick = Nick, role = Role, last_presence = Presence}} = (?DICT):find(jid:tolower(JID), StateData#state.users), Affiliation = get_affiliation(JID, StateData), lists:foreach( fun({_LJID, Info}) when Presence /= undefined -> Item0 = #muc_item{affiliation = Affiliation, role = Role}, Item1 = case Info#user.role == moderator orelse (StateData#state.config)#config.anonymous == false of true -> Item0#muc_item{jid = RealJID, nick = Nick}; false -> Item0#muc_item{nick = Nick} end, Item2 = case Info#user.role == moderator orelse (StateData#state.config)#config.anonymous == false of true -> Item0#muc_item{jid = RealJID}; false -> Item0 end, Status110 = case JID == Info#user.jid of true -> [110]; false -> [] end, Packet1 = #presence{type = unavailable, sub_els = [#muc_user{ items = [Item1], status_codes = [303|Status110]}]}, Packet2 = xmpp:set_subtag(Presence, #muc_user{items = [Item2], status_codes = Status110}), if SendOldUnavailable -> send_wrapped( jid:replace_resource(StateData#state.jid, OldNick), Info#user.jid, Packet1, ?NS_MUCSUB_NODES_PRESENCE, StateData); true -> ok end, if SendNewAvailable -> send_wrapped( jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet2, ?NS_MUCSUB_NODES_PRESENCE, StateData); true -> ok end; (_) -> ok end, (?DICT):to_list(StateData#state.users)). -spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok. maybe_send_affiliation(JID, Affiliation, StateData) -> LJID = jid:tolower(JID), IsOccupant = case LJID of {LUser, LServer, <<"">>} -> not (?DICT):is_empty( (?DICT):filter(fun({U, S, _}, _) -> U == LUser andalso S == LServer end, StateData#state.users)); {_LUser, _LServer, _LResource} -> (?DICT):is_key(LJID, StateData#state.users) end, case IsOccupant of true -> ok; % The new affiliation is published via presence. false -> send_affiliation(LJID, Affiliation, StateData) end. -spec send_affiliation(ljid(), affiliation(), state()) -> ok. send_affiliation(LJID, Affiliation, StateData) -> Item = #muc_item{jid = jid:make(LJID), affiliation = Affiliation, role = none}, Message = #message{id = randoms:get_string(), sub_els = [#muc_user{items = [Item]}]}, Recipients = case (StateData#state.config)#config.anonymous of true -> (?DICT):filter(fun(_, #user{role = moderator}) -> true; (_, _) -> false end, StateData#state.users); false -> StateData#state.users end, send_multiple(StateData#state.jid, StateData#state.server_host, Recipients, Message). -spec status_codes(boolean(), jid(), #user{}, state()) -> [pos_integer()]. status_codes(IsInitialPresence, JID, #user{jid = JID}, StateData) -> S0 = [110], case IsInitialPresence of true -> S1 = case StateData#state.just_created of true -> [201|S0]; false -> S0 end, S2 = case (StateData#state.config)#config.anonymous of true -> S1; false -> [100|S1] end, S3 = case (StateData#state.config)#config.logging of true -> [170|S2]; false -> S2 end, S3; false -> S0 end; status_codes(_IsInitialPresence, _JID, _Info, _StateData) -> []. -spec lqueue_new(non_neg_integer()) -> lqueue(). lqueue_new(Max) -> #lqueue{queue = queue:new(), len = 0, max = Max}. -spec lqueue_in(term(), lqueue()) -> lqueue(). %% 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. -spec lqueue_cut(queue:queue(), non_neg_integer()) -> queue:queue(). lqueue_cut(Q, 0) -> Q; lqueue_cut(Q, N) -> {_, Q1} = queue:out(Q), lqueue_cut(Q1, N - 1). -spec lqueue_to_list(lqueue()) -> list(). lqueue_to_list(#lqueue{queue = Q1}) -> queue:to_list(Q1). -spec add_message_to_history(binary(), jid(), message(), state()) -> state(). add_message_to_history(FromNick, FromJID, Packet, StateData) -> HaveSubject = Packet#message.subject /= [], TimeStamp = p1_time_compat:timestamp(), AddrPacket = case (StateData#state.config)#config.anonymous of true -> Packet; false -> Addresses = #addresses{ list = [#address{type = ofrom, jid = FromJID}]}, xmpp:set_subtag(Packet, Addresses) end, TSPacket = xmpp_util:add_delay_info( AddrPacket, StateData#state.jid, TimeStamp), SPacket = xmpp:set_from_to( TSPacket, jid:replace_resource(StateData#state.jid, FromNick), StateData#state.jid), Size = element_size(SPacket), Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, calendar:now_to_universal_time(TimeStamp), Size}, StateData#state.history), add_to_log(text, {FromNick, Packet}, StateData), StateData#state{history = Q1}. -spec send_history(jid(), integer(), state()) -> boolean(). send_history(JID, Shift, StateData) -> lists:foldl(fun ({Nick, Packet, HaveSubject, _TimeStamp, _Size}, B) -> ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), JID, Packet), B or HaveSubject end, false, lists:nthtail(Shift, lqueue_to_list(StateData#state.history))). -spec send_subject(jid(), state()) -> ok. send_subject(_JID, #state{subject_author = <<"">>}) -> ok; send_subject(JID, #state{subject_author = Nick} = StateData) -> Subject = StateData#state.subject, Packet = #message{type = groupchat, subject = xmpp:mk_text(Subject)}, ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), JID, Packet). -spec check_subject(message()) -> false | binary(). check_subject(#message{subject = []}) -> false; check_subject(#message{subject = Subj}) -> xmpp:get_text(Subj). -spec can_change_subject(role(), state()) -> boolean(). 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 -spec process_iq_admin(jid(), iq(), #state{}) -> {error, error()} | {result, undefined, #state{}} | {result, muc_admin()}. process_iq_admin(_From, #iq{lang = Lang, sub_els = [#muc_admin{items = []}]}, _StateData) -> Txt = <<"No 'item' element found">>, {error, xmpp:err_bad_request(Txt, Lang)}; process_iq_admin(From, #iq{type = set, lang = Lang, sub_els = [#muc_admin{items = Items}]}, StateData) -> process_admin_items_set(From, Items, Lang, StateData); process_iq_admin(From, #iq{type = get, lang = Lang, sub_els = [#muc_admin{items = [Item]}]}, StateData) -> FAffiliation = get_affiliation(From, StateData), FRole = get_role(From, StateData), case Item of #muc_item{role = undefined, affiliation = undefined} -> Txt = <<"Neither 'role' nor 'affiliation' attribute found">>, {error, xmpp:err_bad_request(Txt, Lang)}; #muc_item{role = undefined, affiliation = Affiliation} -> if (FAffiliation == owner) or (FAffiliation == admin) or ((FAffiliation == member) and not (StateData#state.config)#config.anonymous) -> Items = items_with_affiliation(Affiliation, StateData), {result, #muc_admin{items = Items}}; true -> ErrText = <<"Administrator privileges required">>, {error, xmpp:err_forbidden(ErrText, Lang)} end; #muc_item{role = Role} -> if FRole == moderator -> Items = items_with_role(Role, StateData), {result, #muc_admin{items = Items}}; true -> ErrText = <<"Moderator privileges required">>, {error, xmpp:err_forbidden(ErrText, Lang)} end end; process_iq_admin(_From, #iq{type = get, lang = Lang}, _StateData) -> ErrText = <<"Too many elements">>, {error, xmpp:err_bad_request(ErrText, Lang)}. -spec items_with_role(role(), state()) -> [muc_item()]. items_with_role(SRole, StateData) -> lists:map(fun ({_, U}) -> user_to_item(U, StateData) end, search_role(SRole, StateData)). -spec items_with_affiliation(affiliation(), state()) -> [muc_item()]. items_with_affiliation(SAffiliation, StateData) -> lists:map( fun({JID, {Affiliation, Reason}}) -> #muc_item{affiliation = Affiliation, jid = JID, reason = Reason}; ({JID, Affiliation}) -> #muc_item{affiliation = Affiliation, jid = JID} end, search_affiliation(SAffiliation, StateData)). -spec user_to_item(#user{}, state()) -> muc_item(). user_to_item(#user{role = Role, nick = Nick, jid = JID}, StateData) -> Affiliation = get_affiliation(JID, StateData), #muc_item{role = Role, affiliation = Affiliation, nick = Nick, jid = JID}. -spec search_role(role(), state()) -> [{ljid(), #user{}}]. search_role(Role, StateData) -> lists:filter(fun ({_, #user{role = R}}) -> Role == R end, (?DICT):to_list(StateData#state.users)). -spec search_affiliation(affiliation(), state()) -> [{ljid(), affiliation() | {affiliation(), binary()}}]. 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)). -spec process_admin_items_set(jid(), [muc_item()], binary(), #state{}) -> {result, undefined, #state{}} | {error, error()}. process_admin_items_set(UJID, Items, Lang, StateData) -> UAffiliation = get_affiliation(UJID, StateData), URole = get_role(UJID, StateData), case catch 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", [jid:to_string(UJID), jid:to_string(StateData#state.jid), Res]), NSD = lists:foldl(process_item_change(UJID), StateData, lists:flatten(Res)), store_room(NSD), {result, undefined, NSD}; {error, Err} -> {error, Err} end. -spec process_item_change(jid()) -> function(). process_item_change(UJID) -> fun(E, SD) -> process_item_change(E, SD, UJID) end. -type admin_action() :: {jid(), affiliation | role, affiliation() | role(), binary()}. -spec process_item_change(admin_action(), state(), jid()) -> state(). process_item_change(Item, SD, UJID) -> try case Item 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), maybe_send_affiliation(JID, none, SD), SD1 = set_affiliation(JID, none, SD), set_role(JID, none, SD1); _ -> SD1 = set_affiliation(JID, none, SD), send_update_presence(JID, SD1, SD), maybe_send_affiliation(JID, none, SD1), SD1 end; {JID, affiliation, outcast, Reason} -> catch send_kickban_presence(UJID, JID, Reason, 301, outcast, SD), maybe_send_affiliation(JID, 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, SD), maybe_send_affiliation(JID, A, 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, SD), maybe_send_affiliation(JID, member, SD2), SD2; {JID, role, Role, Reason} -> SD1 = set_role(JID, Role, SD), catch send_new_presence(JID, Reason, SD1, SD), SD1; {JID, affiliation, A, _Reason} -> SD1 = set_affiliation(JID, A, SD), send_update_presence(JID, SD1, SD), maybe_send_affiliation(JID, A, SD1), SD1 end catch E:R -> ?ERROR_MSG("failed to set item ~p from ~s: ~p", [Item, jid:to_string(UJID), {E, {R, erlang:get_stacktrace()}}]), SD end. -spec find_changed_items(jid(), affiliation(), role(), [muc_item()], binary(), state(), [admin_action()]) -> {result, [admin_action()]}. find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) -> {result, Res}; find_changed_items(_UJID, _UAffiliation, _URole, [#muc_item{jid = undefined, nick = undefined}|_], Lang, _StateData, _Res) -> Txt = <<"Neither 'jid' nor 'nick' attribute found">>, throw({error, xmpp:err_bad_request(Txt, Lang)}); find_changed_items(_UJID, _UAffiliation, _URole, [#muc_item{role = undefined, affiliation = undefined}|_], Lang, _StateData, _Res) -> Txt = <<"Neither 'role' nor 'affiliation' attribute found">>, throw({error, xmpp:err_bad_request(Txt, Lang)}); find_changed_items(UJID, UAffiliation, URole, [#muc_item{jid = J, nick = Nick, reason = Reason, role = Role, affiliation = Affiliation}|Items], Lang, StateData, Res) -> [JID | _] = JIDs = if J /= undefined -> [J]; Nick /= <<"">> -> case find_jids_by_nick(Nick, StateData) of [] -> ErrText = iolist_to_binary( io_lib:format( translate:translate( Lang, <<"Nickname ~s does not exist in the room">>), [Nick])), throw({error, xmpp:err_not_acceptable(ErrText, Lang)}); JIDList -> JIDList end end, {RoleOrAff, RoleOrAffValue} = if Role == undefined -> {affiliation, Affiliation}; true -> {role, Role} end, TAffiliation = get_affiliation(JID, StateData), TRole = get_role(JID, StateData), ServiceAf = get_service_affiliation(JID, StateData), CanChangeRA = case can_change_ra(UAffiliation, URole, TAffiliation, TRole, RoleOrAff, RoleOrAffValue, ServiceAf) of nothing -> nothing; true -> true; check_owner -> case search_affiliation(owner, StateData) of [{OJID, _}] -> jid:remove_resource(OJID) /= jid:tolower(jid:remove_resource(UJID)); _ -> true end; _ -> false end, case CanChangeRA of nothing -> find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, Res); true -> MoreRes = [{jid:remove_resource(Jidx), RoleOrAff, RoleOrAffValue, Reason} || Jidx <- JIDs], find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, [MoreRes | Res]); false -> Txt = <<"Changing role/affiliation is not allowed">>, throw({error, xmpp:err_not_allowed(Txt, Lang)}) end. -spec can_change_ra(affiliation(), role(), affiliation(), role(), affiliation, affiliation(), affiliation()) -> boolean(); (affiliation(), role(), affiliation(), role(), role, role(), affiliation()) -> boolean(). 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. -spec send_kickban_presence(jid(), jid(), binary(), pos_integer(), state()) -> ok. send_kickban_presence(UJID, JID, Reason, Code, StateData) -> NewAffiliation = get_affiliation(JID, StateData), send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, StateData). -spec send_kickban_presence(jid(), jid(), binary(), pos_integer(), affiliation(), state()) -> ok. send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, StateData) -> LJID = 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). -spec send_kickban_presence1(jid(), jid(), binary(), pos_integer(), affiliation(), state()) -> ok. send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, StateData) -> {ok, #user{jid = RealJID, nick = Nick}} = (?DICT):find(jid:tolower(UJID), StateData#state.users), ActorNick = get_actor_nick(MJID, StateData), lists:foreach( fun({_LJID, Info}) -> Item0 = #muc_item{affiliation = Affiliation, role = none}, Item1 = case Info#user.role == moderator orelse (StateData#state.config)#config.anonymous == false of true -> Item0#muc_item{jid = RealJID}; false -> Item0 end, Item2 = Item1#muc_item{reason = Reason}, Item = case ActorNick of <<"">> -> Item2; _ -> Item2#muc_item{actor = #muc_actor{nick = ActorNick}} end, Packet = #presence{type = unavailable, sub_els = [#muc_user{items = [Item], status_codes = [Code]}]}, RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick), send_wrapped(RoomJIDNick, Info#user.jid, Packet, ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), IsSubscriber = Info#user.is_subscriber, IsOccupant = Info#user.last_presence /= undefined, if (IsSubscriber and not IsOccupant) -> send_wrapped(RoomJIDNick, Info#user.jid, Packet, ?NS_MUCSUB_NODES_PARTICIPANTS, StateData); true -> ok end end, (?DICT):to_list(StateData#state.users)). -spec get_actor_nick(binary() | jid(), state()) -> binary(). get_actor_nick(<<"">>, _StateData) -> <<"">>; get_actor_nick(MJID, StateData) -> case (?DICT):find(jid:tolower(MJID), StateData#state.users) of {ok, #user{nick = ActorNick}} -> ActorNick; _ -> <<"">> end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Owner stuff -spec process_iq_owner(jid(), iq(), state()) -> {result, undefined | muc_owner()} | {result, undefined | muc_owner(), state() | stop} | {error, error()}. process_iq_owner(From, #iq{type = set, lang = Lang, sub_els = [#muc_owner{destroy = Destroy, config = Config, items = Items}]}, StateData) -> FAffiliation = get_affiliation(From, StateData), if FAffiliation /= owner -> ErrText = <<"Owner privileges required">>, {error, xmpp:err_forbidden(ErrText, Lang)}; Destroy /= undefined, Config == undefined, Items == [] -> ?INFO_MSG("Destroyed MUC room ~s by the owner ~s", [jid:to_string(StateData#state.jid), jid:to_string(From)]), add_to_log(room_existence, destroyed, StateData), destroy_room(Destroy, StateData); Config /= undefined, Destroy == undefined, Items == [] -> case Config of #xdata{type = cancel} -> {result, undefined}; #xdata{type = submit} -> case is_allowed_log_change(Config, StateData, From) andalso is_allowed_persistent_change(Config, StateData, From) andalso is_allowed_room_name_desc_limits(Config, StateData) andalso is_password_settings_correct(Config, StateData) of true -> set_config(Config, StateData, Lang); false -> {error, xmpp:err_not_acceptable()} end; _ -> Txt = <<"Incorrect data form">>, {error, xmpp:err_bad_request(Txt, Lang)} end; Items /= [], Config == undefined, Destroy == undefined -> process_admin_items_set(From, Items, Lang, StateData); true -> {error, xmpp:err_bad_request()} end; process_iq_owner(From, #iq{type = get, lang = Lang, sub_els = [#muc_owner{destroy = Destroy, config = Config, items = Items}]}, StateData) -> FAffiliation = get_affiliation(From, StateData), if FAffiliation /= owner -> ErrText = <<"Owner privileges required">>, {error, xmpp:err_forbidden(ErrText, Lang)}; Destroy == undefined, Config == undefined -> case Items of [] -> {result, #muc_owner{config = get_config(Lang, StateData, From)}}; [#muc_item{affiliation = undefined}] -> Txt = <<"No 'affiliation' attribute found">>, {error, xmpp:err_bad_request(Txt, Lang)}; [#muc_item{affiliation = Affiliation}] -> Items = items_with_affiliation(Affiliation, StateData), {result, #muc_owner{items = Items}}; [_|_] -> Txt = <<"Too many elements">>, {error, xmpp:err_bad_request(Txt, Lang)} end; true -> {error, xmpp:err_bad_request()} end. -spec is_allowed_log_change(xdata(), state(), jid()) -> boolean(). is_allowed_log_change(X, StateData, From) -> case xmpp_util:has_xdata_var(<<"muc#roomconfig_enablelogging">>, X) of false -> true; true -> allow == mod_muc_log:check_access_log(StateData#state.server_host, From) end. -spec is_allowed_persistent_change(xdata(), state(), jid()) -> boolean(). is_allowed_persistent_change(X, StateData, From) -> case xmpp_util:has_xdata_var(<<"muc#roomconfig_persistentroom">>, X) 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 -spec is_allowed_room_name_desc_limits(xdata(), state()) -> boolean(). is_allowed_room_name_desc_limits(XData, StateData) -> IsNameAccepted = case xmpp_util:get_xdata_values( <<"muc#roomconfig_roomname">>, XData) of [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 xmpp_util:get_xdata_values( <<"muc#roomconfig_roomdesc">>, XData) of [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" -spec is_password_settings_correct(xdata(), state()) -> boolean(). is_password_settings_correct(XData, StateData) -> Config = StateData#state.config, OldProtected = Config#config.password_protected, OldPassword = Config#config.password, NewProtected = case xmpp_util:get_xdata_values( <<"muc#roomconfig_passwordprotectedroom">>, XData) of [<<"1">>] -> true; [<<"true">>] -> true; [<<"0">>] -> false; [<<"false">>] -> false; _ -> undefined end, NewPassword = case xmpp_util:get_xdata_values( <<"muc#roomconfig_roomsecret">>, XData) of [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, Vals), #xdata_field{type = Type, label = translate:translate(Lang, Label), var = Var, values = Vals}). -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), ?XFIELD('jid-multi', Label, Var, [jid:to_string(JID) || JID <- JIDList])). -spec make_options([{binary(), binary()}], binary()) -> [xdata_option()]. make_options(Options, Lang) -> [#xdata_option{label = translate:translate(Lang, Label), value = Value} || {Label, Value} <- Options]. -spec get_default_room_maxusers(state()) -> non_neg_integer(). 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. -spec get_config(binary(), state(), jid()) -> xdata(). 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, integer_to_binary(N)}; _ -> {0, <<"none">>} end, Title = iolist_to_binary( io_lib:format( translate:translate(Lang, <<"Configuration of room ~s">>), [jid:to_string(StateData#state.jid)])), Fs = [#xdata_field{type = hidden, var = <<"FORM_TYPE">>, values = [<<"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))]; deny -> [] 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), #xdata_field{type = 'list-single', label = translate:translate( Lang, <<"Maximum Number of Occupants">>), var = <<"muc#roomconfig_maxusers">>, values = [MaxUsersRoomString], options = if is_integer(ServiceMaxUsers) -> []; true -> make_options( [{<<"No limit">>, <<"none">>}], Lang) end ++ make_options( [{integer_to_binary(N), integer_to_binary(N)} || N <- lists:usort([ServiceMaxUsers, DefaultRoomMaxUsers, MaxUsersRoomInteger | ?MAX_USERS_DEFAULT_LIST]), N =< ServiceMaxUsers], Lang)}, #xdata_field{type = 'list-single', label = translate:translate( Lang, <<"Present real Jabber IDs to">>), var = <<"muc#roomconfig_whois">>, values = [if Config#config.anonymous -> <<"moderators">>; true -> <<"anyone">> end], options = make_options( [{<<"moderators only">>, <<"moderators">>}, {<<"anyone">>, <<"anyone">>}], Lang)}, #xdata_field{type = 'list-multi', label = translate:translate( Lang, <<"Roles for which Presence is Broadcasted">>), var = <<"muc#roomconfig_presencebroadcast">>, values = [atom_to_binary(Role, utf8) || Role <- Config#config.presence_broadcast], options = make_options( [{<<"Moderator">>, <<"moderator">>}, {<<"Participant">>, <<"participant">>}, {<<"Visitor">>, <<"visitor">>}], Lang)}, ?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)), #xdata_field{type = 'list-single', label = translate:translate( Lang, <<"Allow visitors to send private messages to">>), var = <<"allow_private_messages_from_visitors">>, values = [case Config#config.allow_private_messages_from_visitors of anyone -> <<"anyone">>; moderators -> <<"moderators">>; nobody -> <<"nobody">> end], options = make_options( [{<<"nobody">>, <<"nobody">>}, {<<"moderators only">>, <<"moderators">>}, {<<"anyone">>, <<"anyone">>}], Lang)}, ?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)), ?BOOLXFIELD(<<"Allow subscription">>, <<"muc#roomconfig_allow_subscription">>, (Config#config.allow_subscription)), ?STRINGXFIELD(<<"Minimum interval between voice requests " "(in seconds)">>, <<"muc#roomconfig_voicerequestmininterval">>, 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))]; deny -> [] end, Fields = ejabberd_hooks:run_fold(get_room_config, StateData#state.server_host, Fs, [StateData, From, Lang]), #xdata{type = form, title = Title, fields = Fields}. -spec set_config(xdata(), state(), binary()) -> {error, error()} | {result, undefined, state()}. set_config(#xdata{fields = Fields}, StateData, Lang) -> Options = [{Var, Vals} || #xdata_field{var = Var, values = Vals} <- Fields], case set_xoption(Options, StateData#state.config, StateData#state.server_host, Lang) 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. get_config_opt_name(Pos) -> Fs = [config|record_info(fields, config)], lists:nth(Pos, Fs). -define(SET_BOOL_XOPT(Opt, Val), case Val of <<"0">> -> set_xoption(Opts, setelement(Opt, Config, false), ServerHost, Lang); <<"false">> -> set_xoption(Opts, setelement(Opt, Config, false), ServerHost, Lang); <<"1">> -> set_xoption(Opts, setelement(Opt, Config, true), ServerHost, Lang); <<"true">> -> set_xoption(Opts, setelement(Opt, Config, true), ServerHost, Lang); _ -> Txt = <<"Value of '~s' should be boolean">>, OptName = get_config_opt_name(Opt), ErrTxt = iolist_to_binary(io_lib:format(Txt, [OptName])), {error, xmpp:err_bad_request(ErrTxt, Lang)} end). -define(SET_NAT_XOPT(Opt, Val), case catch binary_to_integer(Val) of I when is_integer(I), I > 0 -> set_xoption(Opts, setelement(Opt, Config, I), ServerHost, Lang); _ -> Txt = <<"Value of '~s' should be integer">>, OptName = get_config_opt_name(Opt), ErrTxt = iolist_to_binary(io_lib:format(Txt, [OptName])), {error, xmpp:err_bad_request(ErrTxt, Lang)} end). -define(SET_STRING_XOPT(Opt, Vals), try V = case Vals of [] -> <<"">>; [Val] -> Val; _ when is_atom(Vals) -> Vals end, set_xoption(Opts, setelement(Opt, Config, V), ServerHost, Lang) catch _:_ -> Txt = <<"Incorrect value of option '~s'">>, OptName = get_config_opt_name(Opt), ErrTxt = iolist_to_binary(io_lib:format(Txt, [OptName])), {error, xmpp:err_bad_request(ErrTxt, Lang)} end). -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, setelement(Opt, Config, Set), ServerHost, Lang) end). -spec set_xoption([{binary(), [binary()]}], #config{}, binary(), binary()) -> #config{} | {error, error()}. set_xoption([], Config, _ServerHost, _Lang) -> Config; set_xoption([{<<"muc#roomconfig_roomname">>, Vals} | Opts], Config, ServerHost, Lang) -> ?SET_STRING_XOPT(#config.title, Vals); set_xoption([{<<"muc#roomconfig_roomdesc">>, Vals} | Opts], Config, ServerHost, Lang) -> ?SET_STRING_XOPT(#config.description, Vals); set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.allow_change_subj, Val); set_xoption([{<<"allow_query_users">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.allow_query_users, Val); set_xoption([{<<"allow_private_messages">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.allow_private_messages, Val); set_xoption([{<<"allow_private_messages_from_visitors">>, [Val]} | Opts], Config, ServerHost, Lang) -> case Val of <<"anyone">> -> ?SET_STRING_XOPT(#config.allow_private_messages_from_visitors, anyone); <<"moderators">> -> ?SET_STRING_XOPT(#config.allow_private_messages_from_visitors, moderators); <<"nobody">> -> ?SET_STRING_XOPT(#config.allow_private_messages_from_visitors, nobody); _ -> Txt = <<"Value of 'allow_private_messages_from_visitors' " "should be anyone|moderators|nobody">>, {error, xmpp:err_bad_request(Txt, Lang)} end; set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.allow_visitor_status, Val); set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.allow_visitor_nickchange, Val); set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.public, Val); set_xoption([{<<"public_list">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.public_list, Val); set_xoption([{<<"muc#roomconfig_persistentroom">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.persistent, Val); set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.moderated, Val); set_xoption([{<<"members_by_default">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.members_by_default, Val); set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.members_only, Val); set_xoption([{<<"captcha_protected">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.captcha_protected, Val); set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.allow_user_invites, Val); set_xoption([{<<"muc#roomconfig_allow_subscription">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.allow_subscription, Val); set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.password_protected, Val); set_xoption([{<<"muc#roomconfig_roomsecret">>, Vals} | Opts], Config, ServerHost, Lang) -> ?SET_STRING_XOPT(#config.password, Vals); set_xoption([{<<"anonymous">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.anonymous, Val); set_xoption([{<<"muc#roomconfig_presencebroadcast">>, Vals} | Opts], Config, ServerHost, Lang) -> Roles = lists:foldl( fun(_S, error) -> error; (S, {M, P, V}) -> case S of <<"moderator">> -> {true, P, V}; <<"participant">> -> {M, true, V}; <<"visitor">> -> {M, P, true}; _ -> error end end, {false, false, false}, Vals), case Roles of error -> Txt = <<"Value of 'muc#roomconfig_presencebroadcast' should " "be moderator|participant|visitor">>, {error, xmpp:err_bad_request(Txt, Lang)}; {M, P, V} -> Res = if M -> [moderator]; true -> [] end ++ if P -> [participant]; true -> [] end ++ if V -> [visitor]; true -> [] end, set_xoption(Opts, Config#config{presence_broadcast = Res}, ServerHost, Lang) end; set_xoption([{<<"muc#roomconfig_allowvoicerequests">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.allow_voice_requests, Val); set_xoption([{<<"muc#roomconfig_voicerequestmininterval">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_NAT_XOPT(#config.voice_request_min_interval, Val); set_xoption([{<<"muc#roomconfig_whois">>, [Val]} | Opts], Config, ServerHost, Lang) -> case Val of <<"moderators">> -> ?SET_BOOL_XOPT(#config.anonymous, (iolist_to_binary(integer_to_list(1)))); <<"anyone">> -> ?SET_BOOL_XOPT(#config.anonymous, (iolist_to_binary(integer_to_list(0)))); _ -> Txt = <<"Value of 'muc#roomconfig_whois' should be " "moderators|anyone">>, {error, xmpp:err_bad_request(Txt, Lang)} end; set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]} | Opts], Config, ServerHost, Lang) -> case Val of <<"none">> -> ?SET_STRING_XOPT(#config.max_users, none); _ -> ?SET_NAT_XOPT(#config.max_users, Val) end; set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(#config.logging, Val); set_xoption([{<<"muc#roomconfig_captcha_whitelist">>, Vals} | Opts], Config, ServerHost, Lang) -> JIDs = [jid:from_string(Val) || Val <- Vals], ?SET_JIDMULTI_XOPT(#config.captcha_whitelist, JIDs); set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config, ServerHost, Lang) -> set_xoption(Opts, Config, ServerHost, Lang); set_xoption([{Opt, Vals} | Opts], Config, ServerHost, Lang) -> Txt = <<"Unknown option '~s'">>, ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), Err = {error, xmpp:err_bad_request(ErrTxt, Lang)}, case ejabberd_hooks:run_fold(set_room_option, ServerHost, Err, [Opt, Vals, Lang]) of {error, Reason} -> {error, Reason}; {Pos, Val} -> set_xoption(Opts, setelement(Pos, Config, Val), ServerHost, Lang) end. -spec change_config(#config{}, state()) -> {result, undefined, state()}. change_config(Config, StateData) -> send_config_change_info(Config, StateData), NSD = remove_subscriptions(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, undefined, NSD1}; _ -> {result, undefined, NSD} end. -spec send_config_change_info(#config{}, state()) -> ok. send_config_change_info(Config, #state{config = Config}) -> ok; send_config_change_info(New, #state{config = Old} = StateData) -> Codes = case {Old#config.logging, New#config.logging} of {false, true} -> [170]; {true, false} -> [171]; _ -> [] end ++ case {Old#config.anonymous, New#config.anonymous} of {true, false} -> [172]; {false, true} -> [173]; _ -> [] end ++ case Old#config{anonymous = New#config.anonymous, logging = New#config.logging} of New -> []; _ -> [104] end, Message = #message{type = groupchat, id = randoms:get_string(), sub_els = [#muc_user{status_codes = Codes}]}, send_wrapped_multiple(StateData#state.jid, StateData#state.users, Message, ?NS_MUCSUB_NODES_CONFIG, StateData). -spec remove_nonmembers(state()) -> state(). 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)). -spec set_opts([{atom(), any()}], state()) -> state(). 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}}; presence_broadcast -> StateData#state{config = (StateData#state.config)#config{presence_broadcast = Val}}; logging -> StateData#state{config = (StateData#state.config)#config{logging = Val}}; mam -> StateData#state{config = (StateData#state.config)#config{mam = Val}}; captcha_whitelist -> StateData#state{config = (StateData#state.config)#config{captcha_whitelist = (?SETS):from_list(Val)}}; allow_voice_requests -> StateData#state{config = (StateData#state.config)#config{allow_voice_requests = Val}}; voice_request_min_interval -> StateData#state{config = (StateData#state.config)#config{voice_request_min_interval = Val}}; max_users -> ServiceMaxUsers = get_service_max_users(StateData), MaxUsers = if Val =< ServiceMaxUsers -> Val; true -> ServiceMaxUsers end, StateData#state{config = (StateData#state.config)#config{max_users = MaxUsers}}; vcard -> StateData#state{config = (StateData#state.config)#config{vcard = Val}}; allow_subscription -> StateData#state{config = (StateData#state.config)#config{allow_subscription = Val}}; subscribers -> lists:foldl( fun({JID, Nick, Nodes}, State) -> User = #user{jid = JID, nick = Nick, subscriptions = Nodes, is_subscriber = true, role = none}, update_online_user(JID, User, State) end, StateData, Val); affiliations -> StateData#state{affiliations = (?DICT):from_list(Val)}; subject -> StateData#state{subject = Val}; subject_author -> StateData#state{subject_author = Val}; _ -> StateData end, set_opts(Opts, NSD). -define(MAKE_CONFIG_OPT(Opt), {get_config_opt_name(Opt), element(Opt, Config)}). -spec make_opts(state()) -> [{atom(), any()}]. make_opts(StateData) -> Config = StateData#state.config, Subscribers = (?DICT):fold( fun(_LJID, #user{is_subscriber = true} = User, Acc) -> [{User#user.jid, User#user.nick, User#user.subscriptions}|Acc]; (_, _, Acc) -> Acc end, [], StateData#state.users), [?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description), ?MAKE_CONFIG_OPT(#config.allow_change_subj), ?MAKE_CONFIG_OPT(#config.allow_query_users), ?MAKE_CONFIG_OPT(#config.allow_private_messages), ?MAKE_CONFIG_OPT(#config.allow_private_messages_from_visitors), ?MAKE_CONFIG_OPT(#config.allow_visitor_status), ?MAKE_CONFIG_OPT(#config.allow_visitor_nickchange), ?MAKE_CONFIG_OPT(#config.public), ?MAKE_CONFIG_OPT(#config.public_list), ?MAKE_CONFIG_OPT(#config.persistent), ?MAKE_CONFIG_OPT(#config.moderated), ?MAKE_CONFIG_OPT(#config.members_by_default), ?MAKE_CONFIG_OPT(#config.members_only), ?MAKE_CONFIG_OPT(#config.allow_user_invites), ?MAKE_CONFIG_OPT(#config.password_protected), ?MAKE_CONFIG_OPT(#config.captcha_protected), ?MAKE_CONFIG_OPT(#config.password), ?MAKE_CONFIG_OPT(#config.anonymous), ?MAKE_CONFIG_OPT(#config.logging), ?MAKE_CONFIG_OPT(#config.max_users), ?MAKE_CONFIG_OPT(#config.allow_voice_requests), ?MAKE_CONFIG_OPT(#config.mam), ?MAKE_CONFIG_OPT(#config.voice_request_min_interval), ?MAKE_CONFIG_OPT(#config.vcard), {captcha_whitelist, (?SETS):to_list((StateData#state.config)#config.captcha_whitelist)}, {affiliations, (?DICT):to_list(StateData#state.affiliations)}, {subject, StateData#state.subject}, {subject_author, StateData#state.subject_author}, {subscribers, Subscribers}]. -spec destroy_room(muc_destroy(), state()) -> {result, undefined, stop}. destroy_room(DEl, StateData) -> Destroy = DEl#muc_destroy{xmlns = ?NS_MUC_USER}, lists:foreach( fun({_LJID, Info}) -> Nick = Info#user.nick, Item = #muc_item{affiliation = none, role = none}, Packet = #presence{ type = unavailable, sub_els = [#muc_user{items = [Item], destroy = Destroy}]}, send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, ?NS_MUCSUB_NODES_CONFIG, StateData) 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, undefined, stop}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Disco -define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse), case Opt of true -> Fiftrue; false -> Fiffalse end). -spec process_iq_disco_info(jid(), iq(), state()) -> {result, disco_info()} | {error, error()}. process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) -> Txt = <<"Value 'set' of 'type' attribute is not allowed">>, {error, xmpp:err_not_allowed(Txt, Lang)}; process_iq_disco_info(_From, #iq{type = get, lang = Lang}, StateData) -> Config = StateData#state.config, Feats = [?NS_VCARD, ?NS_MUC, ?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">>)] ++ case Config#config.allow_subscription of true -> [?NS_MUCSUB]; false -> [] end ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), Config#config.mam} of {true, true} -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1]; _ -> [] end, {result, #disco_info{xdata = [iq_disco_info_extras(Lang, StateData)], identities = [#identity{category = <<"conference">>, type = <<"text">>, name = get_title(StateData)}], features = Feats}}. -spec mk_rfieldt('boolean' | 'fixed' | 'hidden' | 'jid-multi' | 'jid-single' | 'list-multi' | 'list-single' | 'text-multi' | 'text-private' | 'text-single', binary(), binary()) -> xdata_field(). mk_rfieldt(Type, Var, Val) -> #xdata_field{type = Type, var = Var, values = [Val]}. -spec mk_rfield(binary(), binary(), binary(), binary()) -> xdata_field(). mk_rfield(Label, Var, Val, Lang) -> #xdata_field{type = 'text-single', label = translate:translate(Lang, Label), var = Var, values = [Val]}. -spec iq_disco_info_extras(binary(), state()) -> xdata(). iq_disco_info_extras(Lang, StateData) -> Len = (?DICT):size(StateData#state.users), RoomDescription = (StateData#state.config)#config.description, #xdata{type = result, fields = [mk_rfieldt(hidden, <<"FORM_TYPE">>, "http://jabber.org/protocol/muc#roominfo"), mk_rfield(<<"Room description">>, <<"muc#roominfo_description">>, RoomDescription, Lang), mk_rfield(<<"Number of occupants">>, <<"muc#roominfo_occupants">>, integer_to_binary(Len), Lang)]}. -spec process_iq_disco_items(jid(), iq(), state()) -> {error, error()} | {result, disco_items()}. process_iq_disco_items(_From, #iq{type = set, lang = Lang}, _StateData) -> Txt = <<"Value 'set' of 'type' attribute is not allowed">>, {error, xmpp:err_not_allowed(Txt, Lang)}; process_iq_disco_items(From, #iq{type = get, lang = Lang}, StateData) -> case (StateData#state.config)#config.public_list of true -> {result, get_mucroom_disco_items(StateData)}; _ -> case is_occupant_or_admin(From, StateData) of true -> {result, get_mucroom_disco_items(StateData)}; _ -> Txt = <<"Only occupants or administrators can perform this query">>, {error, xmpp:err_forbidden(Txt, Lang)} end end. -spec process_iq_captcha(jid(), iq(), state()) -> {error, error()} | {result, undefined}. process_iq_captcha(_From, #iq{type = get, lang = Lang}, _StateData) -> Txt = <<"Value 'get' of 'type' attribute is not allowed">>, {error, xmpp:err_not_allowed(Txt, Lang)}; process_iq_captcha(_From, #iq{type = set, lang = Lang, sub_els = [SubEl]}, _StateData) -> case ejabberd_captcha:process_reply(SubEl) of ok -> {result, undefined}; {error, malformed} -> Txt = <<"Incorrect CAPTCHA submit">>, {error, xmpp:err_bad_request(Txt, Lang)}; _ -> Txt = <<"The CAPTCHA verification has failed">>, {error, xmpp:err_not_allowed(Txt, Lang)} end. -spec process_iq_vcard(jid(), iq(), state()) -> {result, vcard_temp() | xmlel()} | {result, undefined, state()} | {error, error()}. process_iq_vcard(_From, #iq{type = get}, StateData) -> #state{config = #config{vcard = VCardRaw}} = StateData, case fxml_stream:parse_element(VCardRaw) of #xmlel{} = VCard -> {result, VCard}; {error, _} -> {result, #vcard_temp{}} end; process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [SubEl]}, StateData) -> case get_affiliation(From, StateData) of owner -> VCardRaw = fxml:element_to_binary(xmpp:encode(SubEl)), Config = StateData#state.config, NewConfig = Config#config{vcard = VCardRaw}, change_config(NewConfig, StateData); _ -> ErrText = <<"Owner privileges required">>, {error, xmpp:err_forbidden(ErrText, Lang)} end. -spec process_iq_mucsub(jid(), iq(), state()) -> {error, error()} | {result, undefined | muc_subscribe(), state()} | {ignore, state()}. process_iq_mucsub(_From, #iq{type = set, lang = Lang, sub_els = [#muc_subscribe{}]}, #state{config = #config{allow_subscription = false}}) -> {error, xmpp:err_not_allowed(<<"Subscriptions are not allowed">>, Lang)}; process_iq_mucsub(From, #iq{type = set, lang = Lang, sub_els = [#muc_subscribe{nick = Nick}]} = Packet, StateData) -> LJID = jid:tolower(From), case (?DICT):find(LJID, StateData#state.users) of {ok, #user{role = Role, nick = Nick1}} when Nick1 /= Nick -> Nodes = get_subscription_nodes(Packet), case {nick_collision(From, Nick, StateData), mod_muc:can_use_nick(StateData#state.server_host, StateData#state.host, From, Nick)} of {true, _} -> ErrText = <<"That nickname is already in use by another occupant">>, {error, xmpp:err_conflict(ErrText, Lang)}; {_, false} -> ErrText = <<"That nickname is registered by another person">>, {error, xmpp:err_conflict(ErrText, Lang)}; _ -> NewStateData = add_online_user( From, Nick, Role, true, Nodes, StateData), {result, subscribe_result(Packet), NewStateData} end; {ok, #user{role = Role}} -> Nodes = get_subscription_nodes(Packet), NewStateData = add_online_user( From, Nick, Role, true, Nodes, StateData), {result, subscribe_result(Packet), NewStateData}; error -> add_new_user(From, Nick, Packet, StateData) end; process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]}, StateData) -> LJID = jid:tolower(From), case ?DICT:find(LJID, StateData#state.users) of {ok, #user{is_subscriber = true} = User} -> NewStateData = remove_subscription(From, User, StateData), store_room(NewStateData), {result, undefined, NewStateData}; _ -> {result, undefined, StateData} end; process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) -> Txt = <<"Value 'get' of 'type' attribute is not allowed">>, {error, xmpp:err_bad_request(Txt, Lang)}. -spec remove_subscription(jid(), #user{}, state()) -> state(). remove_subscription(JID, #user{is_subscriber = true} = User, StateData) -> case User#user.last_presence of undefined -> remove_online_user(JID, StateData, false); _ -> LJID = jid:tolower(JID), Users = ?DICT:store(LJID, User#user{is_subscriber = false}, StateData#state.users), StateData#state{users = Users} end; remove_subscription(_JID, #user{}, StateData) -> StateData. -spec remove_subscriptions(state()) -> state(). remove_subscriptions(StateData) -> if not (StateData#state.config)#config.allow_subscription -> dict:fold( fun(_LJID, User, State) -> remove_subscription(User#user.jid, User, State) end, StateData, StateData#state.users); true -> StateData end. -spec get_subscription_nodes(iq()) -> [binary()]. get_subscription_nodes(#iq{sub_els = [#muc_subscribe{events = Nodes}]}) -> lists:filter( fun(Node) -> lists:member(Node, [?NS_MUCSUB_NODES_PRESENCE, ?NS_MUCSUB_NODES_MESSAGES, ?NS_MUCSUB_NODES_AFFILIATIONS, ?NS_MUCSUB_NODES_SUBJECT, ?NS_MUCSUB_NODES_CONFIG, ?NS_MUCSUB_NODES_PARTICIPANTS]) end, Nodes); get_subscription_nodes(_) -> []. -spec subscribe_result(iq()) -> muc_subscribe(). subscribe_result(#iq{sub_els = [#muc_subscribe{nick = Nick}]} = Packet) -> #muc_subscribe{nick = Nick, events = get_subscription_nodes(Packet)}. -spec get_title(state()) -> binary(). get_title(StateData) -> case (StateData#state.config)#config.title of <<"">> -> StateData#state.room; Name -> Name end. -spec get_roomdesc_reply(jid(), state(), binary()) -> {item, binary()} | false. 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. -spec get_roomdesc_tail(state(), binary()) -> binary(). get_roomdesc_tail(StateData, Lang) -> Desc = case (StateData#state.config)#config.public of true -> <<"">>; _ -> translate:translate(Lang, <<"private, ">>) end, Len = (?DICT):size(StateData#state.users), <<" (", Desc/binary, (iolist_to_binary(integer_to_list(Len)))/binary, ")">>. -spec get_mucroom_disco_items(state()) -> disco_items(). get_mucroom_disco_items(StateData) -> Items = lists:map( fun({_LJID, Info}) -> Nick = Info#user.nick, #disco_item{jid = jid:make(StateData#state.room, StateData#state.host, Nick), name = Nick} end, (?DICT):to_list(StateData#state.users)), #disco_items{items = Items}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Voice request support -spec is_voice_request(message()) -> boolean(). is_voice_request(Packet) -> Els = xmpp:get_els(Packet), lists:any( fun(#xdata{} = X) -> case {xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), xmpp_util:get_xdata_values(<<"muc#role">>, X)} of {[<<"http://jabber.org/protocol/muc#request">>], [<<"participant">>]} -> true; _ -> false end; (_) -> false end, Els). -spec prepare_request_form(jid(), binary(), binary()) -> message(). prepare_request_form(Requester, Nick, Lang) -> Title = translate:translate(Lang, <<"Voice request">>), Instruction = translate:translate( Lang, <<"Either approve or decline the voice request.">>), Fs = [#xdata_field{var = <<"FORM_TYPE">>, type = hidden, values = [<<"http://jabber.org/protocol/muc#request">>]}, #xdata_field{var = <<"muc#role">>, type = hidden, values = [<<"participant">>]}, ?STRINGXFIELD(<<"User JID">>, <<"muc#jid">>, jid:to_string(Requester)), ?STRINGXFIELD(<<"Nickname">>, <<"muc#roomnick">>, Nick), ?BOOLXFIELD(<<"Grant voice to this person?">>, <<"muc#request_allow">>, false)], #message{type = normal, sub_els = [#xdata{type = form, title = Title, instructions = [Instruction], fields = Fs}]}. -spec send_voice_request(jid(), binary(), state()) -> ok. send_voice_request(From, Lang, 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, Lang)) end, Moderators). -spec is_voice_approvement(message()) -> boolean(). is_voice_approvement(Packet) -> Els = xmpp:get_els(Packet), lists:any( fun(#xdata{} = X) -> case {xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), xmpp_util:get_xdata_values(<<"muc#role">>, X), xmpp_util:get_xdata_values(<<"muc#request_allow">>, X)} of {[<<"http://jabber.org/protocol/muc#request">>], [<<"participant">>], [Flag]} when Flag == <<"true">>; Flag == <<"1">> -> true; _ -> false end; (_) -> false end, Els). -spec extract_jid_from_voice_approvement(message()) -> jid() | error. extract_jid_from_voice_approvement(Packet) -> Els = xmpp:get_els(Packet), lists:foldl( fun(#xdata{} = X, error) -> case {xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), xmpp_util:get_xdata_values(<<"muc#role">>, X), xmpp_util:get_xdata_values(<<"muc#request_allow">>, X), xmpp_util:get_xdata_values(<<"muc#jid">>, X)} of {[<<"http://jabber.org/protocol/muc#request">>], [<<"participant">>], [Flag], [J]} when Flag == <<"true">>; Flag == <<"1">> -> jid:from_string(J); _ -> error end; (_, Acc) -> Acc end, error, Els). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Invitation support -spec is_invitation(message()) -> boolean(). is_invitation(Packet) -> Els = xmpp:get_els(Packet), lists:any( fun(#muc_user{invites = [_|_]}) -> true; (_) -> false end, Els). -spec check_invitation(jid(), message(), state()) -> {error, error()} | jid(). check_invitation(From, Packet, StateData) -> Lang = xmpp:get_lang(Packet), FAffiliation = get_affiliation(From, StateData), CanInvite = (StateData#state.config)#config.allow_user_invites orelse FAffiliation == admin orelse FAffiliation == owner, case CanInvite of false -> Txt = <<"Invitations are not allowed in this conference">>, {error, xmpp:err_not_allowed(Txt, Lang)}; true -> case xmpp:get_subtag(Packet, #muc_user{}) of #muc_user{invites = [#muc_invite{to = undefined}]} -> Txt = <<"No 'to' attribute found">>, {error, xmpp:err_bad_request(Txt, Lang)}; #muc_user{invites = [#muc_invite{to = JID, reason = Reason} = I]} -> Invite = I#muc_invite{to = undefined, from = From}, Password = case (StateData#state.config)#config.password_protected of true -> (StateData#state.config)#config.password; false -> undefined end, XUser = #muc_user{password = Password, invites = [Invite]}, XConference = #x_conference{jid = jid:make(StateData#state.room, StateData#state.host), reason = Reason}, Body = iolist_to_binary( [io_lib:format( translate:translate( Lang, <<"~s invites you to the room ~s">>), [jid:to_string(From), 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 = #message{type = normal, body = xmpp:mk_text(Body), sub_els = [XUser, XConference]}, ejabberd_router:route(StateData#state.jid, JID, Msg), JID; #muc_user{invites = [_|_]} -> Txt = <<"Multiple elements are not allowed">>, {error, xmpp:err_forbidden(Txt, Lang)}; _ -> Txt = <<"No element found">>, {error, xmpp:err_bad_request(Txt, Lang)} end 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. -spec handle_roommessage_from_nonparticipant(message(), state(), jid()) -> ok. handle_roommessage_from_nonparticipant(Packet, StateData, From) -> case xmpp:get_subtag(Packet, #muc_user{}) of #muc_user{decline = #muc_decline{to = #jid{} = To} = Decline} = XUser -> NewDecline = Decline#muc_decline{to = undefined, from = From}, NewXUser = XUser#muc_user{decline = NewDecline}, NewPacket = xmpp:set_subtag(Packet, NewXUser), ejabberd_router:route(StateData#state.jid, To, NewPacket); _ -> ErrText = <<"Only occupants are allowed to send messages " "to the conference">>, Err = xmpp:err_not_acceptable(ErrText, xmpp:get_lang(Packet)), ejabberd_router:route_error(StateData#state.jid, From, Packet, Err) end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % 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 -spec tab_add_online_user(jid(), state()) -> ok. tab_add_online_user(JID, StateData) -> {LUser, LServer, LResource} = 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}), ok. -spec tab_remove_online_user(jid(), state()) -> ok. tab_remove_online_user(JID, StateData) -> {LUser, LServer, LResource} = 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}), ok. -spec tab_count_user(jid()) -> non_neg_integer(). tab_count_user(JID) -> {LUser, LServer, _} = 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. -spec element_size(stanza()) -> non_neg_integer(). element_size(El) -> byte_size(fxml:element_to_binary(xmpp:encode(El))). -spec store_room(state()) -> ok. store_room(StateData) -> if (StateData#state.config)#config.persistent -> mod_muc:store_room(StateData#state.server_host, StateData#state.host, StateData#state.room, make_opts(StateData)); true -> ok end. -spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok. send_wrapped(From, To, Packet, Node, State) -> LTo = jid:tolower(To), case ?DICT:find(LTo, State#state.users) of {ok, #user{is_subscriber = true, subscriptions = Nodes, last_presence = undefined}} -> case lists:member(Node, Nodes) of true -> NewPacket = wrap(From, To, Packet, Node), ejabberd_router:route(State#state.jid, To, NewPacket); false -> ok end; _ -> ejabberd_router:route(From, To, Packet) end. -spec wrap(jid(), jid(), stanza(), binary()) -> message(). wrap(From, To, Packet, Node) -> El = xmpp:encode(xmpp:set_from_to(Packet, From, To)), #message{ sub_els = [#ps_event{ items = #ps_items{ node = Node, items = [#ps_item{ id = randoms:get_string(), xml_els = [El]}]}}]}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Multicast -spec send_multiple(jid(), binary(), [#user{}], stanza()) -> ok. send_multiple(From, Server, Users, Packet) -> JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). -spec send_wrapped_multiple(jid(), [#user{}], stanza(), binary(), state()) -> ok. send_wrapped_multiple(From, Users, Packet, Node, State) -> lists:foreach( fun({_, #user{jid = To}}) -> send_wrapped(From, To, Packet, Node, State) end, ?DICT:to_list(Users)). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Detect messange stanzas that don't have meaninful content -spec has_body_or_subject(message()) -> boolean(). has_body_or_subject(#message{body = Body, subject = Subj}) -> Body /= [] orelse Subj /= [].