Welcome to mirror list, hosted at ThFree Co, Russian Federation.

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