%%%----------------------------------------------------------------------
%%% 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-2024   ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------

-module(mod_muc_room).

-author('alexey@process-one.net').

-protocol({xep, 317, '0.1', '21.12', "complete", "conversejs/prosody compatible"}).
-protocol({xep, 410, '1.1.0', '18.12', "complete", ""}).

-behaviour(p1_fsm).

%% External exports
-export([start_link/10,
	 start_link/8,
	 start/10,
	 start/8,
	 supervisor/1,
	 get_role/2,
	 get_affiliation/2,
	 is_occupant_or_admin/2,
	 route/2,
	 expand_opts/1,
	 config_fields/0,
	 destroy/1,
	 destroy/2,
	 shutdown/1,
	 get_config/1,
	 set_config/2,
	 get_state/1,
	 get_info/1,
	 change_item/5,
	 change_item_async/5,
	 config_reloaded/1,
	 subscribe/4,
	 unsubscribe/2,
	 is_subscribed/2,
	 get_subscribers/1,
	 service_message/2,
	 get_disco_item/4]).

%% gen_fsm callbacks
-export([init/1,
	 normal_state/2,
	 handle_event/3,
	 handle_sync_event/4,
	 handle_info/3,
	 terminate/3,
	 code_change/4]).

-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("translate.hrl").
-include("mod_muc_room.hrl").
-include("ejabberd_stacktrace.hrl").

-define(MAX_USERS_DEFAULT_LIST,
	[5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]).

-define(MUC_HAT_ADD_CMD, <<"http://prosody.im/protocol/hats#add">>).
-define(MUC_HAT_REMOVE_CMD, <<"http://prosody.im/protocol/hats#remove">>).
-define(MUC_HAT_LIST_CMD, <<"p1:hats#list">>).
-define(MAX_HATS_USERS, 100).
-define(MAX_HATS_PER_USER, 10).
-define(CLEAN_ROOM_TIMEOUT, 30000).

%-define(DBGFSM, true).

-ifdef(DBGFSM).

-define(FSMOPTS, [{debug, [trace]}]).

-else.

-define(FSMOPTS, []).

-endif.

-type state() :: #state{}.
-type fsm_stop() :: {stop, normal, state()}.
-type fsm_next() :: {next_state, normal_state, state()}.
-type fsm_transition() :: fsm_stop() | fsm_next().
-type disco_item_filter() ::  only_non_empty | all | non_neg_integer().
-type admin_action() :: {jid(), affiliation | role, affiliation() | role(), binary()}.
-export_type([state/0, disco_item_filter/0]).

-callback set_affiliation(binary(), binary(), binary(), jid(), affiliation(),
			  binary()) -> ok | {error, any()}.
-callback set_affiliations(binary(), binary(), binary(),
			   affiliations()) -> ok | {error, any()}.
-callback get_affiliation(binary(), binary(), binary(),
			  binary(), binary()) -> {ok, affiliation()} | {error, any()}.
-callback get_affiliations(binary(), binary(), binary()) -> {ok, affiliations()} | {error, any()}.
-callback search_affiliation(binary(), binary(), binary(), affiliation()) ->
    {ok, [{ljid(), {affiliation(), binary()}}]} | {error, any()}.

%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
-spec start(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(),
	    atom(), jid(), binary(), [{atom(), term()}], ram | file) ->
		   {ok, pid()} | {error, any()}.
start(Host, ServerHost, Access, Room, HistorySize, RoomShaper,
      Creator, Nick, DefRoomOpts, QueueType) ->
    supervisor:start_child(
      supervisor(ServerHost),
      [Host, ServerHost, Access, Room, HistorySize,
       RoomShaper, Creator, Nick, DefRoomOpts, QueueType]).

-spec start(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(),
	    atom(), [{atom(), term()}], ram | file) ->
		   {ok, pid()} | {error, any()}.
start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) ->
    supervisor:start_child(
      supervisor(ServerHost),
      [Host, ServerHost, Access, Room, HistorySize,
       RoomShaper, Opts, QueueType]).

-spec start_link(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(),
		 atom(), jid(), binary(), [{atom(), term()}], ram | file) ->
			{ok, pid()} | {error, any()}.
start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper,
	   Creator, Nick, DefRoomOpts, QueueType) ->
    p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
				 RoomShaper, Creator, Nick, DefRoomOpts, QueueType],
		       ?FSMOPTS).

-spec start_link(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(),
		 atom(), [{atom(), term()}], ram | file) ->
			{ok, pid()} | {error, any()}.
start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) ->
    p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
				 RoomShaper, Opts, QueueType],
		       ?FSMOPTS).

-spec supervisor(binary()) -> atom().
supervisor(Host) ->
    gen_mod:get_module_proc(Host, mod_muc_room_sup).

-spec destroy(pid()) -> ok.
destroy(Pid) ->
    p1_fsm:send_all_state_event(Pid, destroy).

-spec destroy(pid(), binary()) -> ok.
destroy(Pid, Reason) ->
    p1_fsm:send_all_state_event(Pid, {destroy, Reason}).

-spec shutdown(pid()) -> boolean().
shutdown(Pid) ->
    ejabberd_cluster:send(Pid, shutdown).

-spec config_reloaded(pid()) -> boolean().
config_reloaded(Pid) ->
    ejabberd_cluster:send(Pid, config_reloaded).

-spec get_config(pid()) -> {ok, config()} | {error, notfound | timeout}.
get_config(Pid) ->
    try p1_fsm:sync_send_all_state_event(Pid, get_config)
    catch _:{timeout, {p1_fsm, _, _}} ->
	    {error, timeout};
	  _:{_, {p1_fsm, _, _}} ->
	    {error, notfound}
    end.

-spec set_config(pid(), config()) -> {ok, config()} | {error, notfound | timeout}.
set_config(Pid, Config) ->
    try p1_fsm:sync_send_all_state_event(Pid, {change_config, Config})
    catch _:{timeout, {p1_fsm, _, _}} ->
	    {error, timeout};
	  _:{_, {p1_fsm, _, _}} ->
	    {error, notfound}
    end.

-spec change_item(pid(), jid(), affiliation | role, affiliation() | role(), binary()) ->
			 {ok, state()} | {error, notfound | timeout}.
change_item(Pid, JID, Type, AffiliationOrRole, Reason) ->
    try p1_fsm:sync_send_all_state_event(
	  Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined})
    catch _:{timeout, {p1_fsm, _, _}} ->
	    {error, timeout};
	  _:{_, {p1_fsm, _, _}} ->
	    {error, notfound}
    end.

-spec change_item_async(pid(), jid(), affiliation | role, affiliation() | role(), binary()) -> ok.
change_item_async(Pid, JID, Type, AffiliationOrRole, Reason) ->
    p1_fsm:send_all_state_event(
      Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}).

-spec get_state(pid()) -> {ok, state()} | {error, notfound | timeout}.
get_state(Pid) ->
    try p1_fsm:sync_send_all_state_event(Pid, get_state)
    catch _:{timeout, {p1_fsm, _, _}} ->
	    {error, timeout};
	  _:{_, {p1_fsm, _, _}} ->
	    {error, notfound}
    end.

-spec get_info(pid()) -> {ok, #{occupants_number => integer()}} |
                         {error, notfound | timeout}.
get_info(Pid) ->
    try
        {ok, p1_fsm:sync_send_all_state_event(Pid, get_info)}
    catch _:{timeout, {p1_fsm, _, _}} ->
	    {error, timeout};
	  _:{_, {p1_fsm, _, _}} ->
	    {error, notfound}
    end.

-spec subscribe(pid(), jid(), binary(), [binary()]) -> {ok, [binary()]} | {error, binary()}.
subscribe(Pid, JID, Nick, Nodes) ->
    try p1_fsm:sync_send_all_state_event(Pid, {muc_subscribe, JID, Nick, Nodes})
    catch _:{timeout, {p1_fsm, _, _}} ->
	    {error, ?T("Request has timed out")};
	  _:{_, {p1_fsm, _, _}} ->
	    {error, ?T("Conference room does not exist")}
    end.

-spec unsubscribe(pid(), jid()) -> ok | {error, binary()}.
unsubscribe(Pid, JID) ->
    try p1_fsm:sync_send_all_state_event(Pid, {muc_unsubscribe, JID})
    catch _:{timeout, {p1_fsm, _, _}} ->
	    {error, ?T("Request has timed out")};
	  exit:{normal, {p1_fsm, _, _}} ->
	    ok;
	  _:{_, {p1_fsm, _, _}} ->
	    {error, ?T("Conference room does not exist")}
    end.

-spec is_subscribed(pid(), jid()) -> {true, binary(), [binary()]} | false.
is_subscribed(Pid, JID) ->
    try p1_fsm:sync_send_all_state_event(Pid, {is_subscribed, JID})
    catch _:{_, {p1_fsm, _, _}} -> false
    end.

-spec get_subscribers(pid()) -> {ok, [jid()]} | {error, notfound | timeout}.
get_subscribers(Pid) ->
    try p1_fsm:sync_send_all_state_event(Pid, get_subscribers)
    catch _:{timeout, {p1_fsm, _, _}} ->
	    {error, timeout};
	  _:{_, {p1_fsm, _, _}} ->
	    {error, notfound}
    end.

-spec service_message(pid(), binary()) -> ok.
service_message(Pid, Text) ->
    p1_fsm:send_all_state_event(Pid, {service_message, Text}).

-spec get_disco_item(pid(), disco_item_filter(), jid(), binary()) ->
			    {ok, binary()} | {error, notfound | timeout}.
get_disco_item(Pid, Filter, JID, Lang) ->
    Timeout = 100,
    Time = erlang:system_time(millisecond),
    Query = {get_disco_item, Filter, JID, Lang, Time+Timeout},
    try p1_fsm:sync_send_all_state_event(Pid, Query, Timeout) of
	{item, Desc} ->
	    {ok, Desc};
	false ->
	    {error, notfound}
    catch _:{timeout, {p1_fsm, _, _}} ->
	    {error, timeout};
	  _:{_, {p1_fsm, _, _}} ->
	    {error, notfound}
    end.

%%%----------------------------------------------------------------------
%%% Callback functions from gen_fsm
%%%----------------------------------------------------------------------

init([Host, ServerHost, Access, Room, HistorySize,
      RoomShaper, Creator, _Nick, DefRoomOpts, QueueType]) ->
    process_flag(trap_exit, true),
    misc:set_proc_label({?MODULE, Room, Host}),
    Shaper = ejabberd_shaper:new(RoomShaper),
    RoomQueue = room_queue_new(ServerHost, Shaper, QueueType),
    State = set_opts(DefRoomOpts,
		     #state{host = Host, server_host = ServerHost,
			    access = Access, room = Room,
			    history = lqueue_new(HistorySize, QueueType),
			    jid = jid:make(Room, Host),
			    just_created = true,
			    room_queue = RoomQueue,
			    room_shaper = Shaper}),
    State1 = set_affiliation(Creator, owner, State),
    store_room(State1),
    ?INFO_MSG("Created MUC room ~ts@~ts by ~ts",
	      [Room, Host, jid:encode(Creator)]),
    add_to_log(room_existence, created, State1),
    add_to_log(room_existence, started, State1),
    ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]),
    erlang:send_after(?CLEAN_ROOM_TIMEOUT, self(),
                      close_room_if_temporary_and_empty),
    {ok, normal_state, reset_hibernate_timer(State1)};
