diff options
author | Evgeniy Khramtsov <ekhramtsov@process-one.net> | 2016-07-25 13:50:30 +0300 |
---|---|---|
committer | Evgeniy Khramtsov <ekhramtsov@process-one.net> | 2016-07-25 13:50:30 +0300 |
commit | 179fcd9521ef8db4626ca110ba80c502d810c814 (patch) | |
tree | 78e0b2410b0f8a4cbe95f84bfb30e58d1b205e3e /src/mod_muc_room.erl | |
parent | Fix hooks de-registration (diff) |
Rewrite mod_mam and mod_muc to use XML generator
Diffstat (limited to 'src/mod_muc_room.erl')
-rw-r--r-- | src/mod_muc_room.erl | 4469 |
1 files changed, 1865 insertions, 2604 deletions
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 773953c4..29b7942c 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -51,7 +51,7 @@ -include("ejabberd.hrl"). -include("logger.hrl"). --include("jlib.hrl"). +-include("xmpp.hrl"). -include("mod_muc_room.hrl"). @@ -72,6 +72,18 @@ -endif. +-type state() :: #state{}. +-type fsm_stop() :: {stop, normal, state()}. +-type fsm_next() :: {next_state, normal_state, state()}. +-type fsm_transition() :: fsm_stop() | fsm_next(). +-type history_element() :: {binary(), %% nick + message(), %% message itself + boolean(), %% have subject + erlang:timestamp(), + non_neg_integer()}. + +-export_type([state/0]). + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- @@ -133,349 +145,187 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) -> {ok, normal_state, State}. normal_state({route, From, <<"">>, - #xmlel{name = <<"message">>, attrs = Attrs, - children = Els} = - Packet}, - StateData) -> - Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), + #message{type = Type, lang = Lang} = Packet}, StateData) -> 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_user_allowed_message_nonparticipant(From, StateData) of + true when Type == groupchat -> + Activity = get_user_activity(From, StateData), + Now = p1_time_compat:system_time(micro_seconds), + MinMessageInterval = trunc(gen_mod:get_module_opt( + StateData#state.server_host, + mod_muc, min_message_interval, + fun(MMI) when is_number(MMI) -> MMI end, 0) + * 1000000), + Size = element_size(Packet), + {MessageShaper, MessageShaperInterval} = + shaper:update(Activity#activity.message_shaper, Size), + if Activity#activity.message /= undefined -> + ErrText = <<"Traffic rate limit is exceeded">>, + Err = xmpp:make_error( + Packet, + xmpp:err_resource_constraint(ErrText, Lang)), + ejabberd_router:route(StateData#state.jid, From, Err), {next_state, normal_state, StateData}; - #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} = + 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 -> - 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 = queue:in({message, From}, + StateData#state.room_queue), + StateData2 = store_user_activity(From, + NewActivity, + StateData1), + StateData3 = StateData2#state{room_queue = RoomQueue}, + {next_state, normal_state, StateData3} end; + true -> + MessageInterval = (Activity#activity.message_time + + MinMessageInterval - Now) div 1000, + Interval = lists:max([MessageInterval, + MessageShaperInterval]), + erlang:send_after(Interval, self(), + {process_user_message, From}), + NewActivity = Activity#activity{ + message = Packet, + message_shaper = MessageShaper}, + StateData1 = store_user_activity(From, NewActivity, StateData), + {next_state, normal_state, StateData1} + end; + true when Type == error -> + case is_user_online(From, StateData) of + true -> + ErrorText = <<"It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room">>, + NewState = expulse_participant(Packet, From, StateData, + translate:translate(Lang, + ErrorText)), + close_room_if_temporary_and_empty(NewState); _ -> - 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 = <<"It is not allowed to send private messages " + "to the conference">>, + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(StateData#state.jid, From, Packet, Err), + {next_state, normal_state, StateData}; + true when Type == normal -> + {next_state, normal_state, + try xmpp:decode_els(Packet) of + Pkt -> process_normal_message(From, Pkt, StateData) + catch _:{xmpp_codec, Why} -> + Txt = xmpp:format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error( + StateData#state.jid, From, Packet, Err), + StateData + end}; + true -> + ErrText = <<"Improper message type">>, + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(StateData#state.jid, From, Packet, Err), + {next_state, normal_state, StateData}; + false when Type /= error -> + handle_roommessage_from_nonparticipant(Packet, StateData, From); + false -> + {next_state, normal_state, StateData} end; -normal_state({route, From, Nick, - #xmlel{name = <<"presence">>} = Packet}, - StateData) -> +normal_state({route, From, <<"">>, + #iq{type = Type, lang = Lang, sub_els = [_]} = IQ0}, + StateData) when Type == get; Type == set -> + try + case ejabberd_hooks:run_fold( + muc_process_iq, + StateData#state.server_host, + xmpp:set_from_to(xmpp:decode_els(IQ0), + From, StateData#state.jid), + [StateData]) of + ignore -> + {next_state, normal_state, StateData}; + #iq{type = T} = IQRes when T == error; T == result -> + ejabberd_router:route(StateData#state.jid, From, IQRes), + {next_state, normal_state, StateData}; + #iq{sub_els = [SubEl]} = IQ -> + Res1 = case xmpp:get_ns(SubEl) of + ?NS_MUC_ADMIN -> + process_iq_admin(From, IQ, StateData); + ?NS_MUC_OWNER -> + process_iq_owner(From, IQ, StateData); + ?NS_DISCO_INFO when SubEl#disco_info.node == undefined -> + process_iq_disco_info(From, IQ, StateData); + ?NS_DISCO_INFO -> + Txt = <<"Disco info is not available for this node">>, + {error, xmpp:err_service_unavailable(Txt, Lang)}; + ?NS_DISCO_ITEMS -> + process_iq_disco_items(From, IQ, StateData); + ?NS_VCARD -> + process_iq_vcard(From, IQ, StateData); + ?NS_MUCSUB -> + process_iq_mucsub(From, IQ, StateData); + ?NS_CAPTCHA -> + process_iq_captcha(From, IQ, StateData); + _ -> + {error, xmpp:err_feature_not_implemented()} + end, + {IQRes, NewStateData} = + case Res1 of + {result, Res, SD} -> + {xmpp:make_iq_result(IQ, Res), SD}; + {result, Res} -> + {xmpp:make_iq_result(IQ, Res), StateData}; + {ignore, SD} -> + {ignore, SD}; + {error, Error, ResStateData} -> + {xmpp:make_error(IQ0, Error), ResStateData}; + {error, Error} -> + {xmpp:make_error(IQ0, Error), StateData} + end, + if IQRes /= ignore -> + ejabberd_router:route(StateData#state.jid, From, IQRes); + true -> + ok + end, + case NewStateData of + stop -> {stop, normal, StateData}; + _ -> {next_state, normal_state, NewStateData} + end + end + catch _:{xmpp_codec, Why} -> + ErrTxt = xmpp:format_error(Why), + Err = xmpp:make_error(IQ0, xmpp:err_bad_request(ErrTxt, Lang)), + ejabberd_router:route(StateData#state.jid, From, Err) + end; +normal_state({route, From, <<"">>, #iq{} = IQ}, StateData) -> + Err = xmpp:err_bad_request(), + ejabberd_router:route_error(StateData#state.jid, From, IQ, Err), + {next_state, normal_state, StateData}; +normal_state({route, From, Nick, #presence{} = Packet}, StateData) -> Activity = get_user_activity(From, StateData), Now = p1_time_compat:system_time(micro_seconds), MinPresenceInterval = @@ -485,185 +335,135 @@ normal_state({route, From, Nick, 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); + if (Now >= Activity#activity.presence_time + MinPresenceInterval) + and (Activity#activity.presence == undefined) -> + NewActivity = Activity#activity{presence_time = Now}, + StateData1 = store_user_activity(From, NewActivity, + StateData), + process_presence(From, Nick, Packet, StateData1); true -> - if Activity#activity.presence == undefined -> - Interval = (Activity#activity.presence_time + - MinPresenceInterval - - Now) - div 1000, - erlang:send_after(Interval, self(), - {process_user_presence, From}); - true -> ok - end, - NewActivity = Activity#activity{presence = - {Nick, Packet}}, - StateData1 = store_user_activity(From, NewActivity, - StateData), - {next_state, normal_state, StateData1} + 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}, + #message{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 = <<"It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room">>, + NewState = expulse_participant(Packet, From, StateData, + translate:translate(Lang, ErrorText)), + {next_state, normal_state, NewState}; + forget_message -> + {next_state, normal_state, StateData}; + continue_delivery -> + case {(StateData#state.config)#config.allow_private_messages, + is_user_online(From, StateData)} of + {true, true} when Type == groupchat -> + ErrText = <<"It is not allowed to send private messages " + "of type \"groupchat\"">>, + Err = xmpp:err_bad_request(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, ToNick), + From, Packet, Err); + {true, true} -> + case find_jids_by_nick(ToNick, StateData) of + [] -> + ErrText = <<"Recipient is not in the conference room">>, + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, ToNick), + From, Packet, Err); ToJIDs -> SrcIsVisitor = is_visitor(From, StateData), - DstIsModerator = is_moderator(hd(ToJIDs), - StateData), + 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 -> + {ok, #user{nick = FromNick}} = + (?DICT):find(jid:tolower(From), + StateData#state.users), + FromNickJID = + jid:replace_resource(StateData#state.jid, + FromNick), + X = #muc_user{}, + PrivMsg = xmpp:set_subtag(Packet, X), + [ejabberd_router:route(FromNickJID, ToJID, PrivMsg) + || ToJID <- ToJIDs]; true -> - ErrText = - <<"It is not allowed to send private messages">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, - ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - ToNick), - From, Err) + ErrText = <<"It is not allowed to send private messages">>, + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, ToNick), + From, Packet, Err) end - end - 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 = <<"Only occupants are allowed to send messages " + "to the conference">>, + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, ToNick), + From, Packet, Err); + {false, _} -> + ErrText = <<"It is not allowed to send private messages">>, + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, ToNick), + From, Packet, Err) + end, {next_state, normal_state, StateData} end; normal_state({route, From, ToNick, - #xmlel{name = <<"iq">>, attrs = Attrs} = Packet}, + #iq{id = StanzaId, lang = Lang} = 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 + is_user_online_iq(StanzaId, From, StateData)} of + {true, {true, NewId, FromFull}} -> + case find_jid_by_nick(ToNick, StateData) of + false -> + ErrText = <<"Recipient is not in the conference room">>, + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, ToNick), + From, Packet, Err); + ToJID -> + {ok, #user{nick = FromNick}} = + (?DICT):find(jid:tolower(FromFull), StateData#state.users), + {ToJID2, Packet2} = handle_iq_vcard(ToJID, NewId, Packet), + ejabberd_router:route( + jid:replace_resource(StateData#state.jid, FromNick), + ToJID2, Packet2) + end; + {_, {false, _, _}} -> + ErrText = <<"Only occupants are allowed to send queries " + "to the conference">>, + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, ToNick), + From, Packet, Err); + _ -> + ErrText = <<"Queries to the conference members are " + "not allowed in this room">>, + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, ToNick), + From, Packet, Err) end, {next_state, normal_state, StateData}; normal_state(_Event, StateData) -> @@ -671,11 +471,7 @@ normal_state(_Event, 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, @@ -687,22 +483,9 @@ 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), + {result, undefined, stop} = + destroy_room(#muc_destroy{xmlns = ?NS_MUC_OWNER, reason = Reason}, + StateData), ?INFO_MSG("Destroyed MUC room ~s with reason: ~p", [jid:to_string(StateData#state.jid), Reason]), add_to_log(room_existence, destroyed, StateData), @@ -710,7 +493,7 @@ handle_event({destroy, Reason}, _StateName, handle_event(destroy, StateName, StateData) -> ?INFO_MSG("Destroyed MUC room ~s", [jid:to_string(StateData#state.jid)]), - handle_event({destroy, none}, StateName, StateData); + handle_event({destroy, undefined}, StateName, StateData); handle_event({set_affiliations, Affiliations}, StateName, StateData) -> {next_state, StateName, @@ -741,7 +524,7 @@ 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) -> @@ -821,12 +604,11 @@ handle_info({captcha_failed, From}, normal_state, {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), + Lang = xmpp:get_lang(Packet), + Err = xmpp:err_not_authorized(Txt, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, Nick), + From, Packet, Err), StateData#state{robots = Robots}; _ -> StateData end, @@ -845,22 +627,12 @@ terminate(Reason, _StateName, StateData) -> "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 = []}]}]}, + Packet = #presence{ + type = unavailable, + sub_els = [#muc_user{items = [#muc_item{affiliation = none, + reason = ReasonT, + role = none}], + status_codes = [332]}]}, (?DICT):fold(fun (LJID, Info, _) -> Nick = Info#user.nick, case Reason of @@ -883,14 +655,12 @@ terminate(Reason, _StateName, StateData) -> %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- - +-spec route(pid(), jid(), binary(), stanza()) -> ok. 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), +-spec process_groupchat_message(jid(), message(), state()) -> fsm_next(). +process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) -> case is_user_online(From, StateData) orelse is_user_allowed_message_nonparticipant(From, StateData) of @@ -932,7 +702,7 @@ process_groupchat_message(From, drop -> {next_state, normal_state, StateData}; NewPacket1 -> - NewPacket = fxml:remove_subtags(NewPacket1, <<"nick">>, {<<"xmlns">>, ?NS_NICK}), + NewPacket = xmpp:remove_subtag(NewPacket1, #nick{}), Node = if Subject == false -> ?NS_MUCSUB_NODES_MESSAGES; true -> ?NS_MUCSUB_NODES_SUBJECT end, @@ -951,41 +721,136 @@ 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( + <<"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( + <<"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( + StateData#state.jid, From, Packet, Err), {next_state, normal_state, StateData} end; true -> ErrText = <<"Visitors are not allowed to send messages " "to all occupants">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error( + StateData#state.jid, From, Packet, Err), {next_state, normal_state, StateData} end; false -> - ErrText = - <<"Only occupants are allowed to send messages " - "to the conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), + ErrText = <<"Only occupants are allowed to send messages " + "to the conference">>, + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(StateData#state.jid, From, Packet, Err), {next_state, normal_state, StateData} end. +-spec process_normal_message(jid(), message(), state()) -> state(). +process_normal_message(From, #message{lang = Lang} = Pkt, StateData) -> + IsInvitation = is_invitation(Pkt), + IsVoiceRequest = is_voice_request(Pkt) and + is_visitor(From, StateData), + IsVoiceApprovement = is_voice_approvement(Pkt) and + not is_visitor(From, StateData), + if IsInvitation -> + case check_invitation(From, Pkt, StateData) of + {error, Error} -> + ejabberd_router:route_error(StateData#state.jid, From, Pkt, Error), + StateData; + IJID -> + Config = StateData#state.config, + case Config#config.members_only of + true -> + case get_affiliation(IJID, StateData) of + none -> + NSD = set_affiliation(IJID, member, StateData), + send_affiliation(IJID, member, StateData), + store_room(NSD), + NSD; + _ -> + StateData + end; + false -> + StateData + end + end; + IsVoiceRequest -> + case (StateData#state.config)#config.allow_voice_requests of + true -> + MinInterval = (StateData#state.config)#config.voice_request_min_interval, + BareFrom = jid:remove_resource(jid:tolower(From)), + NowPriority = -p1_time_compat:system_time(micro_seconds), + CleanPriority = NowPriority + MinInterval * 1000000, + Times = clean_treap(StateData#state.last_voice_request_time, + CleanPriority), + case treap:lookup(BareFrom, Times) of + error -> + Times1 = treap:insert(BareFrom, + NowPriority, + true, Times), + NSD = StateData#state{last_voice_request_time = Times1}, + send_voice_request(From, Lang, NSD), + NSD; + {ok, _, _} -> + ErrText = <<"Please, wait for a while before sending " + "new voice request">>, + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error( + StateData#state.jid, From, Pkt, Err), + StateData#state{last_voice_request_time = Times} + end; + false -> + ErrText = <<"Voice requests are disabled in this conference">>, + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error( + StateData#state.jid, From, Pkt, Err), + StateData + end; + IsVoiceApprovement -> + case is_moderator(From, StateData) of + true -> + case extract_jid_from_voice_approvement(Pkt) of + error -> + ErrText = <<"Failed to extract JID from your voice " + "request approval">>, + Err = xmpp:err_bad_request(ErrText, Lang), + ejabberd_router:route_error( + StateData#state.jid, From, Pkt, Err), + StateData; + TargetJid -> + case is_visitor(TargetJid, StateData) of + true -> + Reason = <<>>, + NSD = set_role(TargetJid, + participant, + StateData), + catch send_new_presence(TargetJid, + Reason, + NSD, + StateData), + NSD; + _ -> + StateData + end + end; + _ -> + ErrText = <<"Only moderators can approve voice requests">>, + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error( + StateData#state.jid, From, Pkt, Err), + StateData + end; + true -> + StateData + end. + %% @doc Check if this non participant can send message to room. %% %% XEP-0045 v1.23: @@ -993,6 +858,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 @@ -1002,6 +868,7 @@ 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(), boolean()}. get_participant_data(From, StateData) -> case (?DICT):find(jid:tolower(From), StateData#state.users) @@ -1011,14 +878,11 @@ get_participant_data(From, StateData) -> error -> {<<"">>, moderator, false} end. -process_presence(From, Nick, - #xmlel{name = <<"presence">>, attrs = Attrs0} = Packet0, - StateData) -> - Type0 = fxml:get_attr_s(<<"type">>, Attrs0), +-spec process_presence(jid(), binary(), presence(), state()) -> fsm_transition(). +process_presence(From, Nick, #presence{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, @@ -1027,119 +891,104 @@ process_presence(From, Nick, From, 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(From, Nick, Packet, StateData)) end; true -> - {next_state, normal_state, StateData} + {next_state, normal_state, StateData} end. +-spec do_process_presence(jid(), binary(), presence(), state()) -> + state(). +do_process_presence(From, Nick, #presence{type = available, lang = Lang} = Packet, + StateData) -> + case is_user_online(From, StateData) of + false -> + add_new_user(From, Nick, Packet, StateData); + true -> + case is_nick_change(From, Nick, StateData) of + true -> + case {nick_collision(From, Nick, StateData), + mod_muc:can_use_nick(StateData#state.server_host, + StateData#state.host, + From, Nick), + {(StateData#state.config)#config.allow_visitor_nickchange, + is_visitor(From, StateData)}} of + {_, _, {false, true}} -> + ErrText = <<"Visitors are not allowed to change their " + "nicknames in this room">>, + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, Nick), + From, Packet, Err), + StateData; + {true, _, _} -> + ErrText = <<"That nickname is already in use by another " + "occupant">>, + Err = xmpp:err_conflict(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, Nick), + From, Packet, Err), + StateData; + {_, false, _} -> + ErrText = <<"That nickname is registered by another " + "person">>, + Err = xmpp:err_conflict(ErrText, Lang), + ejabberd_router:route_error( + jid:replace_resource(StateData#state.jid, Nick), + From, Packet, Err), + StateData; + _ -> + case is_initial_presence(From, StateData) of + true -> + subscriber_becomes_available( + From, Nick, Packet, StateData); + false -> + change_nick(From, Nick, StateData) + end + end; + _NotNickChange -> + case is_initial_presence(From, StateData) of + true -> + subscriber_becomes_available( + From, Nick, Packet, StateData); + false -> + Stanza = maybe_strip_status_from_presence( + From, Packet, StateData), + NewState = add_user_presence(From, Stanza, + StateData), + send_new_presence(From, NewState, StateData), + NewState + end + end + end; +do_process_presence(From, Nick, #presence{type = unavailable} = Packet, + StateData) -> + IsSubscriber = is_subscriber(From, StateData), + NewPacket = case {(StateData#state.config)#config.allow_visitor_status, + is_visitor(From, StateData)} of + {false, true} -> + strip_status(Packet); + _ -> Packet + end, + NewState = add_user_presence_un(From, NewPacket, StateData), + case (?DICT):find(Nick, StateData#state.nicks) of + {ok, [_, _ | _]} -> ok; + _ -> send_new_presence(From, NewState, StateData) + end, + Reason = xmpp:get_text(NewPacket#presence.status), + remove_online_user(From, NewState, IsSubscriber, Reason); +do_process_presence(From, _Nick, #presence{type = error, lang = Lang} = Packet, + StateData) -> + ErrorText = <<"It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room">>, + expulse_participant(Packet, From, StateData, + translate:translate(Lang, ErrorText)). + +-spec maybe_strip_status_from_presence(jid(), presence(), + state()) -> presence(). maybe_strip_status_from_presence(From, Packet, StateData) -> case {(StateData#state.config)#config.allow_visitor_status, is_visitor(From, StateData)} of @@ -1148,6 +997,8 @@ maybe_strip_status_from_presence(From, Packet, StateData) -> _Allowed -> Packet end. +-spec subscriber_becomes_available(jid(), binary(), presence(), + state()) -> state(). subscriber_becomes_available(From, Nick, Packet, StateData) -> Stanza = maybe_strip_status_from_presence(From, Packet, StateData), State1 = add_user_presence(From, Stanza, StateData), @@ -1159,9 +1010,10 @@ subscriber_becomes_available(From, Nick, Packet, StateData) -> 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) == [] + andalso (?DICT):size(StateData1#state.users) == 0 of true -> ?INFO_MSG("Destroyed MUC room ~s because it's temporary " @@ -1172,10 +1024,12 @@ close_room_if_temporary_and_empty(StateData1) -> _ -> {next_state, normal_state, StateData1} end. +-spec is_user_online(jid(), state()) -> boolean(). is_user_online(JID, StateData) -> LJID = jid:tolower(JID), (?DICT):is_key(LJID, StateData#state.users). +-spec is_subscriber(jid(), state()) -> boolean(). is_subscriber(JID, StateData) -> LJID = jid:tolower(JID), case (?DICT):find(LJID, StateData#state.users) of @@ -1186,6 +1040,7 @@ is_subscriber(JID, StateData) -> end. %% Check if the user is occupant of the room, or at least is an admin or owner. +-spec is_occupant_or_admin(jid(), state()) -> boolean(). is_occupant_or_admin(JID, StateData) -> FAffiliation = get_affiliation(JID, StateData), FRole = get_role(JID, StateData), @@ -1200,6 +1055,8 @@ is_occupant_or_admin(JID, StateData) -> %%% %%% Handle IQ queries of vCard %%% +-spec is_user_online_iq(binary(), jid(), state()) -> + {boolean(), binary(), jid()}. is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource /= <<"">> -> {is_user_online(JID, StateData), StanzaId, JID}; @@ -1207,93 +1064,55 @@ is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource == <<"">> -> try stanzaid_unpack(StanzaId) of {OriginalId, Resource} -> - JIDWithResource = jid:replace_resource(JID, - 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) -> +-spec handle_iq_vcard(jid(), binary(), iq()) -> {jid(), iq()}. +handle_iq_vcard(ToJID, NewId, #iq{type = Type, sub_els = SubEls} = IQ) -> ToBareJID = jid:remove_resource(ToJID), - 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)}. + case SubEls of + [SubEl] when Type == get, ToBareJID /= ToJID -> + case xmpp:get_ns(SubEl) of + ?NS_VCARD -> + {ToBareJID, change_stanzaid(ToJID, IQ)}; + _ -> + {ToJID, xmpp:set_id(IQ, NewId)} + end; + _ -> + {ToJID, xmpp:set_id(IQ, NewId)} + end. +-spec stanzaid_pack(binary(), binary()) -> binary(). stanzaid_pack(OriginalId, Resource) -> <<"berd", (jlib:encode_base64(<<"ejab\000", OriginalId/binary, "\000", Resource/binary>>))/binary>>. +-spec stanzaid_unpack(binary()) -> {binary(), binary()}. stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) -> StanzaId = jlib:decode_base64(StanzaIdBase64), [<<"ejab">>, OriginalId, Resource] = str:tokens(StanzaId, <<"\000">>), {OriginalId, Resource}. -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) -> +-spec change_stanzaid(jid(), iq()) -> iq(). +change_stanzaid(ToJID, #iq{id = PreviousId} = 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. + xmpp:set_id(Packet, NewId). %% 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, error = Err}, + From, StateData) -> + PD = case check_error_kick(Err) of %% If this is an error stanza and its condition matches a criteria true -> Reason = @@ -1311,67 +1130,61 @@ 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. - -get_error_condition(Packet) -> - case catch get_error_condition2(Packet) of - {condition, ErrorCondition} -> ErrorCondition; - {'EXIT', _} -> <<"badformed error stanza">> - end. +-spec check_error_kick(error()) -> boolean(). +check_error_kick(#error{reason = Reason}) -> + case Reason of + #gone{} -> true; + 'internal-server-error' -> true; + 'item-not-found' -> true; + 'jid-malformed' -> true; + 'recipient-unavailable' -> true; + #redirect{} -> true; + 'remote-server-not-found' -> true; + 'remote-server-timeout' -> true; + 'service-unavailable' -> true; + _ -> false + end; +check_error_kick(undefined) -> + false. -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}. +-spec get_error_condition(error()) -> string(). +get_error_condition(#error{reason = Reason}) -> + case Reason of + #gone{} -> "gone"; + #redirect{} -> "redirect"; + Atom -> atom_to_list(Atom) + end; +get_error_condition(undefined) -> + "undefined". +-spec make_reason(stanza(), jid(), state(), binary()) -> binary(). make_reason(Packet, From, StateData, Reason1) -> {ok, #user{nick = FromNick}} = (?DICT):find(jid:tolower(From), StateData#state.users), - Condition = get_error_condition(Packet), + Condition = get_error_condition(xmpp:get_error(Packet)), iolist_to_binary(io_lib:format(Reason1, [FromNick, Condition])). +-spec expulse_participant(stanza(), jid(), state(), binary()) -> + state(). expulse_participant(Packet, From, StateData, Reason1) -> IsSubscriber = is_subscriber(From, StateData), Reason2 = make_reason(Packet, From, StateData, Reason1), NewState = add_user_presence_un(From, - #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). +-spec set_affiliation(jid(), affiliation(), state()) -> state(). set_affiliation(JID, Affiliation, StateData) -> set_affiliation(JID, Affiliation, StateData, <<"">>). +-spec set_affiliation(jid(), affiliation(), state(), binary()) -> state(). set_affiliation(JID, Affiliation, StateData, Reason) -> LJID = jid:remove_resource(jid:tolower(JID)), Affiliations = case Affiliation of @@ -1383,6 +1196,7 @@ set_affiliation(JID, Affiliation, StateData, Reason) -> end, StateData#state{affiliations = Affiliations}. +-spec get_affiliation(jid(), state()) -> affiliation(). get_affiliation(JID, StateData) -> {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = @@ -1423,6 +1237,7 @@ get_affiliation(JID, StateData) -> _ -> Res end. +-spec get_service_affiliation(jid(), state()) -> owner | none. get_service_affiliation(JID, StateData) -> {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = @@ -1434,6 +1249,7 @@ 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 @@ -1482,6 +1298,7 @@ set_role(JID, Role, StateData) -> 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 @@ -1489,6 +1306,7 @@ get_role(JID, StateData) -> _ -> none end. +-spec get_default_role(affiliation(), state()) -> role(). get_default_role(Affiliation, StateData) -> case Affiliation of owner -> moderator; @@ -1507,12 +1325,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), @@ -1520,18 +1341,21 @@ 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). +-spec get_max_users_admin_threshold(state()) -> pos_integer(). get_max_users_admin_threshold(StateData) -> gen_mod:get_module_opt(StateData#state.server_host, mod_muc, max_users_admin_threshold, fun(I) when is_integer(I), I>0 -> I end, 5). +-spec get_user_activity(jid(), state()) -> #activity{}. get_user_activity(JID, StateData) -> case treap:lookup(jid:tolower(JID), StateData#state.activity) @@ -1552,6 +1376,7 @@ get_user_activity(JID, StateData) -> 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, @@ -1613,6 +1438,7 @@ store_user_activity(JID, UserActivity, StateData) -> end, StateData1. +-spec clean_treap(treap:treap(), integer()) -> treap:treap(). clean_treap(Treap, CleanPriority) -> case treap:is_empty(Treap) of true -> Treap; @@ -1624,6 +1450,7 @@ clean_treap(Treap, CleanPriority) -> end end. +-spec prepare_room_queue(state()) -> state(). prepare_room_queue(StateData) -> case queue:out(StateData#state.room_queue) of {{value, {message, From}}, _RoomQueue} -> @@ -1647,6 +1474,7 @@ prepare_room_queue(StateData) -> {empty, _} -> StateData end. +-spec update_online_user(jid(), #user{}, state()) -> state(). update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, is_subscriber = IsSubscriber} = User, StateData) -> LJID = jid:tolower(JID), @@ -1681,6 +1509,7 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, end, NewStateData. +-spec add_online_user(jid(), binary(), role(), boolean(), [binary()], state()) -> state(). add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) -> tab_add_online_user(JID, StateData), User = #user{jid = JID, nick = Nick, role = Role, @@ -1693,9 +1522,11 @@ add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) -> end, StateData1. +-spec remove_online_user(jid(), state(), boolean()) -> state(). remove_online_user(JID, StateData, IsSubscriber) -> remove_online_user(JID, StateData, IsSubscriber, <<"">>). +-spec remove_online_user(jid(), state(), boolean(), binary()) -> state(). remove_online_user(JID, StateData, _IsSubscriber = true, _Reason) -> LJID = jid:tolower(JID), Users = case (?DICT):find(LJID, StateData#state.users) of @@ -1723,38 +1554,23 @@ remove_online_user(JID, StateData, _IsSubscriber, Reason) -> 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}. - +-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), @@ -1765,6 +1581,7 @@ add_user_presence(JID, Presence, StateData) -> 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), @@ -1778,15 +1595,17 @@ add_user_presence_un(JID, Presence, StateData) -> %% 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 + error -> [] end. %% Find and return the full JID of the user of Nick with %% highest-priority presence. Return jid record. +-spec find_jid_by_nick(binary(), state()) -> jid() | false. find_jid_by_nick(Nick, StateData) -> case (?DICT):find(Nick, StateData#state.nicks) of {ok, [User]} -> jid:make(User); @@ -1811,6 +1630,8 @@ find_jid_by_nick(Nick, StateData) -> error -> false end. +-spec higher_presence(undefined | presence(), + undefined | presence()) -> boolean(). higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined -> Pri1 = get_priority_from_presence(Pres1), Pri2 = get_priority_from_presence(Pres2), @@ -1818,26 +1639,20 @@ 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), + {ok, #user{nick = Nick}} = (?DICT):find(LJID, StateData#state.users), Nick. +-spec is_nick_change(jid(), binary(), state()) -> boolean(). is_nick_change(JID, Nick, StateData) -> LJID = jid:tolower(JID), case Nick of @@ -1848,16 +1663,20 @@ is_nick_change(JID, Nick, StateData) -> Nick /= OldNick end. +-spec nick_collision(jid(), binary(), state()) -> boolean(). nick_collision(User, Nick, StateData) -> UserOfNick = find_jid_by_nick(Nick, StateData), (UserOfNick /= false andalso jid:remove_resource(jid:tolower(UserOfNick)) /= jid:remove_resource(jid:tolower(User))). -add_new_user(From, Nick, - #xmlel{name = Name, attrs = Attrs, children = Els} = Packet, - StateData) -> - Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), +-spec add_new_user(jid(), binary(), presence() | iq(), state()) -> + state() | + {error, error()} | + {ignore, state()} | + {result, xmpp_element(), state()}. +add_new_user(From, Nick, Packet, StateData) -> + Lang = xmpp:get_lang(Packet), UserRoomJID = jid:replace_resource(StateData#state.jid, Nick), MaxUsers = get_max_users(StateData), MaxAdminUsers = MaxUsers + @@ -1874,7 +1693,7 @@ add_new_user(From, Nick, fun(I) when is_integer(I), I>0 -> I end, 10), 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) @@ -1887,72 +1706,72 @@ add_new_user(From, Nick, 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), + Err = xmpp:err_resource_constraint(Txt, Lang), + ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> - {error, Err, 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), + Err = xmpp:err_resource_constraint(Txt, Lang), + ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> - {error, Err, StateData} + {error, Err} end; {false, _, _, _} -> - Err = ?ERR_SERVICE_UNAVAILABLE, - ErrPacket = jlib:make_error_reply(Packet, Err), + Err = xmpp:err_service_unavailable(), + ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), 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); + xmpp:err_forbidden(ErrText, Lang); _ -> ErrText = <<"Membership is required to enter this room">>, - ?ERRT_REGISTRATION_REQUIRED(Lang, ErrText) + xmpp:err_registration_required(ErrText, Lang) end, - ErrPacket = jlib:make_error_reply(Packet, Err), + ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), 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), + Err = xmpp:err_conflict(ErrText, Lang), + ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> - {error, Err, 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 = xmpp:err_conflict(ErrText, Lang), + ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), 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), @@ -1965,7 +1784,7 @@ add_new_user(From, Nick, Nodes, StateData)), send_existing_presences(From, NewState), send_initial_presence(From, NewState, StateData), - Shift = count_stanza_shift(Nick, Els, NewState), + Shift = count_stanza_shift(Nick, Packet, NewState), case send_history(From, Shift, NewState) of true -> ok; _ -> send_subject(From, StateData) @@ -1985,20 +1804,20 @@ add_new_user(From, Nick, 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), + Err = xmpp:err_not_authorized(ErrText, Lang), + ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> - {error, Err, 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}, @@ -2006,9 +1825,7 @@ add_new_user(From, Nick, Lang, Limiter, From) of {ok, ID, CaptchaEls} -> - MsgPkt = #xmlel{name = <<"message">>, - attrs = [{<<"id">>, ID}], - children = CaptchaEls}, + MsgPkt = #message{id = ID, sub_els = CaptchaEls}, Robots = (?DICT):store(From, {Nick, Packet}, StateData#state.robots), ejabberd_router:route(RoomJID, From, MsgPkt), @@ -2020,49 +1837,51 @@ add_new_user(From, Nick, end; {error, limit} -> ErrText = <<"Too many CAPTCHA requests">>, - Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText), - ErrPacket = jlib:make_error_reply(Packet, Err), + Err = xmpp:err_resource_constraint(ErrText, Lang), + ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> - {error, Err, StateData} + {error, Err} end; _ -> ErrText = <<"Unable to generate a CAPTCHA">>, - Err = ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText), - ErrPacket = jlib:make_error_reply(Packet, Err), + Err = xmpp:err_internal_server_error(ErrText, Lang), + ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> - {error, Err, StateData} + {error, Err} end end; _ -> ErrText = <<"Incorrect password">>, - Err = ?ERRT_NOT_AUTHORIZED(Lang, ErrText), - ErrPacket = jlib:make_error_reply(Packet, Err), + Err = xmpp:err_not_authorized(ErrText, Lang), + ErrPacket = xmpp:make_error(Packet, Err), if not IsSubscribeRequest -> ejabberd_router:route(UserRoomJID, From, ErrPacket), StateData; true -> - {error, Err, StateData} + {error, Err} end end end. -check_password(owner, _Affiliation, _Els, _From, +-spec check_password(affiliation(), affiliation(), + stanza(), jid(), state()) -> boolean() | nopass. +check_password(owner, _Affiliation, _Packet, _From, _StateData) -> %% Don't check pass if user is owner in MUC service (access_admin option) true; -check_password(_ServiceAffiliation, Affiliation, 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; _ -> @@ -2073,6 +1892,7 @@ 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() @@ -2101,47 +1921,52 @@ 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) - end; -extract_password([_ | Els]) -> extract_password(Els). +-spec extract_password(stanza()) -> binary() | false. +extract_password(Packet) -> + case xmpp:get_subtag(Packet, #muc{}) of + #muc{password = Password} when is_binary(Password) -> + Password; + _ -> + false + end. -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]). +-spec count_stanza_shift(binary(), stanza(), state()) -> non_neg_integer(). +count_stanza_shift(Nick, Packet, StateData) -> + case xmpp:get_subtag(Packet, #muc_history{}) of + #muc_history{since = Since, + seconds = Seconds, + maxstanzas = MaxStanzas, + maxchars = MaxChars} -> + HL = lqueue_to_list(StateData#state.history), + Shift0 = case Since of + undefined -> 0; + _ -> + Sin = calendar:datetime_to_gregorian_seconds( + calendar:now_to_datetime(Since)), + count_seconds_shift(Sin, HL) + end, + Shift1 = case Seconds of + undefined -> 0; + _ -> + Sec = calendar:datetime_to_gregorian_seconds( + calendar:universal_time()) - Seconds, + count_seconds_shift(Sec, HL) + end, + Shift2 = case MaxStanzas of + undefined -> 0; + _ -> count_maxstanzas_shift(MaxStanzas, HL) + end, + Shift3 = case MaxChars of + undefined -> 0; + _ -> count_maxchars_shift(Nick, MaxChars, HL) + end, + lists:max([Shift0, Shift1, Shift2, Shift3]); + false -> + 0 + end. +-spec count_seconds_shift(non_neg_integer(), + [history_element()]) -> non_neg_integer(). count_seconds_shift(Seconds, HistoryList) -> lists:sum(lists:map(fun ({_Nick, _Packet, _HaveSubject, TimeStamp, _Size}) -> @@ -2153,12 +1978,16 @@ count_seconds_shift(Seconds, HistoryList) -> end, HistoryList)). +-spec count_maxstanzas_shift(non_neg_integer(), + [history_element()]) -> non_neg_integer(). count_maxstanzas_shift(MaxStanzas, HistoryList) -> S = length(HistoryList) - MaxStanzas, if S =< 0 -> 0; true -> S end. +-spec count_maxchars_shift(binary(), non_neg_integer(), + [history_element()]) -> integer(). count_maxchars_shift(Nick, MaxSize, HistoryList) -> NLen = byte_size(Nick) + 1, Sizes = lists:map(fun ({_Nick, _Packet, _HaveSubject, @@ -2168,41 +1997,20 @@ count_maxchars_shift(Nick, MaxSize, HistoryList) -> HistoryList), calc_shift(MaxSize, Sizes). +-spec calc_shift(non_neg_integer(), [non_neg_integer()]) -> integer(). calc_shift(MaxSize, Sizes) -> Total = lists:sum(Sizes), calc_shift(MaxSize, Total, 0, Sizes). +-spec calc_shift(non_neg_integer(), integer(), integer(), + [non_neg_integer()]) -> integer(). calc_shift(_MaxSize, _Size, Shift, []) -> Shift; calc_shift(MaxSize, Size, Shift, [S | TSizes]) -> if MaxSize >= Size -> Shift; true -> calc_shift(MaxSize, Size - S, Shift + 1, TSizes) end. -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 is_room_overcrowded(state()) -> boolean(). is_room_overcrowded(StateData) -> MaxUsersPresence = gen_mod:get_module_opt(StateData#state.server_host, mod_muc, max_users_presence, @@ -2210,10 +2018,12 @@ is_room_overcrowded(StateData) -> ?DEFAULT_MAX_USERS_PRESENCE), (?DICT):size(StateData#state.users) > MaxUsersPresence. +-spec presence_broadcast_allowed(jid(), state()) -> boolean(). presence_broadcast_allowed(JID, StateData) -> Role = get_role(JID, StateData), lists:member(Role, (StateData#state.config)#config.presence_broadcast). +-spec is_initial_presence(jid(), state()) -> boolean(). is_initial_presence(From, StateData) -> LJID = jid:tolower(From), case (?DICT):find(LJID, StateData#state.users) of @@ -2223,18 +2033,22 @@ is_initial_presence(From, StateData) -> true end. +-spec send_initial_presence(jid(), state(), state()) -> ok. send_initial_presence(NJID, StateData, OldStateData) -> send_new_presence1(NJID, <<"">>, true, StateData, OldStateData). +-spec send_update_presence(jid(), state(), state()) -> ok. send_update_presence(JID, StateData, OldStateData) -> send_update_presence(JID, <<"">>, StateData, OldStateData). +-spec send_update_presence(jid(), binary(), state(), state()) -> ok. send_update_presence(JID, Reason, StateData, OldStateData) -> case is_room_overcrowded(StateData) of true -> ok; false -> send_update_presence1(JID, Reason, StateData, OldStateData) end. +-spec send_update_presence1(jid(), binary(), state(), state()) -> ok. send_update_presence1(JID, Reason, StateData, OldStateData) -> LJID = jid:tolower(JID), LJIDs = case LJID of @@ -2258,12 +2072,15 @@ send_update_presence1(JID, Reason, StateData, OldStateData) -> end, LJIDs). +-spec send_new_presence(jid(), state(), state()) -> ok. send_new_presence(NJID, StateData, OldStateData) -> send_new_presence(NJID, <<"">>, false, StateData, OldStateData). +-spec send_new_presence(jid(), binary(), state(), state()) -> ok. send_new_presence(NJID, Reason, StateData, OldStateData) -> send_new_presence(NJID, Reason, false, StateData, OldStateData). +-spec send_new_presence(jid(), binary(), boolean(), state(), state()) -> ok. send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> case is_room_overcrowded(StateData) of true -> ok; @@ -2271,6 +2088,7 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> OldStateData) end. +-spec is_ra_changed(jid() | ljid(), boolean(), state(), state()) -> boolean(). is_ra_changed(_, _IsInitialPresence = true, _, _) -> false; is_ra_changed(LJID, _IsInitialPresence = false, NewStateData, OldStateData) -> @@ -2289,6 +2107,7 @@ is_ra_changed(LJID, _IsInitialPresence = false, NewStateData, OldStateData) -> (NewRole /= OldRole) or (NewAff /= OldAff) end. +-spec send_new_presence1(jid(), binary(), boolean(), state(), state()) -> ok. send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> LNJID = jid:tolower(NJID), #user{nick = Nick} = (?DICT):fetch(LNJID, StateData#state.users), @@ -2301,15 +2120,9 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> {Role1, Presence1} = case presence_broadcast_allowed(NJID, StateData) 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 @@ -2323,59 +2136,38 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> {Role, Presence} = if LNJID == LUJID -> {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 of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Item = if is_binary(Reason), Reason /= <<"">> -> + Item1#muc_item{reason = Reason}; + true -> + Item1 + end, + StatusCodes = status_codes(IsInitialPresence, NJID, Info, + 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), + Type = xmpp:get_type(Packet), IsSubscriber = Info#user.is_subscriber, IsOccupant = Info#user.last_presence /= undefined, if (IsSubscriber and not IsOccupant) and - (IsInitialPresence or (Type == <<"unavailable">>)) -> + (IsInitialPresence or (Type == unavailable)) -> Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS, send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, Node2, StateData); @@ -2385,12 +2177,14 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> end, UserList). +-spec send_existing_presences(jid(), state()) -> ok. send_existing_presences(ToJID, StateData) -> case is_room_overcrowded(StateData) of true -> ok; false -> send_existing_presences1(ToJID, StateData) end. +-spec send_existing_presences1(jid(), state()) -> ok. send_existing_presences1(ToJID, StateData) -> LToJID = jid:tolower(ToJID), {ok, #user{jid = RealToJID, role = Role}} = @@ -2410,46 +2204,23 @@ 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)). +-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), @@ -2472,6 +2243,7 @@ set_nick(JID, Nick, State) -> 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), @@ -2492,6 +2264,7 @@ 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, @@ -2500,104 +2273,52 @@ send_nick_changing(JID, OldNick, StateData, (?DICT):find(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 + lists:foreach( + fun({_LJID, Info}) when Presence /= undefined -> + Item0 = #muc_item{affiliation = Affiliation, role = Role}, + Item1 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false of + true -> Item0#muc_item{jid = RealJID, nick = Nick}; + false -> Item0#muc_item{nick = Nick} + end, + Item2 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Status110 = case JID == Info#user.jid of + true -> [110]; + false -> [] end, - 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 = [Item1], + status_codes = [303|Status110]}]}, + Packet2 = xmpp:set_subtag(Presence, + #muc_user{items = [Item2], + status_codes = Status110}), + if SendOldUnavailable -> + send_wrapped( + jid:replace_resource(StateData#state.jid, OldNick), + Info#user.jid, Packet1, ?NS_MUCSUB_NODES_PRESENCE, + StateData); + true -> ok + end, + if SendNewAvailable -> + send_wrapped( + jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, Packet2, ?NS_MUCSUB_NODES_PRESENCE, + StateData); + true -> ok + end; + (_) -> + ok + end, + (?DICT):to_list(StateData#state.users)). +-spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok. maybe_send_affiliation(JID, Affiliation, StateData) -> LJID = jid:tolower(JID), IsOccupant = case LJID of @@ -2617,18 +2338,13 @@ maybe_send_affiliation(JID, Affiliation, StateData) -> send_affiliation(LJID, Affiliation, StateData) end. +-spec send_affiliation(ljid(), affiliation(), state()) -> ok. 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}]}]}, + Item = #muc_item{jid = jid:make(LJID), + affiliation = Affiliation, + role = none}, + Message = #message{id = randoms:get_string(), + sub_els = [#muc_user{items = [Item]}]}, Recipients = case (StateData#state.config)#config.anonymous of true -> (?DICT):filter(fun(_, #user{role = moderator}) -> @@ -2643,43 +2359,33 @@ send_affiliation(LJID, Affiliation, StateData) -> StateData#state.server_host, Recipients, Message). -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(), jid(), #user{}, state()) -> [pos_integer()]. +status_codes(IsInitialPresence, JID, #user{jid = JID}, StateData) -> + S0 = [110], + case IsInitialPresence of + true -> + S1 = case StateData#state.just_created of + true -> [201|S0]; + false -> S0 + end, + S2 = case (StateData#state.config)#config.anonymous of + true -> S1; + false -> [100|S1] + end, + S3 = case (StateData#state.config)#config.logging of + true -> [170|S2]; + false -> S2 + end, + S3; + false -> S0 + end; +status_codes(_IsInitialPresence, _JID, _Info, _StateData) -> []. +-spec lqueue_new(non_neg_integer()) -> lqueue(). lqueue_new(Max) -> #lqueue{queue = queue:new(), len = 0, max = Max}. +-spec lqueue_in(term(), lqueue()) -> lqueue(). %% If the message queue limit is set to 0, do not store messages. lqueue_in(_Item, LQ = #lqueue{max = 0}) -> LQ; %% Otherwise, rotate messages in the queue store. @@ -2692,39 +2398,33 @@ lqueue_in(Item, true -> #lqueue{queue = Q2, len = Len + 1, max = Max} end. +-spec lqueue_cut(queue:queue(), non_neg_integer()) -> queue:queue(). lqueue_cut(Q, 0) -> Q; lqueue_cut(Q, N) -> {_, Q1} = queue:out(Q), lqueue_cut(Q1, N - 1). +-spec lqueue_to_list(lqueue()) -> list(). lqueue_to_list(#lqueue{queue = Q1}) -> queue:to_list(Q1). - +-spec add_message_to_history(binary(), jid(), message(), state()) -> state(). add_message_to_history(FromNick, FromJID, Packet, StateData) -> - HaveSubject = case fxml:get_subtag(Packet, <<"subject">>) - of - false -> false; - _ -> true - end, + HaveSubject = Packet#message.subject /= [], 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]) + Addresses = #addresses{ + list = [#address{type = ofrom, + jid = FromJID}]}, + xmpp:set_subtag(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), + TSPacket = xmpp_util:add_delay_info( + AddrPacket, StateData#state.jid, TimeStamp), + SPacket = xmpp:set_from_to( + TSPacket, + jid:replace_resource(StateData#state.jid, FromNick), + StateData#state.jid), Size = element_size(SPacket), Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, calendar:now_to_universal_time(TimeStamp), Size}, @@ -2732,6 +2432,7 @@ add_message_to_history(FromNick, FromJID, Packet, StateData) -> add_to_log(text, {FromNick, Packet}, StateData), StateData#state{history = Q1}. +-spec send_history(jid(), integer(), state()) -> boolean(). send_history(JID, Shift, StateData) -> lists:foldl(fun ({Nick, Packet, HaveSubject, _TimeStamp, _Size}, @@ -2745,23 +2446,19 @@ send_history(JID, Shift, StateData) -> lists:nthtail(Shift, lqueue_to_list(StateData#state.history))). +-spec send_subject(jid(), state()) -> ok. send_subject(_JID, #state{subject_author = <<"">>}) -> ok; send_subject(JID, #state{subject_author = Nick} = StateData) -> Subject = StateData#state.subject, - Packet = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"subject">>, attrs = [], - children = [{xmlcdata, Subject}]}]}, + Packet = #message{type = groupchat, subject = xmpp:mk_text(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. +-spec check_subject(message()) -> false | binary(). +check_subject(#message{subject = []}) -> false; +check_subject(#message{subject = Subj}) -> xmpp:get_text(Subj). +-spec can_change_subject(role(), state()) -> boolean(). can_change_subject(Role, StateData) -> case (StateData#state.config)#config.allow_change_subj of @@ -2772,99 +2469,89 @@ can_change_subject(Role, StateData) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Admin stuff -process_iq_admin(From, set, Lang, SubEl, StateData) -> - #xmlel{children = Items} = SubEl, +-spec process_iq_admin(jid(), iq(), #state{}) -> {error, error()} | + {result, undefined, #state{}} | + {result, muc_admin()}. +process_iq_admin(_From, #iq{lang = Lang, sub_els = [#muc_admin{items = []}]}, + _StateData) -> + Txt = <<"No 'item' element found">>, + {error, xmpp:err_bad_request(Txt, Lang)}; +process_iq_admin(From, #iq{type = set, lang = Lang, + sub_els = [#muc_admin{items = Items}]}, + StateData) -> process_admin_items_set(From, Items, Lang, StateData); -process_iq_admin(From, 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 - 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 = <<"Neither 'role' nor 'affiliation' attribute found">>, + {error, xmpp:err_bad_request(Txt, Lang)}; + #muc_item{role = undefined, affiliation = Affiliation} -> + if (FAffiliation == owner) or + (FAffiliation == admin) or + ((FAffiliation == member) and + not (StateData#state.config)#config.anonymous) -> + Items = items_with_affiliation(Affiliation, StateData), + {result, #muc_admin{items = Items}}; + true -> + ErrText = <<"Administrator privileges required">>, + {error, xmpp:err_forbidden(ErrText, Lang)} + end; + #muc_item{role = Role} -> + if FRole == moderator -> + Items = items_with_role(Role, StateData), + {result, #muc_admin{items = Items}}; + true -> + ErrText = <<"Moderator privileges required">>, + {error, xmpp:err_forbidden(ErrText, Lang)} + end + end; +process_iq_admin(_From, #iq{type = get, lang = Lang}, _StateData) -> + ErrText = <<"Too many <item/> elements">>, + {error, xmpp:err_bad_request(ErrText, Lang)}. +-spec items_with_role(role(), state()) -> [muc_item()]. items_with_role(SRole, StateData) -> lists:map(fun ({_, U}) -> user_to_item(U, StateData) end, search_role(SRole, StateData)). +-spec items_with_affiliation(affiliation(), state()) -> [muc_item()]. items_with_affiliation(SAffiliation, StateData) -> - lists:map(fun ({JID, {Affiliation, Reason}}) -> - #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, + reason = if is_binary(Reason), Reason /= <<"">> -> + Reason; + true -> + undefined + end}; + ({JID, Affiliation}) -> + #muc_item{affiliation = Affiliation, jid = JID} + end, + search_affiliation(SAffiliation, StateData)). +-spec user_to_item(#user{}, state()) -> muc_item(). user_to_item(#user{role = Role, nick = Nick, jid = JID}, StateData) -> Affiliation = get_affiliation(JID, StateData), - #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)). +-spec search_affiliation(affiliation(), state()) -> + [{ljid(), + affiliation() | {affiliation(), binary()}}]. search_affiliation(Affiliation, StateData) -> lists:filter(fun ({_, A}) -> case A of @@ -2874,11 +2561,14 @@ search_affiliation(Affiliation, StateData) -> end, (?DICT):to_list(StateData#state.affiliations)). +-spec process_admin_items_set(jid(), [muc_item()], binary() | undefined, + #state{}) -> {result, undefined, #state{}} | + {error, error()}. process_admin_items_set(UJID, Items, Lang, StateData) -> UAffiliation = get_affiliation(UJID, StateData), URole = get_role(UJID, StateData), - case 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 " @@ -2888,249 +2578,161 @@ process_admin_items_set(UJID, Items, Lang, StateData) -> NSD = lists:foldl(process_item_change(UJID), StateData, lists:flatten(Res)), store_room(NSD), - {result, [], NSD}; - Err -> Err + {result, undefined, NSD}; + {error, Err} -> {error, Err} end. +-spec process_item_change(jid()) -> function(). process_item_change(UJID) -> fun(E, SD) -> process_item_change(E, SD, UJID) end. -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 +-type admin_action() :: {jid(), affiliation | role, + affiliation() | role(), binary()}. + +-spec process_item_change(admin_action(), state(), jid()) -> state(). +process_item_change(Item, SD, UJID) -> + try case Item of + {JID, affiliation, owner, _} when JID#jid.luser == <<"">> -> + %% If the provided JID does not have username, + %% forget the affiliation completely + SD; + {JID, role, none, Reason} -> + catch send_kickban_presence(UJID, JID, Reason, 307, SD), + set_role(JID, none, SD); + {JID, affiliation, none, Reason} -> + case (SD#state.config)#config.members_only of + true -> + catch send_kickban_presence(UJID, JID, Reason, 321, none, SD), + maybe_send_affiliation(JID, none, SD), + SD1 = set_affiliation(JID, none, SD), + set_role(JID, none, SD1); + _ -> + SD1 = set_affiliation(JID, none, SD), + send_update_presence(JID, SD1, SD), + maybe_send_affiliation(JID, none, SD1), + SD1 + end; + {JID, affiliation, outcast, Reason} -> + catch send_kickban_presence(UJID, JID, Reason, 301, outcast, SD), + maybe_send_affiliation(JID, outcast, SD), + set_affiliation(JID, outcast, set_role(JID, none, SD), Reason); + {JID, affiliation, A, Reason} when (A == admin) or (A == owner) -> + SD1 = set_affiliation(JID, A, SD, Reason), + SD2 = set_role(JID, moderator, SD1), + send_update_presence(JID, Reason, SD2, SD), + maybe_send_affiliation(JID, A, SD2), + SD2; + {JID, affiliation, member, Reason} -> + SD1 = set_affiliation(JID, member, SD, Reason), + SD2 = set_role(JID, participant, SD1), + send_update_presence(JID, Reason, SD2, SD), + maybe_send_affiliation(JID, member, SD2), + SD2; + {JID, role, Role, Reason} -> + SD1 = set_role(JID, Role, SD), + catch send_new_presence(JID, Reason, SD1, SD), + SD1; + {JID, affiliation, A, _Reason} -> + SD1 = set_affiliation(JID, A, SD), + send_update_presence(JID, SD1, SD), + maybe_send_affiliation(JID, A, SD1), + SD1 + end + catch E:R -> + ?ERROR_MSG("failed to set item ~p from ~s: ~p", + [Item, jid:to_string(UJID), + {E, {R, erlang:get_stacktrace()}}]), + SD end. +-spec find_changed_items(jid(), affiliation(), role(), + [muc_item()], binary(), state(), [admin_action()]) -> + {result, [admin_action()]}. find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) -> {result, Res}; +find_changed_items(_UJID, _UAffiliation, _URole, + [#muc_item{jid = undefined, nick = undefined}|_], + Lang, _StateData, _Res) -> + Txt = <<"Neither 'jid' nor 'nick' attribute found">>, + throw({error, xmpp:err_bad_request(Txt, Lang)}); +find_changed_items(_UJID, _UAffiliation, _URole, + [#muc_item{role = undefined, affiliation = undefined}|_], + Lang, _StateData, _Res) -> + Txt = <<"Neither 'role' nor 'affiliation' attribute found">>, + throw({error, xmpp:err_bad_request(Txt, Lang)}); find_changed_items(UJID, UAffiliation, URole, - [{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 = Reason0, + 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 /= undefined -> + case find_jids_by_nick(Nick, StateData) of + [] -> + ErrText = iolist_to_binary( + io_lib:format( + translate:translate( + Lang, + <<"Nickname ~s does not exist in the room">>), + [Nick])), + throw({error, xmpp:err_not_acceptable(ErrText, Lang)}); + JIDList -> + JIDList end - end; - 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), + CanChangeRA = case can_change_ra(UAffiliation, + URole, + TAffiliation, + TRole, RoleOrAff, RoleOrAffValue, + ServiceAf) of + nothing -> nothing; + true -> true; + check_owner -> + case search_affiliation(owner, StateData) of + [{OJID, _}] -> + jid:remove_resource(OJID) + /= + jid:tolower(jid:remove_resource(UJID)); + _ -> true + end; + _ -> false + end, + case CanChangeRA of + nothing -> + find_changed_items(UJID, UAffiliation, URole, + Items, Lang, StateData, + Res); + true -> + Reason = if is_binary(Reason0) -> Reason0; + true -> <<"">> + end, + MoreRes = [{jid:remove_resource(Jidx), + RoleOrAff, RoleOrAffValue, Reason} + || Jidx <- JIDs], + find_changed_items(UJID, UAffiliation, URole, + Items, Lang, StateData, + [MoreRes | Res]); + false -> + Txt = <<"Changing role/affiliation is not allowed">>, + throw({error, xmpp:err_not_allowed(Txt, Lang)}) + end. +-spec can_change_ra(affiliation(), role(), affiliation(), role(), + affiliation, affiliation(), affiliation()) -> boolean(); + (affiliation(), role(), affiliation(), role(), + role, role(), affiliation()) -> boolean(). can_change_ra(_FAffiliation, _FRole, owner, _TRole, affiliation, owner, owner) -> %% A room owner tries to add as persistent owner a @@ -3255,11 +2857,15 @@ can_change_ra(_FAffiliation, _FRole, _TAffiliation, _TRole, role, _Value, _ServiceAf) -> false. +-spec send_kickban_presence(jid(), jid(), binary(), + pos_integer(), state()) -> ok. send_kickban_presence(UJID, JID, Reason, Code, StateData) -> NewAffiliation = get_affiliation(JID, StateData), send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, StateData). +-spec send_kickban_presence(jid(), jid(), binary(), pos_integer(), + affiliation(), state()) -> ok. send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, StateData) -> LJID = jid:tolower(JID), @@ -3288,77 +2894,51 @@ send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, end, LJIDs). +-spec send_kickban_presence1(jid(), jid(), binary(), pos_integer(), + affiliation(), state()) -> ok. send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, StateData) -> {ok, #user{jid = RealJID, nick = Nick}} = (?DICT):find(jid:tolower(UJID), StateData#state.users), - SAffiliation = affiliation_to_list(Affiliation), - BannedJIDString = jid:to_string(RealJID), 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)). + lists:foreach( + fun({_LJID, Info}) -> + Item0 = #muc_item{affiliation = Affiliation, + role = none}, + Item1 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Item2 = if is_binary(Reason), Reason /= <<"">> -> + Item1#muc_item{reason = Reason}; + true -> + Item1 + end, + Item = case ActorNick of + <<"">> -> Item2; + _ -> Item2#muc_item{actor = #muc_actor{nick = ActorNick}} + end, + Packet = #presence{type = unavailable, + sub_els = [#muc_user{items = [Item], + status_codes = [Code]}]}, + RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick), + send_wrapped(RoomJIDNick, Info#user.jid, Packet, + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), + IsSubscriber = Info#user.is_subscriber, + IsOccupant = Info#user.last_presence /= undefined, + if (IsSubscriber and not IsOccupant) -> + send_wrapped(RoomJIDNick, Info#user.jid, Packet, + ?NS_MUCSUB_NODES_PARTICIPANTS, StateData); + true -> + ok + end + end, + (?DICT):to_list(StateData#state.users)). +-spec get_actor_nick(binary() | jid(), state()) -> binary(). get_actor_nick(<<"">>, _StateData) -> <<"">>; get_actor_nick(MJID, StateData) -> @@ -3369,97 +2949,86 @@ get_actor_nick(MJID, StateData) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % 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, 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 = <<"Owner privileges required">>, + {error, xmpp:err_forbidden(ErrText, Lang)}; + Destroy /= undefined, Config == undefined, Items == [] -> + ?INFO_MSG("Destroyed MUC room ~s by the owner ~s", + [jid:to_string(StateData#state.jid), jid:to_string(From)]), + add_to_log(room_existence, destroyed, StateData), + destroy_room(Destroy, StateData); + Config /= undefined, Destroy == undefined, Items == [] -> + case Config of + #xdata{type = cancel} -> + {result, undefined}; + #xdata{type = submit} -> + case is_allowed_log_change(Config, StateData, From) andalso + is_allowed_persistent_change(Config, StateData, From) andalso + is_allowed_room_name_desc_limits(Config, StateData) andalso + is_password_settings_correct(Config, StateData) of + true -> set_config(Config, StateData, Lang); + false -> {error, xmpp:err_not_acceptable()} + end; + _ -> + Txt = <<"Incorrect data form">>, + {error, xmpp:err_bad_request(Txt, Lang)} + end; + Items /= [], Config == undefined, Destroy == undefined -> + process_admin_items_set(From, Items, Lang, StateData); + true -> + {error, xmpp:err_bad_request()} end; -process_iq_owner(From, 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 = <<"Owner privileges required">>, + {error, xmpp:err_forbidden(ErrText, Lang)}; + Destroy == undefined, Config == undefined -> + case Items of + [] -> + {result, + #muc_owner{config = get_config(Lang, StateData, From)}}; + [#muc_item{affiliation = undefined}] -> + Txt = <<"No 'affiliation' attribute found">>, + {error, xmpp:err_bad_request(Txt, Lang)}; + [#muc_item{affiliation = Affiliation}] -> + Items = items_with_affiliation(Affiliation, StateData), + {result, #muc_owner{items = Items}}; + [_|_] -> + Txt = <<"Too many <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 - false -> true; - true -> - allow == - mod_muc_log:check_access_log(StateData#state.server_host, - From) +-spec is_allowed_log_change(xdata(), state(), jid()) -> boolean(). +is_allowed_log_change(X, StateData, From) -> + case xmpp_util:has_xdata_var(<<"muc#roomconfig_enablelogging">>, X) of + false -> true; + true -> + allow == + mod_muc_log:check_access_log(StateData#state.server_host, + From) end. -is_allowed_persistent_change(XEl, StateData, From) -> - case - lists:keymember(<<"muc#roomconfig_persistentroom">>, 1, - jlib:parse_xdata_submit(XEl)) - of +-spec is_allowed_persistent_change(xdata(), state(), jid()) -> boolean(). +is_allowed_persistent_change(X, StateData, From) -> + case xmpp_util:has_xdata_var(<<"muc#roomconfig_persistentroom">>, X) of false -> true; true -> {_AccessRoute, _AccessCreate, _AccessAdmin, @@ -3472,58 +3041,57 @@ is_allowed_persistent_change(XEl, StateData, From) -> %% 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 +-spec is_allowed_room_name_desc_limits(xdata(), state()) -> boolean(). +is_allowed_room_name_desc_limits(XData, StateData) -> + IsNameAccepted = case xmpp_util:get_xdata_values( + <<"muc#roomconfig_roomname">>, XData) of + [N] -> + byte_size(N) =< + gen_mod:get_module_opt( + StateData#state.server_host, + mod_muc, max_room_name, + fun(infinity) -> infinity; + (I) when is_integer(I), + I>0 -> I + end, infinity); + _ -> + true end, - IsDescAccepted = case - 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 + IsDescAccepted = case xmpp_util:get_xdata_values( + <<"muc#roomconfig_roomdesc">>, XData) of + [D] -> + byte_size(D) =< + gen_mod:get_module_opt( + StateData#state.server_host, + mod_muc, max_room_desc, + fun(infinity) -> infinity; + (I) when is_integer(I), + I>0 -> + I + end, infinity); + _ -> true end, IsNameAccepted and IsDescAccepted. %% Return false if: %% "the password for a password-protected room is blank" -is_password_settings_correct(XEl, StateData) -> +-spec is_password_settings_correct(xdata(), state()) -> boolean(). +is_password_settings_correct(XData, StateData) -> Config = StateData#state.config, OldProtected = Config#config.password_protected, OldPassword = Config#config.password, - NewProtected = case - lists:keysearch(<<"muc#roomconfig_passwordprotectedroom">>, - 1, jlib:parse_xdata_submit(XEl)) - of - {value, {_, [<<"1">>]}} -> true; - {value, {_, [<<"0">>]}} -> false; - _ -> undefined + NewProtected = case xmpp_util:get_xdata_values( + <<"muc#roomconfig_passwordprotectedroom">>, XData) of + [<<"1">>] -> true; + [<<"true">>] -> true; + [<<"0">>] -> false; + [<<"false">>] -> false; + _ -> undefined end, - NewPassword = case - lists:keysearch(<<"muc#roomconfig_roomsecret">>, 1, - jlib:parse_xdata_submit(XEl)) - of - {value, {_, [P]}} -> P; - _ -> undefined + NewPassword = case xmpp_util:get_xdata_values( + <<"muc#roomconfig_roomsecret">>, XData) of + [P] -> P; + _ -> undefined end, case {OldProtected, NewProtected, OldPassword, NewPassword} @@ -3535,40 +3103,35 @@ is_password_settings_correct(XEl, StateData) -> _ -> 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(XFIELD(Type, Label, Var, Vals), + #xdata_field{type = Type, + label = translate:translate(Lang, Label), + var = Var, + values = Vals}). -define(BOOLXFIELD(Label, Var, Val), - ?XFIELD(<<"boolean">>, Label, Var, + ?XFIELD(boolean, Label, Var, case Val of - true -> <<"1">>; - _ -> <<"0">> + true -> [<<"1">>]; + _ -> [<<"0">>] end)). -define(STRINGXFIELD(Label, Var, Val), - ?XFIELD(<<"text-single">>, Label, Var, Val)). + ?XFIELD('text-single', Label, Var, [Val])). -define(PRIVATEXFIELD(Label, Var, Val), - ?XFIELD(<<"text-private">>, 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]}). + ?XFIELD('jid-multi', Label, Var, + [jid:to_string(JID) || JID <- JIDList])). +-spec make_options([{binary(), binary()}], binary()) -> [xdata_option()]. +make_options(Options, Lang) -> + [#xdata_option{label = translate:translate(Lang, Label), + value = Value} || {Label, Value} <- Options]. + +-spec get_default_room_maxusers(state()) -> non_neg_integer(). get_default_room_maxusers(RoomState) -> DefRoomOpts = gen_mod:get_module_opt(RoomState#state.server_host, @@ -3578,342 +3141,193 @@ get_default_room_maxusers(RoomState) -> 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} = 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 + {MaxUsersRoomInteger, MaxUsersRoomString} = + case get_max_users(StateData) of + N when is_integer(N) -> + {N, integer_to_binary(N)}; + _ -> {0, <<"none">>} + end, + Title = iolist_to_binary( + io_lib:format( + translate:translate(Lang, <<"Configuration of room ~s">>), + [jid:to_string(StateData#state.jid)])), + Fs = [#xdata_field{type = hidden, + var = <<"FORM_TYPE">>, + values = [<<"http://jabber.org/protocol/muc#roomconfig">>]}, + ?STRINGXFIELD(<<"Room title">>, + <<"muc#roomconfig_roomname">>, (Config#config.title)), + ?STRINGXFIELD(<<"Room description">>, + <<"muc#roomconfig_roomdesc">>, + (Config#config.description))] ++ + case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of + allow -> + [?BOOLXFIELD(<<"Make room persistent">>, + <<"muc#roomconfig_persistentroom">>, + (Config#config.persistent))]; + deny -> [] + end ++ + [?BOOLXFIELD(<<"Make room public searchable">>, + <<"muc#roomconfig_publicroom">>, + (Config#config.public)), + ?BOOLXFIELD(<<"Make participants list public">>, + <<"public_list">>, (Config#config.public_list)), + ?BOOLXFIELD(<<"Make room password protected">>, + <<"muc#roomconfig_passwordprotectedroom">>, + (Config#config.password_protected)), + ?PRIVATEXFIELD(<<"Password">>, + <<"muc#roomconfig_roomsecret">>, + case Config#config.password_protected of + true -> Config#config.password; + false -> <<"">> + end), + #xdata_field{type = 'list-single', + label = translate:translate( + Lang, <<"Maximum Number of Occupants">>), + var = <<"muc#roomconfig_maxusers">>, + values = [MaxUsersRoomString], + options = + if is_integer(ServiceMaxUsers) -> []; + true -> make_options( + [{<<"No limit">>, <<"none">>}], + Lang) + end ++ + make_options( + [{integer_to_binary(N), integer_to_binary(N)} + || N <- lists:usort([ServiceMaxUsers, + DefaultRoomMaxUsers, + MaxUsersRoomInteger + | ?MAX_USERS_DEFAULT_LIST]), + N =< ServiceMaxUsers], + Lang)}, + #xdata_field{type = 'list-single', + label = translate:translate( + Lang, <<"Present real Jabber IDs to">>), + var = <<"muc#roomconfig_whois">>, + values = [if Config#config.anonymous -> <<"moderators">>; + true -> <<"anyone">> + end], + options = make_options( + [{<<"moderators only">>, <<"moderators">>}, + {<<"anyone">>, <<"anyone">>}], + Lang)}, + #xdata_field{type = 'list-multi', + label = translate:translate( + Lang, + <<"Roles for which Presence is Broadcasted">>), + var = <<"muc#roomconfig_presencebroadcast">>, + values = [atom_to_binary(Role, utf8) + || Role <- Config#config.presence_broadcast], + options = make_options( + [{<<"Moderator">>, <<"moderator">>}, + {<<"Participant">>, <<"participant">>}, + {<<"Visitor">>, <<"visitor">>}], + Lang)}, + ?BOOLXFIELD(<<"Make room members-only">>, + <<"muc#roomconfig_membersonly">>, + (Config#config.members_only)), + ?BOOLXFIELD(<<"Make room moderated">>, + <<"muc#roomconfig_moderatedroom">>, + (Config#config.moderated)), + ?BOOLXFIELD(<<"Default users as participants">>, + <<"members_by_default">>, + (Config#config.members_by_default)), + ?BOOLXFIELD(<<"Allow users to change the subject">>, + <<"muc#roomconfig_changesubject">>, + (Config#config.allow_change_subj)), + ?BOOLXFIELD(<<"Allow users to send private messages">>, + <<"allow_private_messages">>, + (Config#config.allow_private_messages)), + #xdata_field{type = 'list-single', + label = translate:translate( + Lang, + <<"Allow visitors to send private messages to">>), + var = <<"allow_private_messages_from_visitors">>, + values = [case Config#config.allow_private_messages_from_visitors of + anyone -> <<"anyone">>; + moderators -> <<"moderators">>; + nobody -> <<"nobody">> + end], + options = make_options( + [{<<"nobody">>, <<"nobody">>}, + {<<"moderators only">>, <<"moderators">>}, + {<<"anyone">>, <<"anyone">>}], + Lang)}, + ?BOOLXFIELD(<<"Allow users to query other users">>, + <<"allow_query_users">>, + (Config#config.allow_query_users)), + ?BOOLXFIELD(<<"Allow users to send invites">>, + <<"muc#roomconfig_allowinvites">>, + (Config#config.allow_user_invites)), + ?BOOLXFIELD(<<"Allow visitors to send status text in " + "presence updates">>, + <<"muc#roomconfig_allowvisitorstatus">>, + (Config#config.allow_visitor_status)), + ?BOOLXFIELD(<<"Allow visitors to change nickname">>, + <<"muc#roomconfig_allowvisitornickchange">>, + (Config#config.allow_visitor_nickchange)), + ?BOOLXFIELD(<<"Allow visitors to send voice requests">>, + <<"muc#roomconfig_allowvoicerequests">>, + (Config#config.allow_voice_requests)), + ?BOOLXFIELD(<<"Allow subscription">>, + <<"muc#roomconfig_allow_subscription">>, + (Config#config.allow_subscription)), + ?STRINGXFIELD(<<"Minimum interval between voice requests " + "(in seconds)">>, + <<"muc#roomconfig_voicerequestmininterval">>, + integer_to_binary(Config#config.voice_request_min_interval))] + ++ + case ejabberd_captcha:is_feature_available() of + true -> + [?BOOLXFIELD(<<"Make room CAPTCHA protected">>, + <<"captcha_protected">>, + (Config#config.captcha_protected))]; + false -> [] + end ++ + [?JIDMULTIXFIELD(<<"Exclude Jabber IDs from CAPTCHA challenge">>, + <<"muc#roomconfig_captcha_whitelist">>, + ((?SETS):to_list(Config#config.captcha_whitelist)))] + ++ + case mod_muc_log:check_access_log(StateData#state.server_host, From) of + allow -> + [?BOOLXFIELD(<<"Enable logging">>, + <<"muc#roomconfig_enablelogging">>, + (Config#config.logging))]; + deny -> [] + end, + Fields = ejabberd_hooks:run_fold(get_room_config, + StateData#state.server_host, + Fs, + [StateData, From, Lang]), + #xdata{type = form, title = Title, fields = Fields}. + +-spec set_config(xdata(), state(), binary()) -> {error, error()} | + {result, undefined, state()}. +set_config(#xdata{fields = Fields}, StateData, Lang) -> + Options = [{Var, Vals} || #xdata_field{var = Var, values = Vals} <- Fields], + case set_xoption(Options, StateData#state.config, + StateData#state.server_host, Lang) of + #config{} = Config -> + Res = change_config(Config, StateData), + {result, _, NSD} = Res, + Type = case {(StateData#state.config)#config.logging, + Config#config.logging} + of + {true, false} -> roomconfig_change_disabledlogging; + {false, true} -> roomconfig_change_enabledlogging; + {_, _} -> roomconfig_change + end, + Users = [{U#user.jid, U#user.nick, U#user.role} + || {_, U} <- (?DICT):to_list(StateData#state.users)], + add_to_log(Type, Users, NSD), + Res; + Err -> Err end. -define(SET_BOOL_XOPT(Opt, Val), @@ -3928,21 +3342,32 @@ set_config(XEl, StateData, Lang) -> _ -> Txt = <<"Value of '~s' should be boolean">>, ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), - {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)} + {error, xmpp:err_bad_request(ErrTxt, Lang)} end). -define(SET_NAT_XOPT(Opt, Val), - case catch jlib:binary_to_integer(Val) of + case catch 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)} + {error, xmpp:err_bad_request(ErrTxt, Lang)} end). --define(SET_STRING_XOPT(Opt, Val), - set_xoption(Opts, Config#config{Opt = Val}, ServerHost, Lang)). +-define(SET_STRING_XOPT(Opt, Vals), + try + V = case Vals of + [] -> <<"">>; + [Val] -> Val; + _ when is_atom(Vals) -> Vals + end, + set_xoption(Opts, Config#config{Opt = V}, ServerHost, Lang) + catch _:_ -> + Txt = <<"Incorrect value of option '~s'">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, xmpp:err_bad_request(ErrTxt, Lang)} + end). -define(SET_JIDMULTI_XOPT(Opt, Vals), begin @@ -3957,15 +3382,17 @@ set_config(XEl, StateData, Lang) -> set_xoption(Opts, Config#config{Opt = Set}, ServerHost, Lang) end). +-spec set_xoption([{binary(), [binary()]}], #config{}, + binary(), binary()) -> #config{} | {error, error()}. set_xoption([], Config, _ServerHost, _Lang) -> Config; -set_xoption([{<<"muc#roomconfig_roomname">>, [Val]} +set_xoption([{<<"muc#roomconfig_roomname">>, Vals} | Opts], Config, ServerHost, Lang) -> - ?SET_STRING_XOPT(title, Val); -set_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]} + ?SET_STRING_XOPT(title, Vals); +set_xoption([{<<"muc#roomconfig_roomdesc">>, Vals} | Opts], Config, ServerHost, Lang) -> - ?SET_STRING_XOPT(description, Val); + ?SET_STRING_XOPT(description, Vals); set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]} | Opts], Config, ServerHost, Lang) -> @@ -3994,7 +3421,7 @@ set_xoption([{<<"allow_private_messages_from_visitors">>, _ -> Txt = <<"Value of 'allow_private_messages_from_visitors' " "should be anyone|moderators|nobody">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)} + {error, xmpp:err_bad_request(Txt, Lang)} end; set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>, [Val]} @@ -4045,10 +3472,10 @@ set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>, | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(password_protected, Val); -set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]} +set_xoption([{<<"muc#roomconfig_roomsecret">>, Vals} | Opts], Config, ServerHost, Lang) -> - ?SET_STRING_XOPT(password, Val); + ?SET_STRING_XOPT(password, Vals); set_xoption([{<<"anonymous">>, [Val]} | Opts], Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(anonymous, Val); @@ -4069,7 +3496,7 @@ set_xoption([{<<"muc#roomconfig_presencebroadcast">>, Vals} | Opts], error -> Txt = <<"Value of 'muc#roomconfig_presencebroadcast' should " "be moderator|participant|visitor">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; + {error, xmpp:err_bad_request(Txt, Lang)}; {M, P, V} -> Res = if M -> [moderator]; true -> [] end ++ @@ -4101,7 +3528,7 @@ set_xoption([{<<"muc#roomconfig_whois">>, [Val]} _ -> Txt = <<"Value of 'muc#roomconfig_whois' should be " "moderators|anyone">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)} + {error, xmpp:err_bad_request(Txt, Lang)} end; set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]} | Opts], @@ -4125,7 +3552,7 @@ set_xoption([{<<"FORM_TYPE">>, _} | 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)}, + Err = {error, xmpp:err_bad_request(ErrTxt, Lang)}, case ejabberd_hooks:run_fold(set_room_option, ServerHost, Err, @@ -4136,6 +3563,7 @@ set_xoption([{Opt, Vals} | Opts], Config, ServerHost, Lang) -> set_xoption(Opts, setelement(Pos, Config, Val), ServerHost, Lang) end. +-spec change_config(#config{}, state()) -> {result, undefined, state()}. change_config(Config, StateData) -> send_config_change_info(Config, StateData), NSD = remove_subscriptions(StateData#state{config = Config}), @@ -4154,57 +3582,54 @@ change_config(Config, StateData) -> Config#config.members_only} of {false, true} -> - NSD1 = remove_nonmembers(NSD), {result, [], NSD1}; - _ -> {result, [], NSD} + NSD1 = remove_nonmembers(NSD), {result, undefined, NSD1}; + _ -> {result, undefined, NSD} end. +-spec send_config_change_info(#config{}, state()) -> ok. send_config_change_info(Config, #state{config = Config}) -> ok; send_config_change_info(New, #state{config = Old} = StateData) -> Codes = case {Old#config.logging, New#config.logging} of - {false, true} -> [<<"170">>]; - {true, false} -> [<<"171">>]; + {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}]}, + Message = #message{type = groupchat, + id = randoms:get_string(), + sub_els = [#muc_user{status_codes = Codes}]}, send_wrapped_multiple(StateData#state.jid, StateData#state.users, Message, ?NS_MUCSUB_NODES_CONFIG, StateData). +-spec remove_nonmembers(state()) -> state(). remove_nonmembers(StateData) -> lists:foldl(fun ({_LJID, #user{jid = JID}}, SD) -> Affiliation = get_affiliation(JID, SD), case Affiliation of none -> catch send_kickban_presence(<<"">>, JID, <<"">>, - <<"322">>, SD), + 322, SD), set_role(JID, none, SD); _ -> SD end end, StateData, (?DICT):to_list(StateData#state.users)). +-spec set_opts([{atom(), any()}], state()) -> state(). set_opts([], StateData) -> StateData; set_opts([{Opt, Val} | Opts], StateData) -> NSD = case Opt of @@ -4342,7 +3767,7 @@ set_opts([{Opt, Val} | Opts], StateData) -> -define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}). - +-spec make_opts(state()) -> [{atom(), any()}]. make_opts(StateData) -> Config = StateData#state.config, Subscribers = (?DICT):fold( @@ -4381,243 +3806,221 @@ make_opts(StateData) -> {subject_author, StateData#state.subject_author}, {subscribers, Subscribers}]. +-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)), + Destroy = DEl#muc_destroy{xmlns = ?NS_MUC_USER}, + lists:foreach( + fun({_LJID, Info}) -> + Nick = Info#user.nick, + Item = #muc_item{affiliation = none, + role = none}, + Packet = #presence{ + type = unavailable, + sub_els = [#muc_user{items = [Item], + destroy = Destroy}]}, + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, Packet, + ?NS_MUCSUB_NODES_CONFIG, StateData) + end, + (?DICT):to_list(StateData#state.users)), case (StateData#state.config)#config.persistent of true -> mod_muc:forget_room(StateData#state.server_host, StateData#state.host, StateData#state.room); false -> ok end, - {result, [], stop}. + {result, undefined, stop}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Disco --define(FEATURE(Var), - #xmlel{name = <<"feature">>, attrs = [{<<"var">>, Var}], - children = []}). - -define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse), case Opt of - true -> ?FEATURE(Fiftrue); - false -> ?FEATURE(Fiffalse) + true -> Fiftrue; + false -> Fiffalse end). -process_iq_disco_info(_From, set, Lang, _StateData) -> +-spec process_iq_disco_info(jid(), iq(), state()) -> + {result, disco_info()} | {error, error()}. +process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) -> Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - {error, ?ERRT_NOT_ALLOWED(Lang, Txt)}; -process_iq_disco_info(_From, get, Lang, StateData) -> + {error, xmpp:err_not_allowed(Txt, Lang)}; +process_iq_disco_info(_From, #iq{type = get, lang = Lang}, StateData) -> Config = StateData#state.config, - {result, - [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"conference">>}, - {<<"type">>, <<"text">>}, - {<<"name">>, get_title(StateData)}], - children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_VCARD}], children = []}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_MUC}], children = []}, - ?CONFIG_OPT_TO_FEATURE((Config#config.public), - <<"muc_public">>, <<"muc_hidden">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), - <<"muc_persistent">>, <<"muc_temporary">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.members_only), - <<"muc_membersonly">>, <<"muc_open">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.anonymous), - <<"muc_semianonymous">>, <<"muc_nonanonymous">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.moderated), - <<"muc_moderated">>, <<"muc_unmoderated">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), - <<"muc_passwordprotected">>, <<"muc_unsecured">>)] - ++ 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}]}]}). - + Feats = [?NS_VCARD, ?NS_MUC, + ?CONFIG_OPT_TO_FEATURE((Config#config.public), + <<"muc_public">>, <<"muc_hidden">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), + <<"muc_persistent">>, <<"muc_temporary">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.members_only), + <<"muc_membersonly">>, <<"muc_open">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.anonymous), + <<"muc_semianonymous">>, <<"muc_nonanonymous">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.moderated), + <<"muc_moderated">>, <<"muc_unmoderated">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), + <<"muc_passwordprotected">>, <<"muc_unsecured">>)] + ++ case Config#config.allow_subscription of + true -> [?NS_MUCSUB]; + false -> [] + end + ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), + Config#config.mam} of + {true, true} -> + [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1]; + _ -> + [] + end, + {result, #disco_info{xdata = [iq_disco_info_extras(Lang, StateData)], + identities = [#identity{category = <<"conference">>, + type = <<"text">>, + name = get_title(StateData)}], + features = Feats}}. + +-spec mk_rfieldt('boolean' | 'fixed' | 'hidden' | + 'jid-multi' | 'jid-single' | 'list-multi' | + 'list-single' | 'text-multi' | 'text-private' | + 'text-single', binary(), binary()) -> xdata_field(). +mk_rfieldt(Type, Var, Val) -> + #xdata_field{type = Type, var = Var, values = [Val]}. + +-spec mk_rfield(binary(), binary(), binary(), binary()) -> xdata_field(). +mk_rfield(Label, Var, Val, Lang) -> + #xdata_field{type = 'text-single', + label = translate:translate(Lang, Label), + var = Var, + values = [Val]}. + +-spec iq_disco_info_extras(binary(), state()) -> xdata(). iq_disco_info_extras(Lang, StateData) -> Len = (?DICT):size(StateData#state.users), - RoomDescription = - (StateData#state.config)#config.description, - [#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) -> + RoomDescription = (StateData#state.config)#config.description, + #xdata{type = result, + fields = [mk_rfieldt(hidden, <<"FORM_TYPE">>, + "http://jabber.org/protocol/muc#roominfo"), + mk_rfield(<<"Room description">>, + <<"muc#roominfo_description">>, + RoomDescription, Lang), + mk_rfield(<<"Number of occupants">>, + <<"muc#roominfo_occupants">>, + integer_to_binary(Len), Lang)]}. + +-spec process_iq_disco_items(jid(), iq(), state()) -> + {error, error()} | {result, disco_items()}. +process_iq_disco_items(_From, #iq{type = set, lang = Lang}, _StateData) -> Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - {error, ?ERRT_NOT_ALLOWED(Lang, Txt)}; -process_iq_disco_items(From, get, Lang, StateData) -> + {error, xmpp:err_not_allowed(Txt, Lang)}; +process_iq_disco_items(From, #iq{type = get, lang = Lang}, StateData) -> case (StateData#state.config)#config.public_list of true -> - {result, get_mucroom_disco_items(StateData), 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)} + {error, xmpp:err_forbidden(Txt, Lang)} end end. -process_iq_captcha(_From, get, Lang, _SubEl, - _StateData) -> +-spec process_iq_captcha(jid(), iq(), state()) -> {error, error()} | + {result, undefined}. +process_iq_captcha(_From, #iq{type = get, lang = Lang}, _StateData) -> Txt = <<"Value 'get' of 'type' attribute is not allowed">>, - {error, ?ERRT_NOT_ALLOWED(Lang, Txt)}; -process_iq_captcha(_From, set, Lang, SubEl, - StateData) -> + {error, xmpp:err_not_allowed(Txt, Lang)}; +process_iq_captcha(_From, #iq{type = set, lang = Lang, sub_els = [SubEl]}, + _StateData) -> case ejabberd_captcha:process_reply(SubEl) of - ok -> {result, [], StateData}; + ok -> {result, undefined}; {error, malformed} -> Txt = <<"Incorrect CAPTCHA submit">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; + {error, xmpp:err_bad_request(Txt, Lang)}; _ -> Txt = <<"The CAPTCHA verification has failed">>, - {error, ?ERRT_NOT_ALLOWED(Lang, Txt)} + {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, 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} + {result, #vcard_temp{}} end; -process_iq_vcard(From, set, Lang, SubEl, StateData) -> +process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [SubEl]}, + StateData) -> case get_affiliation(From, StateData) of owner -> - VCardRaw = fxml:element_to_binary(SubEl), + VCardRaw = fxml:element_to_binary(xmpp:encode(SubEl)), Config = StateData#state.config, NewConfig = Config#config{vcard = VCardRaw}, change_config(NewConfig, StateData); _ -> ErrText = <<"Owner privileges required">>, - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + {error, xmpp:err_forbidden(ErrText, Lang)} end. -process_iq_mucsub(From, Packet, +-spec process_iq_mucsub(jid(), iq(), state()) -> + {error, error()} | + {result, undefined | muc_subscribe(), state()} | + {ignore, state()}. +process_iq_mucsub(_From, #iq{type = set, lang = Lang, + sub_els = [#muc_subscribe{}]}, + #state{config = #config{allow_subscription = false}}) -> + {error, xmpp:err_not_allowed(<<"Subscriptions are not allowed">>, Lang)}; +process_iq_mucsub(From, #iq{type = set, lang = Lang, - sub_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), + sub_els = [#muc_subscribe{nick = Nick}]} = Packet, + StateData) -> + LJID = jid:tolower(From), + case (?DICT):find(LJID, StateData#state.users) of + {ok, #user{role = Role, nick = Nick1}} when Nick1 /= Nick -> + Nodes = get_subscription_nodes(Packet), + case {nick_collision(From, Nick, StateData), + mod_muc:can_use_nick(StateData#state.server_host, + StateData#state.host, + From, Nick)} of + {true, _} -> + ErrText = <<"That nickname is already in use by another occupant">>, + {error, xmpp:err_conflict(ErrText, Lang)}; + {_, false} -> + ErrText = <<"That nickname is registered by another person">>, + {error, xmpp:err_conflict(ErrText, Lang)}; + _ -> NewStateData = add_online_user( From, Nick, Role, true, Nodes, StateData), - {result, subscription_nodes_to_events(Nodes), NewStateData}; - error -> - add_new_user(From, Nick, Packet, StateData) + {result, subscribe_result(Packet), NewStateData} end; - _ -> - Err = ?ERRT_NOT_ALLOWED(Lang, <<"Subscriptions are not allowed">>), - {error, Err} + {ok, #user{role = Role}} -> + Nodes = get_subscription_nodes(Packet), + NewStateData = add_online_user( + From, Nick, Role, true, Nodes, StateData), + {result, subscribe_result(Packet), NewStateData}; + error -> + add_new_user(From, Nick, Packet, StateData) end; -process_iq_mucsub(From, _Packet, - #iq{type = set, - sub_el = #xmlel{name = <<"unsubscribe">>}}, +process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]}, StateData) -> LJID = jid:tolower(From), case ?DICT:find(LJID, StateData#state.users) of {ok, #user{is_subscriber = true} = User} -> NewStateData = remove_subscription(From, User, StateData), store_room(NewStateData), - {result, [], NewStateData}; + {result, undefined, NewStateData}; _ -> - {result, [], StateData} + {result, undefined, StateData} 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) -> +process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) -> Txt = <<"Value 'get' of 'type' attribute is not allowed">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}. + {error, xmpp:err_bad_request(Txt, Lang)}. +-spec remove_subscription(jid(), #user{}, state()) -> state(). remove_subscription(JID, #user{is_subscriber = true} = User, StateData) -> case User#user.last_presence of undefined -> @@ -4631,6 +4034,7 @@ remove_subscription(JID, #user{is_subscriber = true} = User, StateData) -> remove_subscription(_JID, #user{}, StateData) -> StateData. +-spec remove_subscriptions(state()) -> state(). remove_subscriptions(StateData) -> if not (StateData#state.config)#config.allow_subscription -> dict:fold( @@ -4641,41 +4045,32 @@ remove_subscriptions(StateData) -> 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(iq()) -> [binary()]. +get_subscription_nodes(#iq{sub_els = [#muc_subscribe{events = Nodes}]}) -> + lists:filter( + fun(Node) -> + lists:member(Node, [?NS_MUCSUB_NODES_PRESENCE, + ?NS_MUCSUB_NODES_MESSAGES, + ?NS_MUCSUB_NODES_AFFILIATIONS, + ?NS_MUCSUB_NODES_SUBJECT, + ?NS_MUCSUB_NODES_CONFIG, + ?NS_MUCSUB_NODES_PARTICIPANTS]) + end, Nodes); get_subscription_nodes(_) -> []. -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), @@ -4689,352 +4084,215 @@ 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, ">>) end, - Len = (?DICT):fold(fun (_, _, Acc) -> Acc + 1 end, 0, - StateData#state.users), + Len = (?DICT):size(StateData#state.users), <<" (", Desc/binary, (iolist_to_binary(integer_to_list(Len)))/binary, ")">>. +-spec get_mucroom_disco_items(state()) -> disco_items(). get_mucroom_disco_items(StateData) -> - lists:map(fun ({_LJID, Info}) -> + Items = 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 = []} + #disco_item{jid = jid:make(StateData#state.room, + StateData#state.host, + Nick), + name = Nick} end, - (?DICT):to_list(StateData#state.users)). + (?DICT):to_list(StateData#state.users)), + #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 is_voice_request(message()) -> boolean(). +is_voice_request(Packet) -> + Els = xmpp:get_els(Packet), + lists:any( + fun(#xdata{} = X) -> + case {xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), + xmpp_util:get_xdata_values(<<"muc#role">>, X)} of + {[<<"http://jabber.org/protocol/muc#request">>], + [<<"participant">>]} -> + true; + _ -> + false + end; + (_) -> + false + end, Els). +-spec prepare_request_form(jid(), binary(), binary()) -> message(). prepare_request_form(Requester, Nick, Lang) -> - #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, <<"Voice request">>), + Instruction = translate:translate( + Lang, <<"Either approve or decline the voice request.">>), + Fs = [#xdata_field{var = <<"FORM_TYPE">>, + type = hidden, + values = [<<"http://jabber.org/protocol/muc#request">>]}, + #xdata_field{var = <<"muc#role">>, + type = hidden, + values = [<<"participant">>]}, + ?STRINGXFIELD(<<"User JID">>, <<"muc#jid">>, + jid:to_string(Requester)), + ?STRINGXFIELD(<<"Nickname">>, <<"muc#roomnick">>, Nick), + ?BOOLXFIELD(<<"Grant voice to this person?">>, + <<"muc#request_allow">>, false)], + #message{type = normal, + sub_els = [#xdata{type = form, + title = Title, + instructions = [Instruction], + fields = Fs}]}. + +-spec send_voice_request(jid(), binary(), state()) -> ok. +send_voice_request(From, Lang, StateData) -> Moderators = search_role(moderator, StateData), FromNick = find_nick_by_jid(From, StateData), lists:foreach(fun ({_, User}) -> ejabberd_router:route( StateData#state.jid, User#user.jid, - prepare_request_form(From, FromNick, <<"">>)) + prepare_request_form(From, FromNick, Lang)) 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). +-spec is_voice_approvement(message()) -> boolean(). +is_voice_approvement(Packet) -> + Els = xmpp:get_els(Packet), + lists:any( + fun(#xdata{} = X) -> + case {xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), + xmpp_util:get_xdata_values(<<"muc#role">>, X), + xmpp_util:get_xdata_values(<<"muc#request_allow">>, X)} of + {[<<"http://jabber.org/protocol/muc#request">>], + [<<"participant">>], [Flag]} when Flag == <<"true">>; + Flag == <<"1">> -> + true; + _ -> + false + end; + (_) -> + false + end, Els). + +-spec extract_jid_from_voice_approvement(message()) -> jid() | error. +extract_jid_from_voice_approvement(Packet) -> + Els = xmpp:get_els(Packet), + lists:foldl( + fun(#xdata{} = X, error) -> + case {xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), + xmpp_util:get_xdata_values(<<"muc#role">>, X), + xmpp_util:get_xdata_values(<<"muc#request_allow">>, X), + xmpp_util:get_xdata_values(<<"muc#jid">>, X)} of + {[<<"http://jabber.org/protocol/muc#request">>], + [<<"participant">>], [Flag], [J]} when Flag == <<"true">>; + Flag == <<"1">> -> + jid:from_string(J); + _ -> + error + end; + (_, Acc) -> + Acc + end, error, Els). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Invitation support -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 is_invitation(message()) -> boolean(). +is_invitation(Packet) -> + Els = xmpp:get_els(Packet), + lists:any( + fun(#muc_user{invites = [_|_]}) -> true; + (_) -> false + end, Els). + +-spec check_invitation(jid(), message(), state()) -> {error, error()} | jid(). +check_invitation(From, Packet, StateData) -> + Lang = xmpp:get_lang(Packet), FAffiliation = get_affiliation(From, StateData), - CanInvite = - (StateData#state.config)#config.allow_user_invites - orelse - FAffiliation == admin orelse FAffiliation == owner, - 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 + false -> + Txt = <<"Invitations are not allowed in this conference">>, + {error, xmpp:err_not_allowed(Txt, Lang)}; + true -> + case xmpp:get_subtag(Packet, #muc_user{}) of + #muc_user{invites = [#muc_invite{to = undefined}]} -> + Txt = <<"No 'to' attribute found">>, + {error, xmpp:err_bad_request(Txt, Lang)}; + #muc_user{invites = [#muc_invite{to = JID, reason = Reason} = I]} -> + Invite = I#muc_invite{to = undefined, from = From}, + Password = case (StateData#state.config)#config.password_protected of + true -> + (StateData#state.config)#config.password; + false -> + undefined + end, + XUser = #muc_user{password = Password, invites = [Invite]}, + XConference = #x_conference{jid = jid:make(StateData#state.room, + StateData#state.host), + reason = Reason}, + Body = iolist_to_binary( + [io_lib:format( + translate:translate( + Lang, + <<"~s invites you to the room ~s">>), + [jid:to_string(From), + jid:to_string({StateData#state.room, + StateData#state.host, + <<"">>})]), + case (StateData#state.config)#config.password_protected of true -> <<", ", - (translate:translate(Lang, - <<"the password is">>))/binary, + (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 + end, + case Reason of + <<"">> -> <<"">>; + _ -> <<" (", Reason/binary, ") ">> + end]), + Msg = #message{type = normal, + body = xmpp:mk_text(Body), + sub_els = [XUser, XConference]}, + ejabberd_router:route(StateData#state.jid, JID, Msg), + JID; + #muc_user{invites = [_|_]} -> + Txt = <<"Multiple <invite/> elements are not allowed">>, + {error, xmpp:err_forbidden(Txt, Lang)}; + _ -> + Txt = <<"No <invite/> element found">>, + {error, xmpp:err_bad_request(Txt, Lang)} + end end. %% Handle a message sent to the room by a non-participant. %% If it is a decline, send to the inviter. %% Otherwise, an error message is sent to the sender. -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) -> + case xmpp:get_subtag(Packet, #muc_user{}) of + #muc_user{decline = #muc_decline{to = #jid{} = To} = Decline} = XUser -> + NewDecline = Decline#muc_decline{to = undefined, from = From}, + NewXUser = XUser#muc_user{decline = NewDecline}, + NewPacket = xmpp:set_subtag(Packet, NewXUser), + ejabberd_router:route(StateData#state.jid, To, NewPacket); + _ -> + ErrText = <<"Only occupants are allowed to send messages " + "to the conference">>, + Err = xmpp:err_not_acceptable(ErrText, xmpp:get_lang(Packet)), + ejabberd_router:route_error(StateData#state.jid, From, Packet, Err) end. -%% 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 @@ -5055,6 +4313,7 @@ add_to_log(Type, Data, StateData) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Users number checking +-spec tab_add_online_user(jid(), state()) -> ok. tab_add_online_user(JID, StateData) -> {LUser, LServer, LResource} = jid:tolower(JID), US = {LUser, LServer}, @@ -5062,8 +4321,10 @@ tab_add_online_user(JID, StateData) -> Host = StateData#state.host, catch ets:insert(muc_online_users, #muc_online_users{us = US, resource = LResource, - room = Room, host = Host}). + room = Room, host = Host}), + ok. +-spec tab_remove_online_user(jid(), state()) -> ok. tab_remove_online_user(JID, StateData) -> {LUser, LServer, LResource} = jid:tolower(JID), US = {LUser, LServer}, @@ -5071,8 +4332,10 @@ tab_remove_online_user(JID, StateData) -> Host = StateData#state.host, catch ets:delete_object(muc_online_users, #muc_online_users{us = US, resource = LResource, - room = Room, host = Host}). + room = Room, host = Host}), + ok. +-spec tab_count_user(jid()) -> non_neg_integer(). tab_count_user(JID) -> {LUser, LServer, _} = jid:tolower(JID), US = {LUser, LServer}, @@ -5083,9 +4346,11 @@ tab_count_user(JID) -> _ -> 0 end. +-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))). +-spec store_room(state()) -> ok. store_room(StateData) -> if (StateData#state.config)#config.persistent -> mod_muc:store_room(StateData#state.server_host, @@ -5095,6 +4360,7 @@ store_room(StateData) -> ok end. +-spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok. send_wrapped(From, To, Packet, Node, State) -> LTo = jid:tolower(To), case ?DICT:find(LTo, State#state.users) of @@ -5112,27 +4378,26 @@ send_wrapped(From, To, Packet, Node, State) -> ejabberd_router:route(From, To, Packet) end. +-spec wrap(jid(), jid(), stanza(), binary()) -> message(). 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]}. + El = xmpp:encode(xmpp:set_from_to(Packet, From, To)), + #message{ + sub_els = [#pubsub_event{ + items = [#pubsub_event_items{ + node = Node, + items = [#pubsub_event_item{ + id = randoms:get_string(), + xml_els = [El]}]}]}]}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Multicast +-spec send_multiple(jid(), binary(), [#user{}], stanza()) -> ok. send_multiple(From, Server, Users, Packet) -> JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). +-spec send_wrapped_multiple(jid(), [#user{}], stanza(), binary(), state()) -> ok. send_wrapped_multiple(From, Users, Packet, Node, State) -> lists:foreach( fun({_, #user{jid = To}}) -> @@ -5141,10 +4406,6 @@ send_wrapped_multiple(From, Users, Packet, Node, State) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% 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). +-spec has_body_or_subject(message()) -> boolean(). +has_body_or_subject(#message{body = Body, subject = Subj}) -> + Body /= [] orelse Subj /= []. |