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