init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) ->
    process_flag(trap_exit, true),
    misc:set_proc_label({?MODULE, Room, Host}),
    Shaper = ejabberd_shaper:new(RoomShaper),
    RoomQueue = room_queue_new(ServerHost, Shaper, QueueType),
    Jid = jid:make(Room, Host),
    State = set_opts(Opts, #state{host = Host,
				  server_host = ServerHost,
				  access = Access,
				  room = Room,
				  history = lqueue_new(HistorySize, QueueType),
				  jid = Jid,
				  room_queue = RoomQueue,
				  room_shaper = Shaper}),
    add_to_log(room_existence, started, State),
    ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]),
    State1 = cleanup_affiliations(State),
    State2 =
    case {lists:keyfind(hibernation_time, 1, Opts),
	  (State1#state.config)#config.mam,
	  (State1#state.history)#lqueue.max} of
	{{_, V}, true, L} when is_integer(V), L > 0 ->
	    {Msgs, _, _} = mod_mam:select(ServerHost, Jid, Jid, [],
					 #rsm_set{max = L, before = <<"9999999999999999">>},
					 groupchat, only_messages),
	    Hist2 =
	    lists:foldl(
		fun({_, TS, #forwarded{sub_els = [#message{meta = #{archive_nick := Nick}} = Msg]}}, Hist) ->
		    Pkt = xmpp:set_from_to(Msg, jid:replace_resource(Jid, Nick), Jid),
		    Size = element_size(Pkt),
		    lqueue_in({Nick, Pkt, false, misc:usec_to_now(TS), Size}, Hist)
		end, State1#state.history, Msgs),
	    State1#state{history = Hist2};
	_ ->
	    State1
    end,
    erlang:send_after(?CLEAN_ROOM_TIMEOUT, self(),
                      close_room_if_temporary_and_empty),
    {ok, normal_state, reset_hibernate_timer(State2)}.

normal_state({route, <<"">>,
	      #message{from = From, type = Type, lang = Lang} = Packet},
	     StateData) ->
    case is_user_online(From, StateData) orelse
	is_subscriber(From, StateData) orelse
	is_user_allowed_message_nonparticipant(From, StateData) of
	true when Type == groupchat ->
	    Activity = get_user_activity(From, StateData),
	    Now = erlang:system_time(microsecond),
	    MinMessageInterval = trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000000),
	    Size = element_size(Packet),
	    {MessageShaper, MessageShaperInterval} =
		ejabberd_shaper:update(Activity#activity.message_shaper, Size),
	    if Activity#activity.message /= undefined ->
		    ErrText = ?T("Traffic rate limit is exceeded"),
		    Err = xmpp:err_resource_constraint(ErrText, Lang),
		    ejabberd_router:route_error(Packet, Err),
		    {next_state, normal_state, StateData};
	       Now >= Activity#activity.message_time + MinMessageInterval,
	       MessageShaperInterval == 0 ->
		    {RoomShaper, RoomShaperInterval} =
			ejabberd_shaper:update(StateData#state.room_shaper, Size),
		    RoomQueueEmpty = case StateData#state.room_queue of
					 undefined -> true;
					 RQ -> p1_queue:is_empty(RQ)
				     end,
		    if RoomShaperInterval == 0, RoomQueueEmpty ->
			    NewActivity = Activity#activity{
					    message_time = Now,
					    message_shaper = MessageShaper},
			    StateData1 = store_user_activity(From,
							     NewActivity,
							     StateData),
			    StateData2 = StateData1#state{room_shaper =
							      RoomShaper},
			    process_groupchat_message(Packet,
						      StateData2);
		       true ->
			    StateData1 = if RoomQueueEmpty ->
						 erlang:send_after(RoomShaperInterval,
								   self(),
								   process_room_queue),
						 StateData#state{room_shaper =
								     RoomShaper};
					    true -> StateData
					 end,
			    NewActivity = Activity#activity{
					    message_time = Now,
					    message_shaper = MessageShaper,
					    message = Packet},
			    RoomQueue = p1_queue:in({message, From},
						    StateData#state.room_queue),
			    StateData2 = store_user_activity(From,
							     NewActivity,
							     StateData1),
			    StateData3 = StateData2#state{room_queue = RoomQueue},
			    {next_state, normal_state, StateData3}
		    end;
	       true ->
		    MessageInterval = (Activity#activity.message_time +
					   MinMessageInterval - Now) div 1000,
		    Interval = lists:max([MessageInterval,
					  MessageShaperInterval]),
		    erlang:send_after(Interval, self(),
				      {process_user_message, From}),
		    NewActivity = Activity#activity{
				    message = Packet,
				    message_shaper = MessageShaper},
		    StateData1 = store_user_activity(From, NewActivity,	StateData),
		    {next_state, normal_state, StateData1}
	    end;
	true when Type == error ->
	    case is_user_online(From, StateData) of
		true ->
		    ErrorText = ?T("It is not allowed to send error messages to the"
				   " room. The participant (~s) has sent an error "
				   "message (~s) and got kicked from the room"),
		    NewState = expulse_participant(Packet, From, StateData,
						   translate:translate(Lang,
								       ErrorText)),
		    close_room_if_temporary_and_empty(NewState);
		_ ->
		    {next_state, normal_state, StateData}
	    end;
	true when Type == chat ->
	    ErrText = ?T("It is not allowed to send private messages "
			 "to the conference"),
	    Err = xmpp:err_not_acceptable(ErrText, Lang),
	    ejabberd_router:route_error(Packet, Err),
	    {next_state, normal_state, StateData};
	true when Type == normal ->
	    {next_state, normal_state,
	     try xmpp:decode_els(Packet) of
		 Pkt -> process_normal_message(From, Pkt, StateData)
	     catch _:{xmpp_codec, Why} ->
		     Txt = xmpp:io_format_error(Why),
		     Err = xmpp:err_bad_request(Txt, Lang),
		     ejabberd_router:route_error(Packet, Err),
		     StateData
	     end};
	true ->
	    ErrText = ?T("Improper message type"),
	    Err = xmpp:err_not_acceptable(ErrText, Lang),
	    ejabberd_router:route_error(Packet, Err),
	    {next_state, normal_state, StateData};
	false when Type /= error ->
	    handle_roommessage_from_nonparticipant(Packet, StateData, From),
	    {next_state, normal_state, StateData};
	false ->
	    {next_state, normal_state, StateData}
    end;
normal_state({route, <<"">>,
	      #iq{from = From, type = Type, lang = Lang, sub_els = [_]} = IQ0},
	     StateData) when Type == get; Type == set ->
    try
	case ejabberd_hooks:run_fold(
	       muc_process_iq,
	       StateData#state.server_host,
	       xmpp:set_from_to(xmpp:decode_els(IQ0),
				From, StateData#state.jid),
	       [StateData]) of
	    ignore ->
		{next_state, normal_state, StateData};
            {ignore, StateData2} ->
		{next_state, normal_state, StateData2};
	    #iq{type = T} = IQRes when T == error; T == result ->
		ejabberd_router:route(IQRes),
		{next_state, normal_state, StateData};
	    #iq{sub_els = [SubEl]} = IQ ->
		Res1 = case SubEl of
			   #muc_admin{} ->
			       process_iq_admin(From, IQ, StateData);
			   #muc_owner{} ->
			       process_iq_owner(From, IQ, StateData);
			   #disco_info{} ->
			       process_iq_disco_info(From, IQ, StateData);
			   #disco_items{} ->
			       process_iq_disco_items(From, IQ, StateData);
			   #vcard_temp{} ->
			       process_iq_vcard(From, IQ, StateData);
			   #muc_subscribe{} ->
			       process_iq_mucsub(From, IQ, StateData);
			   #muc_unsubscribe{} ->
			       process_iq_mucsub(From, IQ, StateData);
			   #muc_subscriptions{} ->
			       process_iq_mucsub(From, IQ, StateData);
			   #xcaptcha{} ->
			       process_iq_captcha(From, IQ, StateData);
			   #adhoc_command{} ->
			       process_iq_adhoc(From, IQ, StateData);
			   #register{} ->
                               mod_muc:process_iq_register(IQ);
			   #message_moderate{id = Id, reason = Reason} -> % moderate:1
			       process_iq_moderate(From, IQ, Id, Reason, StateData);
			   #fasten_apply_to{id = ModerateId} = ApplyTo ->
			       case xmpp:get_subtag(ApplyTo, #message_moderate_21{}) of
				   #message_moderate_21{reason = Reason} -> % moderate:0
				       process_iq_moderate(From, IQ, ModerateId, Reason, StateData);
				   _ ->
				       Txt = ?T("The feature requested is not "
						"supported by the conference"),
				       {error, xmpp:err_service_unavailable(Txt, Lang)}
			       end;
			   _ ->
			       Txt = ?T("The feature requested is not "
					"supported by the conference"),
			       {error, xmpp:err_service_unavailable(Txt, Lang)}
		       end,
		{IQRes, NewStateData} =
		    case Res1 of
			{result, Res, SD} ->
			    {xmpp:make_iq_result(IQ, Res), SD};
			{result, Res} ->
			    {xmpp:make_iq_result(IQ, Res), StateData};
			{ignore, SD} ->
			    {ignore, SD};
			{error, Error} ->
			    {xmpp:make_error(IQ0, Error), StateData}
		    end,
		if IQRes /= ignore ->
			ejabberd_router:route(IQRes);
		   true ->
			ok
		end,
		case NewStateData of
		    stop ->
			Conf = StateData#state.config,
			{stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}};
		    _ when NewStateData#state.just_created ->
			close_room_if_temporary_and_empty(NewStateData);
		    _ ->
			{next_state, normal_state, NewStateData}
		end
	end
    catch _:{xmpp_codec, Why} ->
	    ErrTxt = xmpp:io_format_error(Why),
	    Err = xmpp:err_bad_request(ErrTxt, Lang),
	    ejabberd_router:route_error(IQ0, Err),
	    {next_state, normal_state, StateData}
    end;
normal_state({route, <<"">>, #iq{} = IQ}, StateData) ->
    Err = xmpp:err_bad_request(),
    ejabberd_router:route_error(IQ, Err),
    case StateData#state.just_created of
	true -> {stop, normal, StateData};
	_ -> {next_state, normal_state, StateData}
    end;
normal_state({route, Nick, #presence{from = From} = Packet}, StateData) ->
    Activity = get_user_activity(From, StateData),
    Now = erlang:system_time(microsecond),
    MinPresenceInterval =
	trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000000),
    if (Now >= Activity#activity.presence_time + MinPresenceInterval)
       and (Activity#activity.presence == undefined) ->
	    NewActivity = Activity#activity{presence_time = Now},
	    StateData1 = store_user_activity(From, NewActivity,
					     StateData),
	    process_presence(Nick, Packet, StateData1);
       true ->
	    if Activity#activity.presence == undefined ->
		    Interval = (Activity#activity.presence_time +
				    MinPresenceInterval - Now) div 1000,
		    erlang:send_after(Interval, self(),
				      {process_user_presence, From});
	       true -> ok
	    end,
	    NewActivity = Activity#activity{presence = {Nick, Packet}},
	    StateData1 = store_user_activity(From, NewActivity,
					     StateData),
	    {next_state, normal_state, StateData1}
    end;
normal_state({route, ToNick,
	      #message{from = From, type = Type, lang = Lang} = Packet},
	     StateData) ->
    case decide_fate_message(Packet, From, StateData) of
	{expulse_sender, Reason} ->
	    ?DEBUG(Reason, []),
	    ErrorText = ?T("It is not allowed to send error messages to the"
			   " room. The participant (~s) has sent an error "
			   "message (~s) and got kicked from the room"),
	    NewState = expulse_participant(Packet, From, StateData,
					   translate:translate(Lang, ErrorText)),
	    {next_state, normal_state, NewState};
	forget_message ->
	    {next_state, normal_state, StateData};
	continue_delivery ->
	    case {is_user_allowed_private_message(From, StateData),
		  is_user_online(From, StateData) orelse
		  is_subscriber(From, StateData) orelse
		  is_user_allowed_message_nonparticipant(From, StateData)} of
		{true, true} when Type == groupchat ->
		    ErrText = ?T("It is not allowed to send private messages "
				 "of type \"groupchat\""),
		    Err = xmpp:err_bad_request(ErrText, Lang),
		    ejabberd_router:route_error(Packet, Err);
		{true, true} ->
		    case find_jids_by_nick(ToNick, StateData) of
			[] ->
			    ErrText = ?T("Recipient is not in the conference room"),
			    Err = xmpp:err_item_not_found(ErrText, Lang),
			    ejabberd_router:route_error(Packet, Err);
			ToJIDs ->
			    SrcIsVisitor = is_visitor(From, StateData),
			    DstIsModerator = is_moderator(hd(ToJIDs), StateData),
			    PmFromVisitors =
				(StateData#state.config)#config.allow_private_messages_from_visitors,
			    if SrcIsVisitor == false;
			       PmFromVisitors == anyone;
			       (PmFromVisitors == moderators) and
			       DstIsModerator ->
				   {FromNick, _} = get_participant_data(From, StateData),
				    FromNickJID =
					jid:replace_resource(StateData#state.jid,
							     FromNick),
				    X = #muc_user{},
                                    Packet2 = xmpp:set_subtag(Packet, X),
                                    case ejabberd_hooks:run_fold(muc_filter_message,
                                                                 StateData#state.server_host,
								 xmpp:put_meta(Packet2, mam_ignore, true),
                                                                 [StateData, FromNick]) of
                                        drop ->
                                            ok;
                                        Packet3 ->
                                            PrivMsg = xmpp:set_from(xmpp:del_meta(Packet3, mam_ignore), FromNickJID),
                                            lists:foreach(
                                              fun(ToJID) ->
                                                      ejabberd_router:route(xmpp:set_to(PrivMsg, ToJID))
                                              end, ToJIDs)
                                    end;
			       true ->
				    ErrText = ?T("You are not allowed to send private messages"),
				    Err = xmpp:err_forbidden(ErrText, Lang),
				    ejabberd_router:route_error(Packet, Err)
			    end
		    end;
		{true, false} ->
		    ErrText = ?T("Only occupants are allowed to send messages "
				 "to the conference"),
		    Err = xmpp:err_not_acceptable(ErrText, Lang),
		    ejabberd_router:route_error(Packet, Err);
		{false, _} ->
		    ErrText = ?T("You are not allowed to send private messages"),
		    Err = xmpp:err_forbidden(ErrText, Lang),
		    ejabberd_router:route_error(Packet, Err)
	    end,
	  {next_state, normal_state, StateData}
    end;
normal_state({route, ToNick,
	      #iq{from = From, lang = Lang} = Packet},
	     #state{config = #config{allow_query_users = AllowQuery}} = StateData) ->
    try maps:get(jid:tolower(From), StateData#state.users) of
	#user{nick = FromNick} when AllowQuery orelse ToNick == FromNick ->
	    case find_jid_by_nick(ToNick, StateData) of
		false ->
		    ErrText = ?T("Recipient is not in the conference room"),
		    Err = xmpp:err_item_not_found(ErrText, Lang),
		    ejabberd_router:route_error(Packet, Err);
		To ->
		    FromJID = jid:replace_resource(StateData#state.jid, FromNick),
		    case direct_iq_type(Packet) of
			vcard ->
			    ejabberd_router:route_iq(
			      xmpp:set_from_to(Packet, FromJID, jid:remove_resource(To)),
			      Packet, self());
			pubsub ->
			    ejabberd_router:route_iq(
				xmpp:set_from_to(Packet, FromJID, jid:remove_resource(To)),
				Packet, self());
			ping when ToNick == FromNick ->
			    %% Self-ping optimization from XEP-0410
			    ejabberd_router:route(xmpp:make_iq_result(Packet));
			response ->
			    ejabberd_router:route(xmpp:set_from_to(Packet, FromJID, To));
			#stanza_error{} = Err ->
			    ejabberd_router:route_error(Packet, Err);
			_OtherRequest ->
			    ejabberd_router:route_iq(
			      xmpp:set_from_to(Packet, FromJID, To), Packet, self())
		    end
	    end;
	_ ->
	    ErrText = ?T("Queries to the conference members are "
			 "not allowed in this room"),
	    Err = xmpp:err_not_allowed(ErrText, Lang),
	    ejabberd_router:route_error(Packet, Err)
    catch _:{badkey, _} ->
	    ErrText = ?T("Only occupants are allowed to send queries "
			 "to the conference"),
	    Err = xmpp:err_not_acceptable(ErrText, Lang),
	    ejabberd_router:route_error(Packet, Err)
    end,
    {next_state, normal_state, StateData};
normal_state(hibernate, StateData) ->
    case maps:size(StateData#state.users) of
	0 ->
	    store_room_no_checks(StateData, [], true),
	    ?INFO_MSG("Hibernating room ~ts@~ts", [StateData#state.room, StateData#state.host]),
	    {stop, normal, StateData#state{hibernate_timer = hibernating}};
	_ ->
	    {next_state, normal_state, StateData}
    end;
normal_state(_Event, StateData) ->
    {next_state, normal_state, StateData}.

handle_event({service_message, Msg}, _StateName,
	     StateData) ->
    MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)},
    send_wrapped_multiple(
      StateData#state.jid,
      get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData),
      MessagePkt,
      ?NS_MUCSUB_NODES_MESSAGES,
      StateData),
    NSD = add_message_to_history(<<"">>,
				 StateData#state.jid, MessagePkt, StateData),
    {next_state, normal_state, NSD};
handle_event({destroy, Reason}, _StateName,
	     StateData) ->
    _ = destroy_room(#muc_destroy{xmlns = ?NS_MUC_OWNER, reason = Reason}, StateData),
    ?INFO_MSG("Destroyed MUC room ~ts with reason: ~p",
	      [jid:encode(StateData#state.jid), Reason]),
    add_to_log(room_existence, destroyed, StateData),
    Conf = StateData#state.config,
    {stop, shutdown, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}};
handle_event(destroy, StateName, StateData) ->
    ?INFO_MSG("Destroyed MUC room ~ts",
	      [jid:encode(StateData#state.jid)]),
    handle_event({destroy, <<"">>}, StateName, StateData);
handle_event({set_affiliations, Affiliations},
	     StateName, StateData) ->
    NewStateData = set_affiliations(Affiliations, StateData),
    {next_state, StateName, NewStateData};
handle_event({process_item_change, Item, UJID}, StateName, StateData) ->
    case process_item_change(Item, StateData, UJID) of
	{error, _} ->
            {next_state, StateName, StateData};
        StateData ->
            {next_state, StateName, StateData};
	NSD ->
	    store_room(NSD),
            {next_state, StateName, NSD}
    end;
handle_event(_Event, StateName, StateData) ->
    {next_state, StateName, StateData}.

handle_sync_event({get_disco_item, Filter, JID, Lang, Time}, _From, StateName, StateData) ->
    Len = maps:size(StateData#state.nicks),
    Reply = case (Filter == all) or (Filter == Len) or ((Filter /= 0) and (Len /= 0)) of
	true ->
	    get_roomdesc_reply(JID, StateData,
			       get_roomdesc_tail(StateData, Lang));
	false ->
	    false
    end,
    CurrentTime = erlang:system_time(millisecond),
    if CurrentTime < Time ->
	    {reply, Reply, StateName, StateData};
       true ->
	    {next_state, StateName, StateData}
    end;
%% These two clauses are only for backward compatibility with nodes running old code
handle_sync_event({get_disco_item, JID, Lang}, From, StateName, StateData) ->
    handle_sync_event({get_disco_item, any, JID, Lang}, From, StateName, StateData);
handle_sync_event({get_disco_item, Filter, JID, Lang}, From, StateName, StateData) ->
    handle_sync_event({get_disco_item, Filter, JID, Lang, infinity}, From, StateName, StateData);
handle_sync_event(get_config, _From, StateName,
		  StateData) ->
    {reply, {ok, StateData#state.config}, StateName,
     StateData};
handle_sync_event(get_state, _From, StateName,
		  StateData) ->
    {reply, {ok, StateData}, StateName, StateData};
handle_sync_event(get_info, _From, StateName,
		  StateData) ->
    Result = #{occupants_number => maps:size(StateData#state.users)},
    {reply, Result, StateName, StateData};
handle_sync_event({change_config, Config}, _From,
		  StateName, StateData) ->
    {result, undefined, NSD} = change_config(Config, StateData),
    {reply, {ok, NSD#state.config}, StateName, NSD};
handle_sync_event({change_state, NewStateData}, _From,
		  StateName, _StateData) ->
    Mod = gen_mod:db_mod(NewStateData#state.server_host, mod_muc),
    case erlang:function_exported(Mod, get_subscribed_rooms, 3) of
	true ->
	    ok;
	_ ->
	    erlang:put(muc_subscribers, NewStateData#state.muc_subscribers#muc_subscribers.subscribers)
    end,
    {reply, {ok, NewStateData}, StateName, NewStateData};
handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) ->
    case process_item_change(Item, StateData, UJID) of
	{error, _} = Err ->
	    {reply, Err, StateName, StateData};
        StateData ->
            {reply, {ok, StateData}, StateName, StateData};
	NSD ->
	    store_room(NSD),
	    {reply, {ok, NSD}, StateName, NSD}
    end;
handle_sync_event(get_subscribers, _From, StateName, StateData) ->
    JIDs = muc_subscribers_fold(
             fun(_LBareJID, #subscriber{jid = JID}, Acc) ->
                     [JID | Acc]
             end, [], StateData#state.muc_subscribers),
    {reply, {ok, JIDs}, StateName, StateData};
handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From,
		  StateName, StateData) ->
    IQ = #iq{type = set, id = p1_rand:get_string(),
	     from = From, sub_els = [#muc_subscribe{nick = Nick,
						    events = Nodes}]},
    Config = StateData#state.config,
    CaptchaRequired = Config#config.captcha_protected,
    PasswordProtected = Config#config.password_protected,
    MembersOnly = Config#config.members_only,
    TmpConfig = Config#config{captcha_protected = false,
			      password_protected = false,
			      members_only = false},
    TmpState = StateData#state{config = TmpConfig},
    case process_iq_mucsub(From, IQ, TmpState) of
	{result, #muc_subscribe{events = NewNodes}, NewState} ->
	    NewConfig = (NewState#state.config)#config{
			  captcha_protected = CaptchaRequired,
			  password_protected = PasswordProtected,
			  members_only = MembersOnly},
	    {reply, {ok, NewNodes}, StateName,
	     NewState#state{config = NewConfig}};
	{ignore, NewState} ->
	    NewConfig = (NewState#state.config)#config{
			  captcha_protected = CaptchaRequired,
			  password_protected = PasswordProtected,
			  members_only = MembersOnly},
	    {reply, {error, ?T("Request is ignored")},
	     NewState#state{config = NewConfig}};
	{error, Err} ->
	    {reply, {error, get_error_text(Err)}, StateName, StateData}
    end;
handle_sync_event({muc_unsubscribe, From}, _From, StateName,
		  #state{config = Conf} = StateData) ->
    IQ = #iq{type = set, id = p1_rand:get_string(),
	     from = From, sub_els = [#muc_unsubscribe{}]},
    case process_iq_mucsub(From, IQ, StateData) of
	{result, _, stop} ->
	    {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}};
	{result, _, NewState} ->
	    {reply, ok, StateName, NewState};
	{ignore, NewState} ->
	    {reply, {error, ?T("Request is ignored")}, NewState};
	{error, Err} ->
	    {reply, {error, get_error_text(Err)}, StateName, StateData}
    end;
handle_sync_event({is_subscribed, From}, _From, StateName, StateData) ->
    IsSubs = try muc_subscribers_get(
                   jid:split(From), StateData#state.muc_subscribers) of
		 #subscriber{nick = Nick, nodes = Nodes} -> {true, Nick, Nodes}
	     catch _:{badkey, _} -> false
	     end,
    {reply, IsSubs, StateName, StateData};
handle_sync_event(_Event, _From, StateName,
		  StateData) ->
    Reply = ok, {reply, Reply, StateName, StateData}.

code_change(_OldVsn, StateName, StateData, _Extra) ->
    {ok, StateName, StateData}.

handle_info({process_user_presence, From}, normal_state = _StateName, StateData) ->
    RoomQueueEmpty = p1_queue:is_empty(StateData#state.room_queue),
    RoomQueue = p1_queue:in({presence, From}, StateData#state.room_queue),
    StateData1 = StateData#state{room_queue = RoomQueue},
    if RoomQueueEmpty ->
	   StateData2 = prepare_room_queue(StateData1),
	   {next_state, normal_state, StateData2};
       true -> {next_state, normal_state, StateData1}
    end;
handle_info({process_user_message, From},
	    normal_state = _StateName, StateData) ->
    RoomQueueEmpty =
	p1_queue:is_empty(StateData#state.room_queue),
    RoomQueue = p1_queue:in({message, From},
			    StateData#state.room_queue),
    StateData1 = StateData#state{room_queue = RoomQueue},
    if RoomQueueEmpty ->
	   StateData2 = prepare_room_queue(StateData1),
	   {next_state, normal_state, StateData2};
       true -> {next_state, normal_state, StateData1}
    end;
handle_info(process_room_queue,
	    normal_state = StateName, StateData) ->
    case p1_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(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(Nick, Packet, StateData3);
      {empty, _} -> {next_state, StateName, StateData}
    end;
handle_info({captcha_succeed, From}, normal_state,
	    StateData) ->
    NewState = case maps:get(From, StateData#state.robots, passed) of
		   {Nick, Packet} ->
		       Robots = maps:put(From, passed, StateData#state.robots),
		       add_new_user(From, Nick, Packet,
				    StateData#state{robots = Robots});
		   passed ->
		       StateData
	       end,
    {next_state, normal_state, NewState};
handle_info({captcha_failed, From}, normal_state,
	    StateData) ->
    NewState = case maps:get(From, StateData#state.robots, passed) of
		   {_Nick, Packet} ->
		       Robots = maps:remove(From, StateData#state.robots),
		       Txt = ?T("The CAPTCHA verification has failed"),
		       Lang = xmpp:get_lang(Packet),
		       Err = xmpp:err_not_authorized(Txt, Lang),
		       ejabberd_router:route_error(Packet, Err),
		       StateData#state{robots = Robots};
		   passed ->
		       StateData
	       end,
    {next_state, normal_state, NewState};
handle_info(close_room_if_temporary_and_empty, _StateName, StateData) ->
    close_room_if_temporary_and_empty(StateData);
handle_info(shutdown, _StateName, StateData) ->
    {stop, shutdown, StateData};
handle_info({iq_reply, #iq{type = Type, sub_els = Els},
	     #iq{from = From, to = To} = IQ}, StateName, StateData) ->
    ejabberd_router:route(
      xmpp:set_from_to(
	IQ#iq{type = Type, sub_els = Els},
	To, From)),
    {next_state, StateName, StateData};
handle_info({iq_reply, timeout, IQ}, StateName, StateData) ->
    Txt = ?T("Request has timed out"),
    Err = xmpp:err_recipient_unavailable(Txt, IQ#iq.lang),
    ejabberd_router:route_error(IQ, Err),
    {next_state, StateName, StateData};
handle_info(config_reloaded, StateName, StateData) ->
    Max = mod_muc_opt:history_size(StateData#state.server_host),
    History1 = StateData#state.history,
    Q1 = History1#lqueue.queue,
    Q2 = case p1_queue:len(Q1) of
	     Len when Len > Max ->
		 lqueue_cut(Q1, Len-Max);
	     _ ->
		 Q1
	 end,
    History2 = History1#lqueue{queue = Q2, max = Max},
    {next_state, StateName, StateData#state{history = History2}};
handle_info(_Info, StateName, StateData) ->
    {next_state, StateName, StateData}.

terminate(Reason, _StateName,
	  #state{server_host = LServer, host = Host, room = Room} = StateData) ->
    try
	?INFO_MSG("Stopping MUC room ~ts@~ts", [Room, Host]),
	ReasonT = case Reason of
		      shutdown ->
			  ?T("You are being removed from the room "
			     "because of a system shutdown");
		      _ -> ?T("Room terminates")
		  end,
	Packet = #presence{
		    type = unavailable,
		    sub_els = [#muc_user{items = [#muc_item{affiliation = none,
							    reason = ReasonT,
							    role = none}],
					 status_codes = [332,110]}]},
	maps:fold(
	  fun(_, #user{nick = Nick, jid = JID}, _) ->
		  case Reason of
		      shutdown ->
			  send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
				       JID, Packet,
				       ?NS_MUCSUB_NODES_PARTICIPANTS,
				       StateData);
		      _ -> ok
		  end,
		  tab_remove_online_user(JID, StateData)
	  end, [], get_users_and_subscribers_with_node(
                     ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)),

	disable_hibernate_timer(StateData),
	case StateData#state.hibernate_timer of
	    hibernating ->
		ok;
	    _ ->
		add_to_log(room_existence, stopped, StateData),
		case (StateData#state.config)#config.persistent of
                    false ->
			ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host, false]);
                    {destroying, Persistent} ->
			ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host, Persistent]);
		    _ ->
			ok
		end
	end
    catch ?EX_RULE(E, R, St) ->
	    StackTrace = ?EX_STACK(St),
	    ?ERROR_MSG("Got exception on room termination:~n** ~ts",
		       [misc:format_exception(2, E, R, StackTrace)])
    end.

%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
-spec route(pid(), stanza()) -> ok.
route(Pid, Packet) ->
    ?DEBUG("Routing to MUC room ~p:~n~ts", [Pid, xmpp:pp(Packet)]),
    #jid{lresource = Nick} = xmpp:get_to(Packet),
    p1_fsm:send_event(Pid, {route, Nick, Packet}).

-spec process_groupchat_message(message(), state()) -> fsm_next().
process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData) ->
    IsSubscriber = is_subscriber(From, StateData),
    case is_user_online(From, StateData) orelse IsSubscriber orelse
	   is_user_allowed_message_nonparticipant(From, StateData)
	of
      true ->
	  {FromNick, Role} = get_participant_data(From, StateData),
	  #config{moderated = Moderated} = StateData#state.config,
	  AllowedByModerationRules =
	  case {Role == moderator orelse Role == participant orelse
		not Moderated, IsSubscriber} of
	      {true, _} -> true;
	      {_, true} ->
		  % We assume all subscribers are at least members
		  true;
	      _ ->
		  false
	  end,
	  if AllowedByModerationRules ->
	      Subject = check_subject(Packet),
	      {NewStateData1, IsAllowed} =
	      case Subject of
		  [] ->
		      {StateData, true};
		  _ ->
		      case
			  can_change_subject(Role,
					     IsSubscriber,
					     StateData)
		      of
			  true ->
			      NSD =
			      StateData#state{subject = Subject,
					      subject_author = {FromNick, From}},
			      store_room(NSD),
			      {NSD, true};
			  _ -> {StateData, false}
		      end
	      end,
	      case IsAllowed of
		   true ->
		       case
			 ejabberd_hooks:run_fold(muc_filter_message,
						 StateData#state.server_host,
						 Packet,
						 [StateData, FromNick])
			   of
			 drop ->
			     {next_state, normal_state, StateData};
			 NewPacket1 ->
			     NewPacket = xmpp:put_meta(xmpp:remove_subtag(
				 add_stanza_id(NewPacket1, StateData), #nick{}),
				 muc_sender_real_jid, From),
			     Node = if Subject == [] -> ?NS_MUCSUB_NODES_MESSAGES;
				       true -> ?NS_MUCSUB_NODES_SUBJECT
				    end,
			     NewStateData2 = check_message_for_retractions(NewPacket1, NewStateData1),
			     send_wrapped_multiple(
			       jid:replace_resource(StateData#state.jid, FromNick),
			       get_users_and_subscribers_with_node(Node, StateData),
			       NewPacket, Node, NewStateData2),
			     NewStateData3 = case has_body_or_subject(NewPacket) of
					       true ->
						   add_message_to_history(FromNick, From,
									  NewPacket,
									  NewStateData2);
					       false ->
						   NewStateData2
					     end,
			     {next_state, normal_state, NewStateData3}
		       end;
		   _ ->
		       Err = case (StateData#state.config)#config.allow_change_subj of
			       true ->
				   xmpp:err_forbidden(
				     ?T("Only moderators and participants are "
					"allowed to change the subject in this "
					"room"), Lang);
			       _ ->
				   xmpp:err_forbidden(
				     ?T("Only moderators are allowed to change "
					"the subject in this room"), Lang)
			     end,
		       ejabberd_router:route_error(Packet, Err),
		       {next_state, normal_state, StateData}
		 end;
	     true ->
		 ErrText = ?T("Visitors are not allowed to send messages "
			      "to all occupants"),
		 Err = xmpp:err_forbidden(ErrText, Lang),
		 ejabberd_router:route_error(Packet, Err),
		 {next_state, normal_state, StateData}
	  end;
      false ->
	  ErrText = ?T("Only occupants are allowed to send messages "
		       "to the conference"),
	  Err = xmpp:err_not_acceptable(ErrText, Lang),
	  ejabberd_router:route_error(Packet, Err),
	  {next_state, normal_state, StateData}
    end.

-spec check_message_for_retractions(Packet :: message(), State :: state()) -> state().
check_message_for_retractions(Packet,
			  #state{config = Config, room = Room, host = Host,
                                 server_host = Server} = State) ->
    case xmpp:get_subtag(Packet, #fasten_apply_to{}) of
	#fasten_apply_to{id = ID} = F ->
	    case xmpp:get_subtag(F, #message_retract{}) of
		#message_retract{} ->
		    #jid{luser = U, lserver = S} = xmpp:get_from(Packet),
		    case remove_from_history({U, S}, ID, State) of
			{NewState, StanzaId} when is_integer(StanzaId) ->
			    case Config#config.mam of
				true ->
				    mod_mam:remove_message_from_archive({Room, Host}, Server, StanzaId),
				    NewState;
				_ ->
				    NewState
			    end;
			{NewState, _} ->
			    NewState
		    end;
		_ ->
		    State
	    end;
	_ ->
	    State
    end.

-spec add_stanza_id(Packet :: message(), State :: state()) -> message().
add_stanza_id(Packet, #state{jid = JID}) ->
    {AddId, NewPacket} =
    case xmpp:get_meta(Packet, stanza_id, false) of
	false ->
	    GenID = erlang:system_time(microsecond),
	    {true, xmpp:put_meta(Packet, stanza_id, GenID)};
	_ ->
	    StanzaIds = xmpp:get_subtags(Packet, #stanza_id{by = #jid{}}),
	    HasOurStanzaId = lists:any(
		fun(#stanza_id{by = JID2}) when JID == JID2 -> true;
		   (_) -> false
		end, StanzaIds),
	    {not HasOurStanzaId, Packet}
    end,
    if
	AddId ->
	    ID = xmpp:get_meta(NewPacket, stanza_id),
	    IDs = integer_to_binary(ID),
	    xmpp:append_subtags(NewPacket, [#stanza_id{by = JID, id = IDs}]);
	true ->
	    Packet
    end.

-spec process_normal_message(jid(), message(), state()) -> state().
process_normal_message(From, #message{lang = Lang} = Pkt, StateData) ->
    Action = lists:foldl(
	       fun(_, {error, _} = Err) ->
		       Err;
		  (_, {ok, _} = Result) ->
		       Result;
		  (#muc_user{invites = [_|_] = Invites}, _) ->
		       case check_invitation(From, Invites, Lang, StateData) of
			   ok ->
			       {ok, Invites};
			   {error, _} = Err ->
			       Err
		       end;
		  (#xdata{type = submit, fields = Fs}, _) ->
		       try {ok, muc_request:decode(Fs)}
		       catch _:{muc_request, Why} ->
			       Txt = muc_request:format_error(Why),
			       {error, xmpp:err_bad_request(Txt, Lang)}
		       end;
		  (_, Acc) ->
		       Acc
	       end, ok, xmpp:get_els(Pkt)),
    case Action of
	{ok, [#muc_invite{}|_] = Invitations} ->
	    lists:foldl(
	      fun(Invitation, AccState) ->
		      process_invitation(From, Pkt, Invitation, Lang, AccState)
	      end, StateData, Invitations);
	{ok, [{role, participant}]} ->
	    process_voice_request(From, Pkt, StateData);
	{ok, VoiceApproval} ->
	    process_voice_approval(From, Pkt, VoiceApproval, StateData);
	{error, Err} ->
	    ejabberd_router:route_error(Pkt, Err),
	    StateData;
	ok ->
	    StateData
    end.

-spec process_invitation(jid(), message(), muc_invite(), binary(), state()) -> state().
process_invitation(From, Pkt, Invitation, Lang, StateData) ->
    IJID = route_invitation(From, Pkt, Invitation, Lang, StateData),
    Config = StateData#state.config,
    case Config#config.members_only of
	true ->
	    case get_affiliation(IJID, StateData) of
		none ->
		    NSD = set_affiliation(IJID, member, StateData),
		    send_affiliation(IJID, member, StateData),
		    store_room(NSD),
		    NSD;
		_ ->
		    StateData
	    end;
	false ->
	    StateData
    end.

-spec process_voice_request(jid(), message(), state()) -> state().
process_voice_request(From, Pkt, StateData) ->
    Lang = xmpp:get_lang(Pkt),
    case (StateData#state.config)#config.allow_voice_requests of
	true ->
	    MinInterval = (StateData#state.config)#config.voice_request_min_interval,
	    BareFrom = jid:remove_resource(jid:tolower(From)),
	    NowPriority = -erlang:system_time(microsecond),
	    CleanPriority = NowPriority + MinInterval * 1000000,
	    Times = clean_treap(StateData#state.last_voice_request_time,
				CleanPriority),
	    case treap:lookup(BareFrom, Times) of
		error ->
		    Times1 = treap:insert(BareFrom,
					  NowPriority,
					  true, Times),
		    NSD = StateData#state{last_voice_request_time = Times1},
		    send_voice_request(From, Lang, NSD),
		    NSD;
		{ok, _, _} ->
		    ErrText = ?T("Please, wait for a while before sending "
				 "new voice request"),
		    Err = xmpp:err_resource_constraint(ErrText, Lang),
		    ejabberd_router:route_error(Pkt, Err),
		    StateData#state{last_voice_request_time = Times}
	    end;
	false ->
	    ErrText = ?T("Voice requests are disabled in this conference"),
	    Err = xmpp:err_forbidden(ErrText, Lang),
	    ejabberd_router:route_error(Pkt, Err),
	    StateData
    end.

-spec process_voice_approval(jid(), message(), [muc_request:property()], state()) -> state().
process_voice_approval(From, Pkt, VoiceApproval, StateData) ->
    Lang = xmpp:get_lang(Pkt),
    case is_moderator(From, StateData) of
	true ->
	    case lists:keyfind(jid, 1, VoiceApproval) of
		{_, TargetJid} ->
		    Allow = proplists:get_bool(request_allow, VoiceApproval),
		    case is_visitor(TargetJid, StateData) of
			true when Allow ->
			    Reason = <<>>,
			    NSD = set_role(TargetJid, participant, StateData),
			    catch send_new_presence(
				    TargetJid, Reason, NSD, StateData),
			    NSD;
			_ ->
			    StateData
		    end;
		false ->
		    ErrText = ?T("Failed to extract JID from your voice "
				 "request approval"),
		    Err = xmpp:err_bad_request(ErrText, Lang),
		    ejabberd_router:route_error(Pkt, Err),
		    StateData
	    end;
	false ->
	    ErrText = ?T("Only moderators can approve voice requests"),
	    Err = xmpp:err_not_allowed(ErrText, Lang),
	    ejabberd_router:route_error(Pkt, Err),
	    StateData
    end.

-spec direct_iq_type(iq()) -> vcard | ping | request | response | pubsub | stanza_error().
direct_iq_type(#iq{type = T, sub_els = SubEls, lang = Lang}) when T == get; T == set ->
    case SubEls of
	[El] ->
	    case xmpp:get_ns(El) of
		?NS_VCARD when T == get -> vcard;
		?NS_PUBSUB when T == get -> pubsub;
		?NS_PING when T == get -> ping;
		_ -> request
	    end;
	[] ->
	    xmpp:err_bad_request(?T("No child elements found"), Lang);
	[_|_] ->
	    xmpp:err_bad_request(?T("Too many child elements"), Lang)
    end;
direct_iq_type(#iq{}) ->
    response.

%% @doc Check if this non participant can send message to room.
%%
%% XEP-0045 v1.23:
%% 7.9 Sending a Message to All Occupants
%% an implementation MAY allow users with certain privileges
%% (e.g., a room owner, room admin, or service-level admin)
%% to send messages to the room even if those users are not occupants.
-spec is_user_allowed_message_nonparticipant(jid(), state()) -> boolean().
is_user_allowed_message_nonparticipant(JID,
				       StateData) ->
    case get_service_affiliation(JID, StateData) of
      owner -> true;
      _ -> false
    end.

-spec is_user_allowed_private_message(jid(), state()) -> boolean().
is_user_allowed_private_message(JID, StateData) ->
    case {(StateData#state.config)#config.allowpm,
            get_role(JID, StateData)} of
        {anyone, _} ->
            true;
        {participants, moderator} ->
            true;
        {participants, participant} ->
            true;
        {moderators, moderator} ->
            true;
        {none, _} ->
            false;
        {_, _} ->
            false
    end.

%% @doc Get information of this participant, or default values.
%% If the JID is not a participant, return values for a service message.
-spec get_participant_data(jid(), state()) -> {binary(), role()}.
get_participant_data(From, StateData) ->
    try maps:get(jid:tolower(From), StateData#state.users) of
	#user{nick = FromNick, role = Role} ->
	    {FromNick, Role}
    catch _:{badkey, _} ->
	    try muc_subscribers_get(jid:tolower(jid:remove_resource(From)),
                                    StateData#state.muc_subscribers) of
		#subscriber{nick = FromNick} ->
		    {FromNick, none}
	    catch _:{badkey, _} ->
		    {From#jid.luser, moderator}
	    end
    end.

-spec process_presence(binary(), presence(), state()) -> fsm_transition().
process_presence(Nick, #presence{from = From, type = Type0} = Packet0, StateData) ->
    IsOnline = is_user_online(From, StateData),
    if Type0 == available;
       IsOnline and ((Type0 == unavailable) or (Type0 == error)) ->
	   case ejabberd_hooks:run_fold(muc_filter_presence,
					StateData#state.server_host,
					Packet0,
					[StateData, Nick]) of
	     drop ->
		 {next_state, normal_state, StateData};
	     #presence{} = Packet ->
		 close_room_if_temporary_and_empty(
		   do_process_presence(Nick, Packet, StateData))
	   end;
       true ->
	    {next_state, normal_state, StateData}
    end.

-spec do_process_presence(binary(), presence(), state()) -> state().
do_process_presence(Nick, #presence{from = From, type = available, lang = Lang} = Packet,
		    StateData) ->
    case is_user_online(From, StateData) of
	false ->
	    add_new_user(From, Nick, Packet, StateData);
	true ->
	    case is_nick_change(From, Nick, StateData) of
		true ->
		    case {nick_collision(From, Nick, StateData),
			  mod_muc:can_use_nick(StateData#state.server_host,
					       jid:encode(StateData#state.jid),
					       From, Nick),
			  {(StateData#state.config)#config.allow_visitor_nickchange,
			   is_visitor(From, StateData)}} of
			{_, _, {false, true}} ->
			    Packet1 = Packet#presence{sub_els = [#muc{}]},
			    ErrText = ?T("Visitors are not allowed to change their "
					 "nicknames in this room"),
			    Err = xmpp:err_not_allowed(ErrText, Lang),
			    ejabberd_router:route_error(Packet1, Err),
			    StateData;
			{true, _, _} ->
			    Packet1 = Packet#presence{sub_els = [#muc{}]},
			    ErrText = ?T("That nickname is already in use by another "
					 "occupant"),
			    Err = xmpp:err_conflict(ErrText, Lang),
			    ejabberd_router:route_error(Packet1, Err),
			    StateData;
			{_, false, _} ->
			    Packet1 = Packet#presence{sub_els = [#muc{}]},
			    Err = case Nick of
				      <<>> ->
					  xmpp:err_jid_malformed(?T("Nickname can't be empty"),
								 Lang);
				      _ ->
					  xmpp:err_conflict(?T("That nickname is registered"
							       " by another person"), Lang)
				  end,
			    ejabberd_router:route_error(Packet1, Err),
			    StateData;
			_ ->
			    change_nick(From, Nick, StateData)
		    end;
		false ->
		    Stanza = maybe_strip_status_from_presence(
			       From, Packet, StateData),
		    NewState = add_user_presence(From, Stanza,
						 StateData),
		    case xmpp:has_subtag(Packet, #muc{}) of
			true ->
			    send_initial_presences_and_messages(
			      From, Nick, Packet, NewState, StateData);
			false ->
			    send_new_presence(From, NewState, StateData)
		    end,
		    NewState
	    end
    end;
do_process_presence(Nick, #presence{from = From, type = unavailable} = Packet,
		    StateData) ->
    NewPacket = case {(StateData#state.config)#config.allow_visitor_status,
		      is_visitor(From, StateData)} of
		    {false, true} ->
			strip_status(Packet);
		    _ -> Packet
		end,
    NewState = add_user_presence_un(From, NewPacket, StateData),
    case maps:get(Nick, StateData#state.nicks, []) of
	[_, _ | _] ->
	    Aff = get_affiliation(From, StateData),
	    Item = #muc_item{affiliation = Aff, role = none, jid = From},
	    Pres = xmpp:set_subtag(
		     Packet, #muc_user{items = [Item],
				       status_codes = [110]}),
	    send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
			 From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData);
	_ ->
	    send_new_presence(From, NewState, StateData)
    end,
    Reason = xmpp:get_text(NewPacket#presence.status),
    remove_online_user(From, NewState, Reason);
do_process_presence(_Nick, #presence{from = From, type = error, lang = Lang} = Packet,
		    StateData) ->
    ErrorText = ?T("It is not allowed to send error messages to the"
		   " room. The participant (~s) has sent an error "
		   "message (~s) and got kicked from the room"),
    expulse_participant(Packet, From, StateData,
			translate:translate(Lang, ErrorText)).

-spec maybe_strip_status_from_presence(jid(), presence(),
				       state()) -> presence().
maybe_strip_status_from_presence(From, Packet, StateData) ->
    case {(StateData#state.config)#config.allow_visitor_status,
	  is_visitor(From, StateData)} of
	{false, true} ->
	    strip_status(Packet);
	_Allowed -> Packet
    end.

-spec close_room_if_temporary_and_empty(state()) -> fsm_transition().
close_room_if_temporary_and_empty(StateData1) ->
    case not (StateData1#state.config)#config.persistent
	andalso maps:size(StateData1#state.users) == 0
	andalso muc_subscribers_size(StateData1#state.muc_subscribers) == 0 of
      true ->
	  ?INFO_MSG("Destroyed MUC room ~ts because it's temporary "
		    "and empty",
		    [jid:encode(StateData1#state.jid)]),
	  add_to_log(room_existence, destroyed, StateData1),
	  forget_room(StateData1),
	  {stop, normal, StateData1};
      _ -> {next_state, normal_state, StateData1}
    end.

-spec get_users_and_subscribers(state()) -> users().
get_users_and_subscribers(StateData) ->
    get_users_and_subscribers_aux(
      StateData#state.muc_subscribers#muc_subscribers.subscribers,
      StateData).

-spec get_users_and_subscribers_with_node(binary(), state()) -> users().
get_users_and_subscribers_with_node(Node, StateData) ->
    get_users_and_subscribers_aux(
      muc_subscribers_get_by_node(Node, StateData#state.muc_subscribers),
      StateData).

get_users_and_subscribers_aux(Subscribers, StateData) ->
    OnlineSubscribers = maps:fold(
			   fun(LJID, _, Acc) ->
				   LBareJID = jid:remove_resource(LJID),
				   case is_subscriber(LBareJID, StateData) of
				       true ->
					   ?SETS:add_element(LBareJID, Acc);
				       false ->
					   Acc
				   end
			   end, ?SETS:new(), StateData#state.users),
    maps:fold(
       fun(LBareJID, #subscriber{nick = Nick}, Acc) ->
	       case ?SETS:is_element(LBareJID, OnlineSubscribers) of
		   false ->
		       maps:put(LBareJID,
				#user{jid = jid:make(LBareJID),
				      nick = Nick,
				      role = none,
				      last_presence = undefined},
				Acc);
		   true ->
		       Acc
	       end
       end, StateData#state.users, Subscribers).

-spec is_user_online(jid(), state()) -> boolean().
is_user_online(JID, StateData) ->
    LJID = jid:tolower(JID),
    maps:is_key(LJID, StateData#state.users).

-spec is_subscriber(jid(), state()) -> boolean().
is_subscriber(JID, StateData) ->
    LJID = jid:tolower(jid:remove_resource(JID)),
    muc_subscribers_is_key(LJID, StateData#state.muc_subscribers).

%% Check if the user is occupant of the room, or at least is an admin or owner.
-spec is_occupant_or_admin(jid(), state()) -> boolean().
is_occupant_or_admin(JID, StateData) ->
    FAffiliation = get_affiliation(JID, StateData),
    FRole = get_role(JID, StateData),
    case FRole /= none orelse
	   FAffiliation == member orelse
	   FAffiliation == admin orelse FAffiliation == owner
	of
      true -> true;
      _ -> false
    end.

%% Check if the user is an admin or owner.
-spec is_admin(jid(), state()) -> boolean().
is_admin(JID, StateData) ->
    FAffiliation = get_affiliation(JID, StateData),
    FAffiliation == admin orelse FAffiliation == owner.

%% Decide the fate of the message and its sender
%% Returns: continue_delivery | forget_message | {expulse_sender, Reason}
-spec decide_fate_message(message(), jid(), state()) ->
				 continue_delivery | forget_message |
				 {expulse_sender, binary()}.
decide_fate_message(#message{type = error} = Msg,
		    From, StateData) ->
    Err = xmpp:get_error(Msg),
    PD = case check_error_kick(Err) of
	   %% If this is an error stanza and its condition matches a criteria
	   true ->
	       Reason = str:format("This participant is considered a ghost "
				   "and is expulsed: ~s",
				   [jid:encode(From)]),
	       {expulse_sender, Reason};
	   false -> continue_delivery
	 end,
    case PD of
      {expulse_sender, R} ->
	  case is_user_online(From, StateData) of
	    true -> {expulse_sender, R};
	    false -> forget_message
	  end;
      Other -> Other
    end;
decide_fate_message(_, _, _) -> continue_delivery.

%% Check if the elements of this error stanza indicate
%% that the sender is a dead participant.
%% If so, return true to kick the participant.
-spec check_error_kick(stanza_error()) -> boolean().
check_error_kick(#stanza_error{reason = Reason}) ->
    case Reason of
	#gone{} -> true;
	'internal-server-error' -> true;
	'item-not-found' -> true;
	'jid-malformed' -> true;
	'recipient-unavailable' -> true;
	#redirect{} -> true;
	'remote-server-not-found' -> true;
	'remote-server-timeout' -> true;
	'service-unavailable' -> true;
	_ -> false
    end;
check_error_kick(undefined) ->
    false.

-spec get_error_condition(stanza_error()) -> string().
get_error_condition(#stanza_error{reason = Reason}) ->
    case Reason of
	#gone{} -> "gone";
	#redirect{} -> "redirect";
	Atom -> atom_to_list(Atom)
    end;
get_error_condition(undefined) ->
    "undefined".

-spec get_error_text(stanza_error()) -> binary().
get_error_text(#stanza_error{text = Txt}) ->
    xmpp:get_text(Txt).

-spec make_reason(stanza(), jid(), state(), binary()) -> binary().
make_reason(Packet, From, StateData, Reason1) ->
    #user{nick = FromNick} = maps:get(jid:tolower(From), StateData#state.users),
    Condition = get_error_condition(xmpp:get_error(Packet)),
    Reason2 = unicode:characters_to_list(Reason1),
    str:format(Reason2, [FromNick, Condition]).

-spec expulse_participant(stanza(), jid(), state(), binary()) ->
				 state().
expulse_participant(Packet, From, StateData, Reason1) ->
    Reason2 = make_reason(Packet, From, StateData, Reason1),
    NewState = add_user_presence_un(From,
				    #presence{type = unavailable,
					      status = xmpp:mk_text(Reason2)},
				    StateData),
    LJID = jid:tolower(From),
    #user{nick = Nick} = maps:get(LJID, StateData#state.users),
    case maps:get(Nick, StateData#state.nicks, []) of
	[_, _ | _] ->
	    Aff = get_affiliation(From, StateData),
	    Item = #muc_item{affiliation = Aff, role = none, jid = From},
	    Pres = xmpp:set_subtag(
		     Packet, #muc_user{items = [Item],
				       status_codes = [110]}),
	    send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
			 From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData);
	_ ->
	    send_new_presence(From, NewState, StateData)
    end,
    remove_online_user(From, NewState).

-spec set_affiliation(jid(), affiliation(), state()) -> state().
set_affiliation(JID, Affiliation, StateData) ->
    set_affiliation(JID, Affiliation, StateData, <<"">>).

-spec set_affiliation(jid(), affiliation(), state(), binary()) -> state().
set_affiliation(JID, Affiliation,
		#state{config = #config{persistent = false}} = StateData,
		Reason) ->
    set_affiliation_fallback(JID, Affiliation, StateData, Reason);
set_affiliation(JID, Affiliation, StateData, Reason) ->
    ServerHost = StateData#state.server_host,
    Room = StateData#state.room,
    Host = StateData#state.host,
    Mod = gen_mod:db_mod(ServerHost, mod_muc),
    case Mod:set_affiliation(ServerHost, Room, Host, JID, Affiliation, Reason) of
	ok ->
	    StateData;
	{error, _} ->
	    set_affiliation_fallback(JID, Affiliation, StateData, Reason)
    end.

-spec set_affiliation_fallback(jid(), affiliation(), state(), binary()) -> state().
set_affiliation_fallback(JID, Affiliation, StateData, Reason) ->
    LJID = jid:remove_resource(jid:tolower(JID)),
    Affiliations = case Affiliation of
		       none ->
			   maps:remove(LJID, StateData#state.affiliations);
		       _ ->
			   maps:put(LJID, {Affiliation, Reason},
				    StateData#state.affiliations)
		   end,
    StateData#state{affiliations = Affiliations}.

-spec set_affiliations(affiliations(), state()) -> state().
set_affiliations(Affiliations,
                 #state{config = #config{persistent = false}} = StateData) ->
    set_affiliations_fallback(Affiliations, StateData);
set_affiliations(Affiliations, StateData) ->
    Room = StateData#state.room,
    Host = StateData#state.host,
    ServerHost = StateData#state.server_host,
    Mod = gen_mod:db_mod(ServerHost, mod_muc),
    case Mod:set_affiliations(ServerHost, Room, Host, Affiliations) of
	ok ->
	    StateData;
	{error, _} ->
	    set_affiliations_fallback(Affiliations, StateData)
    end.

-spec set_affiliations_fallback(affiliations(), state()) -> state().
set_affiliations_fallback(Affiliations, StateData) ->
    StateData#state{affiliations = Affiliations}.

-spec get_affiliation(ljid() | jid(), state()) -> affiliation().
get_affiliation(#jid{} = JID, StateData) ->
    case get_service_affiliation(JID, StateData) of
	owner ->
	    owner;
	none ->
	    Aff = case do_get_affiliation(JID, StateData) of
		      {Affiliation, _Reason} -> Affiliation;
		      Affiliation -> Affiliation
		  end,
	    case {Aff, (StateData#state.config)#config.members_only} of
		% Subscribers should be have members affiliation in this case
		{none, true} ->
		    case is_subscriber(JID, StateData) of
			true -> member;
			_ -> none
		    end;
		_ ->
		    Aff
	    end
    end;
get_affiliation(LJID, StateData) ->
    get_affiliation(jid:make(LJID), StateData).

-spec do_get_affiliation(jid(), state()) -> affiliation() | {affiliation(), binary()}.
do_get_affiliation(JID, #state{config = #config{persistent = false}} = StateData) ->
    do_get_affiliation_fallback(JID, StateData);
do_get_affiliation(JID, StateData) ->
    Room = StateData#state.room,
    Host = StateData#state.host,
    LServer = JID#jid.lserver,
    LUser = JID#jid.luser,
    ServerHost = StateData#state.server_host,
    Mod = gen_mod:db_mod(ServerHost, mod_muc),
    case Mod:get_affiliation(ServerHost, Room, Host, LUser, LServer) of
	{error, _} ->
	    do_get_affiliation_fallback(JID, StateData);
	{ok, Affiliation} ->
	    Affiliation
    end.

-spec do_get_affiliation_fallback(jid(), state()) -> affiliation() | {affiliation(),  binary()}.
do_get_affiliation_fallback(JID, StateData) ->
    LJID = jid:tolower(JID),
    try maps:get(LJID, StateData#state.affiliations)
    catch _:{badkey, _} ->
            BareLJID = jid:remove_resource(LJID),
            try maps:get(BareLJID, StateData#state.affiliations)
	    catch _:{badkey, _} ->
                    DomainLJID = setelement(1, LJID, <<"">>),
                    try maps:get(DomainLJID, StateData#state.affiliations)
		    catch _:{badkey, _} ->
                            DomainBareLJID = jid:remove_resource(DomainLJID),
                            try maps:get(DomainBareLJID, StateData#state.affiliations)
			    catch _:{badkey, _} -> none
                            end
                    end
            end
    end.

-spec get_affiliations(state()) -> affiliations().
get_affiliations(#state{config = #config{persistent = false}} = StateData) ->
    get_affiliations_fallback(StateData);
get_affiliations(StateData) ->
    Room = StateData#state.room,
    Host = StateData#state.host,
    ServerHost = StateData#state.server_host,
    Mod = gen_mod:db_mod(ServerHost, mod_muc),
    case Mod:get_affiliations(ServerHost, Room, Host) of
	{error, _} ->
	    get_affiliations_fallback(StateData);
	{ok, Affiliations} ->
	    Affiliations
    end.

-spec get_affiliations_fallback(state()) -> affiliations().
get_affiliations_fallback(StateData) ->
    StateData#state.affiliations.

-spec get_service_affiliation(jid(), state()) -> owner | none.
get_service_affiliation(JID, StateData) ->
    {_AccessRoute, _AccessCreate, AccessAdmin,
     _AccessPersistent, _AccessMam} =
	StateData#state.access,
    case acl:match_rule(StateData#state.server_host,
			AccessAdmin, JID)
	of
      allow -> owner;
      _ -> none
    end.

-spec set_role(jid(), role(), state()) -> state().
set_role(JID, Role, StateData) ->
    LJID = jid:tolower(JID),
    LJIDs = case LJID of
	      {U, S, <<"">>} ->
		  maps:fold(fun (J, _, Js) ->
				    case J of
					{U, S, _} -> [J | Js];
					_ -> Js
				    end
			    end, [], StateData#state.users);
	      _ ->
		  case maps:is_key(LJID, StateData#state.users) of
		    true -> [LJID];
		    _ -> []
		  end
	    end,
    {Users, Nicks} =
	case Role of
	    none ->
		lists:foldl(
		  fun (J, {Us, Ns}) ->
			  NewNs = try maps:get(J, Us) of
				      #user{nick = Nick} ->
					  maps:remove(Nick, Ns)
				  catch _:{badkey, _} ->
					  Ns
				  end,
			  {maps:remove(J, Us), NewNs}
		  end,
		  {StateData#state.users, StateData#state.nicks}, LJIDs);
	    _ ->
		{lists:foldl(
		   fun (J, Us) ->
			   User = maps:get(J, Us),
			   if User#user.last_presence == undefined ->
				   Us;
			      true ->
				   maps:put(J, User#user{role = Role}, Us)
			   end
		   end, StateData#state.users, LJIDs),
		 StateData#state.nicks}
	end,
    Affiliation = get_affiliation(JID, StateData),
    Roles = case Role of
                %% Don't persist 'none' role: if someone is kicked, they will
                %% maintain the same role they had *before* they were kicked,
                %% unless they were banned
                none when Affiliation /= outcast ->
		    maps:remove(jid:remove_resource(LJID), StateData#state.roles);
                NewRole ->
                    maps:put(jid:remove_resource(LJID),
                             NewRole,
                             StateData#state.roles)
    end,
    StateData#state{users = Users, nicks = Nicks, roles = Roles}.

-spec get_role(jid(), state()) -> role().
get_role(JID, StateData) ->
    LJID = jid:tolower(JID),
    try maps:get(LJID, StateData#state.users) of
	#user{role = Role} -> Role
    catch _:{badkey, _} -> none
    end.

-spec get_default_role(affiliation(), state()) -> role().
get_default_role(Affiliation, StateData) ->
    case Affiliation of
      owner -> moderator;
      admin -> moderator;
      member -> participant;
      outcast -> none;
      none ->
	  case (StateData#state.config)#config.members_only of
	    true -> none;
	    _ ->
		case (StateData#state.config)#config.members_by_default
		    of
		  true -> participant;
		  _ -> visitor
		end
	  end
    end.

-spec is_visitor(jid(), state()) -> boolean().
is_visitor(Jid, StateData) ->
    get_role(Jid, StateData) =:= visitor.

-spec is_moderator(jid(), state()) -> boolean().
is_moderator(Jid, StateData) ->
    get_role(Jid, StateData) =:= moderator.

-spec get_max_users(state()) -> non_neg_integer().
get_max_users(StateData) ->
    MaxUsers = (StateData#state.config)#config.max_users,
    ServiceMaxUsers = get_service_max_users(StateData),
    if MaxUsers =< ServiceMaxUsers -> MaxUsers;
       true -> ServiceMaxUsers
    end.

-spec get_service_max_users(state()) -> pos_integer().
get_service_max_users(StateData) ->
    mod_muc_opt:max_users(StateData#state.server_host).

-spec get_max_users_admin_threshold(state()) -> pos_integer().
get_max_users_admin_threshold(StateData) ->
    mod_muc_opt:max_users_admin_threshold(StateData#state.server_host).

-spec room_queue_new(binary(), ejabberd_shaper:shaper(), _) -> p1_queue:queue({message | presence, jid()}) | undefined.
room_queue_new(ServerHost, Shaper, QueueType) ->
    HaveRoomShaper = Shaper /= none,
    HaveMessageShaper = mod_muc_opt:user_message_shaper(ServerHost) /= none,
    HavePresenceShaper = mod_muc_opt:user_presence_shaper(ServerHost) /= none,
    HaveMinMessageInterval = mod_muc_opt:min_message_interval(ServerHost) /= 0,
    HaveMinPresenceInterval = mod_muc_opt:min_presence_interval(ServerHost) /= 0,
    if HaveRoomShaper or HaveMessageShaper or HavePresenceShaper
       or HaveMinMessageInterval or HaveMinPresenceInterval ->
	    p1_queue:new(QueueType);
       true ->
	    undefined
    end.

-spec get_user_activity(jid(), state()) -> #activity{}.
get_user_activity(JID, StateData) ->
    case treap:lookup(jid:tolower(JID),
		      StateData#state.activity)
	of
      {ok, _P, A} -> A;
      error ->
	  MessageShaper =
	      ejabberd_shaper:new(mod_muc_opt:user_message_shaper(StateData#state.server_host)),
	  PresenceShaper =
	      ejabberd_shaper:new(mod_muc_opt:user_presence_shaper(StateData#state.server_host)),
	  #activity{message_shaper = MessageShaper,
		    presence_shaper = PresenceShaper}
    end.

-spec store_user_activity(jid(), #activity{}, state()) -> state().
store_user_activity(JID, UserActivity, StateData) ->
    MinMessageInterval =
	trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000),
    MinPresenceInterval =
	trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000),
    Key = jid:tolower(JID),
    Now = erlang:system_time(microsecond),
    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} =
				 ejabberd_shaper:update(UserActivity#activity.message_shaper,
					       100000),
			     {_, PresenceShaperInterval} =
				 ejabberd_shaper:update(UserActivity#activity.presence_shaper,
					       100000),
			     Delay = lists:max([MessageShaperInterval,
						PresenceShaperInterval,
						MinMessageInterval,
						MinPresenceInterval])
				       * 1000,
			     Priority = {1, -(Now + Delay)},
			     StateData#state{activity =
						 treap:insert(Key, Priority,
							      UserActivity,
							      Activity)};
			 false ->
			     Priority = {0, 0},
			     StateData#state{activity =
						 treap:insert(Key, Priority,
							      UserActivity,
							      Activity)}
		       end
		 end,
    reset_hibernate_timer(StateData1).

-spec clean_treap(treap:treap(), integer() | {1, integer()}) -> treap:treap().
clean_treap(Treap, CleanPriority) ->
    case treap:is_empty(Treap) of
      true -> Treap;
      false ->
	  {_Key, Priority, _Value} = treap:get_root(Treap),
	  if Priority > CleanPriority ->
		 clean_treap(treap:delete_root(Treap), CleanPriority);
	     true -> Treap
	  end
    end.

-spec prepare_room_queue(state()) -> state().
prepare_room_queue(StateData) ->
    case p1_queue:out(StateData#state.room_queue) of
      {{value, {message, From}}, _RoomQueue} ->
	  Activity = get_user_activity(From, StateData),
	  Packet = Activity#activity.message,
	  Size = element_size(Packet),
	  {RoomShaper, RoomShaperInterval} =
	      ejabberd_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} =
	      ejabberd_shaper:update(StateData#state.room_shaper, Size),
	  erlang:send_after(RoomShaperInterval, self(),
			    process_room_queue),
	  StateData#state{room_shaper = RoomShaper};
      {empty, _} -> StateData
    end.

-spec update_online_user(jid(), #user{}, state()) -> state().
update_online_user(JID, #user{nick = Nick} = User, StateData) ->
    LJID = jid:tolower(JID),
    add_to_log(join, Nick, StateData),
    Nicks1 = try maps:get(LJID, StateData#state.users) of
		 #user{nick = OldNick} ->
		     case lists:delete(
			    LJID, maps:get(OldNick, StateData#state.nicks)) of
			 [] ->
			     maps:remove(OldNick, StateData#state.nicks);
			 LJIDs ->
			     maps:put(OldNick, LJIDs, StateData#state.nicks)
		     end
	     catch _:{badkey, _} ->
		     StateData#state.nicks
	     end,
    Nicks = maps:update_with(Nick,
			     fun (LJIDs) -> [LJID|LJIDs -- [LJID]] end,
			     [LJID], Nicks1),
    Users = maps:update_with(LJID,
			     fun(U) ->
				     U#user{nick = Nick}
			     end, User, StateData#state.users),
    NewStateData = StateData#state{users = Users, nicks = Nicks},
    case {maps:get(LJID, StateData#state.users, error),
	  maps:get(LJID, NewStateData#state.users, error)} of
	{#user{nick = Old}, #user{nick = New}} when Old /= New ->
	    send_nick_changing(JID, Old, NewStateData, true, true);
	_ ->
	    ok
    end,
    NewStateData.

-spec set_subscriber(jid(), binary(), [binary()], state()) -> state().
set_subscriber(JID, Nick, Nodes,
	       #state{room = Room, host = Host, server_host = ServerHost} = StateData) ->
    BareJID = jid:remove_resource(JID),
    LBareJID = jid:tolower(BareJID),
    MUCSubscribers =
        muc_subscribers_put(
          #subscriber{jid = BareJID,
                      nick = Nick,
                      nodes = Nodes},
          StateData#state.muc_subscribers),
    NewStateData = StateData#state{muc_subscribers = MUCSubscribers},
    store_room(NewStateData, [{add_subscription, BareJID, Nick, Nodes}]),
    case not muc_subscribers_is_key(LBareJID, StateData#state.muc_subscribers) of
	true ->
	    Packet1a = #message{
		sub_els = [#ps_event{
		    items = #ps_items{
			node = ?NS_MUCSUB_NODES_SUBSCRIBERS,
			items = [#ps_item{
			    id = p1_rand:get_string(),
			    sub_els = [#muc_subscribe{jid = BareJID, nick = Nick}]}]}}]},
	    Packet1b = #message{
		sub_els = [#ps_event{
		    items = #ps_items{
			node = ?NS_MUCSUB_NODES_SUBSCRIBERS,
			items = [#ps_item{
			    id = p1_rand:get_string(),
			    sub_els = [#muc_subscribe{nick = Nick}]}]}}]},
	    {Packet2a, Packet2b} = ejabberd_hooks:run_fold(muc_subscribed, ServerHost, {Packet1a, Packet1b},
							   [ServerHost, Room, Host, BareJID, StateData]),
	    send_subscriptions_change_notifications(Packet2a, Packet2b, NewStateData);
	_ ->
	    ok
    end,
    NewStateData.

-spec add_online_user(jid(), binary(), role(), state()) -> state().
add_online_user(JID, Nick, Role, StateData) ->
    tab_add_online_user(JID, StateData),
    User = #user{jid = JID, nick = Nick, role = Role},
    reset_hibernate_timer(update_online_user(JID, User, StateData)).

-spec remove_online_user(jid(), state()) -> state().
remove_online_user(JID, StateData) ->
    remove_online_user(JID, StateData, <<"">>).

-spec remove_online_user(jid(), state(), binary()) -> state().
remove_online_user(JID, StateData, Reason) ->
    LJID = jid:tolower(JID),
    #user{nick = Nick} = maps:get(LJID, StateData#state.users),
    add_to_log(leave, {Nick, Reason}, StateData),
    tab_remove_online_user(JID, StateData),
    Users = maps:remove(LJID, StateData#state.users),
    Nicks = try maps:get(Nick, StateData#state.nicks) of
		[LJID] ->
		    maps:remove(Nick, StateData#state.nicks);
		U ->
		    maps:put(Nick, U -- [LJID], StateData#state.nicks)
	    catch _:{badkey, _} ->
		    StateData#state.nicks
	    end,
    reset_hibernate_timer(StateData#state{users = Users, nicks = Nicks}).

-spec filter_presence(presence()) -> presence().
filter_presence(Presence) ->
    Els = lists:filter(
	    fun(El) ->
		    XMLNS = xmpp:get_ns(El),
		    case catch binary:part(XMLNS, 0, size(?NS_MUC)) of
			?NS_MUC -> false;
			_ -> XMLNS /= ?NS_HATS
		    end
	    end, xmpp:get_els(Presence)),
    xmpp:set_els(Presence, Els).

-spec strip_status(presence()) -> presence().
strip_status(Presence) ->
    Presence#presence{status = []}.

-spec add_user_presence(jid(), presence(), state()) -> state().
add_user_presence(JID, Presence, StateData) ->
    LJID = jid:tolower(JID),
    FPresence = filter_presence(Presence),
    Users = maps:update_with(LJID,
			     fun (#user{} = User) ->
				     User#user{last_presence = FPresence}
			     end, StateData#state.users),
    StateData#state{users = Users}.

-spec add_user_presence_un(jid(), presence(), state()) -> state().
add_user_presence_un(JID, Presence, StateData) ->
    LJID = jid:tolower(JID),
    FPresence = filter_presence(Presence),
    Users = maps:update_with(LJID,
			     fun (#user{} = User) ->
				     User#user{last_presence = FPresence,
					       role = none}
			     end, StateData#state.users),
    StateData#state{users = Users}.

%% Find and return a list of the full JIDs of the users of Nick.
%% Return jid record.
-spec find_jids_by_nick(binary(), state()) -> [jid()].
find_jids_by_nick(Nick, StateData) ->
    Users = case maps:get(Nick, StateData#state.nicks, []) of
                [] -> muc_subscribers_get_by_nick(
                        Nick, StateData#state.muc_subscribers);
		Us -> Us
	    end,
    [jid:make(LJID) || LJID <- Users].

%% Find and return the full JID of the user of Nick with
%% highest-priority presence.  Return jid record.
-spec find_jid_by_nick(binary(), state()) -> jid() | false.
find_jid_by_nick(Nick, StateData) ->
    try maps:get(Nick, StateData#state.nicks) of
	[User] -> jid:make(User);
	[FirstUser | Users] ->
	    #user{last_presence = FirstPresence} =
		maps:get(FirstUser, StateData#state.users),
	    {LJID, _} = lists:foldl(
			  fun(Compare, {HighestUser, HighestPresence}) ->
				  #user{last_presence = P1} =
				      maps:get(Compare, StateData#state.users),
				  case higher_presence(P1, HighestPresence) of
				      true -> {Compare, P1};
				      false -> {HighestUser, HighestPresence}
				  end
			  end, {FirstUser, FirstPresence}, Users),
	    jid:make(LJID)
    catch _:{badkey, _} ->
	    false
    end.

-spec higher_presence(undefined | presence(),
		      undefined | presence()) -> boolean().
higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined ->
    Pri1 = get_priority_from_presence(Pres1),
    Pri2 = get_priority_from_presence(Pres2),
    Pri1 > Pri2;
higher_presence(Pres1, Pres2) ->
    Pres1 > Pres2.

-spec get_priority_from_presence(presence()) -> integer().
get_priority_from_presence(#presence{priority = Prio}) ->
    case Prio of
        undefined -> 0;
        _ -> Prio
    end.

-spec find_nick_by_jid(jid() | undefined, state()) -> binary().
find_nick_by_jid(undefined, _StateData) ->
    <<>>;
find_nick_by_jid(JID, StateData) ->
    LJID = jid:tolower(JID),
    case maps:find(LJID, StateData#state.users) of
	{ok, #user{nick = Nick}} ->
	    Nick;
	_ ->
	    case maps:find(LJID, (StateData#state.muc_subscribers)#muc_subscribers.subscribers) of
		{ok, #subscriber{nick = Nick}} ->
		    Nick;
		_ ->
		    <<>>
	    end
    end.

-spec is_nick_change(jid(), binary(), state()) -> boolean().
is_nick_change(JID, Nick, StateData) ->
    LJID = jid:tolower(JID),
    case Nick of
      <<"">> -> false;
      _ ->
	  #user{nick = OldNick} = maps:get(LJID, StateData#state.users),
	  Nick /= OldNick
    end.

-spec nick_collision(jid(), binary(), state()) -> boolean().
nick_collision(User, Nick, StateData) ->
    UserOfNick = case find_jid_by_nick(Nick, StateData) of
		     false ->
                         case muc_subscribers_get_by_nick(Nick, StateData#state.muc_subscribers) of
                             [J] -> J;
                             [] -> false
                         end;
		     J -> J
		 end,
    (UserOfNick /= false andalso
      jid:remove_resource(jid:tolower(UserOfNick))
	/= jid:remove_resource(jid:tolower(User))).

-spec add_new_user(jid(), binary(), presence(), state()) -> state();
		  (jid(), binary(), iq(), state()) -> {error, stanza_error()} |
						      {ignore, state()} |
						      {result, muc_subscribe(), state()}.
add_new_user(From, Nick, Packet, StateData) ->
    Lang = xmpp:get_lang(Packet),
    MaxUsers = get_max_users(StateData),
    MaxAdminUsers = MaxUsers +
		      get_max_users_admin_threshold(StateData),
    NUsers = maps:size(StateData#state.users),
    Affiliation = get_affiliation(From, StateData),
    ServiceAffiliation = get_service_affiliation(From,
						 StateData),
    NConferences = tab_count_user(From, StateData),
    MaxConferences =
	mod_muc_opt:max_user_conferences(StateData#state.server_host),
    Collision = nick_collision(From, Nick, StateData),
    IsSubscribeRequest = not is_record(Packet, presence),
    case {ServiceAffiliation == owner orelse
	     ((((Affiliation == admin orelse Affiliation == owner)
	       andalso NUsers < MaxAdminUsers)
	       orelse NUsers < MaxUsers)
	    andalso NConferences < MaxConferences),
	  Collision,
	  mod_muc:can_use_nick(StateData#state.server_host,
			       jid:encode(StateData#state.jid), From, Nick),
	  get_occupant_initial_role(From, Affiliation, StateData)}
	of
      {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers ->
	  Txt = ?T("Too many users in this conference"),
	  Err = xmpp:err_resource_constraint(Txt, Lang),
	  if not IsSubscribeRequest ->
		  ejabberd_router:route_error(Packet, Err),
		  StateData;
	     true ->
		  {error, Err}
	  end;
      {false, _, _, _} when NConferences >= MaxConferences ->
	  Txt = ?T("You have joined too many conferences"),
	  Err = xmpp:err_resource_constraint(Txt, Lang),
	  if not IsSubscribeRequest ->
		  ejabberd_router:route_error(Packet, Err),
		  StateData;
	     true ->
		  {error, Err}
	  end;
      {false, _, _, _} ->
	  Err = xmpp:err_service_unavailable(),
	  if not IsSubscribeRequest ->
		  ejabberd_router:route_error(Packet, Err),
		  StateData;
	     true ->
		  {error, Err}
	  end;
      {_, _, _, none} ->
	  Err = case Affiliation of
		    outcast ->
			ErrText = ?T("You have been banned from this room"),
			xmpp:err_forbidden(ErrText, Lang);
		    _ ->
			ErrText = ?T("Membership is required to enter this room"),
			xmpp:err_registration_required(ErrText, Lang)
		end,
	  if not IsSubscribeRequest ->
		  ejabberd_router:route_error(Packet, Err),
		  StateData;
	     true ->
		  {error, Err}
	  end;
      {_, true, _, _} ->
	  ErrText = ?T("That nickname is already in use by another occupant"),
	  Err = xmpp:err_conflict(ErrText, Lang),
	  if not IsSubscribeRequest ->
		  ejabberd_router:route_error(Packet, Err),
		  StateData;
	     true ->
		  {error, Err}
	  end;
      {_, _, false, _} ->
	  Err = case Nick of
			<<>> ->
			    xmpp:err_jid_malformed(?T("Nickname can't be empty"),
						   Lang);
			_ ->
			    xmpp:err_conflict(?T("That nickname is registered"
						 " by another person"), Lang)
		    end,
	  if not IsSubscribeRequest ->
		  ejabberd_router:route_error(Packet, Err),
		  StateData;
	     true ->
		  {error, Err}
	  end;
      {_, _, _, Role} ->
	  case check_password(ServiceAffiliation, Affiliation,
			      Packet, From, StateData)
	      of
	    true ->
		Nodes = get_subscription_nodes(Packet),
		NewStateData =
		      if not IsSubscribeRequest ->
			      NewState = add_user_presence(
					   From, Packet,
					   add_online_user(From, Nick, Role,
							   StateData)),
			      send_initial_presences_and_messages(
				From, Nick, Packet, NewState, StateData),
			      NewState;
			 true ->
			      set_subscriber(From, Nick, Nodes, StateData)
		      end,
		  ResultState =
		      case NewStateData#state.just_created of
			  true ->
			      NewStateData#state{just_created = erlang:system_time(microsecond)};
			  _ ->
			      Robots = maps:remove(From, StateData#state.robots),
			      NewStateData#state{robots = Robots}
		      end,
		  if not IsSubscribeRequest -> ResultState;
		     true -> {result, subscribe_result(Packet), ResultState}
		  end;
	    need_password ->
		ErrText = ?T("A password is required to enter this room"),
		Err = xmpp:err_not_authorized(ErrText, Lang),
		if not IsSubscribeRequest ->
			ejabberd_router:route_error(Packet, Err),
			StateData;
		   true ->
			{error, Err}
		end;
	    captcha_required ->
		SID = xmpp:get_id(Packet),
		RoomJID = StateData#state.jid,
		To = jid:replace_resource(RoomJID, Nick),
		Limiter = {From#jid.luser, From#jid.lserver},
		case ejabberd_captcha:create_captcha(SID, RoomJID, To,
						     Lang, Limiter, From)
                   of
		  {ok, ID, Body, CaptchaEls} ->
		      MsgPkt = #message{from = RoomJID,
					to = From,
					id = ID, body = Body,
					sub_els = CaptchaEls},
		      Robots = maps:put(From, {Nick, Packet},
					StateData#state.robots),
		      ejabberd_router:route(MsgPkt),
		      NewState = StateData#state{robots = Robots},
		      if not IsSubscribeRequest ->
			      NewState;
			 true ->
			      {ignore, NewState}
		      end;
		  {error, limit} ->
		      ErrText = ?T("Too many CAPTCHA requests"),
		      Err = xmpp:err_resource_constraint(ErrText, Lang),
		      if not IsSubscribeRequest ->
			      ejabberd_router:route_error(Packet, Err),
			      StateData;
			 true ->
			      {error, Err}
		      end;
		  _ ->
		      ErrText = ?T("Unable to generate a CAPTCHA"),
		      Err = xmpp:err_internal_server_error(ErrText, Lang),
		      if not IsSubscribeRequest ->
			      ejabberd_router:route_error(Packet, Err),
			      StateData;
			 true ->
			      {error, Err}
		      end
		end;
	    _ ->
		ErrText = ?T("Incorrect password"),
		Err = xmpp:err_not_authorized(ErrText, Lang),
		if not IsSubscribeRequest ->
			ejabberd_router:route_error(Packet, Err),
			StateData;
		   true ->
			{error, Err}
		end
	  end
    end.

-spec check_password(affiliation(), affiliation(),
		     presence() | iq(), jid(), state()) ->
      boolean() | need_password | captcha_required.
check_password(owner, _Affiliation, _Packet, _From,
	       _StateData) ->
    %% Don't check pass if user is owner in MUC service (access_admin option)
    true;
check_password(_ServiceAffiliation, Affiliation, Packet,
	       From, StateData) ->
    case (StateData#state.config)#config.password_protected
	of
      false -> check_captcha(Affiliation, From, StateData);
      true ->
	  Pass = extract_password(Packet),
	  case Pass of
	    false -> need_password;
	    _ ->
		case (StateData#state.config)#config.password of
		  Pass -> true;
		  _ -> false
		end
	  end
    end.

-spec check_captcha(affiliation(), jid(), state()) -> true | captcha_required.
check_captcha(Affiliation, From, StateData) ->
    case (StateData#state.config)#config.captcha_protected
	   andalso ejabberd_captcha:is_feature_available()
	of
      true when Affiliation == none ->
	  case maps:get(From, StateData#state.robots, error) of
	      passed -> true;
	      _ ->
		WList =
		    (StateData#state.config)#config.captcha_whitelist,
		#jid{luser = U, lserver = S, lresource = R} = From,
		case (?SETS):is_element({U, S, R}, WList) of
		  true -> true;
		  false ->
		      case (?SETS):is_element({U, S, <<"">>}, WList) of
			true -> true;
			false ->
			    case (?SETS):is_element({<<"">>, S, <<"">>}, WList)
				of
			      true -> true;
			      false -> captcha_required
			    end
		      end
		end
	  end;
      _ -> true
    end.

-spec extract_password(presence() | iq()) -> binary() | false.
extract_password(#presence{} = Pres) ->
    case xmpp:get_subtag(Pres, #muc{}) of
	#muc{password = Password} when is_binary(Password) ->
	    Password;
	_ ->
	    false
    end;
extract_password(#iq{} = IQ) ->
    case xmpp:get_subtag(IQ, #muc_subscribe{}) of
	#muc_subscribe{password = Password} when Password /= <<"">> ->
	    Password;
	_ ->
	    false
    end.

-spec get_history(binary(), stanza(), state()) -> [lqueue_elem()].
get_history(Nick, Packet, #state{history = History}) ->
    case xmpp:get_subtag(Packet, #muc{}) of
	#muc{history = #muc_history{} = MUCHistory} ->
	    Now = erlang:timestamp(),
	    Q = History#lqueue.queue,
	    filter_history(Q, Now, Nick, MUCHistory);
	_ ->
	    p1_queue:to_list(History#lqueue.queue)
    end.

-spec filter_history(p1_queue:queue(lqueue_elem()), erlang:timestamp(),
		     binary(), muc_history()) -> [lqueue_elem()].
filter_history(Queue, Now, Nick,
	       #muc_history{since = Since,
			    seconds = Seconds,
			    maxstanzas = MaxStanzas,
			    maxchars = MaxChars}) ->
    {History, _, _} =
	lists:foldr(
	  fun({_, _, _, TimeStamp, Size} = Elem,
	      {Elems, NumStanzas, NumChars} = Acc) ->
		  NowDiff = timer:now_diff(Now, TimeStamp) div 1000000,
		  Chars = Size + byte_size(Nick) + 1,
		  if (NumStanzas < MaxStanzas) andalso
		     (TimeStamp > Since) andalso
		     (NowDiff =< Seconds) andalso
		     (NumChars + Chars =< MaxChars) ->
			  {[Elem|Elems], NumStanzas + 1, NumChars + Chars};
		     true ->
			  Acc
		  end
	  end, {[], 0, 0}, p1_queue:to_list(Queue)),
    History.

-spec is_room_overcrowded(state()) -> boolean().
is_room_overcrowded(StateData) ->
    MaxUsersPresence = mod_muc_opt:max_users_presence(StateData#state.server_host),
    maps:size(StateData#state.users) > MaxUsersPresence.

-spec presence_broadcast_allowed(jid(), state()) -> boolean().
presence_broadcast_allowed(JID, StateData) ->
    Role = get_role(JID, StateData),
    lists:member(Role, (StateData#state.config)#config.presence_broadcast).

-spec send_initial_presences_and_messages(
	jid(), binary(), presence(), state(), state()) -> ok.
send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) ->
    advertise_entity_capabilities(From, NewState),
    send_existing_presences(From, NewState),
    send_self_presence(From, NewState, OldState),
    History = get_history(Nick, Presence, NewState),
    send_history(From, History, NewState),
    send_subject(From, OldState).

-spec advertise_entity_capabilities(jid(), state()) -> ok.
advertise_entity_capabilities(JID, State) ->
    AvatarHash = (State#state.config)#config.vcard_xupdate,
    DiscoInfo = make_disco_info(JID, State),
    Extras = iq_disco_info_extras(<<"en">>, State, true),
    DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]},
    DiscoHash = mod_caps:compute_disco_hash(DiscoInfo1, sha),
    Els1 = [#caps{hash = <<"sha-1">>,
		  node = ejabberd_config:get_uri(),
		  version = DiscoHash}],
    Els2 = if is_binary(AvatarHash) ->
		   [#vcard_xupdate{hash = AvatarHash}|Els1];
	      true ->
		   Els1
	   end,
    ejabberd_router:route(#presence{from = State#state.jid, to = JID,
				    id = p1_rand:get_string(),
				    sub_els = Els2}).

-spec send_self_presence(jid(), state(), state()) -> ok.
send_self_presence(NJID, StateData, OldStateData) ->
    send_new_presence(NJID, <<"">>, true, StateData, OldStateData).

-spec send_update_presence(jid(), state(), state()) -> ok.
send_update_presence(JID, StateData, OldStateData) ->
    send_update_presence(JID, <<"">>, StateData, OldStateData).

-spec send_update_presence(jid(), binary(), state(), state()) -> ok.
send_update_presence(JID, Reason, StateData, OldStateData) ->
    case is_room_overcrowded(StateData) of
	true -> ok;
	false -> send_update_presence1(JID, Reason, StateData, OldStateData)
    end.

-spec send_update_presence1(jid(), binary(), state(), state()) -> ok.
send_update_presence1(JID, Reason, StateData, OldStateData) ->
    LJID = jid:tolower(JID),
    LJIDs = case LJID of
	      {U, S, <<"">>} ->
		    maps:fold(fun (J, _, Js) ->
				      case J of
					  {U, S, _} -> [J | Js];
					  _ -> Js
				      end
			      end, [], StateData#state.users);
	      _ ->
		  case maps:is_key(LJID, StateData#state.users) of
		    true -> [LJID];
		    _ -> []
		  end
	    end,
    lists:foreach(fun (J) ->
			  send_new_presence(J, Reason, false, StateData,
					    OldStateData)
		  end,
		  LJIDs).

-spec send_new_presence(jid(), state(), state()) -> ok.
send_new_presence(NJID, StateData, OldStateData) ->
    send_new_presence(NJID, <<"">>, false, StateData, OldStateData).

-spec send_new_presence(jid(), binary(), state(), state()) -> ok.
send_new_presence(NJID, Reason, StateData, OldStateData) ->
    send_new_presence(NJID, Reason, false, StateData, OldStateData).

-spec is_ra_changed(jid(), boolean(), state(), state()) -> boolean().
is_ra_changed(_, _IsInitialPresence = true, _, _) ->
    false;
is_ra_changed(JID, _IsInitialPresence = false, NewStateData, OldStateData) ->
    NewRole = get_role(JID, NewStateData),
    NewAff = get_affiliation(JID, NewStateData),
    OldRole = get_role(JID, OldStateData),
    OldAff = get_affiliation(JID, OldStateData),
    if (NewRole == none) and (NewAff == OldAff) ->
	    %% A user is leaving the room;
	    false;
       true ->
	    (NewRole /= OldRole) or (NewAff /= OldAff)
    end.

-spec send_new_presence(jid(), binary(), boolean(), state(), state()) -> ok.
send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
    LNJID = jid:tolower(NJID),
    #user{nick = Nick} = maps:get(LNJID, StateData#state.users),
    LJID = find_jid_by_nick(Nick, StateData),
    #user{jid = RealJID, role = Role0,
	  last_presence = Presence0} = UserInfo =
	maps:get(jid:tolower(LJID), StateData#state.users),
    {Role1, Presence1} =
        case (presence_broadcast_allowed(NJID, StateData) orelse
         presence_broadcast_allowed(NJID, OldStateData)) of
            true -> {Role0, Presence0};
            false -> {none, #presence{type = unavailable}}
        end,
    Affiliation = get_affiliation(LJID, StateData),
    Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of
                true -> ?NS_MUCSUB_NODES_AFFILIATIONS;
                false -> ?NS_MUCSUB_NODES_PRESENCE
            end,
    Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS,
    UserMap =
        case is_room_overcrowded(StateData) orelse
	     (not (presence_broadcast_allowed(NJID, StateData) orelse
		   presence_broadcast_allowed(NJID, OldStateData))) of
            true ->
                #{LNJID => UserInfo};
            false ->
                %% TODO: optimize further
                UM1 = get_users_and_subscribers_with_node(Node1, StateData),
                UM2 = get_users_and_subscribers_with_node(Node2, StateData),
                maps:merge(UM1, UM2)
        end,
    maps:fold(
      fun(LUJID, Info, _) ->
	      IsSelfPresence = LNJID == LUJID,
	      {Role, Presence} = if IsSelfPresence -> {Role0, Presence0};
				    true -> {Role1, Presence1}
				 end,
	      Item0 = #muc_item{affiliation = Affiliation,
				role = Role},
	      Item1 = case Info#user.role == moderator orelse
			  (StateData#state.config)#config.anonymous
			  == false orelse IsSelfPresence of
			  true -> Item0#muc_item{jid = RealJID};
			  false -> Item0
		      end,
	      Item = Item1#muc_item{reason = Reason},
	      StatusCodes = status_codes(IsInitialPresence, IsSelfPresence,
					 StateData),
	      Pres = if Presence == undefined -> #presence{};
			true -> Presence
		     end,
              Packet = xmpp:set_subtag(
                         add_presence_hats(NJID, Pres, StateData),
                         #muc_user{items = [Item],
                                   status_codes = StatusCodes}),
	      send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
			   Info#user.jid, Packet, Node1, StateData),
	      Type = xmpp:get_type(Packet),
	      IsSubscriber = is_subscriber(Info#user.jid, StateData),
	      IsOccupant = Info#user.last_presence /= undefined,
	      if (IsSubscriber and not IsOccupant) and
		 (IsInitialPresence or (Type == unavailable)) ->
		      send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
				   Info#user.jid, Packet, Node2, StateData);
		 true ->
		      ok
	      end
      end, ok, UserMap).

-spec send_existing_presences(jid(), state()) -> ok.
send_existing_presences(ToJID, StateData) ->
    case is_room_overcrowded(StateData) of
	true -> ok;
	false -> send_existing_presences1(ToJID, StateData)
    end.

-spec send_existing_presences1(jid(), state()) -> ok.
send_existing_presences1(ToJID, StateData) ->
    LToJID = jid:tolower(ToJID),
    #user{jid = RealToJID, role = Role} = maps:get(LToJID, StateData#state.users),
    maps:fold(
      fun(FromNick, _Users, _) ->
	      LJID = find_jid_by_nick(FromNick, StateData),
	      #user{jid = FromJID, role = FromRole,
		    last_presence = Presence} =
		  maps:get(jid:tolower(LJID), StateData#state.users),
	      PresenceBroadcast =
		  lists:member(
		    FromRole, (StateData#state.config)#config.presence_broadcast),
	      case {RealToJID, PresenceBroadcast} of
		  {FromJID, _} -> ok;
		  {_, false} -> ok;
		  _ ->
		      FromAffiliation = get_affiliation(LJID, StateData),
		      Item0 = #muc_item{affiliation = FromAffiliation,
					role = FromRole},
		      Item = case Role == moderator orelse
				 (StateData#state.config)#config.anonymous
				 == false of
				 true -> Item0#muc_item{jid = FromJID};
				 false -> Item0
			     end,
		      Packet = xmpp:set_subtag(
                                 add_presence_hats(
                                   FromJID, Presence, StateData),
                                 #muc_user{items = [Item]}),
		      send_wrapped(jid:replace_resource(StateData#state.jid, FromNick),
				   RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData)
	      end
      end, ok, StateData#state.nicks).

-spec set_nick(jid(), binary(), state()) -> state().
set_nick(JID, Nick, State) ->
    LJID = jid:tolower(JID),
    #user{nick = OldNick} = maps:get(LJID, State#state.users),
    Users = maps:update_with(LJID,
			     fun (#user{} = User) -> User#user{nick = Nick} end,
			     State#state.users),
    OldNickUsers = maps:get(OldNick, State#state.nicks),
    NewNickUsers = maps:get(Nick, State#state.nicks, []),
    Nicks = case OldNickUsers of
		[LJID] ->
		    maps:put(Nick, [LJID | NewNickUsers -- [LJID]],
			     maps:remove(OldNick, State#state.nicks));
		[_ | _] ->
		    maps:put(Nick, [LJID | NewNickUsers -- [LJID]],
			     maps:put(OldNick, OldNickUsers -- [LJID],
				      State#state.nicks))
	    end,
    State#state{users = Users, nicks = Nicks}.

-spec change_nick(jid(), binary(), state()) -> state().
change_nick(JID, Nick, StateData) ->
    LJID = jid:tolower(JID),
    #user{nick = OldNick} = maps:get(LJID, StateData#state.users),
    OldNickUsers = maps:get(OldNick, StateData#state.nicks),
    NewNickUsers = maps:get(Nick, StateData#state.nicks, []),
    SendOldUnavailable = length(OldNickUsers) == 1,
    SendNewAvailable = SendOldUnavailable orelse NewNickUsers == [],
    NewStateData = set_nick(JID, Nick, StateData),
    case presence_broadcast_allowed(JID, NewStateData) of
        true ->
            send_nick_changing(JID, OldNick, NewStateData,
                               SendOldUnavailable, SendNewAvailable);
        false -> ok
    end,
    add_to_log(nickchange, {OldNick, Nick}, StateData),
    NewStateData.

-spec send_nick_changing(jid(), binary(), state(), boolean(), boolean()) -> ok.
send_nick_changing(JID, OldNick, StateData,
		   SendOldUnavailable, SendNewAvailable) ->
    #user{jid = RealJID, nick = Nick, role = Role,
	  last_presence = Presence} =
	maps:get(jid:tolower(JID), StateData#state.users),
    Affiliation = get_affiliation(JID, StateData),
    maps:fold(
      fun(LJID, Info, _) when Presence /= undefined ->
	      IsSelfPresence = LJID == jid:tolower(JID),
	      Item0 = #muc_item{affiliation = Affiliation, role = Role},
	      Item = case Info#user.role == moderator orelse
			 (StateData#state.config)#config.anonymous
			 == false orelse IsSelfPresence of
			 true -> Item0#muc_item{jid = RealJID};
			 false -> Item0
		     end,
	      Status110 = case IsSelfPresence of
			      true -> [110];
			      false -> []
			  end,
	      Packet1 = #presence{
			   type = unavailable,
			   sub_els = [#muc_user{
					 items = [Item#muc_item{nick = Nick}],
					 status_codes = [303|Status110]}]},
	      Packet2 = xmpp:set_subtag(Presence,
					#muc_user{items = [Item],
						  status_codes = Status110}),
	      if SendOldUnavailable ->
		      send_wrapped(
			jid:replace_resource(StateData#state.jid, OldNick),
			Info#user.jid, Packet1, ?NS_MUCSUB_NODES_PRESENCE,
			StateData);
		 true -> ok
	      end,
	      if SendNewAvailable ->
		      send_wrapped(
			jid:replace_resource(StateData#state.jid, Nick),
			Info#user.jid, Packet2, ?NS_MUCSUB_NODES_PRESENCE,
			StateData);
		 true -> ok
	      end;
	 (_, _, _) ->
	      ok
      end, ok, get_users_and_subscribers_with_node(
                 ?NS_MUCSUB_NODES_PRESENCE, StateData)).

-spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok.
maybe_send_affiliation(JID, Affiliation, StateData) ->
    LJID = jid:tolower(JID),
    %% TODO: there should be a better way to check IsOccupant
    Users = get_users_and_subscribers(StateData),
    IsOccupant = case LJID of
		     {LUser, LServer, <<"">>} ->
			 #{} /= maps:filter(
				  fun({U, S, _}, _) ->
					  U == LUser andalso
					      S == LServer
				  end, Users);
		     {_LUser, _LServer, _LResource} ->
			 maps:is_key(LJID, Users)
		 end,
    case IsOccupant of
      true ->
	  ok; % The new affiliation is published via presence.
      false ->
	  send_affiliation(JID, Affiliation, StateData)
    end.

-spec send_affiliation(jid(), affiliation(), state()) -> ok.
send_affiliation(JID, Affiliation, StateData) ->
    Item = #muc_item{jid = JID,
		     affiliation = Affiliation,
		     role = none},
    Message = #message{id = p1_rand:get_string(),
		       sub_els = [#muc_user{items = [Item]}]},
    Users = get_users_and_subscribers_with_node(
              ?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
    Recipients = case (StateData#state.config)#config.anonymous of
		   true ->
		       maps:filter(fun(_, #user{role = moderator}) ->
					   true;
				      (_, _) ->
					   false
				   end, Users);
		   false ->
		       Users
		 end,
    send_wrapped_multiple(StateData#state.jid, Recipients, Message,
			  ?NS_MUCSUB_NODES_AFFILIATIONS, StateData).

-spec status_codes(boolean(), boolean(), state()) -> [pos_integer()].
status_codes(IsInitialPresence, _IsSelfPresence = true, StateData) ->
    S0 = [110],
    case IsInitialPresence of
	true ->
	    S1 = case StateData#state.just_created of
		     true -> [201|S0];
		     _ -> S0
		 end,
	    S2 = case (StateData#state.config)#config.anonymous of
		     true -> S1;
		     false -> [100|S1]
		 end,
	    S3 = case (StateData#state.config)#config.logging of
		     true -> [170|S2];
		     false -> S2
		 end,
	    S3;
	false -> S0
    end;
status_codes(_IsInitialPresence, _IsSelfPresence = false, _StateData) -> [].

-spec lqueue_new(non_neg_integer(), ram | file) -> lqueue().
lqueue_new(Max, Type) ->
    #lqueue{queue = p1_queue:new(Type), max = Max}.

-spec lqueue_in(lqueue_elem(), lqueue()) -> lqueue().
%% If the message queue limit is set to 0, do not store messages.
lqueue_in(_Item, LQ = #lqueue{max = 0}) -> LQ;
%% Otherwise, rotate messages in the queue store.
lqueue_in(Item, #lqueue{queue = Q1, max = Max}) ->
    Len = p1_queue:len(Q1),
    Q2 = p1_queue:in(Item, Q1),
    if Len >= Max ->
	   Q3 = lqueue_cut(Q2, Len - Max + 1),
	   #lqueue{queue = Q3, max = Max};
       true -> #lqueue{queue = Q2, max = Max}
    end.

-spec lqueue_cut(p1_queue:queue(lqueue_elem()), non_neg_integer()) -> p1_queue:queue(lqueue_elem()).
lqueue_cut(Q, 0) -> Q;
lqueue_cut(Q, N) ->
    {_, Q1} = p1_queue:out(Q),
    lqueue_cut(Q1, N - 1).

-spec add_message_to_history(binary(), jid(), message(), state()) -> state().
add_message_to_history(FromNick, FromJID, Packet, StateData) ->
    add_to_log(text, {FromNick, Packet}, StateData),
    case check_subject(Packet) of
	[] ->
	    TimeStamp = erlang:timestamp(),
	    AddrPacket = case (StateData#state.config)#config.anonymous of
			     true -> Packet;
			     false ->
				 Addresses = #addresses{
						list = [#address{type = ofrom,
								 jid = FromJID}]},
				 xmpp:set_subtag(Packet, Addresses)
			 end,
	    TSPacket = misc:add_delay_info(
			 AddrPacket, StateData#state.jid, TimeStamp),
	    SPacket = xmpp:set_from_to(
			TSPacket,
			jid:replace_resource(StateData#state.jid, FromNick),
			StateData#state.jid),
	    Size = element_size(SPacket),
	    Q1 = lqueue_in({FromNick, TSPacket, false,
			    TimeStamp, Size},
			   StateData#state.history),
	    StateData#state{history = Q1, just_created = erlang:system_time(microsecond)};
	_ ->
	    StateData#state{just_created = erlang:system_time(microsecond)}
    end.

remove_from_history(StanzaId, #state{history = #lqueue{queue = Queue} = LQueue} = StateData) ->
    NewQ = p1_queue:foldl(
	fun({_, Pkt, _, _, _} = Entry, Acc) ->
	    case xmpp:get_meta(Pkt, stanza_id, missing) of
		V when V == StanzaId ->
		    Acc;
		_ ->
		    p1_queue:in(Entry, Acc)
	    end
	end, p1_queue:new(), Queue),
    StateData#state{history = LQueue#lqueue{queue = NewQ}}.

remove_from_history({U1, S1}, OriginId, #state{history = #lqueue{queue = Queue} = LQueue} = StateData) ->
    {NewQ, StanzaId} = p1_queue:foldl(
	fun({_, Pkt, _, _, _} = Entry, {Q, none}) ->
	    case jid:tolower(xmpp:get_from(Pkt)) of
		{U2, S2, _} when U1 == U2, S1 == S2 ->
		    case xmpp:get_subtag(Pkt, #origin_id{}) of
			#origin_id{id = V} when V == OriginId ->
			    {Q, xmpp:get_meta(Pkt, stanza_id, missing)};
			_ ->
			    {p1_queue:in(Entry, Q), none}
		    end;
		_ ->
		    {p1_queue:in(Entry, Q), none}
	    end;
	   (Entry, {Q, S}) ->
	       {p1_queue:in(Entry, Q), S}
	end, {p1_queue:new(), none}, Queue),
    {StateData#state{history = LQueue#lqueue{queue = NewQ}}, StanzaId}.

-spec send_history(jid(), [lqueue_elem()], state()) -> ok.
send_history(JID, History, StateData) ->
    lists:foreach(
      fun({Nick, Packet, _HaveSubject, _TimeStamp, _Size}) ->
	      ejabberd_router:route(
		xmpp:set_from_to(
		  Packet,
		  jid:replace_resource(StateData#state.jid, Nick),
		  JID))
      end, History).

-spec send_subject(jid(), state()) -> ok.
send_subject(JID, #state{subject_author = {Nick, AuthorJID}} = StateData) ->
    Subject = case StateData#state.subject of
		  [] -> [#text{}];
		  [_|_] = S -> S
	      end,
    Packet = #message{from = AuthorJID,
		      to = JID, type = groupchat, subject = Subject},
    case ejabberd_hooks:run_fold(muc_filter_message,
                                 StateData#state.server_host,
                                 xmpp:put_meta(Packet, mam_ignore, true),
                                 [StateData, Nick]) of
        drop ->
            ok;
        NewPacket1 ->
            FromRoomNick = jid:replace_resource(StateData#state.jid, Nick),
            NewPacket2 = xmpp:set_from(NewPacket1, FromRoomNick),
            ejabberd_router:route(NewPacket2)
    end.

-spec check_subject(message()) -> [text()].
check_subject(#message{subject = [_|_] = Subj, body = [],
		       thread = undefined}) ->
    Subj;
check_subject(_) ->
    [].

-spec can_change_subject(role(), boolean(), state()) -> boolean().
can_change_subject(Role, IsSubscriber, StateData) ->
    case (StateData#state.config)#config.allow_change_subj
	of
      true -> Role == moderator orelse Role == participant orelse IsSubscriber == true;
      _ -> Role == moderator
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Admin stuff

-spec process_iq_admin(jid(), iq(), #state{}) -> {error, stanza_error()} |
						 {result, undefined, #state{}} |
						 {result, muc_admin()}.
process_iq_admin(_From, #iq{lang = Lang, sub_els = [#muc_admin{items = []}]},
		 _StateData) ->
    Txt = ?T("No 'item' element found"),
    {error, xmpp:err_bad_request(Txt, Lang)};
process_iq_admin(_From, #iq{type = get, lang = Lang,
			    sub_els = [#muc_admin{items = [_, _|_]}]},
		 _StateData) ->
    ErrText = ?T("Too many <item/> elements"),
    {error, xmpp:err_bad_request(ErrText, Lang)};
process_iq_admin(From, #iq{type = set, lang = Lang,
			   sub_els = [#muc_admin{items = Items}]},
		 StateData) ->
    process_admin_items_set(From, Items, Lang, StateData);
process_iq_admin(From, #iq{type = get, lang = Lang,
			   sub_els = [#muc_admin{items = [Item]}]},
		 StateData) ->
    FAffiliation = get_affiliation(From, StateData),
    FRole = get_role(From, StateData),
    case Item of
	#muc_item{role = undefined, affiliation = undefined} ->
	    Txt = ?T("Neither 'role' nor 'affiliation' attribute found"),
	    {error, xmpp:err_bad_request(Txt, Lang)};
	#muc_item{role = undefined, affiliation = Affiliation} ->
	    if (FAffiliation == owner) or
	       (FAffiliation == admin) or
	       ((FAffiliation == member) and
		not (StateData#state.config)#config.anonymous) ->
		    Items = items_with_affiliation(Affiliation, StateData),
		    {result, #muc_admin{items = Items}};
	       true ->
		    ErrText = ?T("Administrator privileges required"),
		    {error, xmpp:err_forbidden(ErrText, Lang)}
	    end;
	#muc_item{role = Role} ->
	    if FRole == moderator ->
		    Items = items_with_role(Role, StateData),
		    {result, #muc_admin{items = Items}};
	       true ->
		    ErrText = ?T("Moderator privileges required"),
		    {error, xmpp:err_forbidden(ErrText, Lang)}
	    end
    end.

-spec items_with_role(role(), state()) -> [muc_item()].
items_with_role(SRole, StateData) ->
    lists:map(fun ({_, U}) -> user_to_item(U, StateData)
	      end,
	      search_role(SRole, StateData)).

-spec items_with_affiliation(affiliation(), state()) -> [muc_item()].
items_with_affiliation(SAffiliation, StateData) ->
    lists:map(
      fun({JID, {Affiliation, Reason}}) ->
	      #muc_item{affiliation = Affiliation, jid = jid:make(JID),
			reason = Reason};
	 ({JID, Affiliation}) ->
	      #muc_item{affiliation = Affiliation, jid = jid:make(JID)}
      end,
      search_affiliation(SAffiliation, StateData)).

-spec user_to_item(#user{}, state()) -> muc_item().
user_to_item(#user{role = Role, nick = Nick, jid = JID},
	     StateData) ->
    Affiliation = get_affiliation(JID, StateData),
    #muc_item{role = Role,
	      affiliation = Affiliation,
	      nick = Nick,
	      jid = JID}.

-spec search_role(role(), state()) -> [{ljid(), #user{}}].
search_role(Role, StateData) ->
    lists:filter(fun ({_, #user{role = R}}) -> Role == R
		 end,
		 maps:to_list(StateData#state.users)).

-spec search_affiliation(affiliation(), state()) ->
			 [{ljid(),
			   affiliation() | {affiliation(), binary()}}].
search_affiliation(Affiliation,
                   #state{config = #config{persistent = false}} = StateData) ->
    search_affiliation_fallback(Affiliation, StateData);
search_affiliation(Affiliation, StateData) ->
    Room = StateData#state.room,
    Host = StateData#state.host,
    ServerHost = StateData#state.server_host,
    Mod = gen_mod:db_mod(ServerHost, mod_muc),
    case Mod:search_affiliation(ServerHost, Room, Host, Affiliation) of
	{ok, AffiliationList} ->
	    AffiliationList;
	{error, _} ->
	    search_affiliation_fallback(Affiliation, StateData)
    end.

-spec search_affiliation_fallback(affiliation(), state()) ->
				  [{ljid(),
				    affiliation() | {affiliation(), binary()}}].
search_affiliation_fallback(Affiliation, StateData) ->
    lists:filter(
      fun({_, A}) ->
	      case A of
		  {A1, _Reason} -> Affiliation == A1;
		  _ -> Affiliation == A
	      end
      end, maps:to_list(StateData#state.affiliations)).

-spec process_admin_items_set(jid(), [muc_item()], binary(),
			      #state{}) -> {result, undefined, #state{}} |
					   {error, stanza_error()}.
process_admin_items_set(UJID, Items, Lang, StateData) ->
    UAffiliation = get_affiliation(UJID, StateData),
    URole = get_role(UJID, StateData),
    case catch find_changed_items(UJID, UAffiliation, URole,
				  Items, Lang, StateData, [])
	of
      {result, Res} ->
	  ?INFO_MSG("Processing MUC admin query from ~ts in "
		    "room ~ts:~n ~p",
		    [jid:encode(UJID),
		     jid:encode(StateData#state.jid), Res]),
	  case lists:foldl(process_item_change(UJID),
			   StateData, lists:flatten(Res)) of
	      {error, _} = Err ->
		  Err;
	      NSD ->
		  store_room(NSD),
		  {result, undefined, NSD}
	  end;
	{error, Err} -> {error, Err}
    end.

-spec process_item_change(jid()) -> fun((admin_action(), state() | {error, stanza_error()}) ->
					       state() | {error, stanza_error()}).
process_item_change(UJID) ->
    fun(_, {error, _} = Err) ->
	    Err;
       (Item, SD) ->
	    process_item_change(Item, SD, UJID)
    end.

-spec process_item_change(admin_action(), state(), undefined | jid()) -> state() | {error, stanza_error()}.
process_item_change(Item, SD, UJID) ->
    try case Item of
	    {JID, affiliation, owner, _} when JID#jid.luser == <<"">> ->
		%% If the provided JID does not have username,
		%% forget the affiliation completely
		SD;
	    {JID, role, none, Reason} ->
		send_kickban_presence(UJID, JID, Reason, 307, SD),
		set_role(JID, none, SD);
	    {JID, affiliation, none, Reason} ->
                case get_affiliation(JID, SD) of
                    none -> SD;
                    _ ->
                        case (SD#state.config)#config.members_only of
                            true ->
                                send_kickban_presence(UJID, JID, Reason, 321, none, SD),
                                maybe_send_affiliation(JID, none, SD),
                                SD1 = set_affiliation(JID, none, SD),
                                set_role(JID, none, SD1);
                            _ ->
                                SD1 = set_affiliation(JID, none, SD),
                                SD2 = case (SD1#state.config)#config.moderated of
                                          true -> set_role(JID, visitor, SD1);
                                          false -> set_role(JID, participant, SD1)
                                      end,
                                send_update_presence(JID, Reason, SD2, SD),
                                maybe_send_affiliation(JID, none, SD2),
                                SD2
                        end
                end;
	    {JID, affiliation, outcast, Reason} ->
		send_kickban_presence(UJID, JID, Reason, 301, outcast, SD),
		maybe_send_affiliation(JID, outcast, SD),
                {result, undefined, SD2} =
                    process_iq_mucsub(JID,
                                      #iq{type = set,
                                          sub_els = [#muc_unsubscribe{}]}, SD),
		set_role(JID, none, set_affiliation(JID, outcast, SD2, Reason));
	    {JID, affiliation, A, Reason} when (A == admin) or (A == owner) ->
		SD1 = set_affiliation(JID, A, SD, Reason),
		SD2 = set_role(JID, moderator, SD1),
		send_update_presence(JID, Reason, SD2, SD),
		maybe_send_affiliation(JID, A, SD2),
		SD2;
	    {JID, affiliation, member, Reason} ->
		SD1 = set_affiliation(JID, member, SD, Reason),
		SD2 = set_role(JID, participant, SD1),
		send_update_presence(JID, Reason, SD2, SD),
		maybe_send_affiliation(JID, member, SD2),
		SD2;
	    {JID, role, Role, Reason} ->
		SD1 = set_role(JID, Role, SD),
		send_new_presence(JID, Reason, SD1, SD),
		SD1;
	    {JID, affiliation, A, _Reason} ->
		SD1 = set_affiliation(JID, A, SD),
		send_update_presence(JID, SD1, SD),
		maybe_send_affiliation(JID, A, SD1),
		SD1
	end
    catch ?EX_RULE(E, R, St) ->
	    StackTrace = ?EX_STACK(St),
	    FromSuffix = case UJID of
			     #jid{} ->
				 JidString = jid:encode(UJID),
				 <<" from ", JidString/binary>>;
			     undefined ->
				 <<"">>
			 end,
	    ?ERROR_MSG("Failed to set item ~p~ts:~n** ~ts",
		       [Item, FromSuffix,
			misc:format_exception(2, E, R, StackTrace)]),
	    {error, xmpp:err_internal_server_error()}
    end.

-spec find_changed_items(jid(), affiliation(), role(),
			 [muc_item()], binary(), state(), [admin_action()]) ->
				{result, [admin_action()]}.
find_changed_items(_UJID, _UAffiliation, _URole, [],
		   _Lang, _StateData, Res) ->
    {result, Res};
find_changed_items(_UJID, _UAffiliation, _URole,
		   [#muc_item{jid = undefined, nick = <<"">>}|_],
		   Lang, _StateData, _Res) ->
    Txt = ?T("Neither 'jid' nor 'nick' attribute found"),
    throw({error, xmpp:err_bad_request(Txt, Lang)});
find_changed_items(_UJID, _UAffiliation, _URole,
		   [#muc_item{role = undefined, affiliation = undefined}|_],
		   Lang, _StateData, _Res) ->
    Txt = ?T("Neither 'role' nor 'affiliation' attribute found"),
    throw({error, xmpp:err_bad_request(Txt, Lang)});
find_changed_items(UJID, UAffiliation, URole,
		   [#muc_item{jid = J, nick = Nick, reason = Reason,
			      role = Role, affiliation = Affiliation}|Items],
		   Lang, StateData, Res) ->
    [JID | _] = JIDs =
	if J /= undefined ->
		[J];
	   Nick /= <<"">> ->
		case find_jids_by_nick(Nick, StateData) of
		    [] ->
			ErrText = {?T("Nickname ~s does not exist in the room"),
				   [Nick]},
			throw({error, xmpp:err_not_acceptable(ErrText, Lang)});
		    JIDList ->
			JIDList
		end
	end,
    {RoleOrAff, RoleOrAffValue} = if Role == undefined ->
					  {affiliation, Affiliation};
				     true ->
					  {role, Role}
				  end,
    TAffiliation = get_affiliation(JID, StateData),
    TRole = get_role(JID, StateData),
    ServiceAf = get_service_affiliation(JID, StateData),
    UIsSubscriber = is_subscriber(UJID, StateData),
    URole1 = case {URole, UIsSubscriber} of
	{none, true} -> subscriber;
	{UR, _} -> UR
    end,
    CanChangeRA = case can_change_ra(UAffiliation,
				     URole1,
				     TAffiliation,
				     TRole, RoleOrAff, RoleOrAffValue,
				     ServiceAf) of
		      nothing -> nothing;
		      true -> true;
		      check_owner ->
			  case search_affiliation(owner, StateData) of
			      [{OJID, _}] ->
				  jid:remove_resource(OJID)
				      /=
				      jid:tolower(jid:remove_resource(UJID));
			      _ -> true
			  end;
		      _ -> false
		  end,
    case CanChangeRA of
	nothing ->
	    find_changed_items(UJID, UAffiliation, URole,
			       Items, Lang, StateData,
			       Res);
	true ->
	    MoreRes = case RoleOrAff of
			  affiliation ->
			      [{jid:remove_resource(Jidx),
				RoleOrAff, RoleOrAffValue, Reason}
			       || Jidx <- JIDs];
			  role ->
			      [{Jidx, RoleOrAff, RoleOrAffValue, Reason}
			       || Jidx <- JIDs]
		      end,
	    find_changed_items(UJID, UAffiliation, URole,
			       Items, Lang, StateData,
			       MoreRes ++ Res);
	false ->
	    Txt = ?T("Changing role/affiliation is not allowed"),
	    throw({error, xmpp:err_not_allowed(Txt, Lang)})
    end.

-spec can_change_ra(affiliation(), role(), affiliation(), role(),
		    affiliation, affiliation(), affiliation()) -> boolean() | nothing | check_owner;
		   (affiliation(), role(), affiliation(), role(),
		    role, role(), affiliation()) -> boolean() | nothing | check_owner.
can_change_ra(_FAffiliation, _FRole, owner, _TRole,
	      affiliation, owner, owner) ->
    %% A room owner tries to add as persistent owner a
    %% 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, subscriber, _TAffiliation,
	      visitor, role, none, _ServiceAf)
    when (FAffiliation == owner) or
	   (FAffiliation == admin) ->
    true;
can_change_ra(_FAffiliation, moderator, _TAffiliation,
	      visitor, role, participant, _ServiceAf) ->
    true;
can_change_ra(FAffiliation, subscriber, _TAffiliation,
	      visitor, role, participant, _ServiceAf)
    when (FAffiliation == owner) or
	   (FAffiliation == admin) ->
    true;
can_change_ra(FAffiliation, _FRole, _TAffiliation,
	      visitor, role, moderator, _ServiceAf)
    when (FAffiliation == owner) or
	   (FAffiliation == admin) ->
    true;
can_change_ra(_FAffiliation, moderator, _TAffiliation,
	      participant, role, none, _ServiceAf) ->
    true;
can_change_ra(FAffiliation, subscriber, _TAffiliation,
	      participant, role, none, _ServiceAf)
    when (FAffiliation == owner) or
	   (FAffiliation == admin) ->
    true;
can_change_ra(_FAffiliation, moderator, _TAffiliation,
	      participant, role, visitor, _ServiceAf) ->
    true;
can_change_ra(FAffiliation, subscriber, _TAffiliation,
	      participant, role, visitor, _ServiceAf)
    when (FAffiliation == owner) or
	   (FAffiliation == admin) ->
    true;
can_change_ra(FAffiliation, _FRole, _TAffiliation,
	      participant, role, moderator, _ServiceAf)
    when (FAffiliation == owner) or
	   (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(owner, moderator, TAffiliation,
	      moderator, role, none, _ServiceAf)
    when TAffiliation /= owner ->
    true;
can_change_ra(owner, subscriber, TAffiliation,
	      moderator, role, none, _ServiceAf)
    when TAffiliation /= owner ->
    true;
can_change_ra(admin, moderator, TAffiliation,
	      moderator, role, none, _ServiceAf)
    when (TAffiliation /= owner) and
         (TAffiliation /= admin) ->
    true;
can_change_ra(admin, subscriber, TAffiliation,
	      moderator, role, none, _ServiceAf)
    when (TAffiliation /= owner) and
         (TAffiliation /= admin) ->
    true;
can_change_ra(_FAffiliation, _FRole, _TAffiliation,
	      _TRole, role, _Value, _ServiceAf) ->
    false.

-spec send_kickban_presence(undefined | jid(), jid(), binary(),
			    pos_integer(), state()) -> ok.
send_kickban_presence(UJID, JID, Reason, Code, StateData) ->
    NewAffiliation = get_affiliation(JID, StateData),
    send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation,
			  StateData).

-spec send_kickban_presence(undefined | jid(), jid(), binary(), pos_integer(),
			    affiliation(), state()) -> ok.
send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation,
		      StateData) ->
    LJID = jid:tolower(JID),
    LJIDs = case LJID of
		{U, S, <<"">>} ->
		    maps:fold(fun (J, _, Js) ->
				      case J of
					  {U, S, _} -> [J | Js];
					  _ -> Js
				      end
			      end, [], StateData#state.users);
		_ ->
		    case maps:is_key(LJID, StateData#state.users) of
			true -> [LJID];
			_ -> []
		    end
	    end,
    lists:foreach(fun (LJ) ->
			  #user{nick = Nick, jid = J} = maps:get(LJ, StateData#state.users),
			  add_to_log(kickban, {Nick, Reason, Code}, StateData),
			  tab_remove_online_user(J, StateData),
			  send_kickban_presence1(UJID, J, Reason, Code,
						 NewAffiliation, StateData)
		  end,
		  LJIDs).

-spec send_kickban_presence1(undefined | jid(), jid(), binary(), pos_integer(),
			     affiliation(), state()) -> ok.
send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
		       StateData) ->
    #user{jid = RealJID, nick = Nick} = maps:get(jid:tolower(UJID), StateData#state.users),
    ActorNick = find_nick_by_jid(MJID, StateData),
    %% TODO: optimize further
    UserMap =
        maps:merge(
          get_users_and_subscribers_with_node(
            ?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
          get_users_and_subscribers_with_node(
            ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)),
    maps:fold(
      fun(LJID, Info, _) ->
	      IsSelfPresence = jid:tolower(UJID) == LJID,
	      Item0 = #muc_item{affiliation = Affiliation,
				role = none},
	      Item1 = case Info#user.role == moderator orelse
			  (StateData#state.config)#config.anonymous
			  == false orelse IsSelfPresence of
			  true -> Item0#muc_item{jid = RealJID};
			  false -> Item0
		      end,
	      Item2 = Item1#muc_item{reason = Reason},
	      Item = case ActorNick of
			 <<"">> -> Item2;
			 _ -> Item2#muc_item{actor = #muc_actor{nick = ActorNick}}
		     end,
	      Codes = if IsSelfPresence -> [110, Code];
			 true -> [Code]
		      end,
	      Packet = #presence{type = unavailable,
				 sub_els = [#muc_user{items = [Item],
						      status_codes = Codes}]},
	      RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick),
	      send_wrapped(RoomJIDNick, Info#user.jid, Packet,
			   ?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
			  IsSubscriber = is_subscriber(Info#user.jid, StateData),
	      IsOccupant = Info#user.last_presence /= undefined,
	      if (IsSubscriber and not IsOccupant) ->
		      send_wrapped(RoomJIDNick, Info#user.jid, Packet,
				   ?NS_MUCSUB_NODES_PARTICIPANTS, StateData);
		 true ->
		      ok
	      end
      end, ok, UserMap).

-spec convert_legacy_fields([xdata_field()]) -> [xdata_field()].
convert_legacy_fields(Fs) ->
    lists:map(
      fun(#xdata_field{var = Var} = F) ->
	      NewVar = case Var of
			   <<"muc#roomconfig_allowvisitorstatus">> ->
			       <<"allow_visitor_status">>;
			   <<"muc#roomconfig_allowvisitornickchange">> ->
			       <<"allow_visitor_nickchange">>;
			   <<"muc#roomconfig_allowvoicerequests">> ->
			       <<"allow_voice_requests">>;
			   <<"muc#roomconfig_allow_subscription">> ->
			       <<"allow_subscription">>;
			   <<"muc#roomconfig_voicerequestmininterval">> ->
			       <<"voice_request_min_interval">>;
			   <<"muc#roomconfig_captcha_whitelist">> ->
			       <<"captcha_whitelist">>;
			   <<"muc#roomconfig_mam">> ->
			       <<"mam">>;
			   _ ->
			       Var
		       end,
	      F#xdata_field{var = NewVar}
      end, Fs).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Owner stuff
-spec process_iq_owner(jid(), iq(), state()) ->
			      {result, undefined | muc_owner()} |
			      {result, undefined | muc_owner(), state() | stop} |
			      {error, stanza_error()}.
process_iq_owner(From, #iq{type = set, lang = Lang,
			   sub_els = [#muc_owner{destroy = Destroy,
						 config = Config,
						 items = Items}]},
		 StateData) ->
    FAffiliation = get_affiliation(From, StateData),
    if FAffiliation /= owner ->
	    ErrText = ?T("Owner privileges required"),
	    {error, xmpp:err_forbidden(ErrText, Lang)};
       Destroy /= undefined, Config == undefined, Items == [] ->
	    ?INFO_MSG("Destroyed MUC room ~ts by the owner ~ts",
		      [jid:encode(StateData#state.jid), jid:encode(From)]),
	    add_to_log(room_existence, destroyed, StateData),
	    destroy_room(Destroy, StateData);
       Config /= undefined, Destroy == undefined, Items == [] ->
	    case Config of
		#xdata{type = cancel} ->
		    {result, undefined};
		#xdata{type = submit, fields = Fs} ->
		    Fs1 = convert_legacy_fields(Fs),
		    try muc_roomconfig:decode(Fs1) of
			Options ->
			    case is_allowed_log_change(Options, StateData, From) andalso
				is_allowed_persistent_change(Options, StateData, From) andalso
				is_allowed_mam_change(Options, StateData, From) andalso
				is_allowed_string_limits(Options, StateData) andalso
				is_password_settings_correct(Options, StateData) of
				true ->
				    set_config(Options, StateData, Lang);
				false ->
				    {error, xmpp:err_not_acceptable()}
			    end
		    catch _:{muc_roomconfig, Why} ->
			    Txt = muc_roomconfig:format_error(Why),
			    {error, xmpp:err_bad_request(Txt, Lang)}
		    end;
		_ ->
		    Txt = ?T("Incorrect data form"),
		    {error, xmpp:err_bad_request(Txt, Lang)}
	    end;
       Items /= [], Config == undefined, Destroy == undefined ->
	    process_admin_items_set(From, Items, Lang, StateData);
       true ->
	    {error, xmpp:err_bad_request()}
    end;
process_iq_owner(From, #iq{type = get, lang = Lang,
			   sub_els = [#muc_owner{destroy = Destroy,
						 config = Config,
						 items = Items}]},
		 StateData) ->
    FAffiliation = get_affiliation(From, StateData),
    if FAffiliation /= owner ->
	    ErrText = ?T("Owner privileges required"),
	    {error, xmpp:err_forbidden(ErrText, Lang)};
       Destroy == undefined, Config == undefined ->
	    case Items of
		[] ->
		    {result,
		     #muc_owner{config = get_config(Lang, StateData, From)}};
		[#muc_item{affiliation = undefined}] ->
		    Txt = ?T("No 'affiliation' attribute found"),
		    {error, xmpp:err_bad_request(Txt, Lang)};
		[#muc_item{affiliation = Affiliation}] ->
		    Items = items_with_affiliation(Affiliation, StateData),
		    {result, #muc_owner{items = Items}};
		[_|_] ->
		    Txt = ?T("Too many <item/> elements"),
		    {error, xmpp:err_bad_request(Txt, Lang)}
	    end;
       true ->
	    {error, xmpp:err_bad_request()}
    end.

-spec is_allowed_log_change(muc_roomconfig:result(), state(), jid()) -> boolean().
is_allowed_log_change(Options, StateData, From) ->
    case proplists:is_defined(enablelogging, Options) of
	false -> true;
	true ->
	    allow ==
		ejabberd_hooks:run_fold(muc_log_check_access_log,
                                        StateData#state.server_host,
                                        deny,
                                        [StateData#state.server_host, From])
    end.

-spec is_allowed_persistent_change(muc_roomconfig:result(), state(), jid()) -> boolean().
is_allowed_persistent_change(Options, StateData, From) ->
    case proplists:is_defined(persistentroom, Options) of
      false -> true;
      true ->
	  {_AccessRoute, _AccessCreate, _AccessAdmin,
	   AccessPersistent, _AccessMam} =
	      StateData#state.access,
	  allow ==
	    acl:match_rule(StateData#state.server_host,
			   AccessPersistent, From)
    end.

-spec is_allowed_mam_change(muc_roomconfig:result(), state(), jid()) -> boolean().
is_allowed_mam_change(Options, StateData, From) ->
    case proplists:is_defined(mam, Options) of
      false -> true;
      true ->
	  {_AccessRoute, _AccessCreate, _AccessAdmin,
	   _AccessPersistent, AccessMam} =
	      StateData#state.access,
	  allow ==
	    acl:match_rule(StateData#state.server_host,
			   AccessMam, From)
    end.

%% Check if the string fields defined in the Data Form
%% are conformant to the configured limits
-spec is_allowed_string_limits(muc_roomconfig:result(), state()) -> boolean().
is_allowed_string_limits(Options, StateData) ->
    RoomName = proplists:get_value(roomname, Options, <<"">>),
    RoomDesc = proplists:get_value(roomdesc, Options, <<"">>),
    Password = proplists:get_value(roomsecret, Options, <<"">>),
    CaptchaWhitelist = proplists:get_value(captcha_whitelist, Options, []),
    CaptchaWhitelistSize = lists:foldl(
      fun(Jid, Sum) -> byte_size(jid:encode(Jid)) + Sum end,
      0, CaptchaWhitelist),
    MaxRoomName = mod_muc_opt:max_room_name(StateData#state.server_host),
    MaxRoomDesc = mod_muc_opt:max_room_desc(StateData#state.server_host),
    MaxPassword = mod_muc_opt:max_password(StateData#state.server_host),
    MaxCaptchaWhitelist = mod_muc_opt:max_captcha_whitelist(StateData#state.server_host),
    (byte_size(RoomName) =< MaxRoomName)
    andalso (byte_size(RoomDesc) =< MaxRoomDesc)
    andalso (byte_size(Password) =< MaxPassword)
    andalso (CaptchaWhitelistSize =< MaxCaptchaWhitelist).

%% Return false if:
%% "the password for a password-protected room is blank"
-spec is_password_settings_correct(muc_roomconfig:result(), state()) -> boolean().
is_password_settings_correct(Options, StateData) ->
    Config = StateData#state.config,
    OldProtected = Config#config.password_protected,
    OldPassword = Config#config.password,
    NewProtected = proplists:get_value(passwordprotectedroom, Options),
    NewPassword = proplists:get_value(roomsecret, Options),
    case {OldProtected, NewProtected, OldPassword, NewPassword} of
	{true, undefined, <<"">>, undefined} -> false;
	{true, undefined, _, <<"">>} -> false;
	{_, true, <<"">>, undefined} -> false;
	{_, true, _, <<"">>} -> false;
	_ -> true
    end.

-spec get_default_room_maxusers(state()) -> non_neg_integer().
get_default_room_maxusers(RoomState) ->
    DefRoomOpts =
	mod_muc_opt:default_room_options(RoomState#state.server_host),
    RoomState2 = set_opts(DefRoomOpts, RoomState),
    (RoomState2#state.config)#config.max_users.

-spec get_config(binary(), state(), jid()) -> xdata().
get_config(Lang, StateData, From) ->
    {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent, _AccessMam} =
	StateData#state.access,
    ServiceMaxUsers = get_service_max_users(StateData),
    DefaultRoomMaxUsers = get_default_room_maxusers(StateData),
    Config = StateData#state.config,
    MaxUsersRoom = get_max_users(StateData),
    Title = str:translate_and_format(
	      Lang, ?T("Configuration of room ~s"),
	      [jid:encode(StateData#state.jid)]),
    Fs = [{roomname, Config#config.title},
	  {roomdesc, Config#config.description},
	  {lang, Config#config.lang}] ++
	case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of
	    allow -> [{persistentroom, Config#config.persistent}];
	    deny -> []
	end ++
	[{publicroom, Config#config.public},
	 {public_list, Config#config.public_list},
	 {passwordprotectedroom, Config#config.password_protected},
	 {roomsecret, case Config#config.password_protected of
			  true -> Config#config.password;
			  false -> <<"">>
		      end},
	 {maxusers, MaxUsersRoom,
	  [if is_integer(ServiceMaxUsers) -> [];
	      true -> [{?T("No limit"), <<"none">>}]
	   end] ++ [{integer_to_binary(N), N}
		    || N <- lists:usort([ServiceMaxUsers,
					 DefaultRoomMaxUsers,
					 MaxUsersRoom
					 | ?MAX_USERS_DEFAULT_LIST]),
		       N =< ServiceMaxUsers]},
	 {whois, if Config#config.anonymous -> moderators;
		    true -> anyone
		 end},
	 {presencebroadcast, Config#config.presence_broadcast},
	 {membersonly, Config#config.members_only},
	 {moderatedroom, Config#config.moderated},
	 {members_by_default, Config#config.members_by_default},
	 {changesubject, Config#config.allow_change_subj},
	 {allowpm, Config#config.allowpm},
	 {allow_private_messages_from_visitors,
	  Config#config.allow_private_messages_from_visitors},
	 {allow_query_users, Config#config.allow_query_users},
	 {allowinvites, Config#config.allow_user_invites},
	 {allow_visitor_status, Config#config.allow_visitor_status},
	 {allow_visitor_nickchange, Config#config.allow_visitor_nickchange},
	 {allow_voice_requests, Config#config.allow_voice_requests},
	 {allow_subscription, Config#config.allow_subscription},
	 {voice_request_min_interval, Config#config.voice_request_min_interval},
	 {pubsub, Config#config.pubsub},
	 {enable_hats, Config#config.enable_hats}]
	++
	case ejabberd_captcha:is_feature_available() of
	    true ->
		[{captcha_protected, Config#config.captcha_protected},
		 {captcha_whitelist,
		  lists:map(
		    fun jid:make/1,
		    ?SETS:to_list(Config#config.captcha_whitelist))}];
	    false ->
		[]
	end
	++
        case ejabberd_hooks:run_fold(muc_log_check_access_log,
                                     StateData#state.server_host,
                                     deny,
                                     [StateData#state.server_host, From]) of
	    allow -> [{enablelogging, Config#config.logging}];
	    deny -> []
	end,
    Fields = ejabberd_hooks:run_fold(get_room_config,
				     StateData#state.server_host,
				     Fs,
				     [StateData, From, Lang]),
    #xdata{type = form, title = Title,
	   fields = muc_roomconfig:encode(Fields, Lang)}.

-spec set_config(muc_roomconfig:result(), state(), binary()) ->
			{error, stanza_error()} | {result, undefined, state()}.
set_config(Options, StateData, Lang) ->
    try
	#config{} = Config = set_config(Options, StateData#state.config,
					StateData#state.server_host, Lang),
	{result, _, NSD} = Res = change_config(Config, StateData),
	Type = case {(StateData#state.config)#config.logging,
		     Config#config.logging}
	       of
		   {true, false} -> roomconfig_change_disabledlogging;
		   {false, true} -> roomconfig_change_enabledlogging;
		   {_, _} -> roomconfig_change
	       end,
	Users = [{U#user.jid, U#user.nick, U#user.role}
		 || U <- maps:values(StateData#state.users)],
	add_to_log(Type, Users, NSD),
	Res
    catch  _:{badmatch, {error, #stanza_error{}} = Err} ->
	    Err
    end.

-spec get_config_opt_name(pos_integer()) -> atom().
get_config_opt_name(Pos) ->
    Fs = [config|record_info(fields, config)],
    lists:nth(Pos, Fs).

-spec set_config([muc_roomconfig:property()], #config{},
		  binary(), binary()) -> #config{} | {error, stanza_error()}.
set_config(Opts, Config, ServerHost, Lang) ->
    lists:foldl(
      fun(_, {error, _} = Err) -> Err;
	 ({roomname, Title}, C) -> C#config{title = Title};
	 ({roomdesc, Desc}, C) -> C#config{description = Desc};
	 ({changesubject, V}, C) -> C#config{allow_change_subj = V};
	 ({allow_query_users, V}, C) -> C#config{allow_query_users = V};
	 ({allowpm, V}, C) ->
	      C#config{allowpm = V};
	 ({allow_private_messages_from_visitors, V}, C) ->
	      C#config{allow_private_messages_from_visitors = V};
	 ({allow_visitor_status, V}, C) -> C#config{allow_visitor_status = V};
	 ({allow_visitor_nickchange, V}, C) ->
	      C#config{allow_visitor_nickchange = V};
	 ({publicroom, V}, C) -> C#config{public = V};
	 ({public_list, V}, C) -> C#config{public_list = V};
	 ({persistentroom, V}, C) -> C#config{persistent = V};
	 ({moderatedroom, V}, C) -> C#config{moderated = V};
	 ({members_by_default, V}, C) -> C#config{members_by_default = V};
	 ({membersonly, V}, C) -> C#config{members_only = V};
	 ({captcha_protected, V}, C) -> C#config{captcha_protected = V};
	 ({allowinvites, V}, C) -> C#config{allow_user_invites = V};
	 ({allow_subscription, V}, C) -> C#config{allow_subscription = V};
	 ({passwordprotectedroom, V}, C) -> C#config{password_protected = V};
	 ({roomsecret, V}, C) -> C#config{password = V};
	 ({anonymous, V}, C) -> C#config{anonymous = V};
	 ({presencebroadcast, V}, C) -> C#config{presence_broadcast = V};
	 ({allow_voice_requests, V}, C) -> C#config{allow_voice_requests = V};
	 ({voice_request_min_interval, V}, C) ->
	      C#config{voice_request_min_interval = V};
	 ({whois, moderators}, C) -> C#config{anonymous = true};
	 ({whois, anyone}, C) -> C#config{anonymous = false};
	 ({maxusers, V}, C) -> C#config{max_users = V};
	 ({enablelogging, V}, C) -> C#config{logging = V};
	 ({pubsub, V}, C) -> C#config{pubsub = V};
	 ({enable_hats, V}, C) -> C#config{enable_hats = V};
	 ({lang, L}, C) -> C#config{lang = L};
	 ({captcha_whitelist, Js}, C) ->
	      LJIDs = [jid:tolower(J) || J <- Js],
	      C#config{captcha_whitelist = ?SETS:from_list(LJIDs)};
	 ({O, V} = Opt, C) ->
	      case ejabberd_hooks:run_fold(set_room_option,
					   ServerHost,
					   {0, undefined},
					   [Opt, Lang]) of
		  {0, undefined} ->
		      ?ERROR_MSG("set_room_option hook failed for "
				 "option '~ts' with value ~p", [O, V]),
		      Txt = {?T("Failed to process option '~s'"), [O]},
		      {error, xmpp:err_internal_server_error(Txt, Lang)};
		  {Pos, Val} ->
		      setelement(Pos, C, Val)
	      end
      end, Config, Opts).

-spec change_config(#config{}, state()) -> {result, undefined, state()}.
change_config(Config, StateData) ->
    send_config_change_info(Config, StateData),
    StateData0 = StateData#state{config = Config},
    StateData1 = remove_subscriptions(StateData0),
    StateData2 =
        case {(StateData#state.config)#config.persistent,
              Config#config.persistent} of
            {WasPersistent, true} ->
                if not WasPersistent ->
                        set_affiliations(StateData1#state.affiliations,
                                         StateData1);
                   true ->
                        ok
                end,
                store_room(StateData1),
                StateData1;
            {true, false} ->
		Affiliations = get_affiliations(StateData),
		maybe_forget_room(StateData),
		StateData1#state{affiliations = Affiliations};
	    _ ->
		StateData1
        end,
    case {(StateData#state.config)#config.members_only,
	  Config#config.members_only} of
        {false, true} ->
            StateData3 = remove_nonmembers(StateData2),
            {result, undefined, StateData3};
        _ ->
            {result, undefined, StateData2}
    end.

-spec send_config_change_info(#config{}, state()) -> ok.
send_config_change_info(Config, #state{config = Config}) -> ok;
send_config_change_info(New, #state{config = Old} = StateData) ->
    Codes = case {Old#config.logging, New#config.logging} of
	      {false, true} -> [170];
	      {true, false} -> [171];
	      _ -> []
	    end
	      ++
	      case {Old#config.anonymous, New#config.anonymous} of
		{true, false} -> [172];
		{false, true} -> [173];
		_ -> []
	      end
		++
		case Old#config{anonymous = New#config.anonymous,
				logging = New#config.logging} of
		  New -> [];
		  _ -> [104]
		end,
    if Codes /= [] ->
	    maps:fold(
	      fun(_LJID, #user{jid = JID}, _) ->
		      advertise_entity_capabilities(JID, StateData#state{config = New})
	      end, ok, StateData#state.users),
	    Message = #message{type = groupchat,
			       id = p1_rand:get_string(),
			       sub_els = [#muc_user{status_codes = Codes}]},
	    send_wrapped_multiple(StateData#state.jid,
				  get_users_and_subscribers_with_node(
                                    ?NS_MUCSUB_NODES_CONFIG, StateData),
				  Message,
				  ?NS_MUCSUB_NODES_CONFIG,
				  StateData);
       true ->
	    ok
    end.

-spec remove_nonmembers(state()) -> state().
remove_nonmembers(StateData) ->
    maps:fold(
      fun(_LJID, #user{jid = JID}, SD) ->
	      Affiliation = get_affiliation(JID, SD),
	      case Affiliation of
		  none ->
		      catch send_kickban_presence(undefined, JID, <<"">>, 322, SD),
		      set_role(JID, none, SD);
		  _ -> SD
	      end
      end, StateData, get_users_and_subscribers(StateData)).

-spec set_opts([{atom(), any()}], state()) -> state().
set_opts(Opts, StateData) ->
    case lists:keytake(persistent, 1, Opts) of
	false ->
	    set_opts2(Opts, StateData);
	{value, Tuple, Rest} ->
	    set_opts2([Tuple | Rest], StateData)
    end.

-spec set_opts2([{atom(), any()}], state()) -> state().
set_opts2([], StateData) ->
    set_vcard_xupdate(StateData);
set_opts2([{vcard, Val} | Opts], StateData)
  when is_record(Val, vcard_temp) ->
    %% default_room_options is setting a default room vcard
    ValRaw = fxml:element_to_binary(xmpp:encode(Val)),
    set_opts2([{vcard, ValRaw} | Opts], StateData);
set_opts2([{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}};
	    allowpm ->
		StateData#state{config =
				    (StateData#state.config)#config{allowpm
									= Val}};
	    allow_private_messages_from_visitors ->
		StateData#state{config =
				    (StateData#state.config)#config{allow_private_messages_from_visitors
									= Val}};
	    allow_visitor_nickchange ->
		StateData#state{config =
				    (StateData#state.config)#config{allow_visitor_nickchange
									= Val}};
	    allow_visitor_status ->
		StateData#state{config =
				    (StateData#state.config)#config{allow_visitor_status
									= Val}};
	    public ->
		StateData#state{config =
				    (StateData#state.config)#config{public =
									Val}};
	    public_list ->
		StateData#state{config =
				    (StateData#state.config)#config{public_list
									= Val}};
	    persistent ->
		StateData#state{config =
				    (StateData#state.config)#config{persistent =
									Val}};
	    moderated ->
		StateData#state{config =
				    (StateData#state.config)#config{moderated =
									Val}};
	    members_by_default ->
		StateData#state{config =
				    (StateData#state.config)#config{members_by_default
									= Val}};
	    members_only ->
		StateData#state{config =
				    (StateData#state.config)#config{members_only
									= Val}};
	    allow_user_invites ->
		StateData#state{config =
				    (StateData#state.config)#config{allow_user_invites
									= Val}};
	    password_protected ->
		StateData#state{config =
				    (StateData#state.config)#config{password_protected
									= Val}};
	    captcha_protected ->
		StateData#state{config =
				    (StateData#state.config)#config{captcha_protected
									= Val}};
	    password ->
		StateData#state{config =
				    (StateData#state.config)#config{password =
									Val}};
	    anonymous ->
		StateData#state{config =
				    (StateData#state.config)#config{anonymous =
									Val}};
	    presence_broadcast ->
		StateData#state{config =
				    (StateData#state.config)#config{presence_broadcast =
									Val}};
	    logging ->
		StateData#state{config =
				    (StateData#state.config)#config{logging =
									Val}};
	    mam ->
		StateData#state{config =
				    (StateData#state.config)#config{mam = Val}};
	    captcha_whitelist ->
		StateData#state{config =
				    (StateData#state.config)#config{captcha_whitelist
									=
									(?SETS):from_list(Val)}};
	    allow_voice_requests ->
		StateData#state{config =
				    (StateData#state.config)#config{allow_voice_requests
									= Val}};
	    voice_request_min_interval ->
		StateData#state{config =
				    (StateData#state.config)#config{voice_request_min_interval
									= Val}};
	    max_users ->
		ServiceMaxUsers = get_service_max_users(StateData),
		MaxUsers = if Val =< ServiceMaxUsers -> Val;
			      true -> ServiceMaxUsers
			   end,
		StateData#state{config =
				    (StateData#state.config)#config{max_users =
									MaxUsers}};
	    vcard ->
		StateData#state{config =
				    (StateData#state.config)#config{vcard =
									Val}};
	    vcard_xupdate ->
		StateData#state{config =
				    (StateData#state.config)#config{vcard_xupdate =
									Val}};
	    pubsub ->
		StateData#state{config =
				    (StateData#state.config)#config{pubsub = Val}};
	    allow_subscription ->
		StateData#state{config =
				    (StateData#state.config)#config{allow_subscription = Val}};
            enable_hats ->
                StateData#state{config =
                                    (StateData#state.config)#config{enable_hats = Val}};
	    lang ->
		StateData#state{config =
				    (StateData#state.config)#config{lang = Val}};
	    subscribers ->
                  MUCSubscribers =
                      lists:foldl(
                        fun({JID, Nick, Nodes}, MUCSubs) ->
                                BareJID =
                                    case JID of
                                        #jid{} -> jid:remove_resource(JID);
                                        _ ->
                                            ?ERROR_MSG("Invalid subscriber JID in set_opts ~p", [JID]),
                                            jid:remove_resource(jid:make(JID))
                                    end,
                                muc_subscribers_put(
                                  #subscriber{jid = BareJID,
                                              nick = Nick,
                                              nodes = Nodes},
                                  MUCSubs)
                        end, muc_subscribers_new(), Val),
                  StateData#state{muc_subscribers = MUCSubscribers};
	    affiliations ->
		set_affiliations(maps:from_list(Val), StateData);
	    roles ->
		StateData#state{roles = maps:from_list(Val)};
	    subject ->
		  Subj = if Val == <<"">> -> [];
			    is_binary(Val) -> [#text{data = Val}];
			    is_list(Val) -> Val
			 end,
		  StateData#state{subject = Subj};
	    subject_author when is_tuple(Val) ->
                  StateData#state{subject_author = Val};
	    subject_author when is_binary(Val) -> % ejabberd 23.04 or older
                  StateData#state{subject_author = {Val, #jid{}}};
            hats_users ->
                  Hats = maps:from_list(
                           lists:map(fun({U, H}) -> {U, maps:from_list(H)} end,
                                     Val)),
                  StateData#state{hats_users = Hats};
	    hibernation_time -> StateData;
	    Other ->
                  ?INFO_MSG("Unknown MUC room option, will be discarded: ~p", [Other]),
                  StateData
	  end,
    set_opts2(Opts, NSD).

-spec set_vcard_xupdate(state()) -> state().
set_vcard_xupdate(#state{config =
			     #config{vcard = VCardRaw,
				     vcard_xupdate = undefined} = Config} = State)
  when VCardRaw /= <<"">> ->
    case fxml_stream:parse_element(VCardRaw) of
	{error, _} ->
	    State;
	El ->
	    Hash = mod_vcard_xupdate:compute_hash(El),
	    State#state{config = Config#config{vcard_xupdate = Hash}}
    end;
set_vcard_xupdate(State) ->
    State.

get_occupant_initial_role(Jid, Affiliation, #state{roles = Roles} = StateData) ->
    DefaultRole = get_default_role(Affiliation, StateData),
    case (StateData#state.config)#config.moderated of
        true ->
            get_occupant_stored_role(Jid, Roles, DefaultRole);
        false ->
            DefaultRole
    end.

get_occupant_stored_role(Jid, Roles, DefaultRole) ->
    maps:get(jid:split(jid:remove_resource(Jid)), Roles, DefaultRole).

-define(MAKE_CONFIG_OPT(Opt),
	{get_config_opt_name(Opt), element(Opt, Config)}).

-spec make_opts(state(), boolean()) -> [{atom(), any()}].
make_opts(StateData, Hibernation) ->
    Config = StateData#state.config,
    Subscribers = muc_subscribers_fold(
		    fun(_LJID, Sub, Acc) ->
			    [{Sub#subscriber.jid,
			      Sub#subscriber.nick,
			      Sub#subscriber.nodes}|Acc]
		    end, [], StateData#state.muc_subscribers),
    [?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description),
     ?MAKE_CONFIG_OPT(#config.allow_change_subj),
     ?MAKE_CONFIG_OPT(#config.allow_query_users),
     ?MAKE_CONFIG_OPT(#config.allowpm),
     ?MAKE_CONFIG_OPT(#config.allow_private_messages_from_visitors),
     ?MAKE_CONFIG_OPT(#config.allow_visitor_status),
     ?MAKE_CONFIG_OPT(#config.allow_visitor_nickchange),
     ?MAKE_CONFIG_OPT(#config.public), ?MAKE_CONFIG_OPT(#config.public_list),
     ?MAKE_CONFIG_OPT(#config.persistent),
     ?MAKE_CONFIG_OPT(#config.moderated),
     ?MAKE_CONFIG_OPT(#config.members_by_default),
     ?MAKE_CONFIG_OPT(#config.members_only),
     ?MAKE_CONFIG_OPT(#config.allow_user_invites),
     ?MAKE_CONFIG_OPT(#config.password_protected),
     ?MAKE_CONFIG_OPT(#config.captcha_protected),
     ?MAKE_CONFIG_OPT(#config.password), ?MAKE_CONFIG_OPT(#config.anonymous),
     ?MAKE_CONFIG_OPT(#config.logging), ?MAKE_CONFIG_OPT(#config.max_users),
     ?MAKE_CONFIG_OPT(#config.allow_voice_requests),
     ?MAKE_CONFIG_OPT(#config.allow_subscription),
     ?MAKE_CONFIG_OPT(#config.mam),
     ?MAKE_CONFIG_OPT(#config.presence_broadcast),
     ?MAKE_CONFIG_OPT(#config.voice_request_min_interval),
     ?MAKE_CONFIG_OPT(#config.vcard),
     ?MAKE_CONFIG_OPT(#config.vcard_xupdate),
     ?MAKE_CONFIG_OPT(#config.pubsub),
     ?MAKE_CONFIG_OPT(#config.enable_hats),
     ?MAKE_CONFIG_OPT(#config.lang),
     {captcha_whitelist,
      (?SETS):to_list((StateData#state.config)#config.captcha_whitelist)},
     {affiliations,
      maps:to_list(StateData#state.affiliations)},
     {roles, maps:to_list(StateData#state.roles)},
     {subject, StateData#state.subject},
     {subject_author, StateData#state.subject_author},
     {hats_users,
      lists:map(fun({U, H}) -> {U, maps:to_list(H)} end,
                maps:to_list(StateData#state.hats_users))},
     {hibernation_time, if Hibernation -> erlang:system_time(microsecond); true -> undefined end},
     {subscribers, Subscribers}].

expand_opts(CompactOpts) ->
    DefConfig = #config{},
    Fields = record_info(fields, config),
    {_, Opts1} =
        lists:foldl(
          fun(Field, {Pos, Opts}) ->
                  case lists:keyfind(Field, 1, CompactOpts) of
                      false ->
                          DefV = element(Pos, DefConfig),
                          DefVal = case (?SETS):is_set(DefV) of
                                       true -> (?SETS):to_list(DefV);
                                       false -> DefV
                                   end,
                          {Pos+1, [{Field, DefVal}|Opts]};
                      {_, Val} ->
                          {Pos+1, [{Field, Val}|Opts]}
                  end
          end, {2, []}, Fields),
    SubjectAuthor = proplists:get_value(subject_author, CompactOpts, {<<"">>, #jid{}}),
    Subject = proplists:get_value(subject, CompactOpts, <<"">>),
    Subscribers = proplists:get_value(subscribers, CompactOpts, []),
    HibernationTime = proplists:get_value(hibernation_time, CompactOpts, 0),
    [{subject, Subject},
     {subject_author, SubjectAuthor},
     {subscribers, Subscribers},
     {hibernation_time, HibernationTime}
     | lists:reverse(Opts1)].

config_fields() ->
    [subject, subject_author, subscribers, hibernate_time | record_info(fields, config)].

-spec destroy_room(muc_destroy(), state()) -> {result, undefined, stop}.
destroy_room(DEl, StateData) ->
    Destroy = DEl#muc_destroy{xmlns = ?NS_MUC_USER},
    maps:fold(
      fun(_LJID, Info, _) ->
	      Nick = Info#user.nick,
	      Item = #muc_item{affiliation = none,
			       role = none},
	      Packet = #presence{
			  type = unavailable,
			  sub_els = [#muc_user{items = [Item],
					       destroy = Destroy}]},
	      send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
			   Info#user.jid, Packet,
			   ?NS_MUCSUB_NODES_CONFIG, StateData)
      end, ok, get_users_and_subscribers_with_node(
                 ?NS_MUCSUB_NODES_CONFIG, StateData)),
    forget_room(StateData),
    {result, undefined, stop}.

-spec forget_room(state()) -> state().
forget_room(StateData) ->
    mod_muc:forget_room(StateData#state.server_host,
			StateData#state.host,
			StateData#state.room),
    StateData.

-spec maybe_forget_room(state()) -> state().
maybe_forget_room(StateData) ->
    Forget = case (StateData#state.config)#config.persistent of
		 true ->
		     true;
		 _ ->
		     Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc),
		     erlang:function_exported(Mod, get_subscribed_rooms, 3)
	     end,
    case Forget of
	true ->
	    forget_room(StateData);
	_ ->
	    StateData
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Disco

-define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse),
	case Opt of
	  true -> Fiftrue;
	  false -> Fiffalse
	end).

-spec make_disco_info(jid(), state()) -> disco_info().
make_disco_info(From, StateData) ->
    Config = StateData#state.config,
    ServerHost = StateData#state.server_host,
    AccessRegister = mod_muc_opt:access_register(ServerHost),
    Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
             ?NS_COMMANDS,
             ?NS_MESSAGE_MODERATE_0, ?NS_MESSAGE_MODERATE_1,
             ?NS_MESSAGE_RETRACT,
	     ?CONFIG_OPT_TO_FEATURE((Config#config.public),
				    <<"muc_public">>, <<"muc_hidden">>),
	     ?CONFIG_OPT_TO_FEATURE((Config#config.persistent),
				    <<"muc_persistent">>, <<"muc_temporary">>),
	     ?CONFIG_OPT_TO_FEATURE((Config#config.members_only),
				    <<"muc_membersonly">>, <<"muc_open">>),
	     ?CONFIG_OPT_TO_FEATURE((Config#config.anonymous),
				    <<"muc_semianonymous">>, <<"muc_nonanonymous">>),
	     ?CONFIG_OPT_TO_FEATURE((Config#config.moderated),
				    <<"muc_moderated">>, <<"muc_unmoderated">>),
	     ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected),
				    <<"muc_passwordprotected">>, <<"muc_unsecured">>)]
	++ case acl:match_rule(ServerHost, AccessRegister, From) of
	       allow -> [?NS_REGISTER];
	       deny -> []
	   end
	++ case Config#config.allow_subscription of
	       true -> [?NS_MUCSUB];
	       false -> []
	   end
	++ case gen_mod:is_loaded(StateData#state.server_host, mod_muc_occupantid) of
	       true ->
		   [?NS_OCCUPANT_ID];
	       _ ->
		   []
	   end
	++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam),
		 Config#config.mam} of
	       {true, true} ->
		   [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0];
	       _ ->
		   []
	   end,
    #disco_info{identities = [#identity{category = <<"conference">>,
					type = <<"text">>,
					name = (StateData#state.config)#config.title}],
		features = Feats}.

-spec process_iq_disco_info(jid(), iq(), state()) ->
				   {result, disco_info()} | {error, stanza_error()}.
process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) ->
    Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
    {error, xmpp:err_not_allowed(Txt, Lang)};
process_iq_disco_info(From, #iq{type = get, lang = Lang,
				sub_els = [#disco_info{node = <<>>}]},
		      StateData) ->
    DiscoInfo = make_disco_info(From, StateData),
    Extras = iq_disco_info_extras(Lang, StateData, false),
    {result, DiscoInfo#disco_info{xdata = [Extras]}};
process_iq_disco_info(From, #iq{type = get, lang = Lang,
				sub_els = [#disco_info{node = ?NS_COMMANDS}]},
		      StateData) ->
    case (StateData#state.config)#config.enable_hats andalso
        is_admin(From, StateData)
    of
        true ->
            {result,
             #disco_info{
                identities = [#identity{category = <<"automation">>,
                                        type = <<"command-list">>,
                                        name = translate:translate(
                                                 Lang, ?T("Commands"))}]}};
        false ->
            Txt = ?T("Node not found"),
            {error, xmpp:err_item_not_found(Txt, Lang)}
    end;
process_iq_disco_info(From, #iq{type = get, lang = Lang,
				sub_els = [#disco_info{node = ?MUC_HAT_ADD_CMD}]},
		      StateData) ->
    case (StateData#state.config)#config.enable_hats andalso
        is_admin(From, StateData)
    of
        true ->
            {result,
             #disco_info{
                identities = [#identity{category = <<"automation">>,
                                        type = <<"command-node">>,
                                        name = translate:translate(
                                              Lang, ?T("Add a hat to a user"))}],
                features = [?NS_COMMANDS]}};
        false ->
            Txt = ?T("Node not found"),
            {error, xmpp:err_item_not_found(Txt, Lang)}
    end;
process_iq_disco_info(From, #iq{type = get, lang = Lang,
				sub_els = [#disco_info{node = ?MUC_HAT_REMOVE_CMD}]},
		      StateData) ->
    case (StateData#state.config)#config.enable_hats andalso
        is_admin(From, StateData)
    of
        true ->
            {result,
             #disco_info{
                identities = [#identity{category = <<"automation">>,
                                        type = <<"command-node">>,
                                        name = translate:translate(
                                              Lang, ?T("Remove a hat from a user"))}],
                features = [?NS_COMMANDS]}};
        false ->
            Txt = ?T("Node not found"),
            {error, xmpp:err_item_not_found(Txt, Lang)}
    end;
process_iq_disco_info(From, #iq{type = get, lang = Lang,
				sub_els = [#disco_info{node = ?MUC_HAT_LIST_CMD}]},
		      StateData) ->
    case (StateData#state.config)#config.enable_hats andalso
        is_admin(From, StateData)
    of
        true ->
            {result,
             #disco_info{
                identities = [#identity{category = <<"automation">>,
                                        type = <<"command-node">>,
                                        name = translate:translate(
                                              Lang, ?T("List users with hats"))}],
                features = [?NS_COMMANDS]}};
        false ->
            Txt = ?T("Node not found"),
            {error, xmpp:err_item_not_found(Txt, Lang)}
    end;
process_iq_disco_info(From, #iq{type = get, lang = Lang,
				sub_els = [#disco_info{node = Node}]},
		      StateData) ->
    try
	true = mod_caps:is_valid_node(Node),
	DiscoInfo = make_disco_info(From, StateData),
	Extras = iq_disco_info_extras(Lang, StateData, true),
	DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]},
	Hash = mod_caps:compute_disco_hash(DiscoInfo1, sha),
	Node = <<(ejabberd_config:get_uri())/binary, $#, Hash/binary>>,
	{result, DiscoInfo1#disco_info{node = Node}}
    catch _:{badmatch, _} ->
	    Txt = ?T("Invalid node name"),
	    {error, xmpp:err_item_not_found(Txt, Lang)}
    end.

-spec iq_disco_info_extras(binary(), state(), boolean()) -> xdata().
iq_disco_info_extras(Lang, StateData, Static) ->
    Config = StateData#state.config,
    Fs1 = [{roomname, Config#config.title},
	   {description, Config#config.description},
	   {changesubject, Config#config.allow_change_subj},
	   {allowinvites, Config#config.allow_user_invites},
	   {allow_query_users, Config#config.allow_query_users},
	   {allowpm, Config#config.allowpm},
	   {lang, Config#config.lang}],
    Fs2 = case Config#config.pubsub of
	      Node when is_binary(Node), Node /= <<"">> ->
		  [{pubsub, Node}|Fs1];
	      _ ->
		  Fs1
	  end,
    Fs3 = case Static of
	      false ->
		  [{occupants, maps:size(StateData#state.nicks)}|Fs2];
	      true ->
		  Fs2
	  end,
    Fs4 = case Config#config.logging of
	      true ->
		  case ejabberd_hooks:run_fold(muc_log_get_url,
                                               StateData#state.server_host,
                                               error,
                                               [StateData]) of
		      {ok, URL} ->
			  [{logs, URL}|Fs3];
		      error ->
			  Fs3
		  end;
	      false ->
		  Fs3
	  end,
    Fs5 = case (StateData#state.config)#config.vcard_xupdate of
	      Hash when is_binary(Hash) ->
		  [{avatarhash, [Hash]} | Fs4];
	      _ ->
		  Fs4
	  end,
    #xdata{type = result,
	   fields = muc_roominfo:encode(Fs5, Lang)}.

-spec process_iq_disco_items(jid(), iq(), state()) ->
				    {error, stanza_error()} | {result, disco_items()}.
process_iq_disco_items(_From, #iq{type = set, lang = Lang}, _StateData) ->
    Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
    {error, xmpp:err_not_allowed(Txt, Lang)};
process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>>}]},
		       StateData) ->
    case (StateData#state.config)#config.public_list of
      true ->
	  {result, get_mucroom_disco_items(StateData)};
      _ ->
	  case is_occupant_or_admin(From, StateData) of
	    true ->
		{result, get_mucroom_disco_items(StateData)};
	    _ ->
		%% If the list of occupants is private,
		%% the room MUST return an empty <query/> element
		%% (http://xmpp.org/extensions/xep-0045.html#disco-roomitems)
		{result, #disco_items{}}
	  end
    end;
process_iq_disco_items(From, #iq{type = get, lang = Lang,
                                 sub_els = [#disco_items{node = ?NS_COMMANDS}]},
		       StateData) ->
    case (StateData#state.config)#config.enable_hats andalso
        is_admin(From, StateData)
    of
        true ->
            {result,
             #disco_items{
                items = [#disco_item{jid = StateData#state.jid,
                                     node = ?MUC_HAT_ADD_CMD,
                                     name = translate:translate(
                                              Lang, ?T("Add a hat to a user"))},
                         #disco_item{jid = StateData#state.jid,
                                     node = ?MUC_HAT_REMOVE_CMD,
                                     name = translate:translate(
                                              Lang, ?T("Remove a hat from a user"))},
                         #disco_item{jid = StateData#state.jid,
                                     node = ?MUC_HAT_LIST_CMD,
                                     name = translate:translate(
                                              Lang, ?T("List users with hats"))}]}};
        false ->
            Txt = ?T("Node not found"),
            {error, xmpp:err_item_not_found(Txt, Lang)}
    end;
process_iq_disco_items(From, #iq{type = get, lang = Lang,
                                 sub_els = [#disco_items{node = Node}]},
		       StateData)
  when Node == ?MUC_HAT_ADD_CMD;
       Node == ?MUC_HAT_REMOVE_CMD;
       Node == ?MUC_HAT_LIST_CMD ->
    case (StateData#state.config)#config.enable_hats andalso
        is_admin(From, StateData)
    of
        true ->
            {result, #disco_items{}};
        false ->
            Txt = ?T("Node not found"),
            {error, xmpp:err_item_not_found(Txt, Lang)}
    end;
process_iq_disco_items(_From, #iq{lang = Lang}, _StateData) ->
    Txt = ?T("Node not found"),
    {error, xmpp:err_item_not_found(Txt, Lang)}.

-spec process_iq_captcha(jid(), iq(), state()) -> {error, stanza_error()} |
						  {result, undefined}.
process_iq_captcha(_From, #iq{type = get, lang = Lang}, _StateData) ->
    Txt = ?T("Value 'get' of 'type' attribute is not allowed"),
    {error, xmpp:err_not_allowed(Txt, Lang)};
process_iq_captcha(_From, #iq{type = set, lang = Lang, sub_els = [SubEl]},
		   _StateData) ->
    case ejabberd_captcha:process_reply(SubEl) of
      ok -> {result, undefined};
      {error, malformed} ->
	    Txt = ?T("Incorrect CAPTCHA submit"),
	    {error, xmpp:err_bad_request(Txt, Lang)};
      _ ->
	    Txt = ?T("The CAPTCHA verification has failed"),
	    {error, xmpp:err_not_allowed(Txt, Lang)}
    end.

-spec process_iq_vcard(jid(), iq(), state()) ->
			      {result, vcard_temp() | xmlel()} |
			      {result, undefined, state()} |
			      {error, stanza_error()}.
process_iq_vcard(_From, #iq{type = get}, StateData) ->
    #state{config = #config{vcard = VCardRaw}} = StateData,
    case fxml_stream:parse_element(VCardRaw) of
	#xmlel{} = VCard ->
	    {result, VCard};
	{error, _} ->
	    {error, xmpp:err_item_not_found()}
    end;
process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [Pkt]},
		 StateData) ->
    case get_affiliation(From, StateData) of
	owner ->
	    SubEl = xmpp:encode(Pkt),
	    VCardRaw = fxml:element_to_binary(SubEl),
	    Hash = mod_vcard_xupdate:compute_hash(SubEl),
	    Config = StateData#state.config,
	    NewConfig = Config#config{vcard = VCardRaw, vcard_xupdate = Hash},
	    change_config(NewConfig, StateData);
	_ ->
	    ErrText = ?T("Owner privileges required"),
	    {error, xmpp:err_forbidden(ErrText, Lang)}
    end.

-spec process_iq_mucsub(jid(), iq(), state()) ->
      {error, stanza_error()} |
      {result, undefined | muc_subscribe() | muc_subscriptions(), stop | state()} |
      {ignore, state()}.
process_iq_mucsub(_From, #iq{type = set, lang = Lang,
			     sub_els = [#muc_subscribe{}]},
		  #state{just_created = Just, config = #config{allow_subscription = false}}) when Just /= true ->
    {error, xmpp:err_not_allowed(?T("Subscriptions are not allowed"), Lang)};
process_iq_mucsub(From,
		  #iq{type = set, lang = Lang,
		      sub_els = [#muc_subscribe{jid = #jid{} = SubJid} = Mucsub]},
		  StateData) ->
    FAffiliation = get_affiliation(From, StateData),
    FRole = get_role(From, StateData),
    if FRole == moderator; FAffiliation == owner; FAffiliation == admin ->
	    process_iq_mucsub(SubJid,
			      #iq{type = set, lang = Lang,
				  sub_els = [Mucsub#muc_subscribe{jid = undefined}]},
			      StateData);
       true ->
	    Txt = ?T("Moderator privileges required"),
	    {error, xmpp:err_forbidden(Txt, Lang)}
    end;
process_iq_mucsub(From,
		  #iq{type = set, lang = Lang,
		      sub_els = [#muc_subscribe{nick = Nick}]} = Packet,
		  StateData) ->
    LBareJID = jid:tolower(jid:remove_resource(From)),
    try muc_subscribers_get(LBareJID, StateData#state.muc_subscribers) of
	#subscriber{nick = Nick1} when Nick1 /= Nick ->
	    Nodes = get_subscription_nodes(Packet),
	    case nick_collision(From, Nick, StateData) of
                true ->
		    ErrText = ?T("That nickname is already in use by another occupant"),
		    {error, xmpp:err_conflict(ErrText, Lang)};
                false ->
                    case mod_muc:can_use_nick(StateData#state.server_host,
                                              jid:encode(StateData#state.jid),
                                              From, Nick) of
                        false ->
                            Err = case Nick of
                                      <<>> ->
                                          xmpp:err_jid_malformed(
                                            ?T("Nickname can't be empty"),
                                            Lang);
                                      _ ->
                                          xmpp:err_conflict(
                                            ?T("That nickname is registered"
                                               " by another person"), Lang)
                                  end,
                            {error, Err};
                        true ->
                            NewStateData =
                                set_subscriber(From, Nick, Nodes, StateData),
                            {result, subscribe_result(Packet), NewStateData}
                    end
            end;
	#subscriber{} ->
	    Nodes = get_subscription_nodes(Packet),
	    NewStateData = set_subscriber(From, Nick, Nodes, StateData),
	    {result, subscribe_result(Packet), NewStateData}
    catch _:{badkey, _} ->
	    SD2 = StateData#state{config = (StateData#state.config)#config{allow_subscription = true}},
	    add_new_user(From, Nick, Packet, SD2)
    end;
process_iq_mucsub(From, #iq{type = set, lang = Lang,
			    sub_els = [#muc_unsubscribe{jid = #jid{} = UnsubJid}]},
		  StateData) ->
    FAffiliation = get_affiliation(From, StateData),
    FRole = get_role(From, StateData),
    if FRole == moderator; FAffiliation == owner; FAffiliation == admin ->
	    process_iq_mucsub(UnsubJid,
			      #iq{type = set, lang = Lang,
				  sub_els = [#muc_unsubscribe{jid = undefined}]},
			      StateData);
       true ->
	    Txt = ?T("Moderator privileges required"),
	    {error, xmpp:err_forbidden(Txt, Lang)}
    end;
process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]},
		  #state{room = Room, host = Host, server_host = ServerHost} = StateData) ->
    BareJID = jid:remove_resource(From),
    LBareJID = jid:tolower(BareJID),
    try muc_subscribers_remove_exn(LBareJID, StateData#state.muc_subscribers) of
	{MUCSubscribers, #subscriber{nick = Nick}} ->
	    NewStateData = StateData#state{muc_subscribers = MUCSubscribers},
	    store_room(NewStateData, [{del_subscription, LBareJID}]),
	    Packet1a = #message{
		sub_els = [#ps_event{
		    items = #ps_items{
			node = ?NS_MUCSUB_NODES_SUBSCRIBERS,
			items = [#ps_item{
			    id = p1_rand:get_string(),
			    sub_els = [#muc_unsubscribe{jid = BareJID, nick = Nick}]}]}}]},
	    Packet1b = #message{
		sub_els = [#ps_event{
		    items = #ps_items{
			node = ?NS_MUCSUB_NODES_SUBSCRIBERS,
			items = [#ps_item{
			    id = p1_rand:get_string(),
			    sub_els = [#muc_unsubscribe{nick = Nick}]}]}}]},
	    {Packet2a, Packet2b} = ejabberd_hooks:run_fold(muc_unsubscribed, ServerHost, {Packet1a, Packet1b},
							   [ServerHost, Room, Host, BareJID, StateData]),
	    send_subscriptions_change_notifications(Packet2a, Packet2b, StateData),
	    NewStateData2 = case close_room_if_temporary_and_empty(NewStateData) of
		{stop, normal, _} -> stop;
		{next_state, normal_state, SD} -> SD
	    end,
	    {result, undefined, NewStateData2}
	catch _:{badkey, _} ->
	    {result, undefined, StateData}
    end;
process_iq_mucsub(From, #iq{type = get, lang = Lang,
			    sub_els = [#muc_subscriptions{}]},
		  StateData) ->
    FAffiliation = get_affiliation(From, StateData),
    FRole = get_role(From, StateData),
    IsModerator = FRole == moderator orelse FAffiliation == owner orelse
		  FAffiliation == admin,
    case IsModerator orelse is_subscriber(From, StateData) of
	true ->
	    ShowJid = IsModerator orelse
		      (StateData#state.config)#config.anonymous == false,
	    Subs = muc_subscribers_fold(
		     fun(_, #subscriber{jid = J, nick = N, nodes = Nodes}, Acc) ->
			 case ShowJid of
			     true ->
				 [#muc_subscription{jid = J, nick = N, events = Nodes}|Acc];
			     _ ->
				 [#muc_subscription{nick = N, events = Nodes}|Acc]
			 end
		     end, [], StateData#state.muc_subscribers),
	    {result, #muc_subscriptions{list = Subs}, StateData};
	_ ->
	    Txt = ?T("Moderator privileges required"),
	    {error, xmpp:err_forbidden(Txt, Lang)}
    end;
process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) ->
    Txt = ?T("Value 'get' of 'type' attribute is not allowed"),
    {error, xmpp:err_bad_request(Txt, Lang)}.

-spec remove_subscriptions(state()) -> state().
remove_subscriptions(StateData) ->
    if not (StateData#state.config)#config.allow_subscription ->
	    StateData#state{muc_subscribers = muc_subscribers_new()};
       true ->
	    StateData
    end.

-spec get_subscription_nodes(stanza()) -> [binary()].
get_subscription_nodes(#iq{sub_els = [#muc_subscribe{events = Nodes}]}) ->
    lists:filter(
      fun(Node) ->
	      lists:member(Node, [?NS_MUCSUB_NODES_PRESENCE,
				  ?NS_MUCSUB_NODES_MESSAGES,
				  ?NS_MUCSUB_NODES_AFFILIATIONS,
				  ?NS_MUCSUB_NODES_SUBJECT,
				  ?NS_MUCSUB_NODES_CONFIG,
				  ?NS_MUCSUB_NODES_PARTICIPANTS,
				  ?NS_MUCSUB_NODES_SUBSCRIBERS])
      end, Nodes);
get_subscription_nodes(_) ->
    [].

-spec subscribe_result(iq()) -> muc_subscribe().
subscribe_result(#iq{sub_els = [#muc_subscribe{nick = Nick}]} = Packet) ->
    #muc_subscribe{nick = Nick, events = get_subscription_nodes(Packet)}.

-spec get_title(state()) -> binary().
get_title(StateData) ->
    case (StateData#state.config)#config.title of
      <<"">> -> StateData#state.room;
      Name -> Name
    end.

-spec get_roomdesc_reply(jid(), state(), binary()) -> {item, binary()} | false.
get_roomdesc_reply(JID, StateData, Tail) ->
    IsOccupantOrAdmin = is_occupant_or_admin(JID,
					     StateData),
    if (StateData#state.config)#config.public or
	 IsOccupantOrAdmin ->
	   if (StateData#state.config)#config.public_list or
		IsOccupantOrAdmin ->
		  {item, <<(get_title(StateData))/binary,Tail/binary>>};
	      true -> {item, get_title(StateData)}
	   end;
       true -> false
    end.

-spec get_roomdesc_tail(state(), binary()) -> binary().
get_roomdesc_tail(StateData, Lang) ->
    Desc = case (StateData#state.config)#config.public of
	     true -> <<"">>;
	     _ -> translate:translate(Lang, ?T("private, "))
	   end,
    Len = maps:size(StateData#state.nicks),
    <<" (", Desc/binary, (integer_to_binary(Len))/binary, ")">>.

-spec get_mucroom_disco_items(state()) -> disco_items().
get_mucroom_disco_items(StateData) ->
    Items = maps:fold(
	       fun(Nick, _, Acc) ->
		       [#disco_item{jid = jid:make(StateData#state.room,
						   StateData#state.host,
						   Nick),
				    name = Nick}|Acc]
	       end, [], StateData#state.nicks),
    #disco_items{items = Items}.

-spec process_iq_adhoc(jid(), iq(), state()) ->
			      {result, adhoc_command()} |
			      {result, adhoc_command(), state()} |
			      {error, stanza_error()}.
process_iq_adhoc(_From, #iq{type = get}, _StateData) ->
    {error, xmpp:err_bad_request()};
process_iq_adhoc(From, #iq{type = set, lang = Lang1,
                           sub_els = [#adhoc_command{} = Request]},
		 StateData) ->
    % Ad-Hoc Commands are used only for Hats here
    case (StateData#state.config)#config.enable_hats andalso
        is_admin(From, StateData)
    of
        true ->
            #adhoc_command{lang = Lang2, node = Node,
                           action = Action, xdata = XData} = Request,
            Lang = case Lang2 of
                       <<"">> -> Lang1;
                       _ -> Lang2
                   end,
            case {Node, Action} of
                {_, cancel} ->
                    {result,
                     xmpp_util:make_adhoc_response(
                       Request,
                       #adhoc_command{status = canceled, lang = Lang,
                                      node = Node})};
                {?MUC_HAT_ADD_CMD, execute} ->
                    Form =
                        #xdata{
                           title = translate:translate(
                                     Lang, ?T("Add a hat to a user")),
                           type = form,
                           fields =
                               [#xdata_field{
                                   type = 'jid-single',
                                   label = translate:translate(Lang, ?T("Jabber ID")),
                                   required = true,
                                   var = <<"jid">>},
                                #xdata_field{
                                   type = 'text-single',
                                   label = translate:translate(Lang, ?T("Hat title")),
                                   var = <<"hat_title">>},
                                #xdata_field{
                                   type = 'text-single',
                                   label = translate:translate(Lang, ?T("Hat URI")),
                                   required = true,
                                   var = <<"hat_uri">>}
                               ]},
                    {result,
                     xmpp_util:make_adhoc_response(
                       Request,
                       #adhoc_command{
                          status = executing,
                          xdata = Form})};
                {?MUC_HAT_ADD_CMD, complete} when XData /= undefined ->
                    JID = try
                              jid:decode(hd(xmpp_util:get_xdata_values(
                                              <<"jid">>, XData)))
                          catch _:_ -> error
                          end,
                    URI = try
                              hd(xmpp_util:get_xdata_values(
                                   <<"hat_uri">>, XData))
                          catch _:_ -> error
                          end,
                    Title = case xmpp_util:get_xdata_values(
                                   <<"hat_title">>, XData) of
                                [] -> <<"">>;
                                [T] -> T
                            end,
                    if
                        (JID /= error) and (URI /= error) ->
                            case add_hat(JID, URI, Title, StateData) of
                                {ok, NewStateData} ->
                                    store_room(NewStateData),
                                    send_update_presence(
                                      JID, NewStateData, StateData),
                                    {result,
                                     xmpp_util:make_adhoc_response(
                                       Request,
                                       #adhoc_command{status = completed}),
                                     NewStateData};
                                {error, size_limit} ->
                                    Txt = ?T("Hats limit exceeded"),
                                    {error, xmpp:err_not_allowed(Txt, Lang)}
                            end;
                        true ->
                            {error, xmpp:err_bad_request()}
                    end;
                {?MUC_HAT_ADD_CMD, complete} ->
                    {error, xmpp:err_bad_request()};
                {?MUC_HAT_ADD_CMD, _} ->
                    Txt = ?T("Incorrect value of 'action' attribute"),
                    {error, xmpp:err_bad_request(Txt, Lang)};
                {?MUC_HAT_REMOVE_CMD, execute} ->
                    Form =
                        #xdata{
                           title = translate:translate(
                                     Lang, ?T("Remove a hat from a user")),
                           type = form,
                           fields =
                               [#xdata_field{
                                   type = 'jid-single',
                                   label = translate:translate(Lang, ?T("Jabber ID")),
                                   required = true,
                                   var = <<"jid">>},
                                #xdata_field{
                                   type = 'text-single',
                                   label = translate:translate(Lang, ?T("Hat URI")),
                                   required = true,
                                   var = <<"hat_uri">>}
                               ]},
                    {result,
                     xmpp_util:make_adhoc_response(
                       Request,
                       #adhoc_command{
                          status = executing,
                          xdata = Form})};
                {?MUC_HAT_REMOVE_CMD, complete} when XData /= undefined ->
                    JID = try
                              jid:decode(hd(xmpp_util:get_xdata_values(
                                              <<"jid">>, XData)))
                          catch _:_ -> error
                          end,
                    URI = try
                              hd(xmpp_util:get_xdata_values(
                                   <<"hat_uri">>, XData))
                          catch _:_ -> error
                          end,
                    if
                        (JID /= error) and (URI /= error) ->
                            NewStateData = del_hat(JID, URI, StateData),
                            store_room(NewStateData),
                            send_update_presence(
                              JID, NewStateData, StateData),
                            {result,
                             xmpp_util:make_adhoc_response(
                               Request,
                               #adhoc_command{status = completed}),
                             NewStateData};
                        true ->
                            {error, xmpp:err_bad_request()}
                    end;
                {?MUC_HAT_REMOVE_CMD, complete} ->
                    {error, xmpp:err_bad_request()};
                {?MUC_HAT_REMOVE_CMD, _} ->
                    Txt = ?T("Incorrect value of 'action' attribute"),
                    {error, xmpp:err_bad_request(Txt, Lang)};
                {?MUC_HAT_LIST_CMD, execute} ->
                    Hats = get_all_hats(StateData),
                    Items =
                        lists:map(
                          fun({JID, URI, Title}) ->
                                  [#xdata_field{
                                      var = <<"jid">>,
                                      values = [jid:encode(JID)]},
                                   #xdata_field{
                                      var = <<"hat_title">>,
                                      values = [URI]},
                                   #xdata_field{
                                      var = <<"hat_uri">>,
                                      values = [Title]}]
                          end, Hats),
                    Form =
                        #xdata{
                           title = translate:translate(
                                     Lang, ?T("List of users with hats")),
                           type = result,
                           reported =
                               [#xdata_field{
                                   label = translate:translate(Lang, ?T("Jabber ID")),
                                   var = <<"jid">>},
                                #xdata_field{
                                   label = translate:translate(Lang, ?T("Hat title")),
                                   var = <<"hat_title">>},
                                #xdata_field{
                                   label = translate:translate(Lang, ?T("Hat URI")),
                                   var = <<"hat_uri">>}],
                           items = Items},
                    {result,
                     xmpp_util:make_adhoc_response(
                       Request,
                       #adhoc_command{
                          status = completed,
                          xdata = Form})};
                {?MUC_HAT_LIST_CMD, _} ->
                    Txt = ?T("Incorrect value of 'action' attribute"),
                    {error, xmpp:err_bad_request(Txt, Lang)};
                _ ->
                    {error, xmpp:err_item_not_found()}
            end;
	_ ->
	    {error, xmpp:err_forbidden()}
    end.

-spec add_hat(jid(), binary(), binary(), state()) ->
                     {ok, state()} | {error, size_limit}.
add_hat(JID, URI, Title, StateData) ->
    Hats = StateData#state.hats_users,
    LJID = jid:remove_resource(jid:tolower(JID)),
    UserHats = maps:get(LJID, Hats, #{}),
    UserHats2 = maps:put(URI, Title, UserHats),
    USize = maps:size(UserHats2),
    if
        USize =< ?MAX_HATS_PER_USER ->
            Hats2 = maps:put(LJID, UserHats2, Hats),
            Size = maps:size(Hats2),
            if
                Size =< ?MAX_HATS_USERS ->
                    {ok, StateData#state{hats_users = Hats2}};
                true ->
                    {error, size_limit}
            end;
        true ->
            {error, size_limit}
    end.

-spec del_hat(jid(), binary(), state()) -> state().
del_hat(JID, URI, StateData) ->
    Hats = StateData#state.hats_users,
    LJID = jid:remove_resource(jid:tolower(JID)),
    UserHats = maps:get(LJID, Hats, #{}),
    UserHats2 = maps:remove(URI, UserHats),
    Hats2 =
        case maps:size(UserHats2) of
            0 ->
                maps:remove(LJID, Hats);
            _ ->
                maps:put(LJID, UserHats2, Hats)
        end,
    StateData#state{hats_users = Hats2}.

-spec get_all_hats(state()) -> list({jid(), binary(), binary()}).
get_all_hats(StateData) ->
    lists:flatmap(
      fun({LJID, H}) ->
              JID = jid:make(LJID),
              lists:map(fun({URI, Title}) -> {JID, URI, Title} end,
                        maps:to_list(H))
      end,
      maps:to_list(StateData#state.hats_users)).

-spec add_presence_hats(jid(), #presence{}, state()) -> #presence{}.
add_presence_hats(JID, Pres, StateData) ->
    case (StateData#state.config)#config.enable_hats of
        true ->
            Hats = StateData#state.hats_users,
            LJID = jid:remove_resource(jid:tolower(JID)),
            UserHats = maps:get(LJID, Hats, #{}),
            case maps:size(UserHats) of
                0 -> Pres;
                _ ->
                    Items =
                        lists:map(fun({URI, Title}) ->
                                          #muc_hat{uri = URI, title = Title}
                                  end,
                                  maps:to_list(UserHats)),
                    xmpp:set_subtag(Pres,
                                    #muc_hats{hats = Items})
            end;
        false ->
            Pres
    end.

-spec process_iq_moderate(jid(), iq(), binary(), binary() | undefined, state()) ->
    {result, undefined, state()} |
    {error, stanza_error()}.
process_iq_moderate(_From, #iq{type = get}, _Id, _Reason, _StateData) ->
    {error, xmpp:err_bad_request()};
process_iq_moderate(From, #iq{type = set, lang = Lang}, Id, Reason,
		    #state{config = Config, room = Room, host = Host,
                           jid = JID, server_host = Server} = StateData) ->
    FAffiliation = get_affiliation(From, StateData),
    FRole = get_role(From, StateData),
    IsModerator = FRole == moderator orelse FAffiliation == owner orelse
		  FAffiliation == admin,
    case IsModerator of
	false ->
	    {error, xmpp:err_forbidden(
		?T("Only moderators are allowed to retract messages"), Lang)};
	_ ->
	    try binary_to_integer(Id) of
		StanzaId ->
		    case Config#config.mam of
			true ->
			    mod_mam:remove_message_from_archive({Room, Host}, Server, StanzaId);
			_ ->
			    ok
		    end,
		    By = jid:replace_resource(JID, find_nick_by_jid(From, StateData)),
		    Mod21 = #message_moderated_21{by = By,
						  reason = Reason,
						  sub_els = [#message_retract_30{}]},
		    SubEl = [#fasten_apply_to{id = Id,
					      sub_els = [Mod21]},
			     #message_retract{id = Id,
					      reason = Reason,
					      moderated = #message_moderated{by = By}}],
		    Packet0 = #message{type = groupchat,
                                       from = From,
                                       sub_els = SubEl},
	            {FromNick, _Role} = get_participant_data(From, StateData),
                    Packet = ejabberd_hooks:run_fold(muc_filter_message,
						     StateData#state.server_host,
						     xmpp:put_meta(Packet0, mam_ignore, true),
						     [StateData, FromNick]),
		    send_wrapped_multiple(JID,
					  get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData),
					  Packet, ?NS_MUCSUB_NODES_MESSAGES, StateData),
		    NSD = add_message_to_history(<<"">>,
						 StateData#state.jid, Packet, StateData),
		    {result, undefined, remove_from_history(StanzaId, NSD)}
	    catch _:_ ->
		{error, xmpp:err_bad_request(
		    ?T("Stanza id is not valid"), Lang)}
	    end
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Voice request support

-spec prepare_request_form(jid(), binary(), binary()) -> message().
prepare_request_form(Requester, Nick, Lang) ->
    Title = translate:translate(Lang, ?T("Voice request")),
    Instruction = translate:translate(
		    Lang, ?T("Either approve or decline the voice request.")),
    Fs = muc_request:encode([{role, participant},
			     {jid, Requester},
			     {roomnick, Nick},
			     {request_allow, false}],
			    Lang),
    #message{type = normal,
	     sub_els = [#xdata{type = form,
			       title = Title,
			       instructions = [Instruction],
			       fields = Fs}]}.

-spec send_voice_request(jid(), binary(), state()) -> ok.
send_voice_request(From, Lang, StateData) ->
    Moderators = search_role(moderator, StateData),
    FromNick = find_nick_by_jid(From, StateData),
    lists:foreach(
      fun({_, User}) ->
	      ejabberd_router:route(
		xmpp:set_from_to(
		  prepare_request_form(From, FromNick, Lang),
		  StateData#state.jid, User#user.jid))
      end, Moderators).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Invitation support
-spec check_invitation(jid(), [muc_invite()], binary(), state()) ->
			      ok | {error, stanza_error()}.
check_invitation(From, Invitations, Lang, StateData) ->
    FAffiliation = get_affiliation(From, StateData),
    CanInvite = (StateData#state.config)#config.allow_user_invites orelse
	        FAffiliation == admin orelse FAffiliation == owner,
    case CanInvite of
	true ->
	    case lists:all(
		   fun(#muc_invite{to = #jid{}}) -> true;
		      (_) -> false
		   end, Invitations) of
		true ->
		    ok;
		false ->
		    Txt = ?T("No 'to' attribute found in the invitation"),
		    {error, xmpp:err_bad_request(Txt, Lang)}
	    end;
	false ->
	    Txt = ?T("Invitations are not allowed in this conference"),
	    {error, xmpp:err_not_allowed(Txt, Lang)}
    end.

-spec route_invitation(jid(), message(), muc_invite(), binary(), state()) -> jid().
route_invitation(From, Pkt, Invitation, Lang, StateData) ->
    #muc_invite{to = JID, reason = Reason} = Invitation,
    Invite = Invitation#muc_invite{to = undefined, from = From},
    Password = case (StateData#state.config)#config.password_protected of
		   true ->
		       (StateData#state.config)#config.password;
		   false ->
		       undefined
	       end,
    XUser = #muc_user{password = Password, invites = [Invite]},
    XConference = #x_conference{jid = jid:make(StateData#state.room,
					       StateData#state.host),
				reason = Reason},
    Body = iolist_to_binary(
	     [io_lib:format(
		translate:translate(
		  Lang,
		  ?T("~s invites you to the room ~s")),
		[jid:encode(From),
		 jid:encode({StateData#state.room, StateData#state.host, <<"">>})]),
	      case (StateData#state.config)#config.password_protected of
		  true ->
		      <<", ",
			(translate:translate(
			   Lang, ?T("the password is")))/binary,
			" '",
			((StateData#state.config)#config.password)/binary,
			"'">>;
		  _ -> <<"">>
	      end,
	      case Reason of
		  <<"">> -> <<"">>;
		  _ -> <<" (", Reason/binary, ") ">>
	      end]),
    Msg = #message{from = StateData#state.jid,
		   to = JID,
		   type = normal,
		   body = xmpp:mk_text(Body),
		   sub_els = [XUser, XConference]},
    Msg2 = ejabberd_hooks:run_fold(muc_invite,
				   StateData#state.server_host,
				   Msg,
				   [StateData#state.jid, StateData#state.config,
				    From, JID, Reason, Pkt]),
    ejabberd_router:route(Msg2),
    JID.

%% Handle a message sent to the room by a non-participant.
%% If it is a decline, send to the inviter.
%% Otherwise, an error message is sent to the sender.
-spec handle_roommessage_from_nonparticipant(message(), state(), jid()) -> ok.
handle_roommessage_from_nonparticipant(Packet, StateData, From) ->
    try xmpp:try_subtag(Packet, #muc_user{}) of
	#muc_user{decline = #muc_decline{to = #jid{} = To} = Decline} = XUser ->
	    NewDecline = Decline#muc_decline{to = undefined, from = From},
	    NewXUser = XUser#muc_user{decline = NewDecline},
	    NewPacket = xmpp:set_subtag(Packet, NewXUser),
	    ejabberd_router:route(
	      xmpp:set_from_to(NewPacket, StateData#state.jid, To));
	_ ->
	    ErrText = ?T("Only occupants are allowed to send messages "
			 "to the conference"),
	    Err = xmpp:err_not_acceptable(ErrText, xmpp:get_lang(Packet)),
	    ejabberd_router:route_error(Packet, Err)
    catch _:{xmpp_codec, Why} ->
	    Txt = xmpp:io_format_error(Why),
	    Err = xmpp:err_bad_request(Txt, xmpp:get_lang(Packet)),
	    ejabberd_router:route_error(Packet, Err)
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Logging

add_to_log(Type, Data, StateData)
    when Type == roomconfig_change_disabledlogging ->
    ejabberd_hooks:run(muc_log_add,
                       StateData#state.server_host,
                       [StateData#state.server_host,
                        roomconfig_change,
                        Data,
                        StateData#state.jid,
                        make_opts(StateData, false)]);
add_to_log(Type, Data, StateData) ->
    case (StateData#state.config)#config.logging of
      true ->
        ejabberd_hooks:run(muc_log_add,
                           StateData#state.server_host,
                           [StateData#state.server_host,
                            Type,
                            Data,
                            StateData#state.jid,
                            make_opts(StateData, false)]);
      false -> ok
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Users number checking

-spec tab_add_online_user(jid(), state()) -> any().
tab_add_online_user(JID, StateData) ->
    Room = StateData#state.room,
    Host = StateData#state.host,
    ServerHost = StateData#state.server_host,
    ejabberd_hooks:run(join_room, ServerHost, [ServerHost, Room, Host, JID]),
    mod_muc:register_online_user(ServerHost, jid:tolower(JID), Room, Host).

-spec tab_remove_online_user(jid(), state()) -> any().
tab_remove_online_user(JID, StateData) ->
    Room = StateData#state.room,
    Host = StateData#state.host,
    ServerHost = StateData#state.server_host,
    ejabberd_hooks:run(leave_room, ServerHost, [ServerHost, Room, Host, JID]),
    mod_muc:unregister_online_user(ServerHost, jid:tolower(JID), Room, Host).

-spec tab_count_user(jid(), state()) -> non_neg_integer().
tab_count_user(JID, StateData) ->
    ServerHost = StateData#state.server_host,
    {LUser, LServer, _} = jid:tolower(JID),
    mod_muc:count_online_rooms_by_user(ServerHost, LUser, LServer).

-spec element_size(stanza()) -> non_neg_integer().
element_size(El) ->
    byte_size(fxml:element_to_binary(xmpp:encode(El, ?NS_CLIENT))).

-spec store_room(state()) -> ok.
store_room(StateData) ->
    store_room(StateData, []).
store_room(StateData, ChangesHints) ->
    % Let store persistent rooms or on those backends that have get_subscribed_rooms
    Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc),
    HasGSR = erlang:function_exported(Mod, get_subscribed_rooms, 3),
    case HasGSR of
	true ->
	    ok;
	_ ->
	    erlang:put(muc_subscribers, StateData#state.muc_subscribers#muc_subscribers.subscribers)
    end,
    ShouldStore = case (StateData#state.config)#config.persistent of
		      true ->
			  true;
		      _ ->
			  case ChangesHints of
			      [] ->
				  false;
			      _ ->
				  HasGSR
			  end
		  end,
    if ShouldStore ->
            case erlang:function_exported(Mod, store_changes, 4) of
                true when ChangesHints /= [] ->
                    mod_muc:store_changes(
                      StateData#state.server_host,
                      StateData#state.host, StateData#state.room,
                      ChangesHints);
                _ ->
                    store_room_no_checks(StateData, ChangesHints, false),
		    ok
            end;
       true ->
	    ok
    end.

-spec store_room_no_checks(state(), list(), boolean()) -> {atomic, any()}.
store_room_no_checks(StateData, ChangesHints, Hibernation) ->
    mod_muc:store_room(StateData#state.server_host,
		       StateData#state.host, StateData#state.room,
		       make_opts(StateData, Hibernation),
		       ChangesHints).

-spec send_subscriptions_change_notifications(stanza(), stanza(), state()) -> ok.
send_subscriptions_change_notifications(Packet, PacketWithoutJid, State) ->
    {WJ, WN} =
        maps:fold(
          fun(_, #subscriber{jid = JID}, {WithJid, WithNick}) ->
                  case (State#state.config)#config.anonymous == false orelse
                      get_role(JID, State) == moderator orelse
                      get_default_role(get_affiliation(JID, State), State) == moderator of
                      true ->
                          {[JID | WithJid], WithNick};
                      _ ->
                          {WithJid, [JID | WithNick]}
                  end
          end, {[], []},
          muc_subscribers_get_by_node(?NS_MUCSUB_NODES_SUBSCRIBERS,
                                      State#state.muc_subscribers)),
    if WJ /= [] ->
	ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
						  WJ, Packet, false);
	true -> ok
    end,
    if WN /= [] ->
	ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
						  WN, PacketWithoutJid, false);
	true -> ok
    end.

-spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok.
send_wrapped(From, To, Packet, Node, State) ->
    LTo = jid:tolower(To),
    LBareTo = jid:tolower(jid:remove_resource(To)),
    IsOffline = case maps:get(LTo, State#state.users, error) of
		    #user{last_presence = undefined} -> true;
		    error -> true;
		    _ -> false
		end,
    if IsOffline ->
	    try muc_subscribers_get(LBareTo, State#state.muc_subscribers) of
		#subscriber{nodes = Nodes, jid = JID} ->
		    case lists:member(Node, Nodes) of
			true ->
			    MamEnabled = (State#state.config)#config.mam,
			    Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of
				     #stanza_id{id = Id2} ->
					 Id2;
				     _ ->
					 p1_rand:get_string()
				 end,
			    NewPacket = wrap(From, JID, Packet, Node, Id),
			    NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled),
			    ejabberd_router:route(
			      xmpp:set_from_to(NewPacket2, State#state.jid, JID));
			false ->
			    ok
		    end
	    catch _:{badkey, _} ->
		    ok
	    end;
       true ->
	    case Packet of
		#presence{type = unavailable} ->
		    case xmpp:get_subtag(Packet, #muc_user{}) of
			#muc_user{destroy = Destroy,
				  status_codes = Codes} ->
			    case Destroy /= undefined orelse
				 (lists:member(110,Codes) andalso
				  not lists:member(303, Codes)) of
				true ->
				    ejabberd_router:route(
				      #presence{from = State#state.jid, to = To,
						id = p1_rand:get_string(),
						type = unavailable});
				false ->
				    ok
			    end;
			_ ->
			    false
		    end;
		_ ->
		    ok
	    end,
	    ejabberd_router:route(xmpp:set_from_to(Packet, From, To))
    end.

-spec wrap(jid(), undefined | jid(), stanza(), binary(), binary()) -> message().
wrap(From, To, Packet, Node, Id) ->
    El = xmpp:set_from_to(Packet, From, To),
    #message{
	id = Id,
	sub_els = [#ps_event{
	    items = #ps_items{
		node = Node,
		items = [#ps_item{
		    id = Id,
		    sub_els = [El]}]}}]}.

-spec send_wrapped_multiple(jid(), users(), stanza(), binary(), state()) -> ok.
send_wrapped_multiple(From, Users, Packet, Node, State) ->
    {Dir, Wra} =
    maps:fold(
	fun(_, #user{jid = To, last_presence = LP}, {Direct, Wrapped} = Res) ->
	    IsOffline = LP == undefined,
	    if IsOffline ->
		LBareTo = jid:tolower(jid:remove_resource(To)),
		case muc_subscribers_find(LBareTo, State#state.muc_subscribers) of
		    {ok, #subscriber{nodes = Nodes}} ->
			case lists:member(Node, Nodes) of
			    true ->
				{Direct, [To | Wrapped]};
			    _ ->
                                %% TODO: check that this branch is never called
				Res
			end;
		    _ ->
			Res
		end;
		true ->
		    {[To | Direct], Wrapped}
	    end
	end, {[],[]}, Users),
    case Dir of
	[] -> ok;
	_ ->
	    case Packet of
		#presence{type = unavailable} ->
		    case xmpp:get_subtag(Packet, #muc_user{}) of
			#muc_user{destroy = Destroy,
				  status_codes = Codes} ->
			    case Destroy /= undefined orelse
				 (lists:member(110,Codes) andalso
				  not lists:member(303, Codes)) of
				true ->
				    ejabberd_router_multicast:route_multicast(
					From, State#state.server_host, Dir,
					#presence{id = p1_rand:get_string(),
						  type = unavailable}, false);
				false ->
				    ok
			    end;
			_ ->
			    false
		    end;
		_ ->
		    ok
	    end,
	    ejabberd_router_multicast:route_multicast(From, State#state.server_host,
						      Dir, Packet, false)
    end,
    case Wra of
	[] -> ok;
	_ ->
	    MamEnabled = (State#state.config)#config.mam,
	    Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of
		     #stanza_id{id = Id2} ->
			 Id2;
		     _ ->
			 p1_rand:get_string()
		 end,
	    NewPacket = wrap(From, undefined, Packet, Node, Id),
	    NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled),
	    ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
						      Wra, NewPacket2, true)
    end.

%%%----------------------------------------------------------------------
%%% #muc_subscribers API
%%%----------------------------------------------------------------------

-spec muc_subscribers_new() -> #muc_subscribers{}.
muc_subscribers_new() ->
    #muc_subscribers{}.

-spec muc_subscribers_get(ljid(), #muc_subscribers{}) -> #subscriber{}.
muc_subscribers_get({_, _, _} = LJID, MUCSubscribers) ->
    maps:get(LJID, MUCSubscribers#muc_subscribers.subscribers).

-spec muc_subscribers_find(ljid(), #muc_subscribers{}) ->
                                  {ok, #subscriber{}} | error.
muc_subscribers_find({_, _, _} = LJID, MUCSubscribers) ->
    maps:find(LJID, MUCSubscribers#muc_subscribers.subscribers).

-spec muc_subscribers_is_key(ljid(), #muc_subscribers{}) -> boolean().
muc_subscribers_is_key({_, _, _} = LJID, MUCSubscribers) ->
    maps:is_key(LJID, MUCSubscribers#muc_subscribers.subscribers).

-spec muc_subscribers_size(#muc_subscribers{}) -> integer().
muc_subscribers_size(MUCSubscribers) ->
    maps:size(MUCSubscribers#muc_subscribers.subscribers).

-spec muc_subscribers_fold(Fun, Acc, #muc_subscribers{}) -> Acc when
    Fun :: fun((ljid(), #subscriber{}, Acc) -> Acc).
muc_subscribers_fold(Fun, Init, MUCSubscribers) ->
    maps:fold(Fun, Init, MUCSubscribers#muc_subscribers.subscribers).

-spec muc_subscribers_get_by_nick(binary(), #muc_subscribers{}) -> [#subscriber{}].
muc_subscribers_get_by_nick(Nick, MUCSubscribers) ->
    maps:get(Nick, MUCSubscribers#muc_subscribers.subscriber_nicks, []).

-spec muc_subscribers_get_by_node(binary(), #muc_subscribers{}) -> subscribers().
muc_subscribers_get_by_node(Node, MUCSubscribers) ->
    maps:get(Node, MUCSubscribers#muc_subscribers.subscriber_nodes, #{}).

-spec muc_subscribers_remove_exn(ljid(), #muc_subscribers{}) ->
                                        {#muc_subscribers{}, #subscriber{}}.
muc_subscribers_remove_exn({_, _, _} = LJID, MUCSubscribers) ->
    #muc_subscribers{subscribers = Subs,
                     subscriber_nicks = SubNicks,
                     subscriber_nodes = SubNodes} = MUCSubscribers,
    Subscriber = maps:get(LJID, Subs),
    #subscriber{nick = Nick, nodes = Nodes} = Subscriber,
    NewSubNicks = maps:remove(Nick, SubNicks),
    NewSubs = maps:remove(LJID, Subs),
    NewSubNodes =
        lists:foldl(
          fun(Node, Acc) ->
                  NodeSubs = maps:get(Node, Acc, #{}),
                  NodeSubs2 = maps:remove(LJID, NodeSubs),
                  maps:put(Node, NodeSubs2, Acc)
          end, SubNodes, Nodes),
    {#muc_subscribers{subscribers = NewSubs,
                      subscriber_nicks = NewSubNicks,
                      subscriber_nodes = NewSubNodes}, Subscriber}.

-spec muc_subscribers_put(#subscriber{}, #muc_subscribers{}) ->
                                 #muc_subscribers{}.
muc_subscribers_put(Subscriber, MUCSubscribers) ->
    #subscriber{jid = JID,
                nick = Nick,
                nodes = Nodes} = Subscriber,
    #muc_subscribers{subscribers = Subs,
                     subscriber_nicks = SubNicks,
                     subscriber_nodes = SubNodes} = MUCSubscribers,
    LJID = jid:tolower(JID),
    NewSubs = maps:put(LJID, Subscriber, Subs),
    NewSubNicks = maps:put(Nick, [LJID], SubNicks),
    NewSubNodes =
        lists:foldl(
          fun(Node, Acc) ->
                  NodeSubs = maps:get(Node, Acc, #{}),
                  NodeSubs2 = maps:put(LJID, Subscriber, NodeSubs),
                  maps:put(Node, NodeSubs2, Acc)
          end, SubNodes, Nodes),
    #muc_subscribers{subscribers = NewSubs,
                     subscriber_nicks = NewSubNicks,
                     subscriber_nodes = NewSubNodes}.


cleanup_affiliations(State) ->
    case mod_muc_opt:cleanup_affiliations_on_start(State#state.server_host) of
        true ->
            Affiliations =
                maps:filter(
                  fun({LUser, LServer, _}, _) ->
                          case ejabberd_router:is_my_host(LServer) of
                              true ->
                                  ejabberd_auth:user_exists(LUser, LServer);
                              false ->
                                  true
                          end
                  end, State#state.affiliations),
            State#state{affiliations = Affiliations};
        false ->
            State
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Detect messange stanzas that don't have meaningful content
-spec has_body_or_subject(message()) -> boolean().
has_body_or_subject(#message{body = Body, subject = Subj}) ->
    Body /= [] orelse Subj /= [].

-spec reset_hibernate_timer(state()) -> state().
reset_hibernate_timer(State) ->
    case State#state.hibernate_timer of
	hibernating ->
	    ok;
	_ ->
	    disable_hibernate_timer(State),
	    NewTimer = case {mod_muc_opt:hibernation_timeout(State#state.server_host),
			     maps:size(State#state.users)} of
			   {infinity, _} ->
			       none;
			   {Timeout, 0} ->
			       p1_fsm:send_event_after(Timeout, hibernate);
			   _ ->
			       none
		       end,
	    State#state{hibernate_timer = NewTimer}
    end.


-spec disable_hibernate_timer(state()) -> ok.
disable_hibernate_timer(State) ->
    case State#state.hibernate_timer of
	Ref when is_reference(Ref) ->
	    p1_fsm:cancel_timer(Ref),
	    ok;
	_ ->
	    ok
    end.
