diff options
Diffstat (limited to 'src/mod_muc_room.erl')
-rw-r--r-- | src/mod_muc_room.erl | 7005 |
1 files changed, 3263 insertions, 3742 deletions
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index e5ed4cc68..ea2b069d7 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -5,7 +5,7 @@ %%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2016 ProcessOne +%%% ejabberd, Copyright (C) 2002-2019 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,17 +27,34 @@ -author('alexey@process-one.net'). --behaviour(gen_fsm). +-behaviour(p1_fsm). %% External exports --export([start_link/9, - start_link/7, - start/9, - start/7, +-export([start_link/10, + start_link/8, + start/10, + start/8, + supervisor/1, get_role/2, get_affiliation/2, is_occupant_or_admin/2, - route/4]). + route/2, + expand_opts/1, + config_fields/0, + destroy/1, + destroy/2, + shutdown/1, + get_config/1, + set_config/2, + get_state/1, + change_item/5, + config_reloaded/1, + subscribe/4, + unsubscribe/2, + is_subscribed/2, + get_subscribers/1, + service_message/2, + get_disco_item/4]). %% gen_fsm callbacks -export([init/1, @@ -48,12 +65,11 @@ terminate/3, code_change/4]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include("xmpp.hrl"). +-include("translate.hrl"). -include("mod_muc_room.hrl"). +-include("ejabberd_stacktrace.hrl"). -define(MAX_USERS_DEFAULT_LIST, [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). @@ -72,613 +88,555 @@ -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 disco_item_filter() :: only_non_empty | all | non_neg_integer(). +-type admin_action() :: {jid(), affiliation | role, affiliation() | role(), binary()}. +-export_type([state/0, disco_item_filter/0]). + +-callback set_affiliation(binary(), binary(), binary(), jid(), affiliation(), + binary()) -> ok | {error, any()}. +-callback set_affiliations(binary(), binary(), binary(), + affiliations()) -> ok | {error, any()}. +-callback get_affiliation(binary(), binary(), binary(), + binary(), binary()) -> {ok, affiliation()} | {error, any()}. +-callback get_affiliations(binary(), binary(), binary()) -> {ok, affiliations()} | {error, any()}. +-callback search_affiliation(binary(), binary(), binary(), affiliation()) -> + {ok, [{ljid(), {affiliation(), binary()}}]} | {error, any()}. + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- +-spec start(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), + atom(), jid(), binary(), [{atom(), term()}], ram | file) -> + {ok, pid()} | {error, any()}. 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). - + Creator, Nick, DefRoomOpts, QueueType) -> + supervisor:start_child( + supervisor(ServerHost), + [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, Nick, DefRoomOpts, QueueType]). + +-spec start(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), + atom(), [{atom(), term()}], ram | file) -> + {ok, pid()} | {error, any()}. +start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) -> + supervisor:start_child( + supervisor(ServerHost), + [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Opts, QueueType]). + +-spec start_link(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), + atom(), jid(), binary(), [{atom(), term()}], ram | file) -> + {ok, pid()} | {error, any()}. 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], + Creator, Nick, DefRoomOpts, QueueType) -> + p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, Nick, DefRoomOpts, QueueType], ?FSMOPTS). -start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> - gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, - RoomShaper, Opts], +-spec start_link(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), + atom(), [{atom(), term()}], ram | file) -> + {ok, pid()} | {error, any()}. +start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) -> + p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Opts, QueueType], ?FSMOPTS). +-spec supervisor(binary()) -> atom(). +supervisor(Host) -> + gen_mod:get_module_proc(Host, mod_muc_room_sup). + +-spec destroy(pid()) -> ok. +destroy(Pid) -> + p1_fsm:send_all_state_event(Pid, destroy). + +-spec destroy(pid(), binary()) -> ok. +destroy(Pid, Reason) -> + p1_fsm:send_all_state_event(Pid, {destroy, Reason}). + +-spec shutdown(pid()) -> boolean(). +shutdown(Pid) -> + ejabberd_cluster:send(Pid, shutdown). + +-spec config_reloaded(pid()) -> boolean(). +config_reloaded(Pid) -> + ejabberd_cluster:send(Pid, config_reloaded). + +-spec get_config(pid()) -> {ok, config()} | {error, notfound | timeout}. +get_config(Pid) -> + try p1_fsm:sync_send_all_state_event(Pid, get_config) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + +-spec set_config(pid(), config()) -> {ok, config()} | {error, notfound | timeout}. +set_config(Pid, Config) -> + try p1_fsm:sync_send_all_state_event(Pid, {change_config, Config}) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + +-spec change_item(pid(), jid(), affiliation | role, affiliation() | role(), binary()) -> + {ok, state()} | {error, notfound | timeout}. +change_item(Pid, JID, Type, AffiliationOrRole, Reason) -> + try p1_fsm:sync_send_all_state_event( + Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + +-spec get_state(pid()) -> {ok, state()} | {error, notfound | timeout}. +get_state(Pid) -> + try p1_fsm:sync_send_all_state_event(Pid, get_state) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + +-spec subscribe(pid(), jid(), binary(), [binary()]) -> {ok, [binary()]} | {error, binary()}. +subscribe(Pid, JID, Nick, Nodes) -> + try p1_fsm:sync_send_all_state_event(Pid, {muc_subscribe, JID, Nick, Nodes}) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, ?T("Request has timed out")}; + _:{_, {p1_fsm, _, _}} -> + {error, ?T("Conference room does not exist")} + end. + +-spec unsubscribe(pid(), jid()) -> ok | {error, binary()}. +unsubscribe(Pid, JID) -> + try p1_fsm:sync_send_all_state_event(Pid, {muc_unsubscribe, JID}) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, ?T("Request has timed out")}; + _:{_, {p1_fsm, _, _}} -> + {error, ?T("Conference room does not exist")} + end. + +-spec is_subscribed(pid(), jid()) -> {true, [binary()]} | false. +is_subscribed(Pid, JID) -> + try p1_fsm:sync_send_all_state_event(Pid, {is_subscribed, JID}) + catch _:{_, {p1_fsm, _, _}} -> false + end. + +-spec get_subscribers(pid()) -> {ok, [jid()]} | {error, notfound | timeout}. +get_subscribers(Pid) -> + try p1_fsm:sync_send_all_state_event(Pid, get_subscribers) + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + +-spec service_message(pid(), binary()) -> ok. +service_message(Pid, Text) -> + p1_fsm:send_all_state_event(Pid, {service_message, Text}). + +-spec get_disco_item(pid(), disco_item_filter(), jid(), binary()) -> + {ok, binary()} | {error, notfound | timeout}. +get_disco_item(Pid, Filter, JID, Lang) -> + Timeout = 100, + Time = erlang:system_time(millisecond), + Query = {get_disco_item, Filter, JID, Lang, Time+Timeout}, + try p1_fsm:sync_send_all_state_event(Pid, Query, Timeout) of + {item, Desc} -> + {ok, Desc}; + false -> + {error, notfound} + catch _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} + end. + %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- init([Host, ServerHost, Access, Room, HistorySize, - RoomShaper, Creator, _Nick, DefRoomOpts]) -> + RoomShaper, Creator, _Nick, DefRoomOpts, QueueType]) -> process_flag(trap_exit, true), - Shaper = shaper:new(RoomShaper), + Shaper = ejabberd_shaper:new(RoomShaper), + RoomQueue = room_queue_new(ServerHost, Shaper, QueueType), State = set_affiliation(Creator, owner, #state{host = Host, server_host = ServerHost, access = Access, room = Room, - history = lqueue_new(HistorySize), - jid = jid:make(Room, Host, <<"">>), + history = lqueue_new(HistorySize, QueueType), + jid = jid:make(Room, Host), just_created = true, + room_queue = RoomQueue, 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)]), + ?INFO_MSG("Created MUC room ~ts@~ts by ~ts", + [Room, Host, jid:encode(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]) -> + {ok, normal_state, reset_hibernate_timer(State1)}; +init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) -> process_flag(trap_exit, true), - Shaper = shaper:new(RoomShaper), + Shaper = ejabberd_shaper:new(RoomShaper), + RoomQueue = room_queue_new(ServerHost, Shaper, QueueType), State = set_opts(Opts, #state{host = Host, server_host = ServerHost, access = Access, room = Room, - history = lqueue_new(HistorySize), - jid = jid:make(Room, Host, <<"">>), + history = lqueue_new(HistorySize, QueueType), + jid = jid:make(Room, Host), + room_queue = RoomQueue, room_shaper = Shaper}), add_to_log(room_existence, started, State), - {ok, normal_state, State}. + {ok, normal_state, reset_hibernate_timer(State)}. -normal_state({route, From, <<"">>, - #xmlel{name = <<"message">>, attrs = Attrs, - children = Els} = - Packet}, +normal_state({route, <<"">>, + #message{from = From, type = Type, lang = Lang} = Packet}, StateData) -> - Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), case is_user_online(From, StateData) orelse - is_user_allowed_message_nonparticipant(From, StateData) - of - true -> - case fxml:get_attr_s(<<"type">>, Attrs) of - <<"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 = jlib:make_error_reply(Packet, - ?ERRT_RESOURCE_CONSTRAINT(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData}; - Now >= - Activity#activity.message_time + MinMessageInterval, - MessageShaperInterval == 0 -> - {RoomShaper, RoomShaperInterval} = - shaper:update(StateData#state.room_shaper, Size), - RoomQueueEmpty = - queue:is_empty(StateData#state.room_queue), - if RoomShaperInterval == 0, RoomQueueEmpty -> - NewActivity = Activity#activity{message_time = - Now, - message_shaper = - MessageShaper}, - StateData1 = store_user_activity(From, - NewActivity, - StateData), - StateData2 = StateData1#state{room_shaper = - RoomShaper}, - process_groupchat_message(From, Packet, - StateData2); - true -> - StateData1 = if RoomQueueEmpty -> - erlang:send_after(RoomShaperInterval, - self(), - process_room_queue), - StateData#state{room_shaper = - RoomShaper}; - true -> StateData - end, - NewActivity = Activity#activity{message_time = - Now, - message_shaper = - MessageShaper, - message = Packet}, - RoomQueue = queue:in({message, From}, - StateData#state.room_queue), - StateData2 = store_user_activity(From, - NewActivity, - StateData1), - StateData3 = StateData2#state{room_queue = - RoomQueue}, - {next_state, normal_state, StateData3} - end; - true -> - MessageInterval = (Activity#activity.message_time + - MinMessageInterval - - Now) - div 1000, - Interval = lists:max([MessageInterval, - MessageShaperInterval]), - erlang:send_after(Interval, self(), - {process_user_message, From}), - NewActivity = Activity#activity{message = Packet, - message_shaper = - MessageShaper}, - StateData1 = store_user_activity(From, NewActivity, - StateData), - {next_state, normal_state, StateData1} - end; - <<"error">> -> - case is_user_online(From, StateData) of - true -> - ErrorText = <<"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; - <<"chat">> -> - ErrText = - <<"It is not allowed to send private messages " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData}; - Type when (Type == <<"">>) or (Type == <<"normal">>) -> - IsInvitation = is_invitation(Els), - IsVoiceRequest = is_voice_request(Els) and - is_visitor(From, StateData), - IsVoiceApprovement = is_voice_approvement(Els) and - not is_visitor(From, StateData), - if IsInvitation -> - case catch check_invitation(From, Packet, Lang, StateData) - of - {error, Error} -> - Err = jlib:make_error_reply(Packet, Error), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData}; - IJID -> - Config = StateData#state.config, - case Config#config.members_only of - true -> - case get_affiliation(IJID, StateData) of - none -> - NSD = set_affiliation(IJID, member, - StateData), - send_affiliation(IJID, member, - StateData), - store_room(NSD), - {next_state, normal_state, NSD}; - _ -> {next_state, normal_state, StateData} - end; - false -> {next_state, normal_state, StateData} - end - end; - IsVoiceRequest -> - NewStateData = case - (StateData#state.config)#config.allow_voice_requests - of - true -> - MinInterval = - (StateData#state.config)#config.voice_request_min_interval, - BareFrom = - 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, NSD), - NSD; - {ok, _, _} -> - ErrText = - <<"Please, wait for a while before sending " - "new voice request">>, - Err = - jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, - From, Err), - StateData#state{last_voice_request_time - = Times} - end; - false -> - ErrText = - <<"Voice requests are disabled in this " - "conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, - From, Err), - StateData - end, - {next_state, normal_state, NewStateData}; - IsVoiceApprovement -> - NewStateData = case is_moderator(From, StateData) of - true -> - case - extract_jid_from_voice_approvement(Els) - of - error -> - ErrText = - <<"Failed to extract JID from your voice " - "request approval">>, - Err = - jlib:make_error_reply(Packet, - ?ERRT_BAD_REQUEST(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, - From, Err), - StateData; - {ok, TargetJid} -> - case is_visitor(TargetJid, - StateData) - of - true -> - Reason = <<>>, - NSD = - set_role(TargetJid, - participant, - StateData), - catch - send_new_presence(TargetJid, - Reason, - NSD, - StateData), - NSD; - _ -> StateData - end - end; - _ -> - ErrText = - <<"Only moderators can approve voice requests">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ALLOWED(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, - From, Err), - StateData - end, - {next_state, normal_state, NewStateData}; - true -> {next_state, normal_state, StateData} - end; - _ -> - ErrText = <<"Improper message type">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, - ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData} - end; - _ -> - case fxml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> ok; - _ -> - handle_roommessage_from_nonparticipant(Packet, Lang, - StateData, From) - end, - {next_state, normal_state, StateData} - end; -normal_state({route, From, <<"">>, - #xmlel{name = <<"iq">>} = Packet}, - StateData) -> - case jlib:iq_query_info(Packet) of - reply -> - {next_state, normal_state, StateData}; - IQ0 -> - case ejabberd_hooks:run_fold( - muc_process_iq, - StateData#state.server_host, - IQ0, [StateData, From, StateData#state.jid]) of - ignore -> - {next_state, normal_state, StateData}; - #iq{type = T} = IQRes when T == error; T == result -> - ejabberd_router:route(StateData#state.jid, From, jlib:iq_to_xml(IQRes)), + is_subscriber(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData) of + true when Type == groupchat -> + Activity = get_user_activity(From, StateData), + Now = erlang:system_time(microsecond), + MinMessageInterval = trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000000), + Size = element_size(Packet), + {MessageShaper, MessageShaperInterval} = + ejabberd_shaper:update(Activity#activity.message_shaper, Size), + if Activity#activity.message /= undefined -> + ErrText = ?T("Traffic rate limit is exceeded"), + Err = xmpp:err_resource_constraint(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData}; - #iq{type = Type, xmlns = XMLNS, lang = Lang, - sub_el = #xmlel{name = SubElName, attrs = Attrs} = SubEl} = IQ - when (XMLNS == (?NS_MUC_ADMIN)) or - (XMLNS == (?NS_MUC_OWNER)) - or (XMLNS == (?NS_DISCO_INFO)) - or (XMLNS == (?NS_DISCO_ITEMS)) - or (XMLNS == (?NS_VCARD)) - or (XMLNS == (?NS_MUCSUB)) - or (XMLNS == (?NS_CAPTCHA)) -> - Res1 = case XMLNS of - ?NS_MUC_ADMIN -> - process_iq_admin(From, Type, Lang, SubEl, StateData); - ?NS_MUC_OWNER -> - process_iq_owner(From, Type, Lang, SubEl, StateData); - ?NS_DISCO_INFO -> - case fxml:get_attr(<<"node">>, Attrs) of - false -> process_iq_disco_info(From, Type, Lang, StateData); - {value, _} -> - Txt = <<"Disco info is not available for this node">>, - {error, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)} - end; - ?NS_DISCO_ITEMS -> - process_iq_disco_items(From, Type, Lang, StateData); - ?NS_VCARD -> - process_iq_vcard(From, Type, Lang, SubEl, StateData); - ?NS_MUCSUB -> - process_iq_mucsub(From, Packet, IQ, StateData); - ?NS_CAPTCHA -> - process_iq_captcha(From, Type, Lang, SubEl, StateData) - end, - {IQRes, NewStateData} = - case Res1 of - {result, Res, SD} -> - {IQ#iq{type = result, - sub_el = - [#xmlel{name = SubElName, - attrs = - [{<<"xmlns">>, - XMLNS}], - children = Res}]}, - SD}; - {ignore, SD} -> {ignore, SD}; - {error, Error, ResStateData} -> - {IQ#iq{type = error, - sub_el = [SubEl, Error]}, - ResStateData}; - {error, Error} -> - {IQ#iq{type = error, - sub_el = [SubEl, Error]}, - StateData} - end, - if IQRes /= ignore -> - ejabberd_router:route( - StateData#state.jid, From, jlib:iq_to_xml(IQRes)); + Now >= Activity#activity.message_time + MinMessageInterval, + MessageShaperInterval == 0 -> + {RoomShaper, RoomShaperInterval} = + ejabberd_shaper:update(StateData#state.room_shaper, Size), + RoomQueueEmpty = case StateData#state.room_queue of + undefined -> true; + RQ -> p1_queue:is_empty(RQ) + end, + 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(Packet, + StateData2); true -> - ok - end, - case NewStateData of - stop -> {stop, normal, StateData}; - _ -> {next_state, normal_state, NewStateData} + 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 = p1_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 = ?T("It is not allowed to send error messages to the" + " room. The participant (~ts) has sent an error " + "message (~ts) and got kicked from the room"), + NewState = expulse_participant(Packet, From, StateData, + translate:translate(Lang, + ErrorText)), + close_room_if_temporary_and_empty(NewState); _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(StateData#state.jid, From, Err), {next_state, normal_state, StateData} - end + end; + true when Type == chat -> + ErrText = ?T("It is not allowed to send private messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(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:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + StateData + end}; + true -> + ErrText = ?T("Improper message type"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + {next_state, normal_state, StateData}; + false when Type /= error -> + handle_roommessage_from_nonparticipant(Packet, StateData, From), + {next_state, normal_state, StateData}; + false -> + {next_state, normal_state, StateData} end; -normal_state({route, From, Nick, - #xmlel{name = <<"presence">>} = Packet}, - StateData) -> +normal_state({route, <<"">>, + #iq{from = From, 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(IQRes), + {next_state, normal_state, StateData}; + #iq{sub_els = [SubEl]} = IQ -> + Res1 = case SubEl of + #muc_admin{} -> + process_iq_admin(From, IQ, StateData); + #muc_owner{} -> + process_iq_owner(From, IQ, StateData); + #disco_info{} -> + process_iq_disco_info(From, IQ, StateData); + #disco_items{} -> + process_iq_disco_items(From, IQ, StateData); + #vcard_temp{} -> + process_iq_vcard(From, IQ, StateData); + #muc_subscribe{} -> + process_iq_mucsub(From, IQ, StateData); + #muc_unsubscribe{} -> + process_iq_mucsub(From, IQ, StateData); + #muc_subscriptions{} -> + process_iq_mucsub(From, IQ, StateData); + #xcaptcha{} -> + process_iq_captcha(From, IQ, StateData); + _ -> + Txt = ?T("The feature requested is not " + "supported by the conference"), + {error, xmpp:err_service_unavailable(Txt, Lang)} + 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} -> + {xmpp:make_error(IQ0, Error), StateData} + end, + if IQRes /= ignore -> + ejabberd_router:route(IQRes); + true -> + ok + end, + case NewStateData of + stop -> + Conf = StateData#state.config, + {stop, normal, StateData#state{config = Conf#config{persistent = false}}}; + _ when NewStateData#state.just_created -> + close_room_if_temporary_and_empty(NewStateData); + _ -> + {next_state, normal_state, NewStateData} + end + end + catch _:{xmpp_codec, Why} -> + ErrTxt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(ErrTxt, Lang), + ejabberd_router:route_error(IQ0, Err), + {next_state, normal_state, StateData} + end; +normal_state({route, <<"">>, #iq{} = IQ}, StateData) -> + Err = xmpp:err_bad_request(), + ejabberd_router:route_error(IQ, Err), + case StateData#state.just_created of + true -> {stop, normal, StateData}; + _ -> {next_state, normal_state, StateData} + end; +normal_state({route, Nick, #presence{from = From} = Packet}, StateData) -> Activity = get_user_activity(From, StateData), - Now = p1_time_compat:system_time(micro_seconds), + Now = erlang:system_time(microsecond), 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); + trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 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(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} + if Activity#activity.presence == undefined -> + Interval = (Activity#activity.presence_time + + MinPresenceInterval - Now) div 1000, + erlang:send_after(Interval, self(), + {process_user_presence, From}); + true -> ok + end, + NewActivity = Activity#activity{presence = {Nick, Packet}}, + StateData1 = store_user_activity(From, NewActivity, + StateData), + {next_state, normal_state, StateData1} end; -normal_state({route, From, ToNick, - #xmlel{name = <<"message">>, attrs = Attrs} = Packet}, +normal_state({route, ToNick, + #message{from = From, type = Type, lang = Lang} = Packet}, StateData) -> - Type = fxml:get_attr_s(<<"type">>, Attrs), - Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), - case decide_fate_message(Type, 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} -> - case Type of - <<"groupchat">> -> - ErrText = - <<"It is not allowed to send private messages " - "of type \"groupchat\"">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_BAD_REQUEST(Lang, - ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - ToNick), - From, Err); - _ -> - case find_jids_by_nick(ToNick, StateData) of - false -> - ErrText = - <<"Recipient is not in the conference room">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_ITEM_NOT_FOUND(Lang, - ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - ToNick), - From, Err); + case decide_fate_message(Packet, From, StateData) of + {expulse_sender, Reason} -> + ?DEBUG(Reason, []), + ErrorText = ?T("It is not allowed to send error messages to the" + " room. The participant (~ts) has sent an error " + "message (~ts) 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) orelse + is_subscriber(From, StateData)} of + {true, true} when Type == groupchat -> + ErrText = ?T("It is not allowed to send private messages " + "of type \"groupchat\""), + Err = xmpp:err_bad_request(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + {true, true} -> + case find_jids_by_nick(ToNick, StateData) of + [] -> + ErrText = ?T("Recipient is not in the conference room"), + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); ToJIDs -> SrcIsVisitor = is_visitor(From, StateData), - DstIsModerator = is_moderator(hd(ToJIDs), - 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 = #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_MUC_USER}]}, - PrivMsg = fxml:append_subtags(Packet, [X]), - [ejabberd_router:route(FromNickJID, ToJID, PrivMsg) - || ToJID <- ToJIDs]; + DstIsModerator -> + {FromNick, _} = get_participant_data(From, StateData), + FromNickJID = + jid:replace_resource(StateData#state.jid, + FromNick), + X = #muc_user{}, + PrivMsg = xmpp:set_from( + xmpp:set_subtag(Packet, X), + FromNickJID), + lists:foreach( + fun(ToJID) -> + ejabberd_router:route(xmpp:set_to(PrivMsg, ToJID)) + end, ToJIDs); true -> - ErrText = - <<"It is not allowed to send private messages">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, - ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - ToNick), - From, Err) + ErrText = ?T("It is not allowed to send private messages"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) end - end - end; - {true, false} -> - ErrText = - <<"Only occupants are allowed to send messages " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, - ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - ToNick), - From, Err); - {false, _} -> - ErrText = - <<"It is not allowed to send private messages">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - ToNick), - From, Err) - end, + end; + {true, false} -> + ErrText = ?T("Only occupants are allowed to send messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + {false, _} -> + ErrText = ?T("It is not allowed to send private messages"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) + end, {next_state, normal_state, StateData} end; -normal_state({route, From, ToNick, - #xmlel{name = <<"iq">>, attrs = Attrs} = Packet}, - StateData) -> - Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), - StanzaId = fxml:get_attr_s(<<"id">>, Attrs), - case {(StateData#state.config)#config.allow_query_users, - is_user_online_iq(StanzaId, From, StateData)} - of - {true, {true, NewId, FromFull}} -> - case find_jid_by_nick(ToNick, StateData) of - false -> - case jlib:iq_query_info(Packet) of - reply -> ok; - _ -> - ErrText = <<"Recipient is not in the conference room">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_ITEM_NOT_FOUND(Lang, - ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - ToNick), - From, Err) - end; - ToJID -> - {ok, #user{nick = FromNick}} = - (?DICT):find(jid:tolower(FromFull), - StateData#state.users), - {ToJID2, Packet2} = handle_iq_vcard(FromFull, ToJID, - StanzaId, NewId, Packet), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - FromNick), - ToJID2, Packet2) - end; - {_, {false, _, _}} -> - case jlib:iq_query_info(Packet) of - reply -> ok; - _ -> - ErrText = - <<"Only occupants are allowed to send queries " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, - ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - ToNick), - From, Err) - end; - _ -> - case jlib:iq_query_info(Packet) of - reply -> ok; - _ -> - ErrText = <<"Queries to the conference members are " - "not allowed in this room">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ALLOWED(Lang, ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - ToNick), - From, Err) - end +normal_state({route, ToNick, + #iq{from = From, lang = Lang} = Packet}, + #state{config = #config{allow_query_users = AllowQuery}} = StateData) -> + try maps:get(jid:tolower(From), StateData#state.users) of + #user{nick = FromNick} when AllowQuery orelse ToNick == FromNick -> + case find_jid_by_nick(ToNick, StateData) of + false -> + ErrText = ?T("Recipient is not in the conference room"), + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + To -> + FromJID = jid:replace_resource(StateData#state.jid, FromNick), + case direct_iq_type(Packet) of + vcard -> + ejabberd_router:route_iq( + xmpp:set_from_to(Packet, FromJID, jid:remove_resource(To)), + Packet, self()); + ping when ToNick == FromNick -> + %% Self-ping optimization from XEP-0410 + ejabberd_router:route(xmpp:make_iq_result(Packet)); + response -> + ejabberd_router:route(xmpp:set_from_to(Packet, FromJID, To)); + #stanza_error{} = Err -> + ejabberd_router:route_error(Packet, Err); + _OtherRequest -> + ejabberd_router:route_iq( + xmpp:set_from_to(Packet, FromJID, To), Packet, self()) + end + end; + _ -> + ErrText = ?T("Queries to the conference members are " + "not allowed in this room"), + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) + catch _:{badkey, _} -> + ErrText = ?T("Only occupants are allowed to send queries " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) end, {next_state, normal_state, StateData}; +normal_state(hibernate, StateData) -> + case maps:size(StateData#state.users) of + 0 -> + store_room_no_checks(StateData, []), + ?INFO_MSG("Hibernating room ~ts@~ts", [StateData#state.room, StateData#state.host]), + {stop, normal, StateData#state{hibernate_timer = hibernating}}; + _ -> + {next_state, normal_state, StateData} + end; normal_state(_Event, StateData) -> {next_state, normal_state, StateData}. handle_event({service_message, Msg}, _StateName, StateData) -> - MessagePkt = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"body">>, attrs = [], - children = [{xmlcdata, Msg}]}]}, + MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)}, send_wrapped_multiple( StateData#state.jid, - StateData#state.users, + get_users_and_subscribers(StateData), MessagePkt, ?NS_MUCSUB_NODES_MESSAGES, StateData), @@ -687,40 +645,25 @@ handle_event({service_message, Msg}, _StateName, {next_state, normal_state, NSD}; handle_event({destroy, Reason}, _StateName, StateData) -> - {result, [], stop} = destroy_room(#xmlel{name = - <<"destroy">>, - attrs = - [{<<"xmlns">>, ?NS_MUC_OWNER}], - children = - case Reason of - none -> []; - _Else -> - [#xmlel{name = - <<"reason">>, - attrs = [], - children = - [{xmlcdata, - Reason}]}] - end}, - StateData), - ?INFO_MSG("Destroyed MUC room ~s with reason: ~p", - [jid:to_string(StateData#state.jid), Reason]), + _ = destroy_room(#muc_destroy{xmlns = ?NS_MUC_OWNER, reason = Reason}, StateData), + ?INFO_MSG("Destroyed MUC room ~ts with reason: ~p", + [jid:encode(StateData#state.jid), Reason]), add_to_log(room_existence, destroyed, StateData), - {stop, shutdown, StateData}; + Conf = StateData#state.config, + {stop, shutdown, StateData#state{config = Conf#config{persistent = false}}}; handle_event(destroy, StateName, StateData) -> - ?INFO_MSG("Destroyed MUC room ~s", - [jid:to_string(StateData#state.jid)]), - handle_event({destroy, none}, StateName, StateData); + ?INFO_MSG("Destroyed MUC room ~ts", + [jid:encode(StateData#state.jid)]), + handle_event({destroy, <<"">>}, StateName, StateData); handle_event({set_affiliations, Affiliations}, StateName, StateData) -> - {next_state, StateName, - StateData#state{affiliations = Affiliations}}; + NewStateData = set_affiliations(Affiliations, StateData), + {next_state, StateName, NewStateData}; 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), +handle_sync_event({get_disco_item, Filter, JID, Lang, Time}, _From, StateName, StateData) -> + Len = maps:size(StateData#state.nicks), Reply = case (Filter == all) or (Filter == Len) or ((Filter /= 0) and (Len /= 0)) of true -> get_roomdesc_reply(JID, StateData, @@ -728,10 +671,17 @@ handle_sync_event({get_disco_item, Filter, JID, Lang}, _From, StateName, StateDa false -> false end, - {reply, Reply, StateName, StateData}; -%% This clause is only for backwards compatibility + CurrentTime = erlang:system_time(millisecond), + if CurrentTime < Time -> + {reply, Reply, StateName, StateData}; + true -> + {next_state, StateName, StateData} + end; +%% These two clauses are only for backward compatibility with nodes running old code 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_disco_item, Filter, JID, Lang}, From, StateName, StateData) -> + handle_sync_event({get_disco_item, Filter, JID, Lang, infinity}, From, StateName, StateData); handle_sync_event(get_config, _From, StateName, StateData) -> {reply, {ok, StateData#state.config}, StateName, @@ -741,68 +691,73 @@ handle_sync_event(get_state, _From, StateName, {reply, {ok, StateData}, StateName, StateData}; handle_sync_event({change_config, Config}, _From, StateName, StateData) -> - {result, [], NSD} = change_config(Config, StateData), + {result, undefined, NSD} = change_config(Config, StateData), {reply, {ok, NSD#state.config}, StateName, NSD}; handle_sync_event({change_state, NewStateData}, _From, StateName, _StateData) -> + Mod = gen_mod:db_mod(NewStateData#state.server_host, mod_muc), + case erlang:function_exported(Mod, get_subscribed_rooms, 3) of + true -> + ok; + _ -> + erlang:put(muc_subscribers, NewStateData#state.subscribers) + end, {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}; + case process_item_change(Item, StateData, UJID) of + {error, _} = Err -> + {reply, Err, StateName, StateData}; + NSD -> + {reply, {ok, NSD}, StateName, NSD} + end; +handle_sync_event(get_subscribers, _From, StateName, StateData) -> + JIDs = lists:map(fun jid:make/1, + maps:keys(StateData#state.subscribers)), + {reply, {ok, JIDs}, StateName, StateData}; handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From, StateName, StateData) -> - SubEl = #xmlel{name = <<"subscribe">>, - attrs = [{<<"xmlns">>, ?NS_MUCSUB}, {<<"nick">>, Nick}], - children = [#xmlel{name = <<"event">>, - attrs = [{<<"node">>, Node}]} - || Node <- Nodes]}, - IQ = #iq{type = set, id = randoms:get_string(), - xmlns = ?NS_MUCSUB, sub_el = SubEl}, - Packet = jlib:iq_to_xml(IQ#iq{sub_el = [SubEl]}), + IQ = #iq{type = set, id = p1_rand:get_string(), + from = From, sub_els = [#muc_subscribe{nick = Nick, + events = Nodes}]}, Config = StateData#state.config, CaptchaRequired = Config#config.captcha_protected, PasswordProtected = Config#config.password_protected, TmpConfig = Config#config{captcha_protected = false, password_protected = false}, TmpState = StateData#state{config = TmpConfig}, - case process_iq_mucsub(From, Packet, IQ, TmpState) of - {result, _, NewState} -> + case process_iq_mucsub(From, IQ, TmpState) of + {result, #muc_subscribe{events = NewNodes}, NewState} -> NewConfig = (NewState#state.config)#config{ captcha_protected = CaptchaRequired, password_protected = PasswordProtected}, - {reply, {ok, get_subscription_nodes(Packet)}, StateName, + {reply, {ok, NewNodes}, StateName, NewState#state{config = NewConfig}}; {ignore, NewState} -> NewConfig = (NewState#state.config)#config{ captcha_protected = CaptchaRequired, password_protected = PasswordProtected}, - {reply, {error, <<"Requrest is ignored">>}, - NewState#state{config = NewConfig}}; - {error, Err, NewState} -> - NewConfig = (NewState#state.config)#config{ - captcha_protected = CaptchaRequired, - password_protected = PasswordProtected}, - {reply, {error, get_error_text(Err)}, StateName, + {reply, {error, ?T("Request is ignored")}, NewState#state{config = NewConfig}}; {error, Err} -> {reply, {error, get_error_text(Err)}, StateName, StateData} end; handle_sync_event({muc_unsubscribe, From}, _From, StateName, StateData) -> - SubEl = #xmlel{name = <<"unsubscribe">>, - attrs = [{<<"xmlns">>, ?NS_MUCSUB}]}, - IQ = #iq{type = set, id = randoms:get_string(), - xmlns = ?NS_MUCSUB, sub_el = SubEl}, - Packet = jlib:iq_to_xml(IQ), - case process_iq_mucsub(From, Packet, IQ, StateData) of + IQ = #iq{type = set, id = p1_rand:get_string(), + from = From, sub_els = [#muc_unsubscribe{}]}, + case process_iq_mucsub(From, IQ, StateData) of {result, _, NewState} -> {reply, ok, StateName, NewState}; {ignore, NewState} -> - {reply, {error, <<"Requrest is ignored">>}, NewState}; - {error, Err, NewState} -> - {reply, {error, get_error_text(Err)}, StateName, NewState}; + {reply, {error, ?T("Request is ignored")}, NewState}; {error, Err} -> {reply, {error, get_error_text(Err)}, StateName, StateData} end; +handle_sync_event({is_subscribed, From}, _From, StateName, StateData) -> + IsSubs = try maps:get(jid:split(From), StateData#state.subscribers) of + #subscriber{nodes = Nodes} -> {true, Nodes} + catch _:{badkey, _} -> false + end, + {reply, IsSubs, StateName, StateData}; handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. @@ -811,8 +766,8 @@ 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), + RoomQueueEmpty = p1_queue:is_empty(StateData#state.room_queue), + RoomQueue = p1_queue:in({presence, From}, StateData#state.room_queue), StateData1 = StateData#state{room_queue = RoomQueue}, if RoomQueueEmpty -> StateData2 = prepare_room_queue(StateData1), @@ -822,9 +777,9 @@ handle_info({process_user_presence, From}, normal_state = _StateName, StateData) 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), + p1_queue:is_empty(StateData#state.room_queue), + RoomQueue = p1_queue:in({message, From}, + StateData#state.room_queue), StateData1 = StateData#state{room_queue = RoomQueue}, if RoomQueueEmpty -> StateData2 = prepare_room_queue(StateData1), @@ -833,7 +788,7 @@ handle_info({process_user_message, From}, end; handle_info(process_room_queue, normal_state = StateName, StateData) -> - case queue:out(StateData#state.room_queue) of + case p1_queue:out(StateData#state.room_queue) of {{value, {message, From}}, RoomQueue} -> Activity = get_user_activity(From, StateData), Packet = Activity#activity.message, @@ -842,7 +797,7 @@ handle_info(process_room_queue, StateData), StateData2 = StateData1#state{room_queue = RoomQueue}, StateData3 = prepare_room_queue(StateData2), - process_groupchat_message(From, Packet, StateData3); + process_groupchat_message(Packet, StateData3); {{value, {presence, From}}, RoomQueue} -> Activity = get_user_activity(From, StateData), {Nick, Packet} = Activity#activity.presence, @@ -851,113 +806,139 @@ handle_info(process_room_queue, StateData), StateData2 = StateData1#state{room_queue = RoomQueue}, StateData3 = prepare_room_queue(StateData2), - process_presence(From, Nick, Packet, StateData3); + process_presence(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 + NewState = case maps:get(From, StateData#state.robots, passed) of + {Nick, Packet} -> + Robots = maps:put(From, passed, StateData#state.robots), + add_new_user(From, Nick, Packet, + StateData#state{robots = Robots}); + passed -> + 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">>, - Err = jlib:make_error_reply( - Packet, ?ERRT_NOT_AUTHORIZED(?MYLANG, Txt)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData#state{robots = Robots}; - _ -> StateData + NewState = case maps:get(From, StateData#state.robots, passed) of + {_Nick, Packet} -> + Robots = maps:remove(From, StateData#state.robots), + Txt = ?T("The CAPTCHA verification has failed"), + Lang = xmpp:get_lang(Packet), + Err = xmpp:err_not_authorized(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + StateData#state{robots = Robots}; + passed -> + StateData end, {next_state, normal_state, NewState}; handle_info(shutdown, _StateName, StateData) -> {stop, shutdown, StateData}; +handle_info({iq_reply, #iq{type = Type, sub_els = Els}, + #iq{from = From, to = To} = IQ}, StateName, StateData) -> + ejabberd_router:route( + xmpp:set_from_to( + IQ#iq{type = Type, sub_els = Els}, + To, From)), + {next_state, StateName, StateData}; +handle_info({iq_reply, timeout, IQ}, StateName, StateData) -> + Txt = ?T("Request has timed out"), + Err = xmpp:err_recipient_unavailable(Txt, IQ#iq.lang), + ejabberd_router:route_error(IQ, Err), + {next_state, StateName, StateData}; +handle_info(config_reloaded, StateName, StateData) -> + Max = mod_muc_opt:history_size(StateData#state.server_host), + History1 = StateData#state.history, + Q1 = History1#lqueue.queue, + Q2 = case p1_queue:len(Q1) of + Len when Len > Max -> + lqueue_cut(Q1, Len-Max); + _ -> + Q1 + end, + History2 = History1#lqueue{queue = Q2, max = Max}, + {next_state, StateName, StateData#state{history = History2}}; 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, - ItemAttrs = [{<<"affiliation">>, <<"none">>}, - {<<"role">>, <<"none">>}], - ReasonEl = #xmlel{name = <<"reason">>, attrs = [], - children = [{xmlcdata, ReasonT}]}, - Packet = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_MUC_USER}], - children = - [#xmlel{name = <<"item">>, - attrs = ItemAttrs, - children = [ReasonEl]}, - #xmlel{name = <<"status">>, - attrs = [{<<"code">>, <<"332">>}], - children = []}]}]}, - (?DICT):fold(fun (LJID, Info, _) -> - Nick = Info#user.nick, - case Reason of - shutdown -> - 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. +terminate(Reason, _StateName, + #state{server_host = LServer, host = Host, room = Room} = StateData) -> + try + ?INFO_MSG("Stopping MUC room ~ts@~ts", [Room, Host]), + ReasonT = case Reason of + shutdown -> + ?T("You are being removed from the room " + "because of a system shutdown"); + _ -> ?T("Room terminates") + end, + Packet = #presence{ + type = unavailable, + sub_els = [#muc_user{items = [#muc_item{affiliation = none, + reason = ReasonT, + role = none}], + status_codes = [332,110]}]}, + maps:fold( + fun(_, #user{nick = Nick, jid = JID}, _) -> + case Reason of + shutdown -> + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + JID, Packet, + ?NS_MUCSUB_NODES_PARTICIPANTS, + StateData); + _ -> ok + end, + tab_remove_online_user(JID, StateData) + end, [], get_users_and_subscribers(StateData)), + + disable_hibernate_timer(StateData), + case StateData#state.hibernate_timer of + hibernating -> + ok; + _ -> + add_to_log(room_existence, stopped, StateData), + case (StateData#state.config)#config.persistent of + false -> + ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host]); + _ -> + ok + end + end, + mod_muc:room_destroyed(Host, Room, self(), LServer) + catch ?EX_RULE(E, R, St) -> + StackTrace = ?EX_STACK(St), + mod_muc:room_destroyed(Host, Room, self(), LServer), + ?ERROR_MSG("Got exception on room termination:~n** ~ts", + [misc:format_exception(2, E, R, StackTrace)]) + end. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- - -route(Pid, From, ToNick, Packet) -> - gen_fsm:send_event(Pid, {route, From, ToNick, Packet}). - -process_groupchat_message(From, - #xmlel{name = <<"message">>, attrs = Attrs} = Packet, - StateData) -> - Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), - case is_user_online(From, StateData) orelse +-spec route(pid(), stanza()) -> ok. +route(Pid, Packet) -> + ?DEBUG("Routing to MUC room ~p:~n~ts", [Pid, xmpp:pp(Packet)]), + #jid{lresource = Nick} = xmpp:get_to(Packet), + p1_fsm:send_event(Pid, {route, Nick, Packet}). + +-spec process_groupchat_message(message(), state()) -> fsm_next(). +process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData) -> + IsSubscriber = is_subscriber(From, StateData), + case is_user_online(From, StateData) orelse IsSubscriber orelse is_user_allowed_message_nonparticipant(From, StateData) of true -> - {FromNick, Role, IsSubscriber} = get_participant_data(From, StateData), + {FromNick, Role} = 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}; + [] -> {StateData, true}; _ -> case can_change_subject(Role, + IsSubscriber, StateData) of true -> @@ -979,20 +960,19 @@ process_groupchat_message(From, ejabberd_hooks:run_fold(muc_filter_message, StateData#state.server_host, Packet, - [StateData, - StateData#state.jid, - From, FromNick]) + [StateData, FromNick]) of drop -> {next_state, normal_state, StateData}; NewPacket1 -> - NewPacket = fxml:remove_subtags(NewPacket1, <<"nick">>, {<<"xmlns">>, ?NS_NICK}), - Node = if Subject == false -> ?NS_MUCSUB_NODES_MESSAGES; + NewPacket = xmpp:put_meta(xmpp:remove_subtag(NewPacket1, #nick{}), + muc_sender_real_jid, From), + Node = if Subject == [] -> ?NS_MUCSUB_NODES_MESSAGES; true -> ?NS_MUCSUB_NODES_SUBJECT end, send_wrapped_multiple( jid:replace_resource(StateData#state.jid, FromNick), - StateData#state.users, + get_users_and_subscribers(StateData), NewPacket, Node, NewStateData1), NewStateData2 = case has_body_or_subject(NewPacket) of true -> @@ -1005,41 +985,176 @@ process_groupchat_message(From, {next_state, normal_state, NewStateData2} end; _ -> - Err = case - (StateData#state.config)#config.allow_change_subj - of + Err = case (StateData#state.config)#config.allow_change_subj of true -> - ?ERRT_FORBIDDEN(Lang, - <<"Only moderators and participants are " - "allowed to change the subject in this " - "room">>); + xmpp:err_forbidden( + ?T("Only moderators and participants are " + "allowed to change the subject in this " + "room"), Lang); _ -> - ?ERRT_FORBIDDEN(Lang, - <<"Only moderators are allowed to change " - "the subject in this room">>) + xmpp:err_forbidden( + ?T("Only moderators are allowed to change " + "the subject in this room"), Lang) end, - ejabberd_router:route(StateData#state.jid, From, - jlib:make_error_reply(Packet, Err)), + ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData} end; true -> - ErrText = <<"Visitors are not allowed to send messages " - "to all occupants">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), + ErrText = ?T("Visitors are not allowed to send messages " + "to all occupants"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData} end; false -> - ErrText = - <<"Only occupants are allowed to send messages " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), + ErrText = ?T("Only occupants are allowed to send messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(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) -> + Action = lists:foldl( + fun(_, {error, _} = Err) -> + Err; + (_, {ok, _} = Result) -> + Result; + (#muc_user{invites = [_|_] = Invites}, _) -> + case check_invitation(From, Invites, Lang, StateData) of + ok -> + {ok, Invites}; + {error, _} = Err -> + Err + end; + (#xdata{type = submit, fields = Fs}, _) -> + try {ok, muc_request:decode(Fs)} + catch _:{muc_request, Why} -> + Txt = muc_request:format_error(Why), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + (_, Acc) -> + Acc + end, ok, xmpp:get_els(Pkt)), + case Action of + {ok, [#muc_invite{}|_] = Invitations} -> + lists:foldl( + fun(Invitation, AccState) -> + process_invitation(From, Pkt, Invitation, Lang, AccState) + end, StateData, Invitations); + {ok, [{role, participant}]} -> + process_voice_request(From, Pkt, StateData); + {ok, VoiceApproval} -> + process_voice_approval(From, Pkt, VoiceApproval, StateData); + {error, Err} -> + ejabberd_router:route_error(Pkt, Err), + StateData; + ok -> + StateData + end. + +-spec process_invitation(jid(), message(), muc_invite(), binary(), state()) -> state(). +process_invitation(From, Pkt, Invitation, Lang, StateData) -> + IJID = route_invitation(From, Pkt, Invitation, Lang, StateData), + 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. + +-spec process_voice_request(jid(), message(), state()) -> state(). +process_voice_request(From, Pkt, StateData) -> + Lang = xmpp:get_lang(Pkt), + 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 = -erlang:system_time(microsecond), + 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 = ?T("Please, wait for a while before sending " + "new voice request"), + Err = xmpp:err_resource_constraint(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData#state{last_voice_request_time = Times} + end; + false -> + ErrText = ?T("Voice requests are disabled in this conference"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData + end. + +-spec process_voice_approval(jid(), message(), [muc_request:property()], state()) -> state(). +process_voice_approval(From, Pkt, VoiceApproval, StateData) -> + Lang = xmpp:get_lang(Pkt), + case is_moderator(From, StateData) of + true -> + case lists:keyfind(jid, 1, VoiceApproval) of + {_, TargetJid} -> + Allow = proplists:get_bool(request_allow, VoiceApproval), + case is_visitor(TargetJid, StateData) of + true when Allow -> + Reason = <<>>, + NSD = set_role(TargetJid, participant, StateData), + catch send_new_presence( + TargetJid, Reason, NSD, StateData), + NSD; + _ -> + StateData + end; + false -> + ErrText = ?T("Failed to extract JID from your voice " + "request approval"), + Err = xmpp:err_bad_request(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData + end; + false -> + ErrText = ?T("Only moderators can approve voice requests"), + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData + end. + +-spec direct_iq_type(iq()) -> vcard | ping | request | response | stanza_error(). +direct_iq_type(#iq{type = T, sub_els = SubEls, lang = Lang}) when T == get; T == set -> + case SubEls of + [El] -> + case xmpp:get_ns(El) of + ?NS_VCARD when T == get -> vcard; + ?NS_PING when T == get -> ping; + _ -> request + end; + [] -> + xmpp:err_bad_request(?T("No child elements found"), Lang); + [_|_] -> + xmpp:err_bad_request(?T("Too many child elements"), Lang) + end; +direct_iq_type(#iq{}) -> + response. + %% @doc Check if this non participant can send message to room. %% %% XEP-0045 v1.23: @@ -1047,6 +1162,7 @@ process_groupchat_message(From, %% 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 @@ -1056,144 +1172,132 @@ is_user_allowed_message_nonparticipant(JID, %% @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()}. 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} + try maps:get(jid:tolower(From), StateData#state.users) of + #user{nick = FromNick, role = Role} -> + {FromNick, Role} + catch _:{badkey, _} -> + try maps:get(jid:tolower(jid:remove_resource(From)), + StateData#state.subscribers) of + #subscriber{nick = FromNick} -> + {FromNick, none} + catch _:{badkey, _} -> + {<<"">>, moderator} + end end. -process_presence(From, Nick, - #xmlel{name = <<"presence">>, attrs = Attrs0} = Packet0, - StateData) -> - Type0 = fxml:get_attr_s(<<"type">>, Attrs0), +-spec process_presence(binary(), presence(), state()) -> fsm_transition(). +process_presence(Nick, #presence{from = From, type = Type0} = Packet0, StateData) -> IsOnline = is_user_online(From, StateData), - IsSubscriber = is_subscriber(From, StateData), - if Type0 == <<"">>; - IsOnline and ((Type0 == <<"unavailable">>) or (Type0 == <<"error">>)) -> + 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 + [StateData, Nick]) of drop -> {next_state, normal_state, StateData}; - #xmlel{attrs = Attrs} = Packet -> - Type = fxml:get_attr_s(<<"type">>, Attrs), - Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), - StateData1 = case Type of - <<"unavailable">> -> - 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 = case fxml:get_subtag(NewPacket, - <<"status">>) - of - false -> <<"">>; - Status_el -> - fxml:get_tag_cdata(Status_el) - end, - remove_online_user(From, NewState, IsSubscriber, Reason); - <<"error">> -> - 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)); - <<"">> -> - if not IsOnline -> - 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 = jlib:make_error_reply(Packet, - ?ERRT_NOT_ALLOWED(Lang, - ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; - {true, _, _} -> - Lang = fxml:get_attr_s(<<"xml:lang">>, - Attrs), - ErrText = - <<"That nickname is already in use by another " - "occupant">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_CONFLICT(Lang, - ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), % TODO: s/Nick/""/ - From, Err), - StateData; - {_, false, _} -> - ErrText = - <<"That nickname is registered by another " - "person">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_CONFLICT(Lang, - ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - From, 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 - end, - close_room_if_temporary_and_empty(StateData1) + #presence{} = Packet -> + close_room_if_temporary_and_empty( + do_process_presence(Nick, Packet, StateData)) end; true -> - {next_state, normal_state, StateData} + {next_state, normal_state, StateData} end. +-spec do_process_presence(binary(), presence(), state()) -> state(). +do_process_presence(Nick, #presence{from = From, 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}} -> + Packet1 = Packet#presence{sub_els = [#muc{}]}, + ErrText = ?T("Visitors are not allowed to change their " + "nicknames in this room"), + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error(Packet1, Err), + StateData; + {true, _, _} -> + Packet1 = Packet#presence{sub_els = [#muc{}]}, + ErrText = ?T("That nickname is already in use by another " + "occupant"), + Err = xmpp:err_conflict(ErrText, Lang), + ejabberd_router:route_error(Packet1, Err), + StateData; + {_, false, _} -> + Packet1 = Packet#presence{sub_els = [#muc{}]}, + Err = case Nick of + <<>> -> + xmpp:err_jid_malformed(?T("Nickname can't be empty"), + Lang); + _ -> + xmpp:err_conflict(?T("That nickname is registered" + " by another person"), Lang) + end, + ejabberd_router:route_error(Packet1, Err), + StateData; + _ -> + change_nick(From, Nick, StateData) + end; + false -> + Stanza = maybe_strip_status_from_presence( + From, Packet, StateData), + NewState = add_user_presence(From, Stanza, + StateData), + case xmpp:has_subtag(Packet, #muc{}) of + true -> + send_initial_presences_and_messages( + From, Nick, Packet, NewState, StateData); + false -> + send_new_presence(From, NewState, StateData) + end, + NewState + end + end; +do_process_presence(Nick, #presence{from = From, type = unavailable} = Packet, + 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 maps:get(Nick, StateData#state.nicks, []) of + [_, _ | _] -> + Aff = get_affiliation(From, StateData), + Item = #muc_item{affiliation = Aff, role = none, jid = From}, + Pres = xmpp:set_subtag( + Packet, #muc_user{items = [Item], + status_codes = [110]}), + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData); + _ -> + send_new_presence(From, NewState, StateData) + end, + Reason = xmpp:get_text(NewPacket#presence.status), + remove_online_user(From, NewState, Reason); +do_process_presence(_Nick, #presence{from = From, type = error, lang = Lang} = Packet, + StateData) -> + ErrorText = ?T("It is not allowed to send error messages to the" + " room. The participant (~ts) has sent an error " + "message (~ts) 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 @@ -1202,44 +1306,60 @@ maybe_strip_status_from_presence(From, Packet, StateData) -> _Allowed -> Packet end. -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):to_list(StateData1#state.users) == [] - of + andalso maps:size(StateData1#state.users) == 0 + andalso maps:size(StateData1#state.subscribers) == 0 of true -> - ?INFO_MSG("Destroyed MUC room ~s because it's temporary " + ?INFO_MSG("Destroyed MUC room ~ts because it's temporary " "and empty", - [jid:to_string(StateData1#state.jid)]), + [jid:encode(StateData1#state.jid)]), add_to_log(room_existence, destroyed, StateData1), + forget_room(StateData1), {stop, normal, StateData1}; _ -> {next_state, normal_state, StateData1} end. +-spec get_users_and_subscribers(state()) -> users(). +get_users_and_subscribers(StateData) -> + OnlineSubscribers = maps:fold( + fun(LJID, _, Acc) -> + LBareJID = jid:remove_resource(LJID), + case is_subscriber(LBareJID, StateData) of + true -> + ?SETS:add_element(LBareJID, Acc); + false -> + Acc + end + end, ?SETS:new(), StateData#state.users), + maps:fold( + fun(LBareJID, #subscriber{nick = Nick}, Acc) -> + case ?SETS:is_element(LBareJID, OnlineSubscribers) of + false -> + maps:put(LBareJID, + #user{jid = jid:make(LBareJID), + nick = Nick, + role = none, + last_presence = undefined}, + Acc); + true -> + Acc + end + end, StateData#state.users, StateData#state.subscribers). + +-spec is_user_online(jid(), state()) -> boolean(). is_user_online(JID, StateData) -> LJID = jid:tolower(JID), - (?DICT):is_key(LJID, StateData#state.users). + maps: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. + LJID = jid:tolower(jid:remove_resource(JID)), + maps:is_key(LJID, StateData#state.subscribers). %% 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), @@ -1251,109 +1371,20 @@ is_occupant_or_admin(JID, StateData) -> _ -> false end. -%%% -%%% Handle IQ queries of vCard -%%% -is_user_online_iq(StanzaId, JID, StateData) - when JID#jid.lresource /= <<"">> -> - {is_user_online(JID, StateData), StanzaId, JID}; -is_user_online_iq(StanzaId, JID, StateData) - when JID#jid.lresource == <<"">> -> - try stanzaid_unpack(StanzaId) of - {OriginalId, Resource} -> - JIDWithResource = jid:replace_resource(JID, - Resource), - {is_user_online(JIDWithResource, StateData), OriginalId, - JIDWithResource} - catch - _:_ -> {is_user_online(JID, StateData), StanzaId, JID} - end. - -handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, - Packet) -> - ToBareJID = jid:remove_resource(ToJID), - IQ = jlib:iq_query_info(Packet), - handle_iq_vcard2(FromFull, ToJID, ToBareJID, StanzaId, - NewId, IQ, Packet). - -handle_iq_vcard2(_FromFull, ToJID, ToBareJID, StanzaId, - _NewId, #iq{type = get, xmlns = ?NS_VCARD}, Packet) - when ToBareJID /= ToJID -> - {ToBareJID, change_stanzaid(StanzaId, ToJID, Packet)}; -handle_iq_vcard2(_FromFull, ToJID, _ToBareJID, - _StanzaId, NewId, _IQ, Packet) -> - {ToJID, change_stanzaid(NewId, Packet)}. - -stanzaid_pack(OriginalId, Resource) -> - <<"berd", - (jlib:encode_base64(<<"ejab\000", - OriginalId/binary, "\000", - Resource/binary>>))/binary>>. - -stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) -> - StanzaId = jlib:decode_base64(StanzaIdBase64), - [<<"ejab">>, OriginalId, Resource] = - str:tokens(StanzaId, <<"\000">>), - {OriginalId, Resource}. - -change_stanzaid(NewId, Packet) -> - #xmlel{name = Name, attrs = Attrs, children = Els} = - jlib:remove_attr(<<"id">>, Packet), - #xmlel{name = Name, attrs = [{<<"id">>, NewId} | Attrs], - children = Els}. - -change_stanzaid(PreviousId, ToJID, Packet) -> - NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource), - change_stanzaid(NewId, Packet). - -%%% -%%% - -role_to_list(Role) -> - case Role of - moderator -> <<"moderator">>; - participant -> <<"participant">>; - visitor -> <<"visitor">>; - none -> <<"none">> - end. - -affiliation_to_list(Affiliation) -> - case Affiliation of - owner -> <<"owner">>; - admin -> <<"admin">>; - member -> <<"member">>; - outcast -> <<"outcast">>; - none -> <<"none">> - end. - -list_to_role(Role) -> - case Role of - <<"moderator">> -> moderator; - <<"participant">> -> participant; - <<"visitor">> -> visitor; - <<"none">> -> none - end. - -list_to_affiliation(Affiliation) -> - case Affiliation of - <<"owner">> -> owner; - <<"admin">> -> admin; - <<"member">> -> member; - <<"outcast">> -> outcast; - <<"none">> -> none - end. - %% Decide the fate of the message and its sender %% Returns: continue_delivery | forget_message | {expulse_sender, Reason} -decide_fate_message(<<"error">>, Packet, From, - StateData) -> - PD = case check_error_kick(Packet) of +-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)]), + Reason = str:format("This participant is considered a ghost " + "and is expulsed: ~ts", + [jid:encode(From)]), {expulse_sender, Reason}; false -> continue_delivery end, @@ -1365,129 +1396,209 @@ decide_fate_message(<<"error">>, Packet, From, end; Other -> Other end; -decide_fate_message(_, _, _, _) -> continue_delivery. +decide_fate_message(_, _, _) -> continue_delivery. %% Check if the elements of this error stanza indicate %% that the sender is a dead participant. %% If so, return true to kick the participant. -check_error_kick(Packet) -> - case get_error_condition(Packet) of - <<"gone">> -> true; - <<"internal-server-error">> -> true; - <<"item-not-found">> -> true; - <<"jid-malformed">> -> true; - <<"recipient-unavailable">> -> true; - <<"redirect">> -> true; - <<"remote-server-not-found">> -> true; - <<"remote-server-timeout">> -> true; - <<"service-unavailable">> -> true; - _ -> false - end. +-spec check_error_kick(stanza_error()) -> boolean(). +check_error_kick(#stanza_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. -get_error_condition(Packet) -> - case catch get_error_condition2(Packet) of - {condition, ErrorCondition} -> ErrorCondition; - {'EXIT', _} -> <<"badformed error stanza">> - end. +-spec get_error_condition(stanza_error()) -> string(). +get_error_condition(#stanza_error{reason = Reason}) -> + case Reason of + #gone{} -> "gone"; + #redirect{} -> "redirect"; + Atom -> atom_to_list(Atom) + end; +get_error_condition(undefined) -> + "undefined". -get_error_condition2(Packet) -> - #xmlel{children = EEls} = fxml:get_subtag(Packet, - <<"error">>), - [Condition] = [Name - || #xmlel{name = Name, - attrs = [{<<"xmlns">>, ?NS_STANZAS}], - children = []} - <- EEls], - {condition, Condition}. - -get_error_text(Error) -> - case fxml:get_subtag_with_xmlns(Error, <<"text">>, ?NS_STANZAS) of - #xmlel{} = Tag -> - fxml:get_tag_cdata(Tag); - false -> - <<"">> - end. +-spec get_error_text(stanza_error()) -> binary(). +get_error_text(#stanza_error{text = Txt}) -> + xmpp:get_text(Txt). +-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(Packet), - iolist_to_binary(io_lib:format(Reason1, [FromNick, Condition])). + #user{nick = FromNick} = maps:get(jid:tolower(From), StateData#state.users), + Condition = get_error_condition(xmpp:get_error(Packet)), + str: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, - #xmlel{name = <<"presence">>, - attrs = - [{<<"type">>, - <<"unavailable">>}], - children = - [#xmlel{name = <<"status">>, - attrs = [], - children = - [{xmlcdata, - Reason2}]}]}, + #presence{type = unavailable, + status = xmpp:mk_text(Reason2)}, StateData), - send_new_presence(From, NewState, StateData), - remove_online_user(From, NewState, IsSubscriber). - + LJID = jid:tolower(From), + #user{nick = Nick} = maps:get(LJID, StateData#state.users), + case maps:get(Nick, StateData#state.nicks, []) of + [_, _ | _] -> + Aff = get_affiliation(From, StateData), + Item = #muc_item{affiliation = Aff, role = none, jid = From}, + Pres = xmpp:set_subtag( + Packet, #muc_user{items = [Item], + status_codes = [110]}), + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData); + _ -> + send_new_presence(From, NewState, StateData) + end, + remove_online_user(From, NewState). + +-spec get_owners(state()) -> [jid:jid()]. +get_owners(StateData) -> + maps:fold( + fun(LJID, owner, Acc) -> + [jid:make(LJID)|Acc]; + (LJID, {owner, _}, Acc) -> + [jid:make(LJID)|Acc]; + (_, _, Acc) -> + Acc + end, [], StateData#state.affiliations). + +-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, + #state{config = #config{persistent = false}} = StateData, + Reason) -> + set_affiliation_fallback(JID, Affiliation, StateData, Reason); set_affiliation(JID, Affiliation, StateData, Reason) -> + ServerHost = StateData#state.server_host, + Room = StateData#state.room, + Host = StateData#state.host, + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case Mod:set_affiliation(ServerHost, Room, Host, JID, Affiliation, Reason) of + ok -> + StateData; + {error, _} -> + set_affiliation_fallback(JID, Affiliation, StateData, Reason) + end. + +-spec set_affiliation_fallback(jid(), affiliation(), state(), binary()) -> state(). +set_affiliation_fallback(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) + none -> + maps:remove(LJID, StateData#state.affiliations); + _ -> + maps:put(LJID, {Affiliation, Reason}, + StateData#state.affiliations) end, StateData#state{affiliations = Affiliations}. -get_affiliation(JID, StateData) -> - {_AccessRoute, _AccessCreate, AccessAdmin, - _AccessPersistent} = - StateData#state.access, - Res = case acl:match_rule(StateData#state.server_host, - AccessAdmin, JID) - of - allow -> owner; - _ -> - LJID = 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 +-spec set_affiliations(affiliations(), state()) -> state(). +set_affiliations(Affiliations, + #state{config = #config{persistent = false}} = StateData) -> + set_affiliations_fallback(Affiliations, StateData); +set_affiliations(Affiliations, StateData) -> + Room = StateData#state.room, + Host = StateData#state.host, + ServerHost = StateData#state.server_host, + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case Mod:set_affiliations(ServerHost, Room, Host, Affiliations) of + ok -> + StateData; + {error, _} -> + set_affiliations_fallback(Affiliations, StateData) end. +-spec set_affiliations_fallback(affiliations(), state()) -> state(). +set_affiliations_fallback(Affiliations, StateData) -> + StateData#state{affiliations = Affiliations}. + +-spec get_affiliation(ljid() | jid(), state()) -> affiliation(). +get_affiliation(#jid{} = JID, StateData) -> + case get_service_affiliation(JID, StateData) of + owner -> + owner; + none -> + case do_get_affiliation(JID, StateData) of + {Affiliation, _Reason} -> Affiliation; + Affiliation -> Affiliation + end + end; +get_affiliation(LJID, StateData) -> + get_affiliation(jid:make(LJID), StateData). + +-spec do_get_affiliation(jid(), state()) -> affiliation() | {affiliation(), binary()}. +do_get_affiliation(JID, #state{config = #config{persistent = false}} = StateData) -> + do_get_affiliation_fallback(JID, StateData); +do_get_affiliation(JID, StateData) -> + Room = StateData#state.room, + Host = StateData#state.host, + LServer = JID#jid.lserver, + LUser = JID#jid.luser, + ServerHost = StateData#state.server_host, + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case Mod:get_affiliation(ServerHost, Room, Host, LUser, LServer) of + {error, _} -> + do_get_affiliation_fallback(JID, StateData); + {ok, Affiliation} -> + Affiliation + end. + +-spec do_get_affiliation_fallback(jid(), state()) -> affiliation() | {affiliation(), binary()}. +do_get_affiliation_fallback(JID, StateData) -> + LJID = jid:tolower(JID), + try maps:get(LJID, StateData#state.affiliations) + catch _:{badkey, _} -> + BareLJID = jid:remove_resource(LJID), + try maps:get(BareLJID, StateData#state.affiliations) + catch _:{badkey, _} -> + DomainLJID = setelement(1, LJID, <<"">>), + try maps:get(DomainLJID, StateData#state.affiliations) + catch _:{badkey, _} -> + DomainBareLJID = jid:remove_resource(DomainLJID), + try maps:get(DomainBareLJID, StateData#state.affiliations) + catch _:{badkey, _} -> none + end + end + end + end. + +-spec get_affiliations(state()) -> affiliations(). +get_affiliations(#state{config = #config{persistent = false}} = StateData) -> + get_affiliations_callback(StateData); +get_affiliations(StateData) -> + Room = StateData#state.room, + Host = StateData#state.host, + ServerHost = StateData#state.server_host, + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case Mod:get_affiliations(ServerHost, Room, Host) of + {error, _} -> + get_affiliations_callback(StateData); + {ok, Affiliations} -> + Affiliations + end. + +-spec get_affiliations_callback(state()) -> affiliations(). +get_affiliations_callback(StateData) -> + StateData#state.affiliations. + +-spec get_service_affiliation(jid(), state()) -> owner | none. get_service_affiliation(JID, StateData) -> {_AccessRoute, _AccessCreate, AccessAdmin, - _AccessPersistent} = + _AccessPersistent, _AccessMam} = StateData#state.access, case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) @@ -1496,61 +1607,60 @@ get_service_affiliation(JID, StateData) -> _ -> 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); + maps: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 + case maps: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, + {Users, Nicks} = + case Role of + none -> + lists:foldl( + fun (J, {Us, Ns}) -> + NewNs = try maps:get(J, Us) of + #user{nick = Nick} -> + maps:remove(Nick, Ns) + catch _:{badkey, _} -> + Ns + end, + {maps:remove(J, Us), NewNs} + end, + {StateData#state.users, StateData#state.nicks}, LJIDs); + _ -> + {lists:foldl( + fun (J, Us) -> + User = maps:get(J, Us), + if User#user.last_presence == undefined -> + Us; + true -> + maps:put(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 + try maps:get(LJID, StateData#state.users) of + #user{role = Role} -> Role + catch _:{badkey, _} -> none end. +-spec get_default_role(affiliation(), state()) -> role(). get_default_role(Affiliation, StateData) -> case Affiliation of owner -> moderator; @@ -1569,12 +1679,15 @@ get_default_role(Affiliation, StateData) -> 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), @@ -1582,18 +1695,29 @@ get_max_users(StateData) -> 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). + mod_muc_opt:max_users(StateData#state.server_host). +-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). + mod_muc_opt:max_users_admin_threshold(StateData#state.server_host). + +-spec room_queue_new(binary(), ejabberd_shaper:shaper(), _) -> p1_queue:queue({message | presence, jid()}) | undefined. +room_queue_new(ServerHost, Shaper, QueueType) -> + HaveRoomShaper = Shaper /= none, + HaveMessageShaper = mod_muc_opt:user_message_shaper(ServerHost) /= none, + HavePresenceShaper = mod_muc_opt:user_presence_shaper(ServerHost) /= none, + HaveMinMessageInterval = mod_muc_opt:min_message_interval(ServerHost) /= 0, + HaveMinPresenceInterval = mod_muc_opt:min_presence_interval(ServerHost) /= 0, + if HaveRoomShaper or HaveMessageShaper or HavePresenceShaper + or HaveMinMessageInterval or HaveMinPresenceInterval -> + p1_queue:new(QueueType); + true -> + undefined + end. +-spec get_user_activity(jid(), state()) -> #activity{}. get_user_activity(JID, StateData) -> case treap:lookup(jid:tolower(JID), StateData#state.activity) @@ -1601,34 +1725,21 @@ get_user_activity(JID, StateData) -> {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)), + ejabberd_shaper:new(mod_muc_opt:user_message_shaper(StateData#state.server_host)), 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)), + ejabberd_shaper:new(mod_muc_opt:user_presence_shaper(StateData#state.server_host)), #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), + trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 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), + trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000), Key = jid:tolower(JID), - Now = p1_time_compat:system_time(micro_seconds), + Now = erlang:system_time(microsecond), Activity1 = clean_treap(StateData#state.activity, {1, -Now}), Activity = case treap:lookup(Key, Activity1) of @@ -1650,10 +1761,10 @@ store_user_activity(JID, UserActivity, StateData) -> of true -> {_, MessageShaperInterval} = - shaper:update(UserActivity#activity.message_shaper, + ejabberd_shaper:update(UserActivity#activity.message_shaper, 100000), {_, PresenceShaperInterval} = - shaper:update(UserActivity#activity.presence_shaper, + ejabberd_shaper:update(UserActivity#activity.presence_shaper, 100000), Delay = lists:max([MessageShaperInterval, PresenceShaperInterval, @@ -1673,8 +1784,9 @@ store_user_activity(JID, UserActivity, StateData) -> Activity)} end end, - StateData1. + reset_hibernate_timer(StateData1). +-spec clean_treap(treap:treap(), integer() | {1, integer()}) -> treap:treap(). clean_treap(Treap, CleanPriority) -> case treap:is_empty(Treap) of true -> Treap; @@ -1686,14 +1798,15 @@ clean_treap(Treap, CleanPriority) -> end end. +-spec prepare_room_queue(state()) -> state(). prepare_room_queue(StateData) -> - case queue:out(StateData#state.room_queue) of + case p1_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), + ejabberd_shaper:update(StateData#state.room_shaper, Size), erlang:send_after(RoomShaperInterval, self(), process_room_queue), StateData#state{room_shaper = RoomShaper}; @@ -1702,177 +1815,168 @@ prepare_room_queue(StateData) -> {_Nick, Packet} = Activity#activity.presence, Size = element_size(Packet), {RoomShaper, RoomShaperInterval} = - shaper:update(StateData#state.room_shaper, Size), + ejabberd_shaper:update(StateData#state.room_shaper, Size), erlang:send_after(RoomShaperInterval, self(), process_room_queue), StateData#state{room_shaper = RoomShaper}; {empty, _} -> StateData end. -update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, - is_subscriber = IsSubscriber} = User, StateData) -> +-spec update_online_user(jid(), #user{}, state()) -> state(). +update_online_user(JID, #user{nick = Nick} = User, StateData) -> LJID = jid:tolower(JID), - Nicks1 = case (?DICT):find(LJID, StateData#state.users) of - {ok, #user{nick = OldNick}} -> + add_to_log(join, Nick, StateData), + Nicks1 = try maps:get(LJID, StateData#state.users) of + #user{nick = OldNick} -> case lists:delete( - LJID, ?DICT:fetch(OldNick, StateData#state.nicks)) of + LJID, maps:get(OldNick, StateData#state.nicks)) of [] -> - ?DICT:erase(OldNick, StateData#state.nicks); + maps:remove(OldNick, StateData#state.nicks); LJIDs -> - ?DICT:store(OldNick, LJIDs, StateData#state.nicks) - end; - error -> + maps:put(OldNick, LJIDs, StateData#state.nicks) + end + catch _:{badkey, _} -> 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), + Nicks = maps:update_with(Nick, + fun (LJIDs) -> [LJID|LJIDs -- [LJID]] end, + [LJID], Nicks1), + Users = maps:update_with(LJID, + fun(U) -> + U#user{nick = Nick} + 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 -> + case {maps:get(LJID, StateData#state.users, error), + maps:get(LJID, NewStateData#state.users, error)} of + {#user{nick = Old}, #user{nick = New}} when Old /= New -> send_nick_changing(JID, Old, NewStateData, true, true); _ -> ok end, NewStateData. -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 -> +-spec set_subscriber(jid(), binary(), [binary()], state()) -> state(). +set_subscriber(JID, Nick, Nodes, + #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> + BareJID = jid:remove_resource(JID), + LBareJID = jid:tolower(BareJID), + Subscribers = maps:put(LBareJID, + #subscriber{jid = BareJID, + nick = Nick, + nodes = Nodes}, + StateData#state.subscribers), + Nicks = maps:put(Nick, [LBareJID], StateData#state.subscriber_nicks), + NewStateData = StateData#state{subscribers = Subscribers, + subscriber_nicks = Nicks}, + store_room(NewStateData, [{add_subscription, BareJID, Nick, Nodes}]), + case not maps:is_key(LBareJID, StateData#state.subscribers) of + true -> + send_subscriptions_change_notifications(BareJID, Nick, subscribe, NewStateData), + ejabberd_hooks:run(muc_subscribed, ServerHost, [ServerHost, Room, Host, BareJID]); + _ -> ok end, - StateData1. + NewStateData. + +-spec add_online_user(jid(), binary(), role(), state()) -> state(). +add_online_user(JID, Nick, Role, StateData) -> + tab_add_online_user(JID, StateData), + User = #user{jid = JID, nick = Nick, role = Role}, + reset_hibernate_timer(update_online_user(JID, User, StateData)). -remove_online_user(JID, StateData, IsSubscriber) -> - remove_online_user(JID, StateData, IsSubscriber, <<"">>). +-spec remove_online_user(jid(), state()) -> state(). +remove_online_user(JID, StateData) -> + remove_online_user(JID, StateData, <<"">>). -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) -> +-spec remove_online_user(jid(), state(), binary()) -> state(). +remove_online_user(JID, StateData, Reason) -> LJID = jid:tolower(JID), - {ok, #user{nick = Nick}} = (?DICT):find(LJID, - StateData#state.users), + #user{nick = Nick} = maps:get(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 + Users = maps:remove(LJID, StateData#state.users), + Nicks = try maps:get(Nick, StateData#state.nicks) of + [LJID] -> + maps:remove(Nick, StateData#state.nicks); + U -> + maps:put(Nick, U -- [LJID], StateData#state.nicks) + catch _:{badkey, _} -> + StateData#state.nicks end, - StateData#state{users = Users, nicks = Nicks}. - -filter_presence(#xmlel{name = <<"presence">>, - attrs = Attrs, children = Els}) -> - FEls = lists:filter(fun (El) -> - case El of - {xmlcdata, _} -> false; - #xmlel{attrs = Attrs1} -> - XMLNS = fxml:get_attr_s(<<"xmlns">>, - Attrs1), - NS_MUC = ?NS_MUC, - Size = byte_size(NS_MUC), - case XMLNS of - <<NS_MUC:Size/binary, _/binary>> -> - false; - _ -> - true - end - end - end, - Els), - #xmlel{name = <<"presence">>, attrs = Attrs, - children = FEls}. - -strip_status(#xmlel{name = <<"presence">>, - attrs = Attrs, children = Els}) -> - FEls = lists:filter(fun (#xmlel{name = <<"status">>}) -> - false; - (_) -> true - end, - Els), - #xmlel{name = <<"presence">>, attrs = Attrs, - children = FEls}. - + reset_hibernate_timer(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), + Users = maps:update_with(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), + Users = maps:update_with(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 -> false - end. + Users = case maps:get(Nick, StateData#state.nicks, []) of + [] -> maps:get(Nick, StateData#state.subscriber_nicks, []); + Us -> Us + end, + [jid:make(LJID) || LJID <- Users]. %% 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 + try maps:get(Nick, StateData#state.nicks) of + [User] -> jid:make(User); + [FirstUser | Users] -> + #user{last_presence = FirstPresence} = + maps:get(FirstUser, StateData#state.users), + {LJID, _} = lists:foldl( + fun(Compare, {HighestUser, HighestPresence}) -> + #user{last_presence = P1} = + maps:get(Compare, StateData#state.users), + case higher_presence(P1, HighestPresence) of + true -> {Compare, P1}; + false -> {HighestUser, HighestPresence} + end + end, {FirstUser, FirstPresence}, Users), + jid:make(LJID) + catch _:{badkey, _} -> + 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), @@ -1880,63 +1984,61 @@ higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined -> higher_presence(Pres1, Pres2) -> Pres1 > Pres2. -get_priority_from_presence(PresencePacket) -> - case fxml:get_subtag(PresencePacket, <<"priority">>) of - false -> 0; - SubEl -> - case catch - jlib:binary_to_integer(fxml:get_tag_cdata(SubEl)) - of - P when is_integer(P) -> P; - _ -> 0 - end +-spec get_priority_from_presence(presence()) -> integer(). +get_priority_from_presence(#presence{priority = Prio}) -> + case Prio of + undefined -> 0; + _ -> Prio end. -find_nick_by_jid(Jid, StateData) -> - [{_, #user{nick = Nick}}] = lists:filter(fun ({_, - #user{jid = FJid}}) -> - FJid == Jid - end, - (?DICT):to_list(StateData#state.users)), +-spec find_nick_by_jid(jid(), state()) -> binary(). +find_nick_by_jid(JID, StateData) -> + LJID = jid:tolower(JID), + #user{nick = Nick} = maps:get(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), + #user{nick = OldNick} = maps:get(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 = case find_jid_by_nick(Nick, StateData) of + false -> + try maps:get(Nick, StateData#state.subscriber_nicks) of + [J] -> J + catch _:{badkey, _} -> false + end; + J -> J + end, (UserOfNick /= false andalso jid:remove_resource(jid:tolower(UserOfNick)) /= jid:remove_resource(jid:tolower(User))). -add_new_user(From, Nick, - #xmlel{name = Name, attrs = Attrs, children = Els} = Packet, - StateData) -> - Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), - UserRoomJID = jid:replace_resource(StateData#state.jid, Nick), +-spec add_new_user(jid(), binary(), presence(), state()) -> state(); + (jid(), binary(), iq(), state()) -> {error, stanza_error()} | + {ignore, state()} | + {result, muc_subscribe(), state()}. +add_new_user(From, Nick, Packet, StateData) -> + Lang = xmpp:get_lang(Packet), 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), + NUsers = maps:size(StateData#state.users), Affiliation = get_affiliation(From, StateData), ServiceAffiliation = get_service_affiliation(From, StateData), - NConferences = tab_count_user(From), + NConferences = tab_count_user(From, StateData), 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), + mod_muc_opt:max_user_conferences(StateData#state.server_host), Collision = nick_collision(From, Nick, StateData), - IsSubscribeRequest = Name /= <<"presence">>, + IsSubscribeRequest = not is_record(Packet, presence), case {(ServiceAffiliation == owner orelse ((Affiliation == admin orelse Affiliation == owner) andalso NUsers < MaxAdminUsers) @@ -1948,73 +2050,73 @@ add_new_user(From, Nick, get_default_role(Affiliation, StateData)} of {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> - Txt = <<"Too many users in this conference">>, - Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt), - ErrPacket = jlib:make_error_reply(Packet, Err), + Txt = ?T("Too many users in this conference"), + Err = xmpp:err_resource_constraint(Txt, Lang), if not IsSubscribeRequest -> - ejabberd_router:route(UserRoomJID, From, ErrPacket), + ejabberd_router:route_error(Packet, Err), StateData; true -> - {error, Err, StateData} + {error, Err} end; {false, _, _, _} when NConferences >= MaxConferences -> - Txt = <<"You have joined too many conferences">>, - Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt), - ErrPacket = jlib:make_error_reply(Packet, Err), + Txt = ?T("You have joined too many conferences"), + Err = xmpp:err_resource_constraint(Txt, Lang), if not IsSubscribeRequest -> - ejabberd_router:route(UserRoomJID, From, ErrPacket), + ejabberd_router:route_error(Packet, Err), StateData; true -> - {error, Err, StateData} + {error, Err} end; {false, _, _, _} -> - Err = ?ERR_SERVICE_UNAVAILABLE, - ErrPacket = jlib:make_error_reply(Packet, Err), + Err = xmpp:err_service_unavailable(), if not IsSubscribeRequest -> - ejabberd_router:route(UserRoomJID, From, ErrPacket), + ejabberd_router:route_error(Packet, Err), StateData; true -> - {error, Err, StateData} + {error, Err} end; {_, _, _, none} -> Err = case Affiliation of outcast -> - ErrText = <<"You have been banned from this room">>, - ?ERRT_FORBIDDEN(Lang, ErrText); + ErrText = ?T("You have been banned from this room"), + xmpp:err_forbidden(ErrText, Lang); _ -> - ErrText = <<"Membership is required to enter this room">>, - ?ERRT_REGISTRATION_REQUIRED(Lang, ErrText) + ErrText = ?T("Membership is required to enter this room"), + xmpp:err_registration_required(ErrText, Lang) end, - ErrPacket = jlib:make_error_reply(Packet, Err), if not IsSubscribeRequest -> - ejabberd_router:route(UserRoomJID, From, ErrPacket), + ejabberd_router:route_error(Packet, Err), StateData; true -> - {error, Err, StateData} + {error, Err} end; {_, true, _, _} -> - ErrText = <<"That nickname is already in use by another occupant">>, - Err = ?ERRT_CONFLICT(Lang, ErrText), - ErrPacket = jlib:make_error_reply(Packet, Err), + ErrText = ?T("That nickname is already in use by another occupant"), + Err = xmpp:err_conflict(ErrText, Lang), if not IsSubscribeRequest -> - ejabberd_router:route(UserRoomJID, From, ErrPacket), + ejabberd_router:route_error(Packet, Err), StateData; true -> - {error, Err, StateData} + {error, Err} end; {_, _, false, _} -> - ErrText = <<"That nickname is registered by another person">>, - Err = ?ERRT_CONFLICT(Lang, ErrText), - ErrPacket = jlib:make_error_reply(Packet, Err), + Err = case Nick of + <<>> -> + xmpp:err_jid_malformed(?T("Nickname can't be empty"), + Lang); + _ -> + xmpp:err_conflict(?T("That nickname is registered" + " by another person"), Lang) + end, if not IsSubscribeRequest -> - ejabberd_router:route(UserRoomJID, From, ErrPacket), + ejabberd_router:route_error(Packet, Err), StateData; true -> - {error, Err, StateData} + {error, Err} end; {_, _, _, Role} -> case check_password(ServiceAffiliation, Affiliation, - Els, From, StateData) + Packet, From, StateData) of true -> Nodes = get_subscription_nodes(Packet), @@ -2023,57 +2125,49 @@ add_new_user(From, Nick, 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, Els, NewState), - case send_history(From, Shift, NewState) of - true -> ok; - _ -> send_subject(From, StateData) - end, + StateData)), + send_initial_presences_and_messages( + From, Nick, Packet, NewState, StateData), NewState; true -> - add_online_user(From, Nick, none, - IsSubscribeRequest, - Nodes, StateData) + set_subscriber(From, Nick, 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{just_created = erlang:system_time(microsecond)}; + _ -> + Robots = maps:remove(From, StateData#state.robots), NewStateData#state{robots = Robots} end, if not IsSubscribeRequest -> ResultState; - true -> {result, subscription_nodes_to_events(Nodes), ResultState} + true -> {result, subscribe_result(Packet), ResultState} end; - nopass -> - ErrText = <<"A password is required to enter this room">>, - Err = ?ERRT_NOT_AUTHORIZED(Lang, ErrText), - ErrPacket = jlib:make_error_reply(Packet, Err), + need_password -> + ErrText = ?T("A password is required to enter this room"), + Err = xmpp:err_not_authorized(ErrText, Lang), if not IsSubscribeRequest -> - ejabberd_router:route(UserRoomJID, From, ErrPacket), + ejabberd_router:route_error(Packet, Err), StateData; true -> - {error, Err, StateData} + {error, Err} end; captcha_required -> - SID = fxml:get_attr_s(<<"id">>, Attrs), + 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, CaptchaEls} -> - MsgPkt = #xmlel{name = <<"message">>, - attrs = [{<<"id">>, ID}], - children = CaptchaEls}, - Robots = (?DICT):store(From, {Nick, Packet}, - StateData#state.robots), - ejabberd_router:route(RoomJID, From, MsgPkt), + {ok, ID, Body, CaptchaEls} -> + MsgPkt = #message{from = RoomJID, + to = From, + id = ID, body = Body, + sub_els = CaptchaEls}, + Robots = maps:put(From, {Nick, Packet}, + StateData#state.robots), + ejabberd_router:route(MsgPkt), NewState = StateData#state{robots = Robots}, if not IsSubscribeRequest -> NewState; @@ -2081,52 +2175,52 @@ add_new_user(From, Nick, {ignore, NewState} end; {error, limit} -> - ErrText = <<"Too many CAPTCHA requests">>, - Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText), - ErrPacket = jlib:make_error_reply(Packet, Err), + ErrText = ?T("Too many CAPTCHA requests"), + Err = xmpp:err_resource_constraint(ErrText, Lang), if not IsSubscribeRequest -> - ejabberd_router:route(UserRoomJID, From, ErrPacket), + ejabberd_router:route_error(Packet, Err), StateData; true -> - {error, Err, StateData} + {error, Err} end; _ -> - ErrText = <<"Unable to generate a CAPTCHA">>, - Err = ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText), - ErrPacket = jlib:make_error_reply(Packet, Err), + ErrText = ?T("Unable to generate a CAPTCHA"), + Err = xmpp:err_internal_server_error(ErrText, Lang), if not IsSubscribeRequest -> - ejabberd_router:route(UserRoomJID, From, ErrPacket), + ejabberd_router:route_error(Packet, Err), StateData; true -> - {error, Err, StateData} + {error, Err} end end; _ -> - ErrText = <<"Incorrect password">>, - Err = ?ERRT_NOT_AUTHORIZED(Lang, ErrText), - ErrPacket = jlib:make_error_reply(Packet, Err), + ErrText = ?T("Incorrect password"), + Err = xmpp:err_not_authorized(ErrText, Lang), if not IsSubscribeRequest -> - ejabberd_router:route(UserRoomJID, From, ErrPacket), + ejabberd_router:route_error(Packet, Err), StateData; true -> - {error, Err, StateData} + {error, Err} end end end. -check_password(owner, _Affiliation, _Els, _From, +-spec check_password(affiliation(), affiliation(), + presence() | iq(), jid(), state()) -> + boolean() | need_password | captcha_required. +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, Els, +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(Els), + Pass = extract_password(Packet), case Pass of - false -> nopass; + false -> need_password; _ -> case (StateData#state.config)#config.password of Pass -> true; @@ -2135,14 +2229,15 @@ check_password(_ServiceAffiliation, Affiliation, Els, 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; - _ -> + case maps:get(From, StateData#state.robots, error) of + passed -> true; + _ -> WList = (StateData#state.config)#config.captcha_whitelist, #jid{luser = U, lserver = S, lresource = R} = From, @@ -2163,186 +2258,149 @@ check_captcha(Affiliation, From, StateData) -> _ -> true end. -extract_password([]) -> false; -extract_password([#xmlel{attrs = Attrs} = El | Els]) -> - case fxml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_MUC -> - case fxml:get_subtag(El, <<"password">>) of - false -> false; - SubEl -> fxml:get_tag_cdata(SubEl) - end; - _ -> extract_password(Els) +-spec extract_password(presence() | iq()) -> binary() | false. +extract_password(#presence{} = Pres) -> + case xmpp:get_subtag(Pres, #muc{}) of + #muc{password = Password} when is_binary(Password) -> + Password; + _ -> + false end; -extract_password([_ | Els]) -> extract_password(Els). - -count_stanza_shift(Nick, Els, StateData) -> - HL = lqueue_to_list(StateData#state.history), - Since = extract_history(Els, <<"since">>), - Shift0 = case Since of - false -> 0; - _ -> - Sin = calendar:datetime_to_gregorian_seconds(Since), - count_seconds_shift(Sin, HL) - end, - Seconds = extract_history(Els, <<"seconds">>), - Shift1 = case Seconds of - false -> 0; - _ -> - Sec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) - - Seconds, - count_seconds_shift(Sec, HL) - end, - MaxStanzas = extract_history(Els, <<"maxstanzas">>), - Shift2 = case MaxStanzas of - false -> 0; - _ -> count_maxstanzas_shift(MaxStanzas, HL) - end, - MaxChars = extract_history(Els, <<"maxchars">>), - Shift3 = case MaxChars of - false -> 0; - _ -> count_maxchars_shift(Nick, MaxChars, HL) - end, - lists:max([Shift0, Shift1, Shift2, Shift3]). - -count_seconds_shift(Seconds, HistoryList) -> - lists:sum(lists:map(fun ({_Nick, _Packet, _HaveSubject, - TimeStamp, _Size}) -> - T = - calendar:datetime_to_gregorian_seconds(TimeStamp), - if T < Seconds -> 1; - true -> 0 - end - end, - HistoryList)). - -count_maxstanzas_shift(MaxStanzas, HistoryList) -> - S = length(HistoryList) - MaxStanzas, - if S =< 0 -> 0; - true -> S +extract_password(#iq{} = IQ) -> + case xmpp:get_subtag(IQ, #muc_subscribe{}) of + #muc_subscribe{password = Password} when Password /= <<"">> -> + Password; + _ -> + false end. -count_maxchars_shift(Nick, MaxSize, HistoryList) -> - NLen = byte_size(Nick) + 1, - Sizes = lists:map(fun ({_Nick, _Packet, _HaveSubject, - _TimeStamp, Size}) -> - Size + NLen - end, - HistoryList), - calc_shift(MaxSize, Sizes). - -calc_shift(MaxSize, Sizes) -> - Total = lists:sum(Sizes), - calc_shift(MaxSize, Total, 0, Sizes). - -calc_shift(_MaxSize, _Size, Shift, []) -> Shift; -calc_shift(MaxSize, Size, Shift, [S | TSizes]) -> - if MaxSize >= Size -> Shift; - true -> calc_shift(MaxSize, Size - S, Shift + 1, TSizes) +-spec get_history(binary(), stanza(), state()) -> [lqueue_elem()]. +get_history(Nick, Packet, #state{history = History}) -> + case xmpp:get_subtag(Packet, #muc{}) of + #muc{history = #muc_history{} = MUCHistory} -> + Now = erlang:timestamp(), + Q = History#lqueue.queue, + filter_history(Q, Now, Nick, MUCHistory); + _ -> + p1_queue:to_list(History#lqueue.queue) end. -extract_history([], _Type) -> false; -extract_history([#xmlel{attrs = Attrs} = El | Els], - Type) -> - case fxml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_MUC -> - AttrVal = fxml:get_path_s(El, - [{elem, <<"history">>}, {attr, Type}]), - case Type of - <<"since">> -> - case jlib:datetime_string_to_timestamp(AttrVal) of - undefined -> false; - TS -> calendar:now_to_universal_time(TS) - end; - _ -> - case catch jlib:binary_to_integer(AttrVal) of - IntVal when is_integer(IntVal) and (IntVal >= 0) -> - IntVal; - _ -> false - end - end; - _ -> extract_history(Els, Type) - end; -extract_history([_ | Els], Type) -> - extract_history(Els, Type). +-spec filter_history(p1_queue:queue(lqueue_elem()), erlang:timestamp(), + binary(), muc_history()) -> [lqueue_elem()]. +filter_history(Queue, Now, Nick, + #muc_history{since = Since, + seconds = Seconds, + maxstanzas = MaxStanzas, + maxchars = MaxChars}) -> + {History, _, _} = + lists:foldr( + fun({_, _, _, TimeStamp, Size} = Elem, + {Elems, NumStanzas, NumChars} = Acc) -> + NowDiff = timer:now_diff(Now, TimeStamp) div 1000000, + Chars = Size + byte_size(Nick) + 1, + if (NumStanzas < MaxStanzas) andalso + (TimeStamp > Since) andalso + (NowDiff =< Seconds) andalso + (NumChars + Chars =< MaxChars) -> + {[Elem|Elems], NumStanzas + 1, NumChars + Chars}; + true -> + Acc + end + end, {[], 0, 0}, p1_queue:to_list(Queue)), + History. +-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. + MaxUsersPresence = mod_muc_opt:max_users_presence(StateData#state.server_host), + maps: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). -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_presences_and_messages( + jid(), binary(), presence(), state(), state()) -> ok. +send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) -> + advertise_entity_capabilities(From, NewState), + send_existing_presences(From, NewState), + send_self_presence(From, NewState, OldState), + History = get_history(Nick, Presence, NewState), + send_history(From, History, NewState), + send_subject(From, OldState). + +-spec advertise_entity_capabilities(jid(), state()) -> ok. +advertise_entity_capabilities(JID, State) -> + AvatarHash = (State#state.config)#config.vcard_xupdate, + DiscoInfo = make_disco_info(JID, State), + Extras = iq_disco_info_extras(<<"en">>, State, true), + DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]}, + DiscoHash = mod_caps:compute_disco_hash(DiscoInfo1, sha), + Els1 = [#caps{hash = <<"sha-1">>, + node = ejabberd_config:get_uri(), + version = DiscoHash}], + Els2 = if is_binary(AvatarHash) -> + [#vcard_xupdate{hash = AvatarHash}|Els1]; + true -> + Els1 + end, + ejabberd_router:route(#presence{from = State#state.jid, to = JID, + id = p1_rand:get_string(), + sub_els = Els2}). -send_initial_presence(NJID, StateData, OldStateData) -> - send_new_presence1(NJID, <<"">>, true, StateData, OldStateData). +-spec send_self_presence(jid(), state(), state()) -> ok. +send_self_presence(NJID, StateData, OldStateData) -> + send_new_presence(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); + maps: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 + case maps:is_key(LJID, StateData#state.users) of true -> [LJID]; _ -> [] end end, lists:foreach(fun (J) -> - send_new_presence1(J, Reason, false, StateData, - OldStateData) + send_new_presence(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). -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(), 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), +is_ra_changed(JID, _IsInitialPresence = false, NewStateData, OldStateData) -> + NewRole = get_role(JID, NewStateData), NewAff = get_affiliation(JID, NewStateData), - OldRole = get_role(LJID, OldStateData), + OldRole = get_role(JID, OldStateData), OldAff = get_affiliation(JID, OldStateData), if (NewRole == none) and (NewAff == OldAff) -> %% A user is leaving the room; @@ -2351,119 +2409,89 @@ is_ra_changed(LJID, _IsInitialPresence = false, NewStateData, OldStateData) -> (NewRole /= OldRole) or (NewAff /= OldAff) end. -send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> +-spec send_new_presence(jid(), binary(), boolean(), state(), state()) -> ok. +send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> LNJID = jid:tolower(NJID), - #user{nick = Nick} = (?DICT):fetch(LNJID, StateData#state.users), + #user{nick = Nick} = maps:get(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), + #user{jid = RealJID, role = Role0, + last_presence = Presence0} = UserInfo = + maps:get(jid:tolower(LJID), StateData#state.users), {Role1, Presence1} = - case presence_broadcast_allowed(NJID, StateData) of + case (presence_broadcast_allowed(NJID, StateData) orelse + presence_broadcast_allowed(NJID, OldStateData)) of true -> {Role0, Presence0}; - false -> - {none, - #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}], - children = []} - } + false -> {none, #presence{type = unavailable}} end, Affiliation = get_affiliation(LJID, StateData), - SAffiliation = affiliation_to_list(Affiliation), - UserList = - case not (presence_broadcast_allowed(NJID, StateData) orelse - presence_broadcast_allowed(NJID, OldStateData)) of + UserMap = + case is_room_overcrowded(StateData) orelse + (not (presence_broadcast_allowed(NJID, StateData) orelse + presence_broadcast_allowed(NJID, OldStateData))) of true -> - [{LNJID, UserInfo}]; + #{LNJID => UserInfo}; false -> - (?DICT):to_list(StateData#state.users) + get_users_and_subscribers(StateData) end, - lists:foreach( - fun({LUJID, Info}) -> - {Role, Presence} = if LNJID == LUJID -> {Role0, Presence0}; + maps:fold( + fun(LUJID, Info, _) -> + IsSelfPresence = LNJID == LUJID, + {Role, Presence} = if IsSelfPresence -> {Role0, Presence0}; true -> {Role1, Presence1} end, - SRole = role_to_list(Role), - ItemAttrs = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - jid:to_string(RealJID)}, - {<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}]; - _ -> - [{<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}] - end, - ItemEls = case Reason of - <<"">> -> []; - _ -> - [#xmlel{name = <<"reason">>, - attrs = [], - children = - [{xmlcdata, Reason}]}] - end, - StatusEls = status_els(IsInitialPresence, NJID, Info, - StateData), - Pres = if Presence == undefined -> #xmlel{name = <<"presence">>}; + Item0 = #muc_item{affiliation = Affiliation, + role = Role}, + Item1 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false orelse IsSelfPresence of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Item = Item1#muc_item{reason = Reason}, + StatusCodes = status_codes(IsInitialPresence, IsSelfPresence, + StateData), + Pres = if Presence == undefined -> #presence{}; true -> Presence end, - Packet = fxml:append_subtags(Pres, - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"item">>, - attrs - = - ItemAttrs, - children - = - ItemEls} - | StatusEls]}]), + 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 = fxml:get_tag_attr_s(<<"type">>, Packet), - IsSubscriber = Info#user.is_subscriber, + Type = xmpp:get_type(Packet), + IsSubscriber = is_subscriber(Info#user.jid, StateData), IsOccupant = Info#user.last_presence /= undefined, if (IsSubscriber and not IsOccupant) and - (IsInitialPresence or (Type == <<"unavailable">>)) -> + (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). + end, ok, UserMap). +-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}) -> + #user{jid = RealToJID, role = Role} = maps:get(LToJID, StateData#state.users), + maps:fold( + 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), + maps:get(jid:tolower(LJID), StateData#state.users), PresenceBroadcast = lists:member( FromRole, (StateData#state.config)#config.presence_broadcast), @@ -2472,76 +2500,47 @@ send_existing_presences1(ToJID, StateData) -> {_, false} -> ok; _ -> FromAffiliation = get_affiliation(LJID, StateData), - ItemAttrs = case Role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - jid:to_string(FromJID)}, - {<<"affiliation">>, - affiliation_to_list(FromAffiliation)}, - {<<"role">>, - role_to_list(FromRole)}]; - _ -> - [{<<"affiliation">>, - affiliation_to_list(FromAffiliation)}, - {<<"role">>, - role_to_list(FromRole)}] - end, - Packet = fxml:append_subtags( - Presence, - [#xmlel{name = - <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name - = - <<"item">>, - attrs - = - ItemAttrs, - children - = - []}]}]), + 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)). + end, ok, 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, + #user{nick = OldNick} = maps:get(LJID, State#state.users), + Users = maps:update_with(LJID, + fun (#user{} = User) -> User#user{nick = Nick} end, + State#state.users), + OldNickUsers = maps:get(OldNick, State#state.nicks), + NewNickUsers = maps:get(Nick, State#state.nicks, []), Nicks = case OldNickUsers of [LJID] -> - (?DICT):store(Nick, [LJID | NewNickUsers -- [LJID]], - (?DICT):erase(OldNick, State#state.nicks)); + maps:put(Nick, [LJID | NewNickUsers -- [LJID]], + maps:remove(OldNick, State#state.nicks)); [_ | _] -> - (?DICT):store(Nick, [LJID | NewNickUsers -- [LJID]], - (?DICT):store(OldNick, OldNickUsers -- [LJID], - State#state.nicks)) + maps:put(Nick, [LJID | NewNickUsers -- [LJID]], + maps:put(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, + #user{nick = OldNick} = maps:get(LJID, StateData#state.users), + OldNickUsers = maps:get(OldNick, StateData#state.nicks), + NewNickUsers = maps:get(Nick, StateData#state.nicks, []), SendOldUnavailable = length(OldNickUsers) == 1, SendNewAvailable = SendOldUnavailable orelse NewNickUsers == [], NewStateData = set_nick(JID, Nick, StateData), @@ -2554,645 +2553,508 @@ change_nick(JID, Nick, StateData) -> 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), + #user{jid = RealJID, nick = Nick, role = Role, + last_presence = Presence} = + maps:get(jid:tolower(JID), StateData#state.users), Affiliation = get_affiliation(JID, StateData), - SAffiliation = affiliation_to_list(Affiliation), - SRole = role_to_list(Role), - lists:foreach(fun ({_LJID, Info}) when Presence /= undefined -> - ItemAttrs1 = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - jid:to_string(RealJID)}, - {<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}, - {<<"nick">>, Nick}]; - _ -> - [{<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}, - {<<"nick">>, Nick}] - end, - ItemAttrs2 = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - jid:to_string(RealJID)}, - {<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}]; - _ -> - [{<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}] - end, - Status110 = case JID == Info#user.jid of - true -> - [#xmlel{name = <<"status">>, - attrs = [{<<"code">>, <<"110">>}] - }]; - false -> - [] - end, - Packet1 = #xmlel{name = <<"presence">>, - attrs = - [{<<"type">>, - <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"item">>, - attrs = - ItemAttrs1, - children = - []}, - #xmlel{name = - <<"status">>, - attrs = - [{<<"code">>, - <<"303">>}], - children = - []}|Status110]}]}, - Packet2 = fxml:append_subtags(Presence, - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name - = - <<"item">>, - attrs - = - ItemAttrs2, - children - = - []}|Status110]}]), - if SendOldUnavailable -> - send_wrapped(jid:replace_resource(StateData#state.jid, - OldNick), - Info#user.jid, Packet1, - ?NS_MUCSUB_NODES_PRESENCE, - StateData); - true -> ok + maps:fold( + fun(LJID, Info, _) when Presence /= undefined -> + IsSelfPresence = LJID == jid:tolower(JID), + Item0 = #muc_item{affiliation = Affiliation, role = Role}, + Item = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false orelse IsSelfPresence of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Status110 = case IsSelfPresence of + true -> [110]; + false -> [] 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)). - + Packet1 = #presence{ + type = unavailable, + sub_els = [#muc_user{ + items = [Item#muc_item{nick = Nick}], + status_codes = [303|Status110]}]}, + Packet2 = xmpp:set_subtag(Presence, + #muc_user{items = [Item], + 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, ok, get_users_and_subscribers(StateData)). + +-spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok. maybe_send_affiliation(JID, Affiliation, StateData) -> LJID = jid:tolower(JID), + Users = get_users_and_subscribers(StateData), 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) + {LUser, LServer, <<"">>} -> + #{} /= maps:filter( + fun({U, S, _}, _) -> + U == LUser andalso + S == LServer + end, Users); + {_LUser, _LServer, _LResource} -> + maps:is_key(LJID, Users) end, case IsOccupant of true -> ok; % The new affiliation is published via presence. false -> - send_affiliation(LJID, Affiliation, StateData) + send_affiliation(JID, Affiliation, StateData) end. -send_affiliation(LJID, Affiliation, StateData) -> - ItemAttrs = [{<<"jid">>, jid:to_string(LJID)}, - {<<"affiliation">>, affiliation_to_list(Affiliation)}, - {<<"role">>, <<"none">>}], - Message = #xmlel{name = <<"message">>, - attrs = [{<<"id">>, randoms:get_string()}], - children = - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_MUC_USER}], - children = - [#xmlel{name = <<"item">>, - attrs = ItemAttrs}]}]}, +-spec send_affiliation(jid(), affiliation(), state()) -> ok. +send_affiliation(JID, Affiliation, StateData) -> + Item = #muc_item{jid = JID, + affiliation = Affiliation, + role = none}, + Message = #message{id = p1_rand:get_string(), + sub_els = [#muc_user{items = [Item]}]}, + Users = get_users_and_subscribers(StateData), Recipients = case (StateData#state.config)#config.anonymous of true -> - (?DICT):filter(fun(_, #user{role = moderator}) -> - true; - (_, _) -> - false - end, StateData#state.users); + maps:filter(fun(_, #user{role = moderator}) -> + true; + (_, _) -> + false + end, Users); false -> - StateData#state.users + Users end, - send_multiple(StateData#state.jid, - StateData#state.server_host, - Recipients, Message). + send_wrapped_multiple(StateData#state.jid, Recipients, Message, + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData). -status_els(IsInitialPresence, JID, #user{jid = JID}, StateData) -> - Status = case IsInitialPresence of - true -> - S1 = case StateData#state.just_created of - true -> - [#xmlel{name = <<"status">>, - attrs = [{<<"code">>, <<"201">>}], - children = []}]; - false -> [] - end, - S2 = case (StateData#state.config)#config.anonymous of - true -> S1; - false -> - [#xmlel{name = <<"status">>, - attrs = [{<<"code">>, <<"100">>}], - children = []} | S1] - end, - S3 = case (StateData#state.config)#config.logging of - true -> - [#xmlel{name = <<"status">>, - attrs = [{<<"code">>, <<"170">>}], - children = []} | S2]; - false -> S2 - end, - S3; - false -> [] - end, - [#xmlel{name = <<"status">>, - attrs = - [{<<"code">>, - <<"110">>}], - children = []} | Status]; -status_els(_IsInitialPresence, _JID, _Info, _StateData) -> []. +-spec status_codes(boolean(), boolean(), state()) -> [pos_integer()]. +status_codes(IsInitialPresence, _IsSelfPresence = true, StateData) -> + S0 = [110], + case IsInitialPresence of + true -> + S1 = case StateData#state.just_created of + true -> [201|S0]; + _ -> 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, _IsSelfPresence = false, _StateData) -> []. -lqueue_new(Max) -> - #lqueue{queue = queue:new(), len = 0, max = Max}. +-spec lqueue_new(non_neg_integer(), ram | file) -> lqueue(). +lqueue_new(Max, Type) -> + #lqueue{queue = p1_queue:new(Type), max = Max}. +-spec lqueue_in(lqueue_elem(), 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), +lqueue_in(Item, #lqueue{queue = Q1, max = Max}) -> + Len = p1_queue:len(Q1), + Q2 = p1_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} + #lqueue{queue = Q3, max = Max}; + true -> #lqueue{queue = Q2, max = Max} end. +-spec lqueue_cut(p1_queue:queue(lqueue_elem()), non_neg_integer()) -> p1_queue:queue(lqueue_elem()). lqueue_cut(Q, 0) -> Q; lqueue_cut(Q, N) -> - {_, Q1} = queue:out(Q), lqueue_cut(Q1, N - 1). - -lqueue_to_list(#lqueue{queue = Q1}) -> - queue:to_list(Q1). - + {_, Q1} = p1_queue:out(Q), + lqueue_cut(Q1, N - 1). +-spec add_message_to_history(binary(), jid(), message(), state()) -> state(). add_message_to_history(FromNick, FromJID, Packet, StateData) -> - HaveSubject = case fxml:get_subtag(Packet, <<"subject">>) - of - false -> false; - _ -> true - end, - TimeStamp = p1_time_compat:timestamp(), - AddrPacket = case (StateData#state.config)#config.anonymous of - true -> Packet; - false -> - Address = #xmlel{name = <<"address">>, - attrs = [{<<"type">>, <<"ofrom">>}, - {<<"jid">>, - jid:to_string(FromJID)}], - children = []}, - Addresses = #xmlel{name = <<"addresses">>, - attrs = [{<<"xmlns">>, ?NS_ADDRESS}], - children = [Address]}, - fxml:append_subtags(Packet, [Addresses]) - end, - TSPacket = jlib:add_delay_info(AddrPacket, StateData#state.jid, TimeStamp), - SPacket = - jlib:replace_from_to(jid:replace_resource(StateData#state.jid, - FromNick), - StateData#state.jid, TSPacket), - Size = element_size(SPacket), - Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, - calendar:now_to_universal_time(TimeStamp), Size}, - StateData#state.history), add_to_log(text, {FromNick, Packet}, StateData), - StateData#state{history = Q1}. - -send_history(JID, Shift, StateData) -> - lists:foldl(fun ({Nick, Packet, HaveSubject, _TimeStamp, - _Size}, - B) -> - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - JID, Packet), - B or HaveSubject - end, - false, - lists:nthtail(Shift, - lqueue_to_list(StateData#state.history))). + case check_subject(Packet) of + [] -> + TimeStamp = erlang: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 = misc: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, false, + TimeStamp, Size}, + StateData#state.history), + StateData#state{history = Q1}; + _ -> + StateData + end. -send_subject(_JID, #state{subject_author = <<"">>}) -> ok; +-spec send_history(jid(), [lqueue_elem()], state()) -> ok. +send_history(JID, History, StateData) -> + lists:foreach( + fun({Nick, Packet, _HaveSubject, _TimeStamp, _Size}) -> + ejabberd_router:route( + xmpp:set_from_to( + Packet, + jid:replace_resource(StateData#state.jid, Nick), + JID)) + end, History). + +-spec send_subject(jid(), state()) -> ok. send_subject(JID, #state{subject_author = Nick} = StateData) -> - Subject = StateData#state.subject, - Packet = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"subject">>, attrs = [], - children = [{xmlcdata, Subject}]}]}, - ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), JID, - Packet). - -check_subject(Packet) -> - case fxml:get_subtag(Packet, <<"subject">>) of - false -> false; - SubjEl -> fxml:get_tag_cdata(SubjEl) - end. + Subject = case StateData#state.subject of + [] -> [#text{}]; + [_|_] = S -> S + end, + Packet = #message{from = jid:replace_resource(StateData#state.jid, Nick), + to = JID, type = groupchat, subject = Subject}, + ejabberd_router:route(Packet). + +-spec check_subject(message()) -> [text()]. +check_subject(#message{subject = [_|_] = Subj, body = [], + thread = undefined}) -> + Subj; +check_subject(_) -> + []. -can_change_subject(Role, StateData) -> +-spec can_change_subject(role(), boolean(), state()) -> boolean(). +can_change_subject(Role, IsSubscriber, StateData) -> case (StateData#state.config)#config.allow_change_subj of - true -> Role == moderator orelse Role == participant; + true -> Role == moderator orelse Role == participant orelse IsSubscriber == true; _ -> Role == moderator end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Admin stuff -process_iq_admin(From, set, Lang, SubEl, StateData) -> - #xmlel{children = Items} = SubEl, +-spec process_iq_admin(jid(), iq(), #state{}) -> {error, stanza_error()} | + {result, undefined, #state{}} | + {result, muc_admin()}. +process_iq_admin(_From, #iq{lang = Lang, sub_els = [#muc_admin{items = []}]}, + _StateData) -> + Txt = ?T("No 'item' element found"), + {error, xmpp:err_bad_request(Txt, Lang)}; +process_iq_admin(_From, #iq{type = get, lang = Lang, + sub_els = [#muc_admin{items = [_, _|_]}]}, + _StateData) -> + ErrText = ?T("Too many <item/> elements"), + {error, xmpp:err_bad_request(ErrText, 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, get, Lang, SubEl, StateData) -> - case fxml:get_subtag(SubEl, <<"item">>) of - false -> - Txt = <<"No 'item' element found">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; - Item -> - FAffiliation = get_affiliation(From, StateData), - FRole = get_role(From, StateData), - case fxml:get_tag_attr(<<"role">>, Item) of - false -> - case fxml:get_tag_attr(<<"affiliation">>, Item) of - false -> - Txt = <<"No 'affiliation' attribute found">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; - {value, StrAffiliation} -> - case catch list_to_affiliation(StrAffiliation) of - {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; - SAffiliation -> - if (FAffiliation == owner) or - (FAffiliation == admin) or - ((FAffiliation == member) and not - (StateData#state.config)#config.anonymous) -> - Items = items_with_affiliation(SAffiliation, - StateData), - {result, Items, StateData}; - true -> - ErrText = - <<"Administrator privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} - end - end - end; - {value, StrRole} -> - case catch list_to_role(StrRole) of - {'EXIT', _} -> - Txt = <<"Incorrect value of 'role' attribute">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; - SRole -> - if FRole == moderator -> - Items = items_with_role(SRole, StateData), - {result, Items, StateData}; - true -> - ErrText = <<"Moderator privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} - end - end - end +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 = ?T("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 = ?T("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 = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)} + end end. +-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}}) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"affiliation">>, - affiliation_to_list(Affiliation)}, - {<<"jid">>, jid:to_string(JID)}], - children = - [#xmlel{name = <<"reason">>, attrs = [], - children = [{xmlcdata, Reason}]}]}; - ({JID, Affiliation}) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"affiliation">>, - affiliation_to_list(Affiliation)}, - {<<"jid">>, jid:to_string(JID)}], - children = []} - end, - search_affiliation(SAffiliation, StateData)). + lists:map( + fun({JID, {Affiliation, Reason}}) -> + #muc_item{affiliation = Affiliation, jid = jid:make(JID), + reason = Reason}; + ({JID, Affiliation}) -> + #muc_item{affiliation = Affiliation, jid = jid:make(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), - #xmlel{name = <<"item">>, - attrs = - [{<<"role">>, role_to_list(Role)}, - {<<"affiliation">>, affiliation_to_list(Affiliation)}, - {<<"nick">>, Nick}, - {<<"jid">>, jid:to_string(JID)}], - children = []}. + #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)). - + maps:to_list(StateData#state.users)). + +-spec search_affiliation(affiliation(), state()) -> + [{ljid(), + affiliation() | {affiliation(), binary()}}]. +search_affiliation(Affiliation, + #state{config = #config{persistent = false}} = StateData) -> + search_affiliation_fallback(Affiliation, StateData); 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)). + Room = StateData#state.room, + Host = StateData#state.host, + ServerHost = StateData#state.server_host, + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case Mod:search_affiliation(ServerHost, Room, Host, Affiliation) of + {ok, AffiliationList} -> + AffiliationList; + {error, _} -> + search_affiliation_fallback(Affiliation, StateData) + end. +-spec search_affiliation_fallback(affiliation(), state()) -> + [{ljid(), + affiliation() | {affiliation(), binary()}}]. +search_affiliation_fallback(Affiliation, StateData) -> + lists:filter( + fun({_, A}) -> + case A of + {A1, _Reason} -> Affiliation == A1; + _ -> Affiliation == A + end + end, maps:to_list(StateData#state.affiliations)). + +-spec process_admin_items_set(jid(), [muc_item()], binary(), + #state{}) -> {result, undefined, #state{}} | + {error, stanza_error()}. process_admin_items_set(UJID, Items, Lang, StateData) -> UAffiliation = get_affiliation(UJID, StateData), URole = get_role(UJID, StateData), - case find_changed_items(UJID, UAffiliation, URole, - Items, Lang, StateData, []) + 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, [], NSD}; - Err -> Err + ?INFO_MSG("Processing MUC admin query from ~ts in " + "room ~ts:~n ~p", + [jid:encode(UJID), + jid:encode(StateData#state.jid), Res]), + case lists:foldl(process_item_change(UJID), + StateData, lists:flatten(Res)) of + {error, _} = Err -> + Err; + NSD -> + store_room(NSD), + {result, undefined, NSD} + end; + {error, Err} -> {error, Err} end. +-spec process_item_change(jid()) -> fun((admin_action(), state() | {error, stanza_error()}) -> + state() | {error, stanza_error()}). process_item_change(UJID) -> - fun(E, SD) -> - process_item_change(E, SD, UJID) + fun(_, {error, _} = Err) -> + Err; + (Item, SD) -> + process_item_change(Item, SD, UJID) end. -process_item_change(E, SD, UJID) -> - case catch case E of - {JID, affiliation, owner, _} when JID#jid.luser == <<"">> -> - %% If the provided JID does not have username, - %% forget the affiliation completely - SD; - {JID, role, none, Reason} -> - catch - send_kickban_presence(UJID, JID, - Reason, - <<"307">>, - SD), - set_role(JID, none, SD); - {JID, affiliation, none, Reason} -> - case (SD#state.config)#config.members_only of - true -> - catch - send_kickban_presence(UJID, JID, - Reason, - <<"321">>, - none, - SD), - 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 - of - {'EXIT', ErrReason} -> - ?ERROR_MSG("MUC ITEMS SET ERR: ~p~n", [ErrReason]), - SD; - NSD -> NSD +-spec process_item_change(admin_action(), state(), undefined | jid()) -> state() | {error, stanza_error()}. +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} -> + 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 -> + 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), + SD2 = case (SD1#state.config)#config.moderated of + true -> set_role(JID, visitor, SD1); + false -> set_role(JID, participant, SD1) + end, + send_update_presence(JID, Reason, SD2, SD), + maybe_send_affiliation(JID, none, SD2), + SD2 + end; + {JID, affiliation, outcast, Reason} -> + 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), + 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 ?EX_RULE(E, R, St) -> + StackTrace = ?EX_STACK(St), + FromSuffix = case UJID of + #jid{} -> + JidString = jid:encode(UJID), + <<" from ", JidString/binary>>; + undefined -> + <<"">> + end, + ?ERROR_MSG("Failed to set item ~p~ts:~n** ~ts", + [Item, FromSuffix, + misc:format_exception(2, E, R, StackTrace)]), + {error, xmpp:err_internal_server_error()} 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 = <<"">>}|_], + Lang, _StateData, _Res) -> + Txt = ?T("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 = ?T("Neither 'role' nor 'affiliation' attribute found"), + throw({error, xmpp:err_bad_request(Txt, Lang)}); find_changed_items(UJID, UAffiliation, URole, - [{xmlcdata, _} | Items], Lang, StateData, Res) -> - find_changed_items(UJID, UAffiliation, URole, Items, - Lang, StateData, Res); -find_changed_items(UJID, UAffiliation, URole, - [#xmlel{name = <<"item">>, attrs = Attrs} = Item - | Items], + [#muc_item{jid = J, nick = Nick, reason = Reason, + role = Role, affiliation = Affiliation}|Items], Lang, StateData, Res) -> - TJID = case fxml:get_attr(<<"jid">>, Attrs) of - {value, S} -> - case jid:from_string(S) of - error -> - ErrText = iolist_to_binary( - io_lib:format(translate:translate( - Lang, - <<"Jabber ID ~s is invalid">>), - [S])), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; - J -> {value, [J]} - end; - _ -> - case fxml:get_attr(<<"nick">>, Attrs) of - {value, N} -> - case find_jids_by_nick(N, StateData) of - false -> - ErrText = iolist_to_binary( - io_lib:format( - translate:translate( - Lang, - <<"Nickname ~s does not exist in the room">>), - [N])), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; - J -> {value, J} - end; - _ -> - Txt1 = <<"No 'nick' attribute found">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt1)} - end - end, - case TJID of - {value, [JID | _] = JIDs} -> - TAffiliation = get_affiliation(JID, StateData), - TRole = get_role(JID, StateData), - case fxml:get_attr(<<"role">>, Attrs) of - false -> - case fxml:get_attr(<<"affiliation">>, Attrs) of - false -> - Txt2 = <<"No 'affiliation' attribute found">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt2)}; - {value, StrAffiliation} -> - case catch list_to_affiliation(StrAffiliation) of - {'EXIT', _} -> - ErrText1 = iolist_to_binary( - io_lib:format( - translate:translate( - Lang, - <<"Invalid affiliation: ~s">>), - [StrAffiliation])), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText1)}; - SAffiliation -> - ServiceAf = get_service_affiliation(JID, StateData), - CanChangeRA = case can_change_ra(UAffiliation, - URole, - TAffiliation, - TRole, affiliation, - SAffiliation, - ServiceAf) - of - nothing -> nothing; - true -> true; - check_owner -> - case search_affiliation(owner, - StateData) - of - [{OJID, _}] -> - 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 -> - Reason = fxml:get_path_s(Item, - [{elem, <<"reason">>}, - cdata]), - MoreRes = [{jid:remove_resource(Jidx), - affiliation, SAffiliation, Reason} - || Jidx <- JIDs], - find_changed_items(UJID, UAffiliation, URole, - Items, Lang, StateData, - [MoreRes | Res]); - false -> - Txt3 = <<"Changing role/affiliation is not allowed">>, - {error, ?ERRT_NOT_ALLOWED(Lang, Txt3)} - end - end - end; - {value, StrRole} -> - case catch list_to_role(StrRole) of - {'EXIT', _} -> - ErrText1 = iolist_to_binary( - io_lib:format(translate:translate( - Lang, - <<"Invalid role: ~s">>), - [StrRole])), - {error, ?ERRT_BAD_REQUEST(Lang, ErrText1)}; - SRole -> - ServiceAf = get_service_affiliation(JID, StateData), - CanChangeRA = case can_change_ra(UAffiliation, URole, - TAffiliation, TRole, - role, SRole, ServiceAf) - of - nothing -> nothing; - true -> true; - check_owner -> - case search_affiliation(owner, - StateData) - of - [{OJID, _}] -> - 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 -> - Reason = fxml:get_path_s(Item, - [{elem, <<"reason">>}, - cdata]), - MoreRes = [{Jidx, role, SRole, Reason} - || Jidx <- JIDs], - find_changed_items(UJID, UAffiliation, URole, Items, - Lang, StateData, - [MoreRes | Res]); - _ -> - Txt4 = <<"Changing role/affiliation is not allowed">>, - {error, ?ERRT_NOT_ALLOWED(Lang, Txt4)} - end + [JID | _] = JIDs = + if J /= undefined -> + [J]; + Nick /= <<"">> -> + case find_jids_by_nick(Nick, StateData) of + [] -> + ErrText = {?T("Nickname ~ts does not exist in the room"), + [Nick]}, + throw({error, xmpp:err_not_acceptable(ErrText, Lang)}); + JIDList -> + JIDList end - end; - Err -> Err - end; -find_changed_items(_UJID, _UAffiliation, _URole, _Items, - _Lang, _StateData, _Res) -> - {error, ?ERR_BAD_REQUEST}. + 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), + UIsSubscriber = is_subscriber(UJID, StateData), + URole1 = case {URole, UIsSubscriber} of + {none, true} -> subscriber; + {UR, _} -> UR + end, + CanChangeRA = case can_change_ra(UAffiliation, + URole1, + 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 = case RoleOrAff of + affiliation -> + [{jid:remove_resource(Jidx), + RoleOrAff, RoleOrAffValue, Reason} + || Jidx <- JIDs]; + role -> + [{Jidx, RoleOrAff, RoleOrAffValue, Reason} + || Jidx <- JIDs] + end, + find_changed_items(UJID, UAffiliation, URole, + Items, Lang, StateData, + MoreRes ++ Res); + false -> + Txt = ?T("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() | nothing | check_owner; + (affiliation(), role(), affiliation(), role(), + role, role(), affiliation()) -> boolean() | nothing | check_owner. can_change_ra(_FAffiliation, _FRole, owner, _TRole, affiliation, owner, owner) -> %% A room owner tries to add as persistent owner a @@ -3270,9 +3132,19 @@ can_change_ra(_FAffiliation, _FRole, _TAffiliation, can_change_ra(_FAffiliation, moderator, _TAffiliation, visitor, role, none, _ServiceAf) -> true; +can_change_ra(FAffiliation, subscriber, _TAffiliation, + visitor, role, none, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> + true; can_change_ra(_FAffiliation, moderator, _TAffiliation, visitor, role, participant, _ServiceAf) -> true; +can_change_ra(FAffiliation, subscriber, _TAffiliation, + visitor, role, participant, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> + true; can_change_ra(FAffiliation, _FRole, _TAffiliation, visitor, role, moderator, _ServiceAf) when (FAffiliation == owner) or @@ -3281,9 +3153,19 @@ can_change_ra(FAffiliation, _FRole, _TAffiliation, can_change_ra(_FAffiliation, moderator, _TAffiliation, participant, role, none, _ServiceAf) -> true; +can_change_ra(FAffiliation, subscriber, _TAffiliation, + participant, role, none, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> + true; can_change_ra(_FAffiliation, moderator, _TAffiliation, participant, role, visitor, _ServiceAf) -> true; +can_change_ra(FAffiliation, subscriber, _TAffiliation, + participant, role, visitor, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> + true; can_change_ra(FAffiliation, _FRole, _TAffiliation, participant, role, moderator, _ServiceAf) when (FAffiliation == owner) or @@ -3313,36 +3195,56 @@ can_change_ra(_FAffiliation, _FRole, admin, moderator, can_change_ra(admin, _FRole, _TAffiliation, moderator, role, participant, _ServiceAf) -> true; +can_change_ra(owner, moderator, TAffiliation, + moderator, role, none, _ServiceAf) + when TAffiliation /= owner -> + true; +can_change_ra(owner, subscriber, TAffiliation, + moderator, role, none, _ServiceAf) + when TAffiliation /= owner -> + true; +can_change_ra(admin, moderator, TAffiliation, + moderator, role, none, _ServiceAf) + when (TAffiliation /= owner) and + (TAffiliation /= admin) -> + true; +can_change_ra(admin, subscriber, TAffiliation, + moderator, role, none, _ServiceAf) + when (TAffiliation /= owner) and + (TAffiliation /= admin) -> + true; can_change_ra(_FAffiliation, _FRole, _TAffiliation, _TRole, role, _Value, _ServiceAf) -> false. +-spec send_kickban_presence(undefined | 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(undefined | 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 + {U, S, <<"">>} -> + maps:fold(fun (J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, [], StateData#state.users); + _ -> + case maps: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), + lists:foreach(fun (LJ) -> + #user{nick = Nick, jid = J} = maps:get(LJ, StateData#state.users), add_to_log(kickban, {Nick, Reason, Code}, StateData), tab_remove_online_user(J, StateData), send_kickban_presence1(UJID, J, Reason, Code, @@ -3350,924 +3252,476 @@ send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, end, LJIDs). +-spec send_kickban_presence1(undefined | 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), - SAffiliation = affiliation_to_list(Affiliation), - BannedJIDString = jid:to_string(RealJID), + #user{jid = RealJID, nick = Nick} = maps:get(jid:tolower(UJID), StateData#state.users), ActorNick = get_actor_nick(MJID, StateData), - lists:foreach(fun ({_LJID, Info}) -> - JidAttrList = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, BannedJIDString}]; - false -> [] - end, - ItemAttrs = [{<<"affiliation">>, SAffiliation}, - {<<"role">>, <<"none">>}] - ++ JidAttrList, - ItemEls = case Reason of - <<"">> -> []; - _ -> - [#xmlel{name = <<"reason">>, - attrs = [], - children = - [{xmlcdata, Reason}]}] - end, - ItemElsActor = case MJID of - <<"">> -> []; - _ -> [#xmlel{name = <<"actor">>, - attrs = - [{<<"nick">>, ActorNick}]}] - end, - Packet = #xmlel{name = <<"presence">>, - attrs = - [{<<"type">>, <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"item">>, - attrs = - ItemAttrs, - children = - ItemElsActor ++ ItemEls}, - #xmlel{name = - <<"status">>, - attrs = - [{<<"code">>, - Code}], - children = - []}]}]}, - 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)). + maps:fold( + fun(LJID, Info, _) -> + IsSelfPresence = jid:tolower(UJID) == LJID, + Item0 = #muc_item{affiliation = Affiliation, + role = none}, + Item1 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false orelse IsSelfPresence 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, + Codes = if IsSelfPresence -> [110, Code]; + true -> [Code] + end, + Packet = #presence{type = unavailable, + sub_els = [#muc_user{items = [Item], + status_codes = Codes}]}, + RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick), + send_wrapped(RoomJIDNick, Info#user.jid, Packet, + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), + IsSubscriber = is_subscriber(Info#user.jid, StateData), + 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, ok, get_users_and_subscribers(StateData)). -get_actor_nick(<<"">>, _StateData) -> +-spec get_actor_nick(undefined | jid(), state()) -> binary(). +get_actor_nick(undefined, _StateData) -> <<"">>; get_actor_nick(MJID, StateData) -> - case (?DICT):find(jid:tolower(MJID), StateData#state.users) of - {ok, #user{nick = ActorNick}} -> ActorNick; - _ -> <<"">> + try maps:get(jid:tolower(MJID), StateData#state.users) of + #user{nick = ActorNick} -> ActorNick + catch _:{badkey, _} -> <<"">> end. +-spec convert_legacy_fields([xdata_field()]) -> [xdata_field()]. +convert_legacy_fields(Fs) -> + lists:map( + fun(#xdata_field{var = Var} = F) -> + NewVar = case Var of + <<"muc#roomconfig_allowvisitorstatus">> -> + <<"allow_visitor_status">>; + <<"muc#roomconfig_allowvisitornickchange">> -> + <<"allow_visitor_nickchange">>; + <<"muc#roomconfig_allowvoicerequests">> -> + <<"allow_voice_requests">>; + <<"muc#roomconfig_allow_subscription">> -> + <<"allow_subscription">>; + <<"muc#roomconfig_voicerequestmininterval">> -> + <<"voice_request_min_interval">>; + <<"muc#roomconfig_captcha_whitelist">> -> + <<"captcha_whitelist">>; + <<"muc#roomconfig_mam">> -> + <<"mam">>; + _ -> + Var + end, + F#xdata_field{var = NewVar} + end, Fs). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Owner stuff - -process_iq_owner(From, set, Lang, SubEl, StateData) -> +-spec process_iq_owner(jid(), iq(), state()) -> + {result, undefined | muc_owner()} | + {result, undefined | muc_owner(), state() | stop} | + {error, stanza_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), - case FAffiliation of - owner -> - #xmlel{children = Els} = SubEl, - case fxml:remove_cdata(Els) of - [#xmlel{name = <<"x">>} = XEl] -> - case {fxml:get_tag_attr_s(<<"xmlns">>, XEl), - fxml:get_tag_attr_s(<<"type">>, XEl)} - of - {?NS_XDATA, <<"cancel">>} -> {result, [], StateData}; - {?NS_XDATA, <<"submit">>} -> - case is_allowed_log_change(XEl, StateData, From) andalso - is_allowed_persistent_change(XEl, StateData, From) - andalso - is_allowed_room_name_desc_limits(XEl, StateData) - andalso - is_password_settings_correct(XEl, StateData) - of - true -> set_config(XEl, StateData, Lang); - false -> {error, ?ERR_NOT_ACCEPTABLE} - end; - _ -> - Txt = <<"Incorrect data form">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)} - end; - [#xmlel{name = <<"destroy">>} = SubEl1] -> - ?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(SubEl1, StateData); - Items -> - process_admin_items_set(From, Items, Lang, StateData) - end; - _ -> - ErrText = <<"Owner privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + if FAffiliation /= owner -> + ErrText = ?T("Owner privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)}; + Destroy /= undefined, Config == undefined, Items == [] -> + ?INFO_MSG("Destroyed MUC room ~ts by the owner ~ts", + [jid:encode(StateData#state.jid), jid:encode(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, fields = Fs} -> + Fs1 = convert_legacy_fields(Fs), + try muc_roomconfig:decode(Fs1) of + Options -> + case is_allowed_log_change(Options, StateData, From) andalso + is_allowed_persistent_change(Options, StateData, From) andalso + is_allowed_mam_change(Options, StateData, From) andalso + is_allowed_room_name_desc_limits(Options, StateData) andalso + is_password_settings_correct(Options, StateData) of + true -> + set_config(Options, StateData, Lang); + false -> + {error, xmpp:err_not_acceptable()} + end + catch _:{muc_roomconfig, Why} -> + Txt = muc_roomconfig:format_error(Why), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + _ -> + Txt = ?T("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, get, Lang, SubEl, StateData) -> +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), - case FAffiliation of - owner -> - #xmlel{children = Els} = SubEl, - case fxml:remove_cdata(Els) of - [] -> get_config(Lang, StateData, From); - [Item] -> - case fxml:get_tag_attr(<<"affiliation">>, Item) of - false -> - Txt = <<"No 'affiliation' attribute found">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; - {value, StrAffiliation} -> - case catch list_to_affiliation(StrAffiliation) of - {'EXIT', _} -> - ErrText = iolist_to_binary( - io_lib:format( - translate:translate( - Lang, - <<"Invalid affiliation: ~s">>), - [StrAffiliation])), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; - SAffiliation -> - Items = items_with_affiliation(SAffiliation, - StateData), - {result, Items, StateData} - end - end; - _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} - end; - _ -> - ErrText = <<"Owner privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + if FAffiliation /= owner -> + ErrText = ?T("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 = ?T("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 = ?T("Too many <item/> elements"), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + true -> + {error, xmpp:err_bad_request()} end. -is_allowed_log_change(XEl, StateData, From) -> - case lists:keymember(<<"muc#roomconfig_enablelogging">>, - 1, jlib:parse_xdata_submit(XEl)) - of +-spec is_allowed_log_change(muc_roomconfig:result(), state(), jid()) -> boolean(). +is_allowed_log_change(Options, StateData, From) -> + case proplists:is_defined(enablelogging, Options) of + false -> true; + true -> + allow == + mod_muc_log:check_access_log(StateData#state.server_host, + From) + end. + +-spec is_allowed_persistent_change(muc_roomconfig:result(), state(), jid()) -> boolean(). +is_allowed_persistent_change(Options, StateData, From) -> + case proplists:is_defined(persistentroom, Options) of false -> true; true -> + {_AccessRoute, _AccessCreate, _AccessAdmin, + AccessPersistent, _AccessMam} = + StateData#state.access, allow == - mod_muc_log:check_access_log(StateData#state.server_host, - From) + acl:match_rule(StateData#state.server_host, + AccessPersistent, From) end. -is_allowed_persistent_change(XEl, StateData, From) -> - case - lists:keymember(<<"muc#roomconfig_persistentroom">>, 1, - jlib:parse_xdata_submit(XEl)) - of +-spec is_allowed_mam_change(muc_roomconfig:result(), state(), jid()) -> boolean(). +is_allowed_mam_change(Options, StateData, From) -> + case proplists:is_defined(mam, Options) of false -> true; true -> {_AccessRoute, _AccessCreate, _AccessAdmin, - AccessPersistent} = + _AccessPersistent, AccessMam} = StateData#state.access, allow == acl:match_rule(StateData#state.server_host, - AccessPersistent, From) + AccessMam, From) end. %% Check if the Room Name and Room Description defined in the Data Form %% are conformant to the configured limits -is_allowed_room_name_desc_limits(XEl, StateData) -> - IsNameAccepted = case - lists:keysearch(<<"muc#roomconfig_roomname">>, 1, - jlib:parse_xdata_submit(XEl)) - of - {value, {_, [N]}} -> - byte_size(N) =< - gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_room_name, - fun(infinity) -> infinity; - (I) when is_integer(I), - I>0 -> I - end, infinity); - _ -> true - end, - IsDescAccepted = case - lists:keysearch(<<"muc#roomconfig_roomdesc">>, 1, - jlib:parse_xdata_submit(XEl)) - of - {value, {_, [D]}} -> - byte_size(D) =< - gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_room_desc, - fun(infinity) -> infinity; - (I) when is_integer(I), - I>0 -> - I - end, infinity); - _ -> true - end, - IsNameAccepted and IsDescAccepted. +-spec is_allowed_room_name_desc_limits(muc_roomconfig:result(), state()) -> boolean(). +is_allowed_room_name_desc_limits(Options, StateData) -> + RoomName = proplists:get_value(roomname, Options, <<"">>), + RoomDesc = proplists:get_value(roomdesc, Options, <<"">>), + MaxRoomName = mod_muc_opt:max_room_name(StateData#state.server_host), + MaxRoomDesc = mod_muc_opt:max_room_desc(StateData#state.server_host), + (byte_size(RoomName) =< MaxRoomName) + andalso (byte_size(RoomDesc) =< MaxRoomDesc). %% Return false if: %% "the password for a password-protected room is blank" -is_password_settings_correct(XEl, StateData) -> +-spec is_password_settings_correct(muc_roomconfig:result(), state()) -> boolean(). +is_password_settings_correct(Options, StateData) -> Config = StateData#state.config, OldProtected = Config#config.password_protected, OldPassword = Config#config.password, - NewProtected = case - lists:keysearch(<<"muc#roomconfig_passwordprotectedroom">>, - 1, jlib:parse_xdata_submit(XEl)) - of - {value, {_, [<<"1">>]}} -> true; - {value, {_, [<<"0">>]}} -> false; - _ -> undefined - end, - NewPassword = case - lists:keysearch(<<"muc#roomconfig_roomsecret">>, 1, - jlib:parse_xdata_submit(XEl)) - of - {value, {_, [P]}} -> P; - _ -> undefined - end, - case {OldProtected, NewProtected, OldPassword, - NewPassword} - of - {true, undefined, <<"">>, undefined} -> false; - {true, undefined, _, <<"">>} -> false; - {_, true, <<"">>, undefined} -> false; - {_, true, _, <<"">>} -> false; - _ -> true + NewProtected = proplists:get_value(passwordprotectedroom, Options), + NewPassword = proplists:get_value(roomsecret, Options), + case {OldProtected, NewProtected, OldPassword, NewPassword} of + {true, undefined, <<"">>, undefined} -> false; + {true, undefined, _, <<"">>} -> false; + {_, true, <<"">>, undefined} -> false; + {_, true, _, <<"">>} -> false; + _ -> true end. --define(XFIELD(Type, Label, Var, Val), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - --define(BOOLXFIELD(Label, Var, Val), - ?XFIELD(<<"boolean">>, Label, Var, - case Val of - true -> <<"1">>; - _ -> <<"0">> - end)). - --define(STRINGXFIELD(Label, Var, Val), - ?XFIELD(<<"text-single">>, Label, Var, Val)). - --define(PRIVATEXFIELD(Label, Var, Val), - ?XFIELD(<<"text-private">>, Label, Var, Val)). - --define(JIDMULTIXFIELD(Label, Var, JIDList), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"jid-multi">>}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, jid:to_string(JID)}]} - || JID <- JIDList]}). - +-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, - []), + mod_muc_opt:default_room_options(RoomState#state.server_host), 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} = + {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent, _AccessMam} = StateData#state.access, ServiceMaxUsers = get_service_max_users(StateData), - DefaultRoomMaxUsers = - get_default_room_maxusers(StateData), + DefaultRoomMaxUsers = get_default_room_maxusers(StateData), Config = StateData#state.config, - {MaxUsersRoomInteger, MaxUsersRoomString} = case - get_max_users(StateData) - of - N when is_integer(N) -> - {N, - jlib:integer_to_binary(N)}; - _ -> {0, <<"none">>} - end, - Res = [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - iolist_to_binary( - io_lib:format( - translate:translate( - Lang, - <<"Configuration of room ~s">>), - [jid:to_string(StateData#state.jid)]))}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"hidden">>}, - {<<"var">>, <<"FORM_TYPE">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"http://jabber.org/protocol/muc#roomconfig">>}]}]}, - ?STRINGXFIELD(<<"Room title">>, - <<"muc#roomconfig_roomname">>, (Config#config.title)), - ?STRINGXFIELD(<<"Room description">>, - <<"muc#roomconfig_roomdesc">>, - (Config#config.description))] - ++ - case acl:match_rule(StateData#state.server_host, - AccessPersistent, From) - of - allow -> - [?BOOLXFIELD(<<"Make room persistent">>, - <<"muc#roomconfig_persistentroom">>, - (Config#config.persistent))]; - _ -> [] - end - ++ - [?BOOLXFIELD(<<"Make room public searchable">>, - <<"muc#roomconfig_publicroom">>, - (Config#config.public)), - ?BOOLXFIELD(<<"Make participants list public">>, - <<"public_list">>, (Config#config.public_list)), - ?BOOLXFIELD(<<"Make room password protected">>, - <<"muc#roomconfig_passwordprotectedroom">>, - (Config#config.password_protected)), - ?PRIVATEXFIELD(<<"Password">>, - <<"muc#roomconfig_roomsecret">>, - case Config#config.password_protected of - true -> Config#config.password; - false -> <<"">> - end), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"Maximum Number of Occupants">>)}, - {<<"var">>, <<"muc#roomconfig_maxusers">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, MaxUsersRoomString}]}] - ++ - if is_integer(ServiceMaxUsers) -> []; - true -> - [#xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"No limit">>)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - <<"none">>}]}]}] - end - ++ - [#xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - jlib:integer_to_binary(N)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = - [{xmlcdata, - jlib:integer_to_binary(N)}]}]} - || N - <- lists:usort([ServiceMaxUsers, - DefaultRoomMaxUsers, - MaxUsersRoomInteger - | ?MAX_USERS_DEFAULT_LIST]), - N =< ServiceMaxUsers]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"Present real Jabber IDs to">>)}, - {<<"var">>, <<"muc#roomconfig_whois">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - if Config#config.anonymous -> - <<"moderators">>; - true -> <<"anyone">> - end}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"moderators only">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"moderators">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"anyone">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"anyone">>}]}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-multi">>}, - {<<"label">>, - translate:translate(Lang, - <<"Roles for which Presence is Broadcasted">>)}, - {<<"var">>, <<"muc#roomconfig_presencebroadcast">>}], - children = - lists:map( - fun(Role) -> - #xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - atom_to_binary(Role, utf8)}]} - end, Config#config.presence_broadcast - ) ++ - [#xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"Moderator">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"moderator">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"Participant">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"participant">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"Visitor">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"visitor">>}]}]} - ]}, - ?BOOLXFIELD(<<"Make room members-only">>, - <<"muc#roomconfig_membersonly">>, - (Config#config.members_only)), - ?BOOLXFIELD(<<"Make room moderated">>, - <<"muc#roomconfig_moderatedroom">>, - (Config#config.moderated)), - ?BOOLXFIELD(<<"Default users as participants">>, - <<"members_by_default">>, - (Config#config.members_by_default)), - ?BOOLXFIELD(<<"Allow users to change the subject">>, - <<"muc#roomconfig_changesubject">>, - (Config#config.allow_change_subj)), - ?BOOLXFIELD(<<"Allow users to send private messages">>, - <<"allow_private_messages">>, - (Config#config.allow_private_messages)), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, <<"list-single">>}, - {<<"label">>, - translate:translate(Lang, - <<"Allow visitors to send private messages to">>)}, - {<<"var">>, - <<"allow_private_messages_from_visitors">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - case - Config#config.allow_private_messages_from_visitors - of - anyone -> <<"anyone">>; - moderators -> <<"moderators">>; - nobody -> <<"nobody">> - end}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"nobody">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, <<"nobody">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"moderators only">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"moderators">>}]}]}, - #xmlel{name = <<"option">>, - attrs = - [{<<"label">>, - translate:translate(Lang, - <<"anyone">>)}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"anyone">>}]}]}]}, - ?BOOLXFIELD(<<"Allow users to query other users">>, - <<"allow_query_users">>, - (Config#config.allow_query_users)), - ?BOOLXFIELD(<<"Allow users to send invites">>, - <<"muc#roomconfig_allowinvites">>, - (Config#config.allow_user_invites)), - ?BOOLXFIELD(<<"Allow visitors to send status text in " - "presence updates">>, - <<"muc#roomconfig_allowvisitorstatus">>, - (Config#config.allow_visitor_status)), - ?BOOLXFIELD(<<"Allow visitors to change nickname">>, - <<"muc#roomconfig_allowvisitornickchange">>, - (Config#config.allow_visitor_nickchange)), - ?BOOLXFIELD(<<"Allow visitors to send voice requests">>, - <<"muc#roomconfig_allowvoicerequests">>, - (Config#config.allow_voice_requests)), - ?BOOLXFIELD(<<"Allow subscription">>, - <<"muc#roomconfig_allow_subscription">>, - (Config#config.allow_subscription)), - ?STRINGXFIELD(<<"Minimum interval between voice requests " - "(in seconds)">>, - <<"muc#roomconfig_voicerequestmininterval">>, - (jlib:integer_to_binary(Config#config.voice_request_min_interval)))] - ++ - case ejabberd_captcha:is_feature_available() of - true -> - [?BOOLXFIELD(<<"Make room CAPTCHA protected">>, - <<"captcha_protected">>, - (Config#config.captcha_protected))]; - false -> [] - end ++ - [?JIDMULTIXFIELD(<<"Exclude Jabber IDs from CAPTCHA challenge">>, - <<"muc#roomconfig_captcha_whitelist">>, - ((?SETS):to_list(Config#config.captcha_whitelist)))] - ++ - case - mod_muc_log:check_access_log(StateData#state.server_host, - From) - of - allow -> - [?BOOLXFIELD(<<"Enable logging">>, - <<"muc#roomconfig_enablelogging">>, - (Config#config.logging))]; - _ -> [] - end, - X = ejabberd_hooks:run_fold(get_room_config, - StateData#state.server_host, - Res, - [StateData, From, Lang]), - {result, - [#xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"You need an x:data capable client to " - "configure room">>)}]}, - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = X}], - StateData}. - -set_config(XEl, StateData, Lang) -> - XData = jlib:parse_xdata_submit(XEl), - case XData of - invalid -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Incorrect data form">>)}; - _ -> - case set_xoption(XData, 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 + MaxUsersRoom = get_max_users(StateData), + Title = str:format( + translate:translate(Lang, ?T("Configuration of room ~ts")), + [jid:encode(StateData#state.jid)]), + Fs = [{roomname, Config#config.title}, + {roomdesc, Config#config.description}, + {lang, Config#config.lang}] ++ + case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of + allow -> [{persistentroom, Config#config.persistent}]; + deny -> [] + end ++ + [{publicroom, Config#config.public}, + {public_list, Config#config.public_list}, + {passwordprotectedroom, Config#config.password_protected}, + {roomsecret, case Config#config.password_protected of + true -> Config#config.password; + false -> <<"">> + end}, + {maxusers, MaxUsersRoom, + [if is_integer(ServiceMaxUsers) -> []; + true -> [{?T("No limit"), <<"none">>}] + end] ++ [{integer_to_binary(N), N} + || N <- lists:usort([ServiceMaxUsers, + DefaultRoomMaxUsers, + MaxUsersRoom + | ?MAX_USERS_DEFAULT_LIST]), + N =< ServiceMaxUsers]}, + {whois, if Config#config.anonymous -> moderators; + true -> anyone + end}, + {presencebroadcast, Config#config.presence_broadcast}, + {membersonly, Config#config.members_only}, + {moderatedroom, Config#config.moderated}, + {members_by_default, Config#config.members_by_default}, + {changesubject, Config#config.allow_change_subj}, + {allow_private_messages, Config#config.allow_private_messages}, + {allow_private_messages_from_visitors, + Config#config.allow_private_messages_from_visitors}, + {allow_query_users, Config#config.allow_query_users}, + {allowinvites, Config#config.allow_user_invites}, + {allow_visitor_status, Config#config.allow_visitor_status}, + {allow_visitor_nickchange, Config#config.allow_visitor_nickchange}, + {allow_voice_requests, Config#config.allow_voice_requests}, + {allow_subscription, Config#config.allow_subscription}, + {voice_request_min_interval, Config#config.voice_request_min_interval}, + {pubsub, Config#config.pubsub}] + ++ + case ejabberd_captcha:is_feature_available() of + true -> + [{captcha_protected, Config#config.captcha_protected}, + {captcha_whitelist, + lists:map( + fun jid:make/1, + ?SETS:to_list(Config#config.captcha_whitelist))}]; + false -> + [] + end + ++ + case mod_muc_log:check_access_log(StateData#state.server_host, From) of + allow -> [{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 = muc_roomconfig:encode(Fields, Lang)}. + +-spec set_config(muc_roomconfig:result(), state(), binary()) -> + {error, stanza_error()} | {result, undefined, state()}. +set_config(Options, StateData, Lang) -> + try + #config{} = Config = set_config(Options, StateData#state.config, + StateData#state.server_host, Lang), + {result, _, NSD} = Res = change_config(Config, StateData), + 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 <- maps:values(StateData#state.users)], + add_to_log(Type, Users, NSD), + Res + catch _:{badmatch, {error, #stanza_error{}} = Err} -> + Err end. --define(SET_BOOL_XOPT(Opt, Val), - case Val of - <<"0">> -> - set_xoption(Opts, Config#config{Opt = false}, ServerHost, Lang); - <<"false">> -> - set_xoption(Opts, Config#config{Opt = false}, ServerHost, Lang); - <<"1">> -> set_xoption(Opts, Config#config{Opt = true}, ServerHost, Lang); - <<"true">> -> - set_xoption(Opts, Config#config{Opt = true}, ServerHost, Lang); - _ -> - Txt = <<"Value of '~s' should be boolean">>, - ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), - {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)} - end). - --define(SET_NAT_XOPT(Opt, Val), - case catch jlib:binary_to_integer(Val) of - I when is_integer(I), I > 0 -> - set_xoption(Opts, Config#config{Opt = I}, ServerHost, Lang); - _ -> - Txt = <<"Value of '~s' should be integer">>, - ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), - {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)} - end). - --define(SET_STRING_XOPT(Opt, Val), - set_xoption(Opts, Config#config{Opt = Val}, ServerHost, Lang)). - --define(SET_JIDMULTI_XOPT(Opt, Vals), - begin - Set = lists:foldl(fun ({U, S, R}, Set1) -> - (?SETS):add_element({U, S, R}, Set1); - (#jid{luser = U, lserver = S, lresource = R}, - Set1) -> - (?SETS):add_element({U, S, R}, Set1); - (_, Set1) -> Set1 - end, - (?SETS):empty(), Vals), - set_xoption(Opts, Config#config{Opt = Set}, ServerHost, Lang) - end). - -set_xoption([], Config, _ServerHost, _Lang) -> Config; -set_xoption([{<<"muc#roomconfig_roomname">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_STRING_XOPT(title, Val); -set_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_STRING_XOPT(description, Val); -set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(allow_change_subj, Val); -set_xoption([{<<"allow_query_users">>, [Val]} | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(allow_query_users, Val); -set_xoption([{<<"allow_private_messages">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(allow_private_messages, Val); -set_xoption([{<<"allow_private_messages_from_visitors">>, - [Val]} - | Opts], - Config, ServerHost, Lang) -> - case Val of - <<"anyone">> -> - ?SET_STRING_XOPT(allow_private_messages_from_visitors, - anyone); - <<"moderators">> -> - ?SET_STRING_XOPT(allow_private_messages_from_visitors, - moderators); - <<"nobody">> -> - ?SET_STRING_XOPT(allow_private_messages_from_visitors, - nobody); - _ -> - Txt = <<"Value of 'allow_private_messages_from_visitors' " - "should be anyone|moderators|nobody">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)} - end; -set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>, - [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(allow_visitor_status, Val); -set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>, - [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(allow_visitor_nickchange, Val); -set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(public, Val); -set_xoption([{<<"public_list">>, [Val]} | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(public_list, Val); -set_xoption([{<<"muc#roomconfig_persistentroom">>, - [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(persistent, Val); -set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(moderated, Val); -set_xoption([{<<"members_by_default">>, [Val]} | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(members_by_default, Val); -set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(members_only, Val); -set_xoption([{<<"captcha_protected">>, [Val]} | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(captcha_protected, Val); -set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(allow_user_invites, Val); -set_xoption([{<<"muc#roomconfig_allow_subscription">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(allow_subscription, Val); -set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>, - [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(password_protected, Val); -set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_STRING_XOPT(password, Val); -set_xoption([{<<"anonymous">>, [Val]} | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(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, ?ERRT_BAD_REQUEST(Lang, Txt)}; - {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(allow_voice_requests, Val); -set_xoption([{<<"muc#roomconfig_voicerequestmininterval">>, - [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_NAT_XOPT(voice_request_min_interval, Val); -set_xoption([{<<"muc#roomconfig_whois">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - case Val of - <<"moderators">> -> - ?SET_BOOL_XOPT(anonymous, - (iolist_to_binary(integer_to_list(1)))); - <<"anyone">> -> - ?SET_BOOL_XOPT(anonymous, - (iolist_to_binary(integer_to_list(0)))); - _ -> - Txt = <<"Value of 'muc#roomconfig_whois' should be " - "moderators|anyone">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)} - end; -set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - case Val of - <<"none">> -> ?SET_STRING_XOPT(max_users, none); - _ -> ?SET_NAT_XOPT(max_users, Val) - end; -set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]} - | Opts], - Config, ServerHost, Lang) -> - ?SET_BOOL_XOPT(logging, Val); -set_xoption([{<<"muc#roomconfig_captcha_whitelist">>, - Vals} - | Opts], - Config, ServerHost, Lang) -> - JIDs = [jid:from_string(Val) || Val <- Vals], - ?SET_JIDMULTI_XOPT(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, ?ERRT_BAD_REQUEST(Lang, ErrTxt)}, - 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 get_config_opt_name(pos_integer()) -> atom(). +get_config_opt_name(Pos) -> + Fs = [config|record_info(fields, config)], + lists:nth(Pos, Fs). + +-spec set_config([muc_roomconfig:property()], #config{}, + binary(), binary()) -> #config{} | {error, stanza_error()}. +set_config(Opts, Config, ServerHost, Lang) -> + lists:foldl( + fun(_, {error, _} = Err) -> Err; + ({roomname, Title}, C) -> C#config{title = Title}; + ({roomdesc, Desc}, C) -> C#config{description = Desc}; + ({changesubject, V}, C) -> C#config{allow_change_subj = V}; + ({allow_query_users, V}, C) -> C#config{allow_query_users = V}; + ({allow_private_messages, V}, C) -> + C#config{allow_private_messages = V}; + ({allow_private_messages_from_visitors, V}, C) -> + C#config{allow_private_messages_from_visitors = V}; + ({allow_visitor_status, V}, C) -> C#config{allow_visitor_status = V}; + ({allow_visitor_nickchange, V}, C) -> + C#config{allow_visitor_nickchange = V}; + ({publicroom, V}, C) -> C#config{public = V}; + ({public_list, V}, C) -> C#config{public_list = V}; + ({persistentroom, V}, C) -> C#config{persistent = V}; + ({moderatedroom, V}, C) -> C#config{moderated = V}; + ({members_by_default, V}, C) -> C#config{members_by_default = V}; + ({membersonly, V}, C) -> C#config{members_only = V}; + ({captcha_protected, V}, C) -> C#config{captcha_protected = V}; + ({allowinvites, V}, C) -> C#config{allow_user_invites = V}; + ({allow_subscription, V}, C) -> C#config{allow_subscription = V}; + ({passwordprotectedroom, V}, C) -> C#config{password_protected = V}; + ({roomsecret, V}, C) -> C#config{password = V}; + ({anonymous, V}, C) -> C#config{anonymous = V}; + ({presencebroadcast, V}, C) -> C#config{presence_broadcast = V}; + ({allow_voice_requests, V}, C) -> C#config{allow_voice_requests = V}; + ({voice_request_min_interval, V}, C) -> + C#config{voice_request_min_interval = V}; + ({whois, moderators}, C) -> C#config{anonymous = true}; + ({whois, anyone}, C) -> C#config{anonymous = false}; + ({maxusers, V}, C) -> C#config{max_users = V}; + ({enablelogging, V}, C) -> C#config{logging = V}; + ({pubsub, V}, C) -> C#config{pubsub = V}; + ({lang, L}, C) -> C#config{lang = L}; + ({captcha_whitelist, Js}, C) -> + LJIDs = [jid:tolower(J) || J <- Js], + C#config{captcha_whitelist = ?SETS:from_list(LJIDs)}; + ({O, V} = Opt, C) -> + case ejabberd_hooks:run_fold(set_room_option, + ServerHost, + {0, undefined}, + [Opt, Lang]) of + {0, undefined} -> + ?ERROR_MSG("set_room_option hook failed for " + "option '~ts' with value ~p", [O, V]), + Txt = {?T("Failed to process option '~ts'"), [O]}, + {error, xmpp:err_internal_server_error(Txt, Lang)}; + {Pos, Val} -> + setelement(Pos, C, Val) + end + end, Config, Opts). +-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, + StateData0 = StateData#state{config = Config}, + StateData1 = remove_subscriptions(StateData0), + StateData2 = + case {(StateData#state.config)#config.persistent, + Config#config.persistent} of + {WasPersistent, true} -> + if not WasPersistent -> + set_affiliations(StateData1#state.affiliations, + StateData1); + true -> + ok + end, + store_room(StateData1), + StateData1; + {true, false} -> + Affiliations = get_affiliations(StateData), + maybe_forget_room(StateData), + StateData1#state{affiliations = Affiliations}; + _ -> + StateData1 + end, case {(StateData#state.config)#config.members_only, - Config#config.members_only} - of - {false, true} -> - NSD1 = remove_nonmembers(NSD), {result, [], NSD1}; - _ -> {result, [], NSD} + Config#config.members_only} of + {false, true} -> + StateData3 = remove_nonmembers(StateData2), + {result, undefined, StateData3}; + _ -> + {result, undefined, StateData2} 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">>]; + {false, true} -> [170]; + {true, false} -> [171]; _ -> [] end ++ case {Old#config.anonymous, New#config.anonymous} of - {true, false} -> [<<"172">>]; - {false, true} -> [<<"173">>]; + {true, false} -> [172]; + {false, true} -> [173]; _ -> [] end ++ case Old#config{anonymous = New#config.anonymous, logging = New#config.logging} of New -> []; - _ -> [<<"104">>] + _ -> [104] end, - StatusEls = [#xmlel{name = <<"status">>, - attrs = [{<<"code">>, Code}], - children = []} || Code <- Codes], - Message = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}, - {<<"id">>, randoms:get_string()}], - children = [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_MUC_USER}], - children = StatusEls}]}, - send_wrapped_multiple(StateData#state.jid, - StateData#state.users, - Message, - ?NS_MUCSUB_NODES_CONFIG, - StateData). + if Codes /= [] -> + maps:fold( + fun(_LJID, #user{jid = JID}, _) -> + advertise_entity_capabilities(JID, StateData#state{config = New}) + end, ok, StateData#state.users), + Message = #message{type = groupchat, + id = p1_rand:get_string(), + sub_els = [#muc_user{status_codes = Codes}]}, + send_wrapped_multiple(StateData#state.jid, + get_users_and_subscribers(StateData), + Message, + ?NS_MUCSUB_NODES_CONFIG, + StateData); + true -> + ok + end. +-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)). + maps:fold( + fun(_LJID, #user{jid = JID}, SD) -> + Affiliation = get_affiliation(JID, SD), + case Affiliation of + none -> + catch send_kickban_presence(undefined, JID, <<"">>, 322, SD), + set_role(JID, none, SD); + _ -> SD + end + end, StateData, get_users_and_subscribers(StateData)). -set_opts([], StateData) -> StateData; +-spec set_opts([{atom(), any()}], state()) -> state(). +set_opts([], StateData) -> + set_vcard_xupdate(StateData); set_opts([{Opt, Val} | Opts], StateData) -> NSD = case Opt of title -> @@ -4382,374 +3836,526 @@ set_opts([{Opt, Val} | Opts], StateData) -> StateData#state{config = (StateData#state.config)#config{vcard = Val}}; + vcard_xupdate -> + StateData#state{config = + (StateData#state.config)#config{vcard_xupdate = + Val}}; + pubsub -> + StateData#state{config = + (StateData#state.config)#config{pubsub = Val}}; allow_subscription -> StateData#state{config = (StateData#state.config)#config{allow_subscription = Val}}; + lang -> + StateData#state{config = + (StateData#state.config)#config{lang = 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); + {Subscribers, Nicks} = + lists:foldl( + fun({JID, Nick, Nodes}, {SubAcc, NickAcc}) -> + BareJID = case JID of + #jid{} -> jid:remove_resource(JID); + _ -> + ?ERROR_MSG("Invalid subscriber JID in set_opts ~p", [JID]), + jid:remove_resource(jid:make(JID)) + end, + LBareJID = jid:tolower(BareJID), + {maps:put( + LBareJID, + #subscriber{jid = BareJID, + nick = Nick, + nodes = Nodes}, + SubAcc), + maps:put(Nick, [LBareJID], NickAcc)} + end, {#{}, #{}}, Val), + StateData#state{subscribers = Subscribers, + subscriber_nicks = Nicks}; affiliations -> - StateData#state{affiliations = (?DICT):from_list(Val)}; - subject -> StateData#state{subject = Val}; + StateData#state{affiliations = maps:from_list(Val)}; + subject -> + Subj = if Val == <<"">> -> []; + is_binary(Val) -> [#text{data = Val}]; + is_list(Val) -> Val + end, + StateData#state{subject = Subj}; subject_author -> StateData#state{subject_author = Val}; _ -> StateData end, set_opts(Opts, NSD). --define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}). +-spec set_vcard_xupdate(state()) -> state(). +set_vcard_xupdate(#state{config = + #config{vcard = VCardRaw, + vcard_xupdate = undefined} = Config} = State) + when VCardRaw /= <<"">> -> + case fxml_stream:parse_element(VCardRaw) of + {error, _} -> + State; + El -> + Hash = mod_vcard_xupdate:compute_hash(El), + State#state{config = Config#config{vcard_xupdate = Hash}} + end; +set_vcard_xupdate(State) -> + State. +-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(title), ?MAKE_CONFIG_OPT(description), - ?MAKE_CONFIG_OPT(allow_change_subj), - ?MAKE_CONFIG_OPT(allow_query_users), - ?MAKE_CONFIG_OPT(allow_private_messages), - ?MAKE_CONFIG_OPT(allow_private_messages_from_visitors), - ?MAKE_CONFIG_OPT(allow_visitor_status), - ?MAKE_CONFIG_OPT(allow_visitor_nickchange), - ?MAKE_CONFIG_OPT(public), ?MAKE_CONFIG_OPT(public_list), - ?MAKE_CONFIG_OPT(persistent), - ?MAKE_CONFIG_OPT(moderated), - ?MAKE_CONFIG_OPT(members_by_default), - ?MAKE_CONFIG_OPT(members_only), - ?MAKE_CONFIG_OPT(allow_user_invites), - ?MAKE_CONFIG_OPT(password_protected), - ?MAKE_CONFIG_OPT(captcha_protected), - ?MAKE_CONFIG_OPT(password), ?MAKE_CONFIG_OPT(anonymous), - ?MAKE_CONFIG_OPT(logging), ?MAKE_CONFIG_OPT(max_users), - ?MAKE_CONFIG_OPT(allow_voice_requests), - ?MAKE_CONFIG_OPT(mam), - ?MAKE_CONFIG_OPT(voice_request_min_interval), - ?MAKE_CONFIG_OPT(vcard), + Subscribers = maps:fold( + fun(_LJID, Sub, Acc) -> + [{Sub#subscriber.jid, + Sub#subscriber.nick, + Sub#subscriber.nodes}|Acc] + end, [], StateData#state.subscribers), + [?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.allow_subscription), + ?MAKE_CONFIG_OPT(#config.mam), + ?MAKE_CONFIG_OPT(#config.presence_broadcast), + ?MAKE_CONFIG_OPT(#config.voice_request_min_interval), + ?MAKE_CONFIG_OPT(#config.vcard), + ?MAKE_CONFIG_OPT(#config.vcard_xupdate), + ?MAKE_CONFIG_OPT(#config.pubsub), + ?MAKE_CONFIG_OPT(#config.lang), {captcha_whitelist, (?SETS):to_list((StateData#state.config)#config.captcha_whitelist)}, {affiliations, - (?DICT):to_list(StateData#state.affiliations)}, + maps:to_list(StateData#state.affiliations)}, {subject, StateData#state.subject}, {subject_author, StateData#state.subject_author}, {subscribers, Subscribers}]. +expand_opts(CompactOpts) -> + DefConfig = #config{}, + Fields = record_info(fields, config), + {_, Opts1} = + lists:foldl( + fun(Field, {Pos, Opts}) -> + case lists:keyfind(Field, 1, CompactOpts) of + false -> + DefV = element(Pos, DefConfig), + DefVal = case (?SETS):is_set(DefV) of + true -> (?SETS):to_list(DefV); + false -> DefV + end, + {Pos+1, [{Field, DefVal}|Opts]}; + {_, Val} -> + {Pos+1, [{Field, Val}|Opts]} + end + end, {2, []}, Fields), + SubjectAuthor = proplists:get_value(subject_author, CompactOpts, <<"">>), + Subject = proplists:get_value(subject, CompactOpts, <<"">>), + Subscribers = proplists:get_value(subscribers, CompactOpts, []), + [{subject, Subject}, + {subject_author, SubjectAuthor}, + {subscribers, Subscribers} + | lists:reverse(Opts1)]. + +config_fields() -> + [subject, subject_author, subscribers | record_info(fields, config)]. + +-spec destroy_room(muc_destroy(), state()) -> {result, undefined, stop}. destroy_room(DEl, StateData) -> - lists:foreach(fun ({_LJID, Info}) -> - Nick = Info#user.nick, - ItemAttrs = [{<<"affiliation">>, <<"none">>}, - {<<"role">>, <<"none">>}], - Packet = #xmlel{name = <<"presence">>, - attrs = - [{<<"type">>, <<"unavailable">>}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"item">>, - attrs = - ItemAttrs, - children = - []}, - DEl]}]}, - 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, [], stop}. + Destroy = DEl#muc_destroy{xmlns = ?NS_MUC_USER}, + maps:fold( + 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, ok, get_users_and_subscribers(StateData)), + forget_room(StateData), + {result, undefined, stop}. + +-spec forget_room(state()) -> state(). +forget_room(StateData) -> + mod_muc:forget_room(StateData#state.server_host, + StateData#state.host, + StateData#state.room), + StateData. + +-spec maybe_forget_room(state()) -> state(). +maybe_forget_room(StateData) -> + Forget = case (StateData#state.config)#config.persistent of + true -> + true; + _ -> + Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc), + erlang:function_exported(Mod, get_subscribed_rooms, 3) + end, + case Forget of + true -> + forget_room(StateData); + _ -> + StateData + end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Disco --define(FEATURE(Var), - #xmlel{name = <<"feature">>, attrs = [{<<"var">>, Var}], - children = []}). - -define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse), case Opt of - true -> ?FEATURE(Fiftrue); - false -> ?FEATURE(Fiffalse) + true -> Fiftrue; + false -> Fiffalse end). -process_iq_disco_info(_From, set, Lang, _StateData) -> - Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - {error, ?ERRT_NOT_ALLOWED(Lang, Txt)}; -process_iq_disco_info(_From, get, Lang, StateData) -> +-spec make_disco_info(jid(), state()) -> disco_info(). +make_disco_info(_From, StateData) -> Config = StateData#state.config, - {result, - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"conference">>}, - {<<"type">>, <<"text">>}, - {<<"name">>, get_title(StateData)}], - children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_VCARD}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_MUC}], children = []}, - ?CONFIG_OPT_TO_FEATURE((Config#config.public), - <<"muc_public">>, <<"muc_hidden">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), - <<"muc_persistent">>, <<"muc_temporary">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.members_only), - <<"muc_membersonly">>, <<"muc_open">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.anonymous), - <<"muc_semianonymous">>, <<"muc_nonanonymous">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.moderated), - <<"muc_moderated">>, <<"muc_unmoderated">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), - <<"muc_passwordprotected">>, <<"muc_unsecured">>)] - ++ case Config#config.allow_subscription of - true -> [?FEATURE(?NS_MUCSUB)]; - false -> [] - end - ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), - Config#config.mam} of - {true, true} -> - [?FEATURE(?NS_MAM_TMP), - ?FEATURE(?NS_MAM_0), - ?FEATURE(?NS_MAM_1)]; - _ -> - [] - end - ++ iq_disco_info_extras(Lang, StateData), - StateData}. - --define(RFIELDT(Type, Var, Val), - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, Type}, {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - --define(RFIELD(Label, Var, Val), - #xmlel{name = <<"field">>, - attrs = - [{<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - -iq_disco_info_extras(Lang, StateData) -> - Len = (?DICT):size(StateData#state.users), - RoomDescription = - (StateData#state.config)#config.description, - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], - children = - [?RFIELDT(<<"hidden">>, <<"FORM_TYPE">>, - <<"http://jabber.org/protocol/muc#roominfo">>), - ?RFIELD(<<"Room description">>, - <<"muc#roominfo_description">>, RoomDescription), - ?RFIELD(<<"Number of occupants">>, - <<"muc#roominfo_occupants">>, - (iolist_to_binary(integer_to_list(Len))))]}]. - -process_iq_disco_items(_From, set, Lang, _StateData) -> - Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - {error, ?ERRT_NOT_ALLOWED(Lang, Txt)}; -process_iq_disco_items(From, get, Lang, StateData) -> + Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?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, ?NS_MAM_2, ?NS_SID_0]; + _ -> + [] + end, + #disco_info{identities = [#identity{category = <<"conference">>, + type = <<"text">>, + name = get_title(StateData)}], + features = Feats}. + +-spec process_iq_disco_info(jid(), iq(), state()) -> + {result, disco_info()} | {error, stanza_error()}. +process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) -> + Txt = ?T("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, + sub_els = [#disco_info{node = <<>>}]}, + StateData) -> + DiscoInfo = make_disco_info(From, StateData), + Extras = iq_disco_info_extras(Lang, StateData, false), + {result, DiscoInfo#disco_info{xdata = [Extras]}}; +process_iq_disco_info(From, #iq{type = get, lang = Lang, + sub_els = [#disco_info{node = Node}]}, + StateData) -> + try + true = mod_caps:is_valid_node(Node), + DiscoInfo = make_disco_info(From, StateData), + Extras = iq_disco_info_extras(Lang, StateData, true), + DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]}, + Hash = mod_caps:compute_disco_hash(DiscoInfo1, sha), + Node = <<(ejabberd_config:get_uri())/binary, $#, Hash/binary>>, + {result, DiscoInfo1#disco_info{node = Node}} + catch _:{badmatch, _} -> + Txt = ?T("Invalid node name"), + {error, xmpp:err_item_not_found(Txt, Lang)} + end. + +-spec iq_disco_info_extras(binary(), state(), boolean()) -> xdata(). +iq_disco_info_extras(Lang, StateData, Static) -> + Config = StateData#state.config, + AllowPM = case Config#config.allow_private_messages of + false -> none; + true -> + case Config#config.allow_private_messages_from_visitors of + nobody -> participants; + _ -> anyone + end + end, + Fs1 = [{roomname, Config#config.title}, + {description, Config#config.description}, + {contactjid, get_owners(StateData)}, + {changesubject, Config#config.allow_change_subj}, + {allowinvites, Config#config.allow_user_invites}, + {allowpm, AllowPM}, + {lang, Config#config.lang}], + Fs2 = case Config#config.pubsub of + Node when is_binary(Node), Node /= <<"">> -> + [{pubsub, Node}|Fs1]; + _ -> + Fs1 + end, + Fs3 = case Static of + false -> + [{occupants, maps:size(StateData#state.nicks)}|Fs2]; + true -> + Fs2 + end, + Fs4 = case Config#config.logging of + true -> + case mod_muc_log:get_url(StateData) of + {ok, URL} -> + [{logs, URL}|Fs3]; + error -> + Fs3 + end; + false -> + Fs3 + end, + #xdata{type = result, + fields = muc_roominfo:encode(Fs4, Lang)}. + +-spec process_iq_disco_items(jid(), iq(), state()) -> + {error, stanza_error()} | {result, disco_items()}. +process_iq_disco_items(_From, #iq{type = set, lang = Lang}, _StateData) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + {error, xmpp:err_not_allowed(Txt, Lang)}; +process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>>}]}, + StateData) -> case (StateData#state.config)#config.public_list of true -> - {result, get_mucroom_disco_items(StateData), StateData}; + {result, get_mucroom_disco_items(StateData)}; _ -> case is_occupant_or_admin(From, StateData) of true -> - {result, get_mucroom_disco_items(StateData), StateData}; + {result, get_mucroom_disco_items(StateData)}; _ -> - Txt = <<"Only occupants or administrators can perform this query">>, - {error, ?ERRT_FORBIDDEN(Lang, Txt)} + %% If the list of occupants is private, + %% the room MUST return an empty <query/> element + %% (http://xmpp.org/extensions/xep-0045.html#disco-roomitems) + {result, #disco_items{}} end - end. - -process_iq_captcha(_From, get, Lang, _SubEl, + end; +process_iq_disco_items(_From, #iq{lang = Lang}, _StateData) -> + Txt = ?T("Node not found"), + {error, xmpp:err_item_not_found(Txt, Lang)}. + +-spec process_iq_captcha(jid(), iq(), state()) -> {error, stanza_error()} | + {result, undefined}. +process_iq_captcha(_From, #iq{type = get, lang = Lang}, _StateData) -> + Txt = ?T("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) -> - Txt = <<"Value 'get' of 'type' attribute is not allowed">>, - {error, ?ERRT_NOT_ALLOWED(Lang, Txt)}; -process_iq_captcha(_From, set, Lang, SubEl, - StateData) -> case ejabberd_captcha:process_reply(SubEl) of - ok -> {result, [], StateData}; + ok -> {result, undefined}; {error, malformed} -> - Txt = <<"Incorrect CAPTCHA submit">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; + Txt = ?T("Incorrect CAPTCHA submit"), + {error, xmpp:err_bad_request(Txt, Lang)}; _ -> - Txt = <<"The CAPTCHA verification has failed">>, - {error, ?ERRT_NOT_ALLOWED(Lang, Txt)} + Txt = ?T("The CAPTCHA verification has failed"), + {error, xmpp:err_not_allowed(Txt, Lang)} end. -process_iq_vcard(_From, get, _Lang, _SubEl, StateData) -> +-spec process_iq_vcard(jid(), iq(), state()) -> + {result, vcard_temp() | xmlel()} | + {result, undefined, state()} | + {error, stanza_error()}. +process_iq_vcard(_From, #iq{type = get}, StateData) -> #state{config = #config{vcard = VCardRaw}} = StateData, case fxml_stream:parse_element(VCardRaw) of - #xmlel{children = VCardEls} -> - {result, VCardEls, StateData}; + #xmlel{} = VCard -> + {result, VCard}; {error, _} -> - {result, [], StateData} + {error, xmpp:err_item_not_found()} end; -process_iq_vcard(From, set, Lang, SubEl, StateData) -> +process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [Pkt]}, + StateData) -> case get_affiliation(From, StateData) of owner -> + SubEl = xmpp:encode(Pkt), VCardRaw = fxml:element_to_binary(SubEl), + Hash = mod_vcard_xupdate:compute_hash(SubEl), Config = StateData#state.config, - NewConfig = Config#config{vcard = VCardRaw}, + NewConfig = Config#config{vcard = VCardRaw, vcard_xupdate = Hash}, change_config(NewConfig, StateData); _ -> - ErrText = <<"Owner privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + ErrText = ?T("Owner privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)} end. -process_iq_mucsub(From, Packet, +-spec process_iq_mucsub(jid(), iq(), state()) -> + {error, stanza_error()} | + {result, undefined | muc_subscribe() | muc_subscriptions(), state()} | + {ignore, state()}. +process_iq_mucsub(_From, #iq{type = set, lang = Lang, + sub_els = [#muc_subscribe{}]}, + #state{just_created = Just, config = #config{allow_subscription = false}}) when Just /= true -> + {error, xmpp:err_not_allowed(?T("Subscriptions are not allowed"), Lang)}; +process_iq_mucsub(From, #iq{type = set, lang = Lang, - sub_el = #xmlel{name = <<"subscribe">>} = SubEl}, - #state{config = Config} = StateData) -> - case fxml:get_tag_attr_s(<<"nick">>, SubEl) of - <<"">> -> - Err = ?ERRT_BAD_REQUEST(Lang, <<"Missing 'nick' attribute">>), - {error, Err}; - Nick when Config#config.allow_subscription -> - 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, ?ERRT_CONFLICT(Lang, ErrText)}; - {_, false} -> - ErrText = <<"That nickname is registered by another person">>, - {error, ?ERRT_CONFLICT(Lang, ErrText)}; - _ -> - NewStateData = add_online_user( - From, Nick, Role, true, Nodes, StateData), - {result, subscription_nodes_to_events(Nodes), NewStateData} - end; - {ok, #user{role = Role}} -> - Nodes = get_subscription_nodes(Packet), - NewStateData = add_online_user( - From, Nick, Role, true, Nodes, StateData), - {result, subscription_nodes_to_events(Nodes), NewStateData}; - error -> - add_new_user(From, Nick, Packet, StateData) + sub_els = [#muc_subscribe{jid = #jid{} = SubJid} = Mucsub]}, + StateData) -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> + process_iq_mucsub(SubJid, + #iq{type = set, lang = Lang, + sub_els = [Mucsub#muc_subscribe{jid = undefined}]}, + StateData); + true -> + Txt = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(Txt, Lang)} + end; +process_iq_mucsub(From, + #iq{type = set, lang = Lang, + sub_els = [#muc_subscribe{nick = Nick}]} = Packet, + StateData) -> + LBareJID = jid:tolower(jid:remove_resource(From)), + try maps:get(LBareJID, StateData#state.subscribers) of + #subscriber{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 = ?T("That nickname is already in use by another occupant"), + {error, xmpp:err_conflict(ErrText, Lang)}; + {_, false} -> + Err = case Nick of + <<>> -> + xmpp:err_jid_malformed(?T("Nickname can't be empty"), + Lang); + _ -> + xmpp:err_conflict(?T("That nickname is registered" + " by another person"), Lang) + end, + {error, Err}; + _ -> + NewStateData = set_subscriber(From, Nick, Nodes, StateData), + {result, subscribe_result(Packet), NewStateData} end; - _ -> - Err = ?ERRT_NOT_ALLOWED(Lang, <<"Subscriptions are not allowed">>), - {error, Err} + #subscriber{} -> + Nodes = get_subscription_nodes(Packet), + NewStateData = set_subscriber(From, Nick, Nodes, StateData), + {result, subscribe_result(Packet), NewStateData} + catch _:{badkey, _} -> + SD2 = StateData#state{config = (StateData#state.config)#config{allow_subscription = true}}, + add_new_user(From, Nick, Packet, SD2) end; -process_iq_mucsub(From, _Packet, - #iq{type = set, - sub_el = #xmlel{name = <<"unsubscribe">>}}, +process_iq_mucsub(From, #iq{type = set, lang = Lang, + sub_els = [#muc_unsubscribe{jid = #jid{} = UnsubJid}]}, 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, [], NewStateData}; - error when From#jid.lresource == <<"">> -> - {LUser, LServer, _} = LJID, - NewStateData = - dict:fold( - fun({U, S, _}, #user{jid = J, is_subscriber = true} = User, - AccState) when U == LUser, S == LServer -> - remove_subscription(J, User, AccState); - (_, _, AccState) -> - AccState - end, StateData, StateData#state.users), - store_room(NewStateData), - {result, [], NewStateData}; - _ -> - {result, [], StateData} + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> + process_iq_mucsub(UnsubJid, + #iq{type = set, lang = Lang, + sub_els = [#muc_unsubscribe{jid = undefined}]}, + StateData); + true -> + Txt = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(Txt, Lang)} end; -process_iq_mucsub(_From, _Packet, #iq{type = set, lang = Lang}, _StateData) -> - Txt = <<"Unrecognized subscription command">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; -process_iq_mucsub(_From, _Packet, #iq{type = get, lang = Lang}, _StateData) -> - Txt = <<"Value 'get' of 'type' attribute is not allowed">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}. - -remove_subscription(JID, #user{is_subscriber = true} = User, StateData) -> - case User#user.last_presence of - undefined -> - remove_online_user(JID, StateData, false); +process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]}, + #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> + BareJID = jid:remove_resource(From), + LBareJID = jid:tolower(BareJID), + try maps:get(LBareJID, StateData#state.subscribers) of + #subscriber{nick = Nick} -> + Nicks = maps:remove(Nick, StateData#state.subscriber_nicks), + Subscribers = maps:remove(LBareJID, StateData#state.subscribers), + NewStateData = StateData#state{subscribers = Subscribers, + subscriber_nicks = Nicks}, + store_room(NewStateData, [{del_subscription, LBareJID}]), + send_subscriptions_change_notifications(BareJID, Nick, unsubscribe, StateData), + ejabberd_hooks:run(muc_unsubscribed, ServerHost, [ServerHost, Room, Host, BareJID]), + NewStateData2 = case close_room_if_temporary_and_empty(NewStateData) of + {stop, normal, _} -> stop; + {next_state, normal_state, SD} -> SD + end, + {result, undefined, NewStateData2} + catch _:{badkey, _} -> + {result, undefined, StateData} + end; +process_iq_mucsub(From, #iq{type = get, lang = Lang, + sub_els = [#muc_subscriptions{}]}, + StateData) -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + IsModerator = FRole == moderator orelse FAffiliation == owner orelse + FAffiliation == admin, + case IsModerator orelse is_subscriber(From, StateData) of + true -> + ShowJid = IsModerator orelse + (StateData#state.config)#config.anonymous == false, + Subs = maps:fold( + fun(_, #subscriber{jid = J, nick = N, nodes = Nodes}, Acc) -> + case ShowJid of + true -> + [#muc_subscription{jid = J, events = Nodes}|Acc]; + _ -> + [#muc_subscription{nick = N, events = Nodes}|Acc] + end + end, [], StateData#state.subscribers), + {result, #muc_subscriptions{list = Subs}, StateData}; _ -> - LJID = jid:tolower(JID), - Users = ?DICT:store(LJID, User#user{is_subscriber = false}, - StateData#state.users), - StateData#state{users = Users} + Txt = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(Txt, Lang)} end; -remove_subscription(_JID, #user{}, StateData) -> - StateData. +process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) -> + Txt = ?T("Value 'get' of 'type' attribute is not allowed"), + {error, xmpp:err_bad_request(Txt, Lang)}. +-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); + StateData#state{subscribers = #{}, + subscriber_nicks = #{}}; true -> StateData end. -get_subscription_nodes(#xmlel{name = <<"iq">>} = Packet) -> - case fxml:get_subtag_with_xmlns(Packet, <<"subscribe">>, ?NS_MUCSUB) of - #xmlel{children = Els} -> - lists:flatmap( - fun(#xmlel{name = <<"event">>, attrs = Attrs}) -> - Node = fxml:get_attr_s(<<"node">>, Attrs), - case 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]) of - true -> - [Node]; - false -> - [] - end; - (_) -> - [] - end, Els); - false -> - [] - end; +-spec get_subscription_nodes(stanza()) -> [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, + ?NS_MUCSUB_NODES_SUBSCRIBERS]) + end, Nodes); get_subscription_nodes(_) -> []. -subscription_nodes_to_events(Nodes) -> - [#xmlel{name = <<"event">>, attrs = [{<<"node">>, Node}]} || Node <- 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), @@ -4763,352 +4369,153 @@ get_roomdesc_reply(JID, StateData, Tail) -> 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, ">>) + _ -> translate:translate(Lang, ?T("private, ")) end, - Len = (?DICT):fold(fun (_, _, Acc) -> Acc + 1 end, 0, - StateData#state.users), - <<" (", Desc/binary, - (iolist_to_binary(integer_to_list(Len)))/binary, ")">>. + Len = maps:size(StateData#state.nicks), + <<" (", Desc/binary, (integer_to_binary(Len))/binary, ")">>. +-spec get_mucroom_disco_items(state()) -> disco_items(). get_mucroom_disco_items(StateData) -> - lists:map(fun ({_LJID, Info}) -> - Nick = Info#user.nick, - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - jid:to_string({StateData#state.room, - StateData#state.host, - Nick})}, - {<<"name">>, Nick}], - children = []} - end, - (?DICT):to_list(StateData#state.users)). + Items = maps:fold( + fun(Nick, _, Acc) -> + [#disco_item{jid = jid:make(StateData#state.room, + StateData#state.host, + Nick), + name = Nick}|Acc] + end, [], StateData#state.nicks), + #disco_items{items = Items}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Voice request support -is_voice_request(Els) -> - lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = - El, - false) -> - case fxml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_XDATA -> - case jlib:parse_xdata_submit(El) of - [_ | _] = Fields -> - case {lists:keysearch(<<"FORM_TYPE">>, 1, - Fields), - lists:keysearch(<<"muc#role">>, 1, - Fields)} - of - {{value, - {_, - [<<"http://jabber.org/protocol/muc#request">>]}}, - {value, {_, [<<"participant">>]}}} -> - true; - _ -> false - end; - _ -> false - end; - _ -> false - end; - (_, Acc) -> Acc - end, - false, Els). - +-spec prepare_request_form(jid(), binary(), binary()) -> message(). prepare_request_form(Requester, Nick, Lang) -> - #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"normal">>}], - children = - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Voice request">>)}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Either approve or decline the voice " - "request.">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"FORM_TYPE">>}, - {<<"type">>, <<"hidden">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"http://jabber.org/protocol/muc#request">>}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"muc#role">>}, - {<<"type">>, <<"hidden">>}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = - [{xmlcdata, - <<"participant">>}]}]}, - ?STRINGXFIELD(<<"User JID">>, <<"muc#jid">>, - (jid:to_string(Requester))), - ?STRINGXFIELD(<<"Nickname">>, <<"muc#roomnick">>, - Nick), - ?BOOLXFIELD(<<"Grant voice to this person?">>, - <<"muc#request_allow">>, - (jlib:binary_to_atom(<<"false">>)))]}]}. - -send_voice_request(From, StateData) -> + Title = translate:translate(Lang, ?T("Voice request")), + Instruction = translate:translate( + Lang, ?T("Either approve or decline the voice request.")), + Fs = muc_request:encode([{role, participant}, + {jid, Requester}, + {roomnick, Nick}, + {request_allow, false}], + Lang), + #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, <<"">>)) - end, - Moderators). - -is_voice_approvement(Els) -> - lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = - El, - false) -> - case fxml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_XDATA -> - case jlib:parse_xdata_submit(El) of - [_ | _] = Fs -> - case {lists:keysearch(<<"FORM_TYPE">>, 1, - Fs), - lists:keysearch(<<"muc#role">>, 1, - Fs), - lists:keysearch(<<"muc#request_allow">>, - 1, Fs)} - of - {{value, - {_, - [<<"http://jabber.org/protocol/muc#request">>]}}, - {value, {_, [<<"participant">>]}}, - {value, {_, [Flag]}}} - when Flag == <<"true">>; - Flag == <<"1">> -> - true; - _ -> false - end; - _ -> false - end; - _ -> false - end; - (_, Acc) -> Acc - end, - false, Els). - -extract_jid_from_voice_approvement(Els) -> - lists:foldl(fun (#xmlel{name = <<"x">>} = El, error) -> - Fields = case jlib:parse_xdata_submit(El) of - invalid -> []; - Res -> Res - end, - lists:foldl(fun ({<<"muc#jid">>, [JIDStr]}, error) -> - case jid:from_string(JIDStr) of - error -> error; - J -> {ok, J} - end; - (_, Acc) -> Acc - end, - error, Fields); - (_, Acc) -> Acc - end, - error, Els). + lists:foreach( + fun({_, User}) -> + ejabberd_router:route( + xmpp:set_from_to( + prepare_request_form(From, FromNick, Lang), + StateData#state.jid, User#user.jid)) + end, Moderators). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Invitation support - -is_invitation(Els) -> - lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = - El, - false) -> - case fxml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_MUC_USER -> - case fxml:get_subtag(El, <<"invite">>) of - false -> false; - _ -> true - end; - _ -> false - end; - (_, Acc) -> Acc - end, - false, Els). - -check_invitation(From, Packet, Lang, StateData) -> +-spec check_invitation(jid(), [muc_invite()], binary(), state()) -> + ok | {error, stanza_error()}. +check_invitation(From, Invitations, Lang, StateData) -> FAffiliation = get_affiliation(From, StateData), - CanInvite = - (StateData#state.config)#config.allow_user_invites - orelse - FAffiliation == admin orelse FAffiliation == owner, - InviteEl = case fxml:get_subtag_with_xmlns(Packet, <<"x">>, ?NS_MUC_USER) of - false -> - Txt1 = <<"No 'x' element found">>, - throw({error, ?ERRT_BAD_REQUEST(Lang, Txt1)}); - XEl -> - case fxml:get_subtag(XEl, <<"invite">>) of - false -> - Txt2 = <<"No 'invite' element found">>, - throw({error, ?ERRT_BAD_REQUEST(Lang, Txt2)}); - InviteEl1 -> - InviteEl1 - end - end, - JID = case - jid:from_string(fxml:get_tag_attr_s(<<"to">>, - InviteEl)) - of - error -> - Txt = <<"Incorrect value of 'to' attribute">>, - throw({error, ?ERRT_JID_MALFORMED(Lang, Txt)}); - JID1 -> JID1 - end, + CanInvite = (StateData#state.config)#config.allow_user_invites orelse + FAffiliation == admin orelse FAffiliation == owner, case CanInvite of - false -> - Txt3 = <<"Invitations are not allowed in this conference">>, - throw({error, ?ERRT_NOT_ALLOWED(Lang, Txt3)}); - true -> - Reason = fxml:get_path_s(InviteEl, - [{elem, <<"reason">>}, cdata]), - ContinueEl = case fxml:get_path_s(InviteEl, - [{elem, <<"continue">>}]) - of - <<>> -> []; - Continue1 -> [Continue1] - end, - IEl = [#xmlel{name = <<"invite">>, - attrs = [{<<"from">>, jid:to_string(From)}], - children = - [#xmlel{name = <<"reason">>, attrs = [], - children = [{xmlcdata, Reason}]}] - ++ ContinueEl}], - PasswdEl = case - (StateData#state.config)#config.password_protected - of - true -> - [#xmlel{name = <<"password">>, attrs = [], - children = - [{xmlcdata, - (StateData#state.config)#config.password}]}]; - _ -> [] - end, - Body = #xmlel{name = <<"body">>, attrs = [], - children = - [{xmlcdata, - iolist_to_binary( - [io_lib:format( - translate:translate( - Lang, - <<"~s invites you to the room ~s">>), - [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 = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"normal">>}], - children = - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_MUC_USER}], - children = IEl ++ PasswdEl}, - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XCONFERENCE}, - {<<"jid">>, - jid:to_string({StateData#state.room, - StateData#state.host, - <<"">>})}], - children = [{xmlcdata, Reason}]}, - Body]}, - ejabberd_router:route(StateData#state.jid, JID, Msg), - JID + true -> + case lists:all( + fun(#muc_invite{to = #jid{}}) -> true; + (_) -> false + end, Invitations) of + true -> + ok; + false -> + Txt = ?T("No 'to' attribute found in the invitation"), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + false -> + Txt = ?T("Invitations are not allowed in this conference"), + {error, xmpp:err_not_allowed(Txt, Lang)} end. +-spec route_invitation(jid(), message(), muc_invite(), binary(), state()) -> jid(). +route_invitation(From, Pkt, Invitation, Lang, StateData) -> + #muc_invite{to = JID, reason = Reason} = Invitation, + Invite = Invitation#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, + ?T("~ts invites you to the room ~ts")), + [jid:encode(From), + jid:encode({StateData#state.room, StateData#state.host, <<"">>})]), + case (StateData#state.config)#config.password_protected of + true -> + <<", ", + (translate:translate( + Lang, ?T("the password is")))/binary, + " '", + ((StateData#state.config)#config.password)/binary, + "'">>; + _ -> <<"">> + end, + case Reason of + <<"">> -> <<"">>; + _ -> <<" (", Reason/binary, ") ">> + end]), + Msg = #message{from = StateData#state.jid, + to = JID, + type = normal, + body = xmpp:mk_text(Body), + sub_els = [XUser, XConference]}, + Msg2 = ejabberd_hooks:run_fold(muc_invite, + StateData#state.server_host, + Msg, + [StateData#state.jid, StateData#state.config, + From, JID, Reason, Pkt]), + ejabberd_router:route(Msg2), + JID. + %% Handle a message sent to the room by a non-participant. %% If it is a decline, send to the inviter. %% Otherwise, an error message is sent to the sender. -handle_roommessage_from_nonparticipant(Packet, Lang, - StateData, From) -> - case catch check_decline_invitation(Packet) of - {true, Decline_data} -> - send_decline_invitation(Decline_data, - StateData#state.jid, From); - _ -> - send_error_only_occupants(Packet, Lang, - StateData#state.jid, From) +-spec handle_roommessage_from_nonparticipant(message(), state(), jid()) -> ok. +handle_roommessage_from_nonparticipant(Packet, StateData, From) -> + try xmpp:try_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( + xmpp:set_from_to(NewPacket, StateData#state.jid, To)); + _ -> + ErrText = ?T("Only occupants are allowed to send messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, xmpp:get_lang(Packet)), + ejabberd_router:route_error(Packet, Err) + catch _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, xmpp:get_lang(Packet)), + ejabberd_router:route_error(Packet, Err) end. -%% Check in the packet is a decline. -%% If so, also returns the splitted packet. -%% This function must be catched, -%% because it crashes when the packet is not a decline message. -check_decline_invitation(Packet) -> - #xmlel{name = <<"message">>} = Packet, - XEl = fxml:get_subtag(Packet, <<"x">>), - (?NS_MUC_USER) = fxml:get_tag_attr_s(<<"xmlns">>, XEl), - DEl = fxml:get_subtag(XEl, <<"decline">>), - ToString = fxml:get_tag_attr_s(<<"to">>, DEl), - ToJID = jid:from_string(ToString), - {true, {Packet, XEl, DEl, ToJID}}. - -%% Send the decline to the inviter user. -%% The original stanza must be slightly modified. -send_decline_invitation({Packet, XEl, DEl, ToJID}, - RoomJID, FromJID) -> - FromString = - jid:to_string(jid:remove_resource(FromJID)), - #xmlel{name = <<"decline">>, attrs = DAttrs, - children = DEls} = - DEl, - DAttrs2 = lists:keydelete(<<"to">>, 1, DAttrs), - DAttrs3 = [{<<"from">>, FromString} | DAttrs2], - DEl2 = #xmlel{name = <<"decline">>, attrs = DAttrs3, - children = DEls}, - XEl2 = replace_subelement(XEl, DEl2), - Packet2 = replace_subelement(Packet, XEl2), - ejabberd_router:route(RoomJID, ToJID, Packet2). - -%% Given an element and a new subelement, -%% replace the instance of the subelement in element with the new subelement. -replace_subelement(#xmlel{name = Name, attrs = Attrs, - children = SubEls}, - NewSubEl) -> - {_, NameNewSubEl, _, _} = NewSubEl, - SubEls2 = lists:keyreplace(NameNewSubEl, 2, SubEls, NewSubEl), - #xmlel{name = Name, attrs = Attrs, children = SubEls2}. - -send_error_only_occupants(Packet, Lang, RoomJID, From) -> - ErrText = - <<"Only occupants are allowed to send messages " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route(RoomJID, From, Err). - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Logging @@ -5129,96 +4536,210 @@ add_to_log(Type, Data, StateData) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Users number checking +-spec tab_add_online_user(jid(), state()) -> any(). 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}). + ServerHost = StateData#state.server_host, + ejabberd_hooks:run(join_room, ServerHost, [ServerHost, Room, Host, JID]), + mod_muc:register_online_user(ServerHost, jid:tolower(JID), Room, Host). +-spec tab_remove_online_user(jid(), state()) -> any(). 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}). + ServerHost = StateData#state.server_host, + ejabberd_hooks:run(leave_room, ServerHost, [ServerHost, Room, Host, JID]), + mod_muc:unregister_online_user(ServerHost, jid:tolower(JID), Room, Host). -tab_count_user(JID) -> +-spec tab_count_user(jid(), state()) -> non_neg_integer(). +tab_count_user(JID, StateData) -> + ServerHost = StateData#state.server_host, {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. + mod_muc:count_online_rooms_by_user(ServerHost, LUser, LServer). +-spec element_size(stanza()) -> non_neg_integer(). element_size(El) -> - byte_size(fxml:element_to_binary(El)). + byte_size(fxml:element_to_binary(xmpp:encode(El, ?NS_CLIENT))). +-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)); + store_room(StateData, []). +store_room(StateData, ChangesHints) -> + % Let store persistent rooms or on those backends that have get_subscribed_rooms + Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc), + HasGSR = erlang:function_exported(Mod, get_subscribed_rooms, 3), + case HasGSR of + true -> + ok; + _ -> + erlang:put(muc_subscribers, StateData#state.subscribers) + end, + ShouldStore = case (StateData#state.config)#config.persistent of + true -> + true; + _ -> + case ChangesHints of + [] -> + false; + _ -> + HasGSR + end + end, + if ShouldStore -> + store_room_no_checks(StateData, ChangesHints); true -> ok end. +store_room_no_checks(StateData, ChangesHints) -> + mod_muc:store_room(StateData#state.server_host, + StateData#state.host, StateData#state.room, + make_opts(StateData), + ChangesHints). + +-spec send_subscriptions_change_notifications(jid(), binary(), subscribe|unsubscribe, state()) -> ok. +send_subscriptions_change_notifications(From, Nick, Type, State) -> + maps:fold(fun(_, #subscriber{nodes = Nodes, jid = JID}, _) -> + case lists:member(?NS_MUCSUB_NODES_SUBSCRIBERS, Nodes) of + true -> + ShowJid = case (State#state.config)#config.anonymous == false orelse + get_role(JID, State) == moderator orelse + get_default_role(get_affiliation(JID, State), State) == moderator of + true -> true; + _ -> false + end, + Payload = case {Type, ShowJid} of + {subscribe, true} -> + #muc_subscribe{jid = From, nick = Nick}; + {subscribe, _} -> + #muc_subscribe{nick = Nick}; + {unsubscribe, true} -> + #muc_unsubscribe{jid = From, nick = Nick}; + {unsubscribe, _} -> + #muc_unsubscribe{nick = Nick} + end, + Packet = #message{ + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_SUBSCRIBERS, + items = [#ps_item{ + id = p1_rand:get_string(), + sub_els = [Payload]}]}}]}, + ejabberd_router:route(xmpp:set_from_to(Packet, State#state.jid, JID)); + false -> + ok + end + end, ok, State#state.subscribers). + +-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 -> + LBareTo = jid:tolower(jid:remove_resource(To)), + IsOffline = case maps:get(LTo, State#state.users, error) of + #user{last_presence = undefined} -> true; + error -> true; + _ -> false + end, + if IsOffline -> + try maps:get(LBareTo, State#state.subscribers) of + #subscriber{nodes = Nodes, jid = JID} -> + case lists:member(Node, Nodes) of + true -> + MamEnabled = (State#state.config)#config.mam, + Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of + #stanza_id{id = Id2} -> + Id2; + _ -> + p1_rand:get_string() + end, + NewPacket = wrap(From, JID, Packet, Node, Id), + NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled), + ejabberd_router:route( + xmpp:set_from_to(NewPacket2, State#state.jid, JID)); + false -> + ok + end + catch _:{badkey, _} -> ok end; - _ -> - ejabberd_router:route(From, To, Packet) + true -> + case Packet of + #presence{type = unavailable} -> + case xmpp:get_subtag(Packet, #muc_user{}) of + #muc_user{destroy = Destroy, + status_codes = Codes} -> + case Destroy /= undefined orelse + (lists:member(110,Codes) andalso + not lists:member(303, Codes)) of + true -> + ejabberd_router:route( + #presence{from = State#state.jid, to = To, + id = p1_rand:get_string(), + type = unavailable}); + false -> + ok + end; + _ -> + false + end; + _ -> + ok + end, + ejabberd_router:route(xmpp:set_from_to(Packet, From, To)) end. -wrap(From, To, Packet, Node) -> - Pkt1 = jlib:replace_from_to(From, To, Packet), - Pkt2 = #xmlel{attrs = Attrs} = jlib:remove_attr(<<"xmlns">>, Pkt1), - Pkt3 = Pkt2#xmlel{attrs = [{<<"xmlns">>, <<"jabber:client">>}|Attrs]}, - Item = #xmlel{name = <<"item">>, - attrs = [{<<"id">>, randoms:get_string()}], - children = [Pkt3]}, - Items = #xmlel{name = <<"items">>, attrs = [{<<"node">>, Node}], - children = [Item]}, - Event = #xmlel{name = <<"event">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB_EVENT}], - children = [Items]}, - #xmlel{name = <<"message">>, children = [Event]}. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Multicast - -send_multiple(From, Server, Users, Packet) -> - JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], - ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). - +-spec wrap(jid(), jid(), stanza(), binary(), binary()) -> message(). +wrap(From, To, Packet, Node, Id) -> + El = xmpp:set_from_to(Packet, From, To), + #message{ + id = Id, + sub_els = [#ps_event{ + items = #ps_items{ + node = Node, + items = [#ps_item{ + id = Id, + sub_els = [El]}]}}]}. + +-spec send_wrapped_multiple(jid(), users(), stanza(), binary(), state()) -> ok. send_wrapped_multiple(From, Users, Packet, Node, State) -> - lists:foreach( - fun({_, #user{jid = To}}) -> + maps:fold( + fun(_, #user{jid = To}, _) -> send_wrapped(From, To, Packet, Node, State) - end, ?DICT:to_list(Users)). + end, ok, Users). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Detect messange stanzas that don't have meaninful content - -has_body_or_subject(Packet) -> - [] /= lists:dropwhile(fun - (#xmlel{name = <<"body">>}) -> false; - (#xmlel{name = <<"subject">>}) -> false; - (_) -> true - end, Packet#xmlel.children). +%% Detect messange stanzas that don't have meaningful content +-spec has_body_or_subject(message()) -> boolean(). +has_body_or_subject(#message{body = Body, subject = Subj}) -> + Body /= [] orelse Subj /= []. + +-spec reset_hibernate_timer(state()) -> state(). +reset_hibernate_timer(State) -> + case State#state.hibernate_timer of + hibernating -> + ok; + _ -> + disable_hibernate_timer(State), + NewTimer = case {mod_muc_opt:hibernation_timeout(State#state.server_host), + maps:size(State#state.users)} of + {infinity, _} -> + none; + {Timeout, 0} -> + p1_fsm:send_event_after(Timeout, hibernate); + _ -> + none + end, + State#state{hibernate_timer = NewTimer} + end. + + +-spec disable_hibernate_timer(state()) -> ok. +disable_hibernate_timer(State) -> + case State#state.hibernate_timer of + Ref when is_reference(Ref) -> + p1_fsm:cancel_timer(Ref), + ok; + _ -> + ok + end. |