25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-24 16:23:40 +01:00
xmpp.chapril.org-ejabberd/src/mod_muc_room.erl
2024-07-16 18:55:02 +02:00

5752 lines
209 KiB
Erlang

%%%----------------------------------------------------------------------
%%% 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', "", "conversejs/prosody compatible"}).
-protocol({xep, 410, '1.1.0', '18.12', "", ""}).
-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 = false}}};
_ when NewStateData#state.just_created ->
close_room_if_temporary_and_empty(NewStateData);
_ ->
{next_state, normal_state, NewStateData}
end
end
catch _:{xmpp_codec, Why} ->
ErrTxt = xmpp:io_format_error(Why),
Err = xmpp:err_bad_request(ErrTxt, Lang),
ejabberd_router:route_error(IQ0, Err),
{next_state, normal_state, StateData}
end;
normal_state({route, <<"">>, #iq{} = IQ}, StateData) ->
Err = xmpp:err_bad_request(),
ejabberd_router:route_error(IQ, Err),
case StateData#state.just_created of
true -> {stop, normal, StateData};
_ -> {next_state, normal_state, StateData}
end;
normal_state({route, Nick, #presence{from = From} = Packet}, StateData) ->
Activity = get_user_activity(From, StateData),
Now = 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 = false}}};
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 = false}}};
{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]);
_ ->
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.