-type fsm_stop() :: {stop, normal, state()}.
-type fsm_next() :: {next_state, normal_state, state()}.
-type fsm_transition() :: fsm_stop() | fsm_next().
--type history_element() :: {binary(), %% nick
- message(), %% message itself
- boolean(), %% have subject
- erlang:timestamp(),
- non_neg_integer()}.
-export_type([state/0]).
ejabberd_router:route_error(StateData#state.jid, From, Packet, Err),
{next_state, normal_state, StateData};
false when Type /= error ->
- handle_roommessage_from_nonparticipant(Packet, StateData, From);
+ handle_roommessage_from_nonparticipant(Packet, StateData, From),
+ {next_state, normal_state, StateData};
false ->
{next_state, normal_state, StateData}
end;
process_iq_owner(From, IQ, StateData);
?NS_DISCO_INFO when SubEl#disco_info.node == <<>> ->
process_iq_disco_info(From, IQ, StateData);
- ?NS_DISCO_INFO ->
- Txt = <<"Disco info is not available for this node">>,
- {error, xmpp:err_service_unavailable(Txt, Lang)};
?NS_DISCO_ITEMS ->
process_iq_disco_items(From, IQ, StateData);
?NS_VCARD ->
?NS_CAPTCHA ->
process_iq_captcha(From, IQ, StateData);
_ ->
- {error, xmpp:err_feature_not_implemented()}
+ Txt = <<"The feature requested is not "
+ "supported by the conference">>,
+ {error, xmpp:err_service_unavailable(Txt, Lang)}
end,
{IQRes, NewStateData} =
case Res1 of
ok
end,
case NewStateData of
- stop -> {stop, normal, StateData};
- _ -> {next_state, normal_state, NewStateData}
+ stop ->
+ {stop, normal, StateData};
+ _ when NewStateData#state.just_created ->
+ close_room_if_temporary_and_empty(NewStateData);
+ _ ->
+ {next_state, normal_state, NewStateData}
end
end
catch _:{xmpp_codec, Why} ->
normal_state({route, From, <<"">>, #iq{} = IQ}, StateData) ->
Err = xmpp:err_bad_request(),
ejabberd_router:route_error(StateData#state.jid, From, IQ, Err),
- {next_state, normal_state, StateData};
+ case StateData#state.just_created of
+ true -> {stop, normal, StateData};
+ false -> {next_state, normal_state, StateData}
+ end;
normal_state({route, From, Nick, #presence{} = Packet}, StateData) ->
Activity = get_user_activity(From, StateData),
Now = p1_time_compat:system_time(micro_seconds),
StateName, _StateData) ->
{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({is_subscriber, From}, _From, StateName, StateData) ->
+ {reply, is_subscriber(From, StateData), StateName, StateData};
handle_sync_event(_Event, _From, StateName,
StateData) ->
Reply = ok, {reply, Reply, StateName, StateData}.
(#muc_user{invites = [I]}, _) ->
{ok, I};
(#muc_user{invites = [_|_]}, _) ->
- Txt = <<"Multiple <invite/> elements are not allowed">>,
+ Txt = <<"Multiple invitations are not allowed">>,
{error, xmpp:err_resource_constraint(Txt, Lang)};
(#xdata{type = submit, fields = Fs}, _) ->
try {ok, muc_request:decode(Fs)}
{ok, _, _} ->
ErrText = <<"Please, wait for a while before sending "
"new voice request">>,
- Err = xmpp:err_not_acceptable(ErrText, Lang),
+ Err = xmpp:err_resource_constraint(ErrText, Lang),
ejabberd_router:route_error(
StateData#state.jid, From, Pkt, Err),
StateData#state{last_voice_request_time = Times}
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: ~s",
+ [jid:to_string(From)]),
{expulse_sender, Reason};
false -> continue_delivery
end,
make_reason(Packet, From, StateData, Reason1) ->
{ok, #user{nick = FromNick}} = (?DICT):find(jid:tolower(From), StateData#state.users),
Condition = get_error_condition(xmpp:get_error(Packet)),
- iolist_to_binary(io_lib:format(Reason1, [FromNick, Condition])).
+ str:format(Reason1, [FromNick, Condition]).
-spec expulse_participant(stanza(), jid(), state(), binary()) ->
state().
Nodes, StateData)),
send_existing_presences(From, NewState),
send_initial_presence(From, NewState, StateData),
- Shift = count_stanza_shift(Nick, Packet, NewState),
- case send_history(From, Shift, NewState) of
- true -> ok;
- _ -> send_subject(From, StateData)
- end,
+ History = get_history(Nick, Packet, NewState),
+ send_history(From, History, NewState),
+ send_subject(From, StateData),
NewState;
true ->
add_online_user(From, Nick, none,
false
end.
--spec count_stanza_shift(binary(), stanza(), state()) -> non_neg_integer().
-count_stanza_shift(Nick, Packet, StateData) ->
- case xmpp:get_subtag(Packet, #muc_history{}) of
- #muc_history{since = Since,
- seconds = Seconds,
- maxstanzas = MaxStanzas,
- maxchars = MaxChars} ->
- HL = lqueue_to_list(StateData#state.history),
- Shift0 = case Since of
- undefined -> 0;
- _ ->
- Sin = calendar:datetime_to_gregorian_seconds(
- calendar:now_to_datetime(Since)),
- count_seconds_shift(Sin, HL)
- end,
- Shift1 = case Seconds of
- undefined -> 0;
- _ ->
- Sec = calendar:datetime_to_gregorian_seconds(
- calendar:universal_time()) - Seconds,
- count_seconds_shift(Sec, HL)
- end,
- Shift2 = case MaxStanzas of
- undefined -> 0;
- _ -> count_maxstanzas_shift(MaxStanzas, HL)
- end,
- Shift3 = case MaxChars of
- undefined -> 0;
- _ -> count_maxchars_shift(Nick, MaxChars, HL)
- end,
- lists:max([Shift0, Shift1, Shift2, Shift3]);
- false ->
- 0
- end.
-
--spec count_seconds_shift(non_neg_integer(),
- [history_element()]) -> non_neg_integer().
-count_seconds_shift(Seconds, HistoryList) ->
- lists:sum(lists:map(fun ({_Nick, _Packet, _HaveSubject,
- TimeStamp, _Size}) ->
- T =
- calendar:datetime_to_gregorian_seconds(TimeStamp),
- if T < Seconds -> 1;
- true -> 0
- end
- end,
- HistoryList)).
-
--spec count_maxstanzas_shift(non_neg_integer(),
- [history_element()]) -> non_neg_integer().
-count_maxstanzas_shift(MaxStanzas, HistoryList) ->
- S = length(HistoryList) - MaxStanzas,
- if S =< 0 -> 0;
- true -> S
- end.
-
--spec count_maxchars_shift(binary(), non_neg_integer(),
- [history_element()]) -> integer().
-count_maxchars_shift(Nick, MaxSize, HistoryList) ->
- NLen = byte_size(Nick) + 1,
- Sizes = lists:map(fun ({_Nick, _Packet, _HaveSubject,
- _TimeStamp, Size}) ->
- Size + NLen
- end,
- HistoryList),
- calc_shift(MaxSize, Sizes).
-
--spec calc_shift(non_neg_integer(), [non_neg_integer()]) -> integer().
-calc_shift(MaxSize, Sizes) ->
- Total = lists:sum(Sizes),
- calc_shift(MaxSize, Total, 0, Sizes).
-
--spec calc_shift(non_neg_integer(), integer(), integer(),
- [non_neg_integer()]) -> integer().
-calc_shift(_MaxSize, _Size, Shift, []) -> Shift;
-calc_shift(MaxSize, Size, Shift, [S | TSizes]) ->
- if MaxSize >= Size -> Shift;
- true -> calc_shift(MaxSize, Size - S, Shift + 1, TSizes)
+-spec get_history(binary(), stanza(), state()) -> lqueue().
+get_history(Nick, Packet, #state{history = History}) ->
+ case xmpp:get_subtag(Packet, #muc{}) of
+ #muc{history = #muc_history{} = MUCHistory} ->
+ Now = p1_time_compat:timestamp(),
+ Q = History#lqueue.queue,
+ {NewQ, Len} = filter_history(Q, MUCHistory, Now, Nick, queue:new(), 0, 0),
+ History#lqueue{queue = NewQ, len = Len};
+ _ ->
+ History
+ end.
+
+-spec filter_history(?TQUEUE, muc_history(), erlang:timestamp(), binary(),
+ ?TQUEUE, non_neg_integer(), non_neg_integer()) ->
+ {?TQUEUE, non_neg_integer()}.
+filter_history(Queue, #muc_history{since = Since,
+ seconds = Seconds,
+ maxstanzas = MaxStanzas,
+ maxchars = MaxChars} = MUC,
+ Now, Nick, AccQueue, NumStanzas, NumChars) ->
+ case queue:out_r(Queue) of
+ {{value, {_, _, _, TimeStamp, Size} = Elem}, NewQueue} ->
+ 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) ->
+ filter_history(NewQueue, MUC, Now, Nick,
+ queue:in_r(Elem, AccQueue),
+ NumStanzas + 1,
+ NumChars + Chars);
+ true ->
+ {AccQueue, NumStanzas}
+ end;
+ {empty, _} ->
+ {AccQueue, NumStanzas}
end.
-spec is_room_overcrowded(state()) -> boolean().
end,
lists:foreach(
fun({LUJID, Info}) ->
- {Role, Presence} = if LNJID == LUJID -> {Role0, Presence0};
+ IsSelfPresence = LNJID == LUJID,
+ {Role, Presence} = if IsSelfPresence -> {Role0, Presence0};
true -> {Role1, Presence1}
end,
Item0 = #muc_item{affiliation = Affiliation,
role = Role},
Item1 = case Info#user.role == moderator orelse
(StateData#state.config)#config.anonymous
- == false of
+ == false orelse IsSelfPresence of
true -> Item0#muc_item{jid = RealJID};
false -> Item0
end,
Item = Item1#muc_item{reason = Reason},
- StatusCodes = status_codes(IsInitialPresence, NJID, Info,
+ StatusCodes = status_codes(IsInitialPresence, IsSelfPresence,
StateData),
Pres = if Presence == undefined -> #presence{};
true -> Presence
StateData#state.users),
Affiliation = get_affiliation(JID, StateData),
lists:foreach(
- fun({_LJID, Info}) when Presence /= undefined ->
+ fun({LJID, Info}) when Presence /= undefined ->
+ IsSelfPresence = LJID == jid:tolower(JID),
Item0 = #muc_item{affiliation = Affiliation, role = Role},
- Item1 = case Info#user.role == moderator orelse
- (StateData#state.config)#config.anonymous
- == false of
- true -> Item0#muc_item{jid = RealJID, nick = Nick};
- false -> Item0#muc_item{nick = Nick}
- end,
- Item2 = case Info#user.role == moderator orelse
- (StateData#state.config)#config.anonymous
- == false of
- true -> Item0#muc_item{jid = RealJID};
- false -> Item0
- end,
- Status110 = case JID == Info#user.jid of
+ Item = case Info#user.role == moderator orelse
+ (StateData#state.config)#config.anonymous
+ == false orelse IsSelfPresence of
+ true -> Item0#muc_item{jid = RealJID};
+ false -> Item0
+ end,
+ Status110 = case IsSelfPresence of
true -> [110];
false -> []
end,
- Packet1 = #presence{type = unavailable,
- sub_els = [#muc_user{
- items = [Item1],
- status_codes = [303|Status110]}]},
+ 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 = [Item2],
+ #muc_user{items = [Item],
status_codes = Status110}),
if SendOldUnavailable ->
send_wrapped(
true ->
ok; % The new affiliation is published via presence.
false ->
- send_affiliation(LJID, Affiliation, StateData)
+ send_affiliation(JID, Affiliation, StateData)
end.
--spec send_affiliation(ljid(), affiliation(), state()) -> ok.
-send_affiliation(LJID, Affiliation, StateData) ->
- Item = #muc_item{jid = jid:make(LJID),
+-spec send_affiliation(jid(), affiliation(), state()) -> ok.
+send_affiliation(JID, Affiliation, StateData) ->
+ Item = #muc_item{jid = JID,
affiliation = Affiliation,
role = none},
Message = #message{id = randoms:get_string(),
StateData#state.server_host,
Recipients, Message).
--spec status_codes(boolean(), jid(), #user{}, state()) -> [pos_integer()].
-status_codes(IsInitialPresence, JID, #user{jid = JID}, StateData) ->
+-spec status_codes(boolean(), boolean(), state()) -> [pos_integer()].
+status_codes(IsInitialPresence, _IsSelfPresence = true, StateData) ->
S0 = [110],
case IsInitialPresence of
true ->
S3;
false -> S0
end;
-status_codes(_IsInitialPresence, _JID, _Info, _StateData) -> [].
+status_codes(_IsInitialPresence, _IsSelfPresence = false, _StateData) -> [].
-spec lqueue_new(non_neg_integer()) -> lqueue().
lqueue_new(Max) ->
-spec add_message_to_history(binary(), jid(), message(), state()) -> state().
add_message_to_history(FromNick, FromJID, Packet, StateData) ->
- HaveSubject = Packet#message.subject /= [],
- TimeStamp = p1_time_compat: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 = xmpp_util:add_delay_info(
- AddrPacket, StateData#state.jid, TimeStamp),
- SPacket = xmpp:set_from_to(
- TSPacket,
- jid:replace_resource(StateData#state.jid, FromNick),
- StateData#state.jid),
- Size = element_size(SPacket),
- Q1 = lqueue_in({FromNick, TSPacket, HaveSubject,
- calendar:now_to_universal_time(TimeStamp), Size},
- StateData#state.history),
add_to_log(text, {FromNick, Packet}, StateData),
- StateData#state{history = Q1}.
-
--spec send_history(jid(), integer(), state()) -> boolean().
-send_history(JID, Shift, StateData) ->
- lists:foldl(fun ({Nick, Packet, HaveSubject, _TimeStamp,
- _Size},
- 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
+ false ->
+ TimeStamp = p1_time_compat: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 = xmpp_util:add_delay_info(
+ AddrPacket, StateData#state.jid, TimeStamp),
+ SPacket = xmpp:set_from_to(
+ TSPacket,
+ jid:replace_resource(StateData#state.jid, FromNick),
+ StateData#state.jid),
+ Size = element_size(SPacket),
+ Q1 = lqueue_in({FromNick, TSPacket, false,
+ TimeStamp, Size},
+ StateData#state.history),
+ StateData#state{history = Q1};
+ _ ->
+ StateData
+ end.
+
+-spec send_history(jid(), lqueue(), state()) -> boolean().
+send_history(JID, History, StateData) ->
+ lists:foreach(
+ fun({Nick, Packet, _HaveSubject, _TimeStamp, _Size}) ->
+ ejabberd_router:route(
+ jid:replace_resource(StateData#state.jid, Nick),
+ JID, Packet)
+ end, lqueue_to_list(History)).
-spec send_subject(jid(), state()) -> ok.
-send_subject(_JID, #state{subject_author = <<"">>}) -> ok;
send_subject(JID, #state{subject_author = Nick} = StateData) ->
- Subject = StateData#state.subject,
- Packet = #message{type = groupchat, subject = xmpp:mk_text(Subject)},
+ Subject = case StateData#state.subject of
+ <<"">> -> [#text{}];
+ Subj -> xmpp:mk_text(Subj)
+ end,
+ Packet = #message{type = groupchat, subject = Subject},
ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), JID,
Packet).
-spec check_subject(message()) -> false | binary().
-check_subject(#message{subject = []}) -> false;
-check_subject(#message{subject = Subj}) -> xmpp:get_text(Subj).
+check_subject(#message{subject = [_|_] = Subj, body = [],
+ thread = undefined}) ->
+ xmpp:get_text(Subj);
+check_subject(_) ->
+ false.
-spec can_change_subject(role(), state()) -> boolean().
can_change_subject(Role, StateData) ->
items_with_affiliation(SAffiliation, StateData) ->
lists:map(
fun({JID, {Affiliation, Reason}}) ->
- #muc_item{affiliation = Affiliation, jid = JID,
+ #muc_item{affiliation = Affiliation, jid = jid:make(JID),
reason = Reason};
({JID, Affiliation}) ->
- #muc_item{affiliation = Affiliation, jid = JID}
+ #muc_item{affiliation = Affiliation, jid = jid:make(JID)}
end,
search_affiliation(SAffiliation, StateData)).
"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, undefined, NSD};
- {error, Err} -> {error, Err}
+ 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()) -> function().
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.
-type admin_action() :: {jid(), affiliation | role,
affiliation() | role(), binary()}.
--spec process_item_change(admin_action(), state(), jid()) -> state().
+-spec process_item_change(admin_action(), state(), jid()) -> state() | {error, stanza_error()}.
process_item_change(Item, SD, UJID) ->
try case Item of
{JID, affiliation, owner, _} when JID#jid.luser == <<"">> ->
%% forget the affiliation completely
SD;
{JID, role, none, Reason} ->
- catch send_kickban_presence(UJID, JID, Reason, 307, SD),
+ 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),
+ 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),
+ send_update_presence(JID, Reason, SD1, SD),
maybe_send_affiliation(JID, none, SD1),
SD1
end;
{JID, affiliation, outcast, Reason} ->
- catch send_kickban_presence(UJID, JID, Reason, 301, outcast, SD),
+ 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) ->
SD2;
{JID, role, Role, Reason} ->
SD1 = set_role(JID, Role, SD),
- catch send_new_presence(JID, Reason, SD1, SD),
+ send_new_presence(JID, Reason, SD1, SD),
SD1;
{JID, affiliation, A, _Reason} ->
SD1 = set_affiliation(JID, A, SD),
?ERROR_MSG("failed to set item ~p from ~s: ~p",
[Item, jid:to_string(UJID),
{E, {R, erlang:get_stacktrace()}}]),
- SD
+ {error, xmpp:err_internal_server_error()}
end.
-spec find_changed_items(jid(), affiliation(), role(),
Nick /= <<"">> ->
case find_jids_by_nick(Nick, StateData) of
[] ->
- ErrText = iolist_to_binary(
- io_lib:format(
- translate:translate(
- Lang,
- <<"Nickname ~s does not exist in the room">>),
- [Nick])),
+ ErrText = str:format(
+ translate:translate(
+ Lang,
+ <<"Nickname ~s does not exist in the room">>),
+ [Nick]),
throw({error, xmpp:err_not_acceptable(ErrText, Lang)});
JIDList ->
JIDList
Items, Lang, StateData,
Res);
true ->
- MoreRes = [{jid:remove_resource(Jidx),
- RoleOrAff, RoleOrAffValue, Reason}
- || Jidx <- JIDs],
+ 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]);
StateData#state.users),
ActorNick = get_actor_nick(MJID, StateData),
lists:foreach(
- fun({_LJID, Info}) ->
+ 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 of
+ == false orelse IsSelfPresence of
true -> Item0#muc_item{jid = RealJID};
false -> Item0
end,
<<"">> -> 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 = [Code]}]},
+ status_codes = Codes}]},
RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick),
send_wrapped(RoomJIDNick, Info#user.jid, Packet,
?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
case Config of
#xdata{type = cancel} ->
{result, undefined};
- #xdata{type = submit} ->
- case is_allowed_log_change(Config, StateData, From) andalso
- is_allowed_persistent_change(Config, StateData, From) andalso
- is_allowed_room_name_desc_limits(Config, StateData) andalso
- is_password_settings_correct(Config, StateData) of
- true -> set_config(Config, StateData, Lang);
- false -> {error, xmpp:err_not_acceptable()}
+ #xdata{type = submit, fields = Fs} ->
+ try muc_roomconfig:decode(Fs) of
+ Options ->
+ case is_allowed_log_change(Options, StateData, From) andalso
+ is_allowed_persistent_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 = <<"Incorrect data form">>,
{error, xmpp:err_bad_request()}
end.
--spec is_allowed_log_change(xdata(), state(), jid()) -> boolean().
-is_allowed_log_change(X, StateData, From) ->
- case xmpp_util:has_xdata_var(<<"muc#roomconfig_enablelogging">>, X) of
+-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 ==
From)
end.
--spec is_allowed_persistent_change(xdata(), state(), jid()) -> boolean().
-is_allowed_persistent_change(X, StateData, From) ->
- case xmpp_util:has_xdata_var(<<"muc#roomconfig_persistentroom">>, X) of
+-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,
%% Check if the Room Name and Room Description defined in the Data Form
%% are conformant to the configured limits
--spec is_allowed_room_name_desc_limits(xdata(), state()) -> boolean().
-is_allowed_room_name_desc_limits(XData, StateData) ->
- IsNameAccepted = case xmpp_util:get_xdata_values(
- <<"muc#roomconfig_roomname">>, XData) of
- [N] ->
- byte_size(N) =<
- gen_mod:get_module_opt(
- StateData#state.server_host,
- mod_muc, max_room_name,
- fun(infinity) -> infinity;
- (I) when is_integer(I),
- I>0 -> I
- end, infinity);
- _ ->
- true
- end,
- IsDescAccepted = case xmpp_util:get_xdata_values(
- <<"muc#roomconfig_roomdesc">>, XData) of
- [D] ->
- byte_size(D) =<
- gen_mod:get_module_opt(
- StateData#state.server_host,
- mod_muc, max_room_desc,
- fun(infinity) -> infinity;
- (I) when is_integer(I),
- I>0 ->
- I
- end, infinity);
- _ -> true
- end,
- IsNameAccepted and IsDescAccepted.
+-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 = 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),
+ MaxRoomDesc = 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),
+ (byte_size(RoomName) =< MaxRoomName)
+ andalso (byte_size(RoomDesc) =< MaxRoomDesc).
%% Return false if:
%% "the password for a password-protected room is blank"
--spec is_password_settings_correct(xdata(), state()) -> boolean().
-is_password_settings_correct(XData, 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 xmpp_util:get_xdata_values(
- <<"muc#roomconfig_passwordprotectedroom">>, XData) of
- [<<"1">>] -> true;
- [<<"true">>] -> true;
- [<<"0">>] -> false;
- [<<"false">>] -> false;
- _ -> undefined
- end,
- NewPassword = case xmpp_util:get_xdata_values(
- <<"muc#roomconfig_roomsecret">>, XData) of
- [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.
-spec get_default_room_maxusers(state()) -> non_neg_integer().
{N, N};
_ -> {0, none}
end,
- Title = iolist_to_binary(
- io_lib:format(
- translate:translate(Lang, <<"Configuration of room ~s">>),
- [jid:to_string(StateData#state.jid)])),
+ Title = str:format(
+ translate:translate(Lang, <<"Configuration of room ~s">>),
+ [jid:to_string(StateData#state.jid)]),
Fs = [{roomname, Config#config.title},
{roomdesc, Config#config.description}] ++
case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of
fields = muc_roomconfig:encode(
Fields, fun(T) -> translate:translate(Lang, T) end)}.
--spec set_config(xdata(), state(), binary()) -> {error, stanza_error()} |
- {result, undefined, state()}.
-set_config(#xdata{fields = Fields}, StateData, Lang) ->
+-spec set_config(muc_roomconfig:result(), state(), binary()) ->
+ {error, stanza_error()} | {result, undefined, state()}.
+set_config(Options, StateData, Lang) ->
try
- Options = muc_roomconfig:decode(Fields),
#config{} = Config = set_config(Options, StateData#state.config,
StateData#state.server_host, Lang),
{result, _, NSD} = Res = change_config(Config, StateData),
|| {_, U} <- (?DICT):to_list(StateData#state.users)],
add_to_log(Type, Users, NSD),
Res
- catch _:{muc_roomconfig, Why} ->
- Txt = muc_roomconfig:format_error(Why),
- {error, xmpp:err_bad_request(Txt, Lang)};
- _:{badmatch, {error, #stanza_error{}} = Err} ->
+ catch _:{badmatch, {error, #stanza_error{}} = Err} ->
Err
end.
end
++
case Old#config{anonymous = New#config.anonymous,
+ vcard = New#config.vcard,
logging = New#config.logging} of
New -> [];
_ -> [104]
end,
- Message = #message{type = groupchat,
- id = randoms:get_string(),
- sub_els = [#muc_user{status_codes = Codes}]},
- send_wrapped_multiple(StateData#state.jid,
- StateData#state.users,
- Message,
- ?NS_MUCSUB_NODES_CONFIG,
- StateData).
+ if Codes /= [] ->
+ Message = #message{type = groupchat,
+ id = randoms:get_string(),
+ sub_els = [#muc_user{status_codes = Codes}]},
+ send_wrapped_multiple(StateData#state.jid,
+ StateData#state.users,
+ Message,
+ ?NS_MUCSUB_NODES_CONFIG,
+ StateData);
+ true ->
+ ok
+ end.
-spec remove_nonmembers(state()) -> state().
remove_nonmembers(StateData) ->
process_iq_disco_items(_From, #iq{type = set, lang = Lang}, _StateData) ->
Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
{error, xmpp:err_not_allowed(Txt, Lang)};
-process_iq_disco_items(From, #iq{type = get, lang = Lang}, StateData) ->
+process_iq_disco_items(From, #iq{type = get}, StateData) ->
case (StateData#state.config)#config.public_list of
true ->
{result, get_mucroom_disco_items(StateData)};
true ->
{result, get_mucroom_disco_items(StateData)};
_ ->
- Txt = <<"Only occupants or administrators can perform this query">>,
- {error, xmpp:err_forbidden(Txt, Lang)}
+ %% 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.
#xmlel{} = VCard ->
{result, VCard};
{error, _} ->
- {result, #vcard_temp{}}
+ {error, xmpp:err_item_not_found()}
end;
process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [SubEl]},
StateData) ->
_ -> translate:translate(Lang, <<"private, ">>)
end,
Len = (?DICT):size(StateData#state.users),
- <<" (", Desc/binary,
- (iolist_to_binary(integer_to_list(Len)))/binary, ")">>.
+ <<" (", Desc/binary, (integer_to_binary(Len))/binary, ")">>.
-spec get_mucroom_disco_items(state()) -> disco_items().
get_mucroom_disco_items(StateData) ->
-compile(export_all).
--import(suite, [init_config/1, connect/1, disconnect/1,
- recv/1, send/2, send_recv/2, my_jid/1, server_jid/1,
- pubsub_jid/1, proxy_jid/1, muc_jid/1, muc_room_jid/1,
- mix_jid/1, mix_room_jid/1, get_features/2, re_register/1,
- is_feature_advertised/2, subscribe_to_events/1,
+-import(suite, [init_config/1, connect/1, disconnect/1, recv_message/1,
+ recv/1, recv_presence/1, send/2, send_recv/2, my_jid/1,
+ server_jid/1, pubsub_jid/1, proxy_jid/1, muc_jid/1,
+ muc_room_jid/1, my_muc_jid/1, peer_muc_jid/1,
+ mix_jid/1, mix_room_jid/1, get_features/2, recv_iq/1,
+ re_register/1, is_feature_advertised/2, subscribe_to_events/1,
is_feature_advertised/3, set_opt/3, auth_SASL/2,
- wait_for_master/1, wait_for_slave/1,
- make_iq_result/1, start_event_relay/0,
+ wait_for_master/1, wait_for_slave/1, flush/1,
+ make_iq_result/1, start_event_relay/0, alt_room_jid/1,
stop_event_relay/1, put_event/2, get_event/1,
bind/1, auth/1, auth/2, open_session/1, open_session/2,
zlib/1, starttls/1, starttls/2, close_socket/1, init_stream/1,
auth_legacy/2, auth_legacy/3, tcp_connect/1, send_text/2]).
-
-include("suite.hrl").
suite() ->
- [{timetrap, {seconds,60}}].
+ [{timetrap, {seconds, 30}}].
init_per_suite(Config) ->
NewConfig = init_config(Config),
do_init_per_group(no_db, Config) ->
re_register(Config),
- Config;
+ set_opt(persistent_room, false, Config);
do_init_per_group(mnesia, Config) ->
mod_muc:shutdown_rooms(?MNESIA_VHOST),
set_opt(server, ?MNESIA_VHOST, Config);
Test = atom_to_list(TestCase),
IsMaster = lists:suffix("_master", Test),
IsSlave = lists:suffix("_slave", Test),
+ Mode = if IsSlave -> slave;
+ IsMaster -> master;
+ true -> single
+ end,
IsCarbons = lists:prefix("carbons_", Test),
IsReplaced = lists:prefix("replaced_", Test),
User = if IsReplaced -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">>;
IsSlave -> <<"test_slave!#$%^*()`~+-;_=[]{}|\\">>;
true -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">>
end,
+ Nick = if IsSlave -> ?config(slave_nick, OrigConfig);
+ IsMaster -> ?config(master_nick, OrigConfig);
+ true -> ?config(nick, OrigConfig)
+ end,
MyResource = if IsMaster and IsCarbons -> MasterResource;
IsSlave and IsCarbons -> SlaveResource;
true -> Resource
true ->
jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource)
end,
- Config = set_opt(user, User,
- set_opt(slave, Slave,
- set_opt(master, Master,
- set_opt(resource, MyResource, OrigConfig)))),
+ Config1 = set_opt(user, User,
+ set_opt(slave, Slave,
+ set_opt(master, Master,
+ set_opt(resource, MyResource,
+ set_opt(nick, Nick,
+ set_opt(mode, Mode, OrigConfig)))))),
+ Config2 = if IsSlave ->
+ set_opt(peer_nick, ?config(master_nick, Config1), Config1);
+ IsMaster ->
+ set_opt(peer_nick, ?config(slave_nick, Config1), Config1);
+ true ->
+ Config1
+ end,
+ Config = if IsSlave -> set_opt(peer, Master, Config2);
+ IsMaster -> set_opt(peer, Slave, Config2);
+ true -> Config2
+ end,
case Test of
"test_connect" ++ _ ->
Config;
[sm,
sm_resume,
sm_resume_failed]},
+ muc_tests:single_cases(),
+ muc_tests:master_slave_cases(),
{test_proxy65, [parallel],
[proxy65_master, proxy65_slave]},
{replaced, [parallel],
privacy,
blocking,
vcard,
+ muc_tests:single_cases(),
test_unregister]},
- {test_muc_register, [parallel],
- [muc_register_master, muc_register_slave]},
+ muc_tests:master_slave_cases(),
{test_roster_subscribe, [parallel],
[roster_subscribe_master,
roster_subscribe_slave]},
[flex_offline_master, flex_offline_slave]},
{test_offline, [sequence],
[offline_master, offline_slave]},
- {test_muc, [parallel],
- [muc_master, muc_slave]},
{test_announce, [sequence],
[announce_master, announce_slave]},
{test_vcard_xupdate, [parallel],
blocking,
vcard,
pubsub_single_tests(),
+ muc_tests:single_cases(),
test_unregister]},
+ muc_tests:master_slave_cases(),
pubsub_multiple_tests(),
- {test_muc_register, [parallel],
- [muc_register_master, muc_register_slave]},
{test_mix, [parallel],
[mix_master, mix_slave]},
{test_roster_subscribe, [parallel],
[carbons_master, carbons_slave]},
{test_client_state, [parallel],
[client_state_master, client_state_slave]},
- {test_muc, [parallel],
- [muc_master, muc_slave]},
{test_muc_mam, [parallel],
[muc_mam_master, muc_mam_slave]},
{test_announce, [sequence],
[{single_user, [sequence],
[test_register,
legacy_auth_tests(),
- auth_plain,
- auth_md5,
- presence_broadcast,
- last,
- roster_get,
- roster_ver,
- private,
- privacy,
- blocking,
- vcard,
- pubsub_single_tests(),
- test_unregister]},
+ auth_plain,
+ auth_md5,
+ presence_broadcast,
+ last,
+ roster_get,
+ roster_ver,
+ private,
+ privacy,
+ blocking,
+ vcard,
+ pubsub_single_tests(),
+ muc_tests:single_cases(),
+ test_unregister]},
+ muc_tests:master_slave_cases(),
pubsub_multiple_tests(),
- {test_muc_register, [parallel],
- [muc_register_master, muc_register_slave]},
{test_mix, [parallel],
[mix_master, mix_slave]},
{test_roster_subscribe, [parallel],
[mam_old_master, mam_old_slave]},
{test_new_mam, [parallel],
[mam_new_master, mam_new_slave]},
- {test_muc, [parallel],
- [muc_master, muc_slave]},
{test_muc_mam, [parallel],
[muc_mam_master, muc_mam_slave]},
{test_announce, [sequence],
close_socket(Config0).
test_connect_bad_lang(Config) ->
- Config0 = init_stream(set_opt(lang, lists:duplicate(36, $x), Config)),
+ Lang = iolist_to_binary(lists:duplicate(36, $x)),
+ Config0 = init_stream(set_opt(lang, Lang, Config)),
?recv1(#stream_error{reason = 'policy-violation'}),
?recv1({xmlstreamend, <<"stream:stream">>}),
close_socket(Config0).
muc_mam_slave(Config) ->
disconnect(Config).
-muc_master(Config) ->
- MyJID = my_jid(Config),
- PeerJID = ?config(slave, Config),
- PeerBareJID = jid:remove_resource(PeerJID),
- PeerJIDStr = jid:to_string(PeerJID),
- MUC = muc_jid(Config),
- Room = muc_room_jid(Config),
- MyNick = ?config(master_nick, Config),
- MyNickJID = jid:replace_resource(Room, MyNick),
- PeerNick = ?config(slave_nick, Config),
- PeerNickJID = jid:replace_resource(Room, PeerNick),
- Subject = ?config(room_subject, Config),
- Localhost = jid:make(<<"">>, <<"localhost">>, <<"">>),
- true = is_feature_advertised(Config, ?NS_MUC, MUC),
- %% Joining
- send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}),
- %% As per XEP-0045 we MUST receive stanzas in the following order:
- %% 1. In-room presence from other occupants
- %% 2. In-room presence from the joining entity itself (so-called "self-presence")
- %% 3. Room history (if any)
- %% 4. The room subject
- %% 5. Live messages, presence updates, new user joins, etc.
- %% As this is the newly created room, we receive only the 2nd stanza.
- #muc_user{
- status_codes = Codes,
- items = [#muc_item{role = moderator,
- jid = MyJID,
- affiliation = owner}]} =
- xmpp:get_subtag(?recv1(#presence{from = MyNickJID}), #muc_user{}),
- %% 110 -> Inform user that presence refers to itself
- %% 201 -> Inform user that a new room has been created
- [110, 201] = lists:sort(Codes),
- %% Request the configuration
- #iq{type = result, sub_els = [#muc_owner{config = #xdata{} = RoomCfg}]} =
- send_recv(Config, #iq{type = get, sub_els = [#muc_owner{}],
- to = Room}),
- NewFields =
- lists:flatmap(
- fun(#xdata_field{var = Var, values = OrigVals}) ->
- Vals = case Var of
- <<"FORM_TYPE">> ->
- OrigVals;
- <<"muc#roomconfig_roomname">> ->
- [<<"Test room">>];
- <<"muc#roomconfig_roomdesc">> ->
- [<<"Trying to break the server">>];
- <<"muc#roomconfig_persistentroom">> ->
- [<<"1">>];
- <<"members_by_default">> ->
- [<<"0">>];
- <<"muc#roomconfig_allowvoicerequests">> ->
- [<<"1">>];
- <<"public_list">> ->
- [<<"1">>];
- <<"muc#roomconfig_publicroom">> ->
- [<<"1">>];
- _ ->
- []
- end,
- if Vals /= [] ->
- [#xdata_field{values = Vals, var = Var}];
- true ->
- []
- end
- end, RoomCfg#xdata.fields),
- NewRoomCfg = #xdata{type = submit, fields = NewFields},
- ID = send(Config, #iq{type = set, to = Room,
- sub_els = [#muc_owner{config = NewRoomCfg}]}),
- ?recv2(#iq{type = result, id = ID},
- #message{from = Room, type = groupchat,
- sub_els = [#muc_user{status_codes = [104]}]}),
- %% Set subject
- send(Config, #message{to = Room, type = groupchat,
- body = [#text{data = Subject}]}),
- ?recv1(#message{from = MyNickJID, type = groupchat,
- body = [#text{data = Subject}]}),
- %% Sending messages (and thus, populating history for our peer)
- lists:foreach(
- fun(N) ->
- Text = #text{data = integer_to_binary(N)},
- I = send(Config, #message{to = Room, body = [Text],
- type = groupchat}),
- ?recv1(#message{from = MyNickJID, id = I,
- type = groupchat,
- body = [Text]})
- end, lists:seq(1, 5)),
- %% Inviting the peer
- send(Config, #message{to = Room, type = normal,
- sub_els =
- [#muc_user{
- invites =
- [#muc_invite{to = PeerJID}]}]}),
- #muc_user{
- items = [#muc_item{role = visitor,
- jid = PeerJID,
- affiliation = none}]} =
- xmpp:get_subtag(?recv1(#presence{from = PeerNickJID}), #muc_user{}),
- %% Receiving a voice request
- #message{from = Room,
- sub_els = [#xdata{type = form,
- instructions = [_],
- fields = VoiceReqFs}]} = recv(Config),
- %% Approving the voice request
- ReplyVoiceReqFs =
- lists:map(
- fun(#xdata_field{var = Var, values = OrigVals}) ->
- Vals = case {Var, OrigVals} of
- {<<"FORM_TYPE">>,
- [<<"http://jabber.org/protocol/muc#request">>]} ->
- OrigVals;
- {<<"muc#role">>, [<<"participant">>]} ->
- [<<"participant">>];
- {<<"muc#jid">>, [PeerJIDStr]} ->
- [PeerJIDStr];
- {<<"muc#roomnick">>, [PeerNick]} ->
- [PeerNick];
- {<<"muc#request_allow">>, [<<"0">>]} ->
- [<<"1">>]
- end,
- #xdata_field{values = Vals, var = Var}
- end, VoiceReqFs),
- send(Config, #message{to = Room,
- sub_els = [#xdata{type = submit,
- fields = ReplyVoiceReqFs}]}),
- %% Peer is becoming a participant
- #muc_user{items = [#muc_item{role = participant,
- jid = PeerJID,
- affiliation = none}]} =
- xmpp:get_subtag(?recv1(#presence{from = PeerNickJID}), #muc_user{}),
- %% Receive private message from the peer
- ?recv1(#message{from = PeerNickJID, body = [#text{data = Subject}]}),
- %% Granting membership to the peer and localhost server
- I1 = send(Config,
- #iq{type = set, to = Room,
- sub_els =
- [#muc_admin{
- items = [#muc_item{jid = Localhost,
- affiliation = member},
- #muc_item{nick = PeerNick,
- jid = PeerBareJID,
- affiliation = member}]}]}),
- %% Peer became a member
- #muc_user{items = [#muc_item{affiliation = member,
- jid = PeerJID,
- role = participant}]} =
- xmpp:get_subtag(?recv1(#presence{from = PeerNickJID}), #muc_user{}),
- ?recv1(#message{from = Room,
- sub_els = [#muc_user{
- items = [#muc_item{affiliation = member,
- jid = Localhost,
- role = none}]}]}),
- ?recv1(#iq{type = result, id = I1, sub_els = []}),
- %% Receive groupchat message from the peer
- ?recv1(#message{type = groupchat, from = PeerNickJID,
- body = [#text{data = Subject}]}),
- %% Retrieving a member list
- #iq{type = result, sub_els = [#muc_admin{items = MemberList}]} =
- send_recv(Config,
- #iq{type = get, to = Room,
- sub_els =
- [#muc_admin{items = [#muc_item{affiliation = member}]}]}),
- [#muc_item{affiliation = member,
- jid = Localhost},
- #muc_item{affiliation = member,
- jid = PeerBareJID}] = lists:keysort(#muc_item.jid, MemberList),
- %% Kick the peer
- I2 = send(Config,
- #iq{type = set, to = Room,
- sub_els = [#muc_admin{
- items = [#muc_item{nick = PeerNick,
- role = none}]}]}),
- %% Got notification the peer is kicked
- %% 307 -> Inform user that he or she has been kicked from the room
- ?recv1(#presence{from = PeerNickJID, type = unavailable,
- sub_els = [#muc_user{
- status_codes = [307],
- items = [#muc_item{affiliation = member,
- jid = PeerJID,
- role = none}]}]}),
- ?recv1(#iq{type = result, id = I2, sub_els = []}),
- %% Destroying the room
- I3 = send(Config,
- #iq{type = set, to = Room,
- sub_els = [#muc_owner{
- destroy = #muc_destroy{
- reason = Subject}}]}),
- %% Kicked off
- ?recv1(#presence{from = MyNickJID, type = unavailable,
- sub_els = [#muc_user{items = [#muc_item{role = none,
- affiliation = none}],
- destroy = #muc_destroy{
- reason = Subject}}]}),
- ?recv1(#iq{type = result, id = I3, sub_els = []}),
- disconnect(Config).
-
-muc_slave(Config) ->
- PeerJID = ?config(master, Config),
- MUC = muc_jid(Config),
- Room = muc_room_jid(Config),
- MyNick = ?config(slave_nick, Config),
- MyNickJID = jid:replace_resource(Room, MyNick),
- PeerNick = ?config(master_nick, Config),
- PeerNickJID = jid:replace_resource(Room, PeerNick),
- Subject = ?config(room_subject, Config),
- %% Receive an invite from the peer
- #muc_user{invites = [#muc_invite{from = PeerJID}]} =
- xmpp:get_subtag(?recv1(#message{from = Room, type = normal}),
- #muc_user{}),
- %% But before joining we discover the MUC service first
- %% to check if the room is in the disco list
- #iq{type = result,
- sub_els = [#disco_items{items = [#disco_item{jid = Room}]}]} =
- send_recv(Config, #iq{type = get, to = MUC,
- sub_els = [#disco_items{}]}),
- %% Now check if the peer is in the room. We check this via disco#items
- #iq{type = result,
- sub_els = [#disco_items{items = [#disco_item{jid = PeerNickJID,
- name = PeerNick}]}]} =
- send_recv(Config, #iq{type = get, to = Room,
- sub_els = [#disco_items{}]}),
- %% Now joining
- send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}),
- %% First presence is from the participant, i.e. from the peer
- #muc_user{
- status_codes = [],
- items = [#muc_item{role = moderator,
- affiliation = owner}]} =
- xmpp:get_subtag(?recv1(#presence{from = PeerNickJID}), #muc_user{}),
- %% The next is the self-presence (code 110 means it)
- #muc_user{status_codes = [110],
- items = [#muc_item{role = visitor,
- affiliation = none}]} =
- xmpp:get_subtag(?recv1(#presence{from = MyNickJID}), #muc_user{}),
- %% Receive the room subject
- ?recv1(#message{from = PeerNickJID, type = groupchat,
- body = [#text{data = Subject}],
- sub_els = [#delay{}]}),
- %% Receive MUC history
- lists:foreach(
- fun(N) ->
- Text = #text{data = integer_to_binary(N)},
- ?recv1(#message{from = PeerNickJID,
- type = groupchat,
- body = [Text],
- sub_els = [#delay{}]})
- end, lists:seq(1, 5)),
- %% Sending a voice request
- VoiceReq = #xdata{
- type = submit,
- fields =
- [#xdata_field{
- var = <<"FORM_TYPE">>,
- values = [<<"http://jabber.org/protocol/muc#request">>]},
- #xdata_field{
- var = <<"muc#role">>,
- type = 'text-single',
- values = [<<"participant">>]}]},
- send(Config, #message{to = Room, sub_els = [VoiceReq]}),
- %% Becoming a participant
- #muc_user{items = [#muc_item{role = participant,
- affiliation = none}]} =
- xmpp:get_subtag(?recv1(#presence{from = MyNickJID}), #muc_user{}),
- %% Sending private message to the peer
- send(Config, #message{to = PeerNickJID,
- body = [#text{data = Subject}]}),
- %% Becoming a member
- #muc_user{items = [#muc_item{role = participant,
- affiliation = member}]} =
- xmpp:get_subtag(?recv1(#presence{from = MyNickJID}), #muc_user{}),
- %% Sending groupchat message
- send(Config, #message{to = Room, type = groupchat,
- body = [#text{data = Subject}]}),
- %% Receive this message back
- ?recv1(#message{type = groupchat, from = MyNickJID,
- body = [#text{data = Subject}]}),
- %% We're kicked off
- %% 307 -> Inform user that he or she has been kicked from the room
- ?recv1(#presence{from = MyNickJID, type = unavailable,
- sub_els = [#muc_user{
- status_codes = [307],
- items = [#muc_item{affiliation = member,
- role = none}]}]}),
- disconnect(Config).
-
-muc_register_nick(Config, MUC, PrevNick, Nick) ->
- PrevRegistered = if PrevNick /= <<"">> -> true;
- true -> false
- end,
- NewRegistered = if Nick /= <<"">> -> true;
- true -> false
- end,
- %% Request register form
- #iq{type = result,
- sub_els = [#register{registered = PrevRegistered,
- xdata = #xdata{type = form,
- fields = FsWithoutNick}}]} =
- send_recv(Config, #iq{type = get, to = MUC,
- sub_els = [#register{}]}),
- %% Check if previous nick is registered
- PrevNick = proplists:get_value(
- roomnick, muc_register:decode(FsWithoutNick)),
- X = #xdata{type = submit, fields = muc_register:encode([{roomnick, Nick}])},
- %% Submitting form
- #iq{type = result, sub_els = []} =
- send_recv(Config, #iq{type = set, to = MUC,
- sub_els = [#register{xdata = X}]}),
- %% Check if new nick was registered
- #iq{type = result,
- sub_els = [#register{registered = NewRegistered,
- xdata = #xdata{type = form,
- fields = FsWithNick}}]} =
- send_recv(Config, #iq{type = get, to = MUC,
- sub_els = [#register{}]}),
- Nick = proplists:get_value(
- roomnick, muc_register:decode(FsWithNick)).
+%% OK, I know this is retarded, but I didn't find a better way to
+%% split the test cases into different modules
+muc_service_presence_error(Config) ->
+ muc_tests:muc_service_presence_error(Config).
+muc_service_message_error(Config) ->
+ muc_tests:muc_service_message_error(Config).
+muc_service_unknown_ns_iq_error(Config) ->
+ muc_tests:muc_service_unknown_ns_iq_error(Config).
+muc_service_iq_set_error(Config) ->
+ muc_tests:muc_service_iq_set_error(Config).
+muc_service_improper_iq_error(Config) ->
+ muc_tests:muc_service_improper_iq_error(Config).
+muc_service_features(Config) ->
+ muc_tests:muc_service_features(Config).
+muc_service_disco_info_node_error(Config) ->
+ muc_tests:muc_service_disco_info_node_error(Config).
+muc_service_disco_items(Config) ->
+ muc_tests:muc_service_disco_items(Config).
+muc_service_vcard(Config) ->
+ muc_tests:muc_service_vcard(Config).
+muc_service_unique(Config) ->
+ muc_tests:muc_service_unique(Config).
+muc_service_subscriptions(Config) ->
+ muc_tests:muc_service_subscriptions(Config).
+muc_configure_non_existent(Config) ->
+ muc_tests:muc_configure_non_existent(Config).
+muc_cancel_configure_non_existent(Config) ->
+ muc_tests:muc_cancel_configure_non_existent(Config).
muc_register_master(Config) ->
- MUC = muc_jid(Config),
- %% Register nick "master1"
- muc_register_nick(Config, MUC, <<"">>, <<"master1">>),
- %% Unregister nick "master1" via jabber:register
- #iq{type = result, sub_els = []} =
- send_recv(Config, #iq{type = set, to = MUC,
- sub_els = [#register{remove = true}]}),
- %% Register nick "master2"
- muc_register_nick(Config, MUC, <<"">>, <<"master2">>),
- %% Now register nick "master"
- muc_register_nick(Config, MUC, <<"master2">>, <<"master">>),
- %% Wait for slave to fail trying to register nick "master"
- wait_for_slave(Config),
- wait_for_slave(Config),
- %% Now register empty ("") nick, which means we're unregistering
- muc_register_nick(Config, MUC, <<"master">>, <<"">>),
- disconnect(Config).
-
+ muc_tests:muc_register_master(Config).
muc_register_slave(Config) ->
- MUC = muc_jid(Config),
- wait_for_master(Config),
- %% Trying to register occupied nick "master"
- Fs = muc_register:encode([{roomnick, <<"master">>}]),
- X = #xdata{type = submit, fields = Fs},
- #iq{type = error} =
- send_recv(Config, #iq{type = set, to = MUC,
- sub_els = [#register{xdata = X}]}),
- wait_for_master(Config),
- disconnect(Config).
+ muc_tests:muc_register_slave(Config).
+muc_join_conflict_master(Config) ->
+ muc_tests:muc_join_conflict_master(Config).
+muc_join_conflict_slave(Config) ->
+ muc_tests:muc_join_conflict_slave(Config).
+muc_groupchat_msg_master(Config) ->
+ muc_tests:muc_groupchat_msg_master(Config).
+muc_groupchat_msg_slave(Config) ->
+ muc_tests:muc_groupchat_msg_slave(Config).
+muc_private_msg_master(Config) ->
+ muc_tests:muc_private_msg_master(Config).
+muc_private_msg_slave(Config) ->
+ muc_tests:muc_private_msg_slave(Config).
+muc_set_subject_master(Config) ->
+ muc_tests:muc_set_subject_master(Config).
+muc_set_subject_slave(Config) ->
+ muc_tests:muc_set_subject_slave(Config).
+muc_history_master(Config) ->
+ muc_tests:muc_history_master(Config).
+muc_history_slave(Config) ->
+ muc_tests:muc_history_slave(Config).
+muc_invite_master(Config) ->
+ muc_tests:muc_invite_master(Config).
+muc_invite_slave(Config) ->
+ muc_tests:muc_invite_slave(Config).
+muc_invite_members_only_master(Config) ->
+ muc_tests:muc_invite_members_only_master(Config).
+muc_invite_members_only_slave(Config) ->
+ muc_tests:muc_invite_members_only_slave(Config).
+muc_invite_password_protected_master(Config) ->
+ muc_tests:muc_invite_password_protected_master(Config).
+muc_invite_password_protected_slave(Config) ->
+ muc_tests:muc_invite_password_protected_slave(Config).
+muc_voice_request_master(Config) ->
+ muc_tests:muc_voice_request_master(Config).
+muc_voice_request_slave(Config) ->
+ muc_tests:muc_voice_request_slave(Config).
+muc_change_role_master(Config) ->
+ muc_tests:muc_change_role_master(Config).
+muc_change_role_slave(Config) ->
+ muc_tests:muc_change_role_slave(Config).
+muc_kick_master(Config) ->
+ muc_tests:muc_kick_master(Config).
+muc_kick_slave(Config) ->
+ muc_tests:muc_kick_slave(Config).
+muc_change_affiliation_master(Config) ->
+ muc_tests:muc_change_affiliation_master(Config).
+muc_change_affiliation_slave(Config) ->
+ muc_tests:muc_change_affiliation_slave(Config).
+muc_destroy_master(Config) ->
+ muc_tests:muc_destroy_master(Config).
+muc_destroy_slave(Config) ->
+ muc_tests:muc_destroy_slave(Config).
+muc_vcard_master(Config) ->
+ muc_tests:muc_vcard_master(Config).
+muc_vcard_slave(Config) ->
+ muc_tests:muc_vcard_slave(Config).
+muc_nick_change_master(Config) ->
+ muc_tests:muc_nick_change_master(Config).
+muc_nick_change_slave(Config) ->
+ muc_tests:muc_nick_change_slave(Config).
+muc_config_title_desc_master(Config) ->
+ muc_tests:muc_config_title_desc_master(Config).
+muc_config_title_desc_slave(Config) ->
+ muc_tests:muc_config_title_desc_slave(Config).
+muc_config_public_list_master(Config) ->
+ muc_tests:muc_config_public_list_master(Config).
+muc_config_public_list_slave(Config) ->
+ muc_tests:muc_config_public_list_slave(Config).
+muc_config_password_master(Config) ->
+ muc_tests:muc_config_password_master(Config).
+muc_config_password_slave(Config) ->
+ muc_tests:muc_config_password_slave(Config).
+muc_config_whois_master(Config) ->
+ muc_tests:muc_config_whois_master(Config).
+muc_config_whois_slave(Config) ->
+ muc_tests:muc_config_whois_slave(Config).
+muc_config_members_only_master(Config) ->
+ muc_tests:muc_config_members_only_master(Config).
+muc_config_members_only_slave(Config) ->
+ muc_tests:muc_config_members_only_slave(Config).
+muc_config_moderated_master(Config) ->
+ muc_tests:muc_config_moderated_master(Config).
+muc_config_moderated_slave(Config) ->
+ muc_tests:muc_config_moderated_slave(Config).
+muc_config_private_messages_master(Config) ->
+ muc_tests:muc_config_private_messages_master(Config).
+muc_config_private_messages_slave(Config) ->
+ muc_tests:muc_config_private_messages_slave(Config).
+muc_config_query_master(Config) ->
+ muc_tests:muc_config_query_master(Config).
+muc_config_query_slave(Config) ->
+ muc_tests:muc_config_query_slave(Config).
+muc_config_allow_invites_master(Config) ->
+ muc_tests:muc_config_allow_invites_master(Config).
+muc_config_allow_invites_slave(Config) ->
+ muc_tests:muc_config_allow_invites_slave(Config).
+muc_config_visitor_status_master(Config) ->
+ muc_tests:muc_config_visitor_status_master(Config).
+muc_config_visitor_status_slave(Config) ->
+ muc_tests:muc_config_visitor_status_slave(Config).
+muc_config_allow_voice_requests_master(Config) ->
+ muc_tests:muc_config_allow_voice_requests_master(Config).
+muc_config_allow_voice_requests_slave(Config) ->
+ muc_tests:muc_config_allow_voice_requests_slave(Config).
+muc_config_voice_request_interval_master(Config) ->
+ muc_tests:muc_config_voice_request_interval_master(Config).
+muc_config_voice_request_interval_slave(Config) ->
+ muc_tests:muc_config_voice_request_interval_slave(Config).
+muc_config_visitor_nickchange_master(Config) ->
+ muc_tests:muc_config_visitor_nickchange_master(Config).
+muc_config_visitor_nickchange_slave(Config) ->
+ muc_tests:muc_config_visitor_nickchange_slave(Config).
announce_master(Config) ->
MyJID = my_jid(Config),
--- /dev/null
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 15 Oct 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(muc_tests).
+
+%% API
+-compile(export_all).
+-import(suite, [recv_presence/1, send_recv/2, my_jid/1, muc_room_jid/1,
+ send/2, recv_message/1, recv_iq/1, recv/1, muc_jid/1,
+ alt_room_jid/1, wait_for_slave/1, wait_for_master/1,
+ disconnect/1, put_event/2, get_event/1, peer_muc_jid/1,
+ my_muc_jid/1, get_features/2, flush/1, set_opt/3]).
+-include("suite.hrl").
+-include("jid.hrl").
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+%%%===================================================================
+%%% Single tests
+%%%===================================================================
+single_cases() ->
+ {muc_single, [sequence],
+ [muc_service_presence_error,
+ muc_service_message_error,
+ muc_service_unknown_ns_iq_error,
+ muc_service_iq_set_error,
+ muc_service_improper_iq_error,
+ muc_service_features,
+ muc_service_disco_info_node_error,
+ muc_service_disco_items,
+ muc_service_unique,
+ muc_service_vcard,
+ muc_configure_non_existent,
+ muc_cancel_configure_non_existent,
+ muc_service_subscriptions]}.
+
+muc_service_presence_error(Config) ->
+ Service = muc_jid(Config),
+ ServiceResource = jid:replace_resource(Service, randoms:get_string()),
+ lists:foreach(
+ fun(To) ->
+ send(Config, #presence{type = error, to = To}),
+ lists:foreach(
+ fun(Type) ->
+ #presence{type = error} = Err =
+ send_recv(Config, #presence{type = Type, to = To}),
+ #stanza_error{reason = 'service-unavailable'} =
+ xmpp:get_error(Err)
+ end, [available, unavailable])
+ end, [Service, ServiceResource]),
+ disconnect(Config).
+
+muc_service_message_error(Config) ->
+ Service = muc_jid(Config),
+ send(Config, #message{type = error, to = Service}),
+ lists:foreach(
+ fun(Type) ->
+ #message{type = error} = Err1 =
+ send_recv(Config, #message{type = Type, to = Service}),
+ #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err1)
+ end, [chat, normal, headline, groupchat]),
+ ServiceResource = jid:replace_resource(Service, randoms:get_string()),
+ send(Config, #message{type = error, to = ServiceResource}),
+ lists:foreach(
+ fun(Type) ->
+ #message{type = error} = Err2 =
+ send_recv(Config, #message{type = Type, to = ServiceResource}),
+ #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err2)
+ end, [chat, normal, headline, groupchat]),
+ disconnect(Config).
+
+muc_service_unknown_ns_iq_error(Config) ->
+ Service = muc_jid(Config),
+ ServiceResource = jid:replace_resource(Service, randoms:get_string()),
+ lists:foreach(
+ fun(To) ->
+ send(Config, #iq{type = result, to = To}),
+ send(Config, #iq{type = error, to = To}),
+ lists:foreach(
+ fun(Type) ->
+ #iq{type = error} = Err1 =
+ send_recv(Config, #iq{type = Type, to = To,
+ sub_els = [#presence{}]}),
+ #stanza_error{reason = 'service-unavailable'} =
+ xmpp:get_error(Err1)
+ end, [set, get])
+ end, [Service, ServiceResource]),
+ disconnect(Config).
+
+muc_service_iq_set_error(Config) ->
+ Service = muc_jid(Config),
+ lists:foreach(
+ fun(SubEl) ->
+ send(Config, #iq{type = result, to = Service,
+ sub_els = [SubEl]}),
+ #iq{type = error} = Err2 =
+ send_recv(Config, #iq{type = set, to = Service,
+ sub_els = [SubEl]}),
+ #stanza_error{reason = 'not-allowed'} =
+ xmpp:get_error(Err2)
+ end, [#disco_items{}, #disco_info{}, #vcard_temp{},
+ #muc_unique{}, #muc_subscriptions{}]),
+ disconnect(Config).
+
+muc_service_improper_iq_error(Config) ->
+ Service = muc_jid(Config),
+ lists:foreach(
+ fun(SubEl) ->
+ send(Config, #iq{type = result, to = Service,
+ sub_els = [SubEl]}),
+ lists:foreach(
+ fun(Type) ->
+ #iq{type = error} = Err3 =
+ send_recv(Config, #iq{type = Type, to = Service,
+ sub_els = [SubEl]}),
+ #stanza_error{reason = Reason} = xmpp:get_error(Err3),
+ true = Reason /= 'internal-server-error'
+ end, [set, get])
+ end, [#disco_item{jid = Service},
+ #identity{category = <<"category">>, type = <<"type">>},
+ #vcard_email{}, #muc_subscribe{nick = ?config(nick, Config)}]),
+ disconnect(Config).
+
+muc_service_features(Config) ->
+ ServerHost = ?config(server_host, Config),
+ MUC = muc_jid(Config),
+ Features = sets:from_list(get_features(Config, MUC)),
+ MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of
+ true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1];
+ false -> []
+ end,
+ RequiredFeatures = sets:from_list(
+ [?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
+ ?NS_REGISTER, ?NS_MUC, ?NS_RSM,
+ ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE
+ | MAMFeatures]),
+ ct:comment("Checking if all needed disco features are set"),
+ true = sets:is_subset(RequiredFeatures, Features),
+ disconnect(Config).
+
+muc_service_disco_info_node_error(Config) ->
+ MUC = muc_jid(Config),
+ Node = randoms:get_string(),
+ #iq{type = error} = Err =
+ send_recv(Config, #iq{type = get, to = MUC,
+ sub_els = [#disco_info{node = Node}]}),
+ #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err),
+ disconnect(Config).
+
+muc_service_disco_items(Config) ->
+ #jid{server = Service} = muc_jid(Config),
+ Rooms = lists:sort(
+ lists:map(
+ fun(I) ->
+ RoomName = integer_to_binary(I),
+ jid:make(RoomName, Service)
+ end, lists:seq(1, 5))),
+ lists:foreach(
+ fun(Room) ->
+ ok = muc_join_new(Config, Room)
+ end, Rooms),
+ Items = muc_disco_items(Config),
+ Rooms = [J || #disco_item{jid = J} <- Items],
+ lists:foreach(
+ fun(Room) ->
+ ok = muc_leave(Config, Room)
+ end, Rooms),
+ [] = muc_disco_items(Config),
+ disconnect(Config).
+
+muc_service_vcard(Config) ->
+ MUC = muc_jid(Config),
+ ct:comment("Retreiving vCard from ~s", [jid:to_string(MUC)]),
+ #iq{type = result, sub_els = [#vcard_temp{}]} =
+ send_recv(Config, #iq{type = get, to = MUC, sub_els = [#vcard_temp{}]}),
+ disconnect(Config).
+
+muc_service_unique(Config) ->
+ MUC = muc_jid(Config),
+ ct:comment("Requesting muc unique from ~s", [jid:to_string(MUC)]),
+ #iq{type = result, sub_els = [#muc_unique{name = Name}]} =
+ send_recv(Config, #iq{type = get, to = MUC, sub_els = [#muc_unique{}]}),
+ ct:comment("Checking if unique name is set in the response"),
+ <<_, _/binary>> = Name,
+ disconnect(Config).
+
+muc_configure_non_existent(Config) ->
+ [_|_] = muc_get_config(Config),
+ disconnect(Config).
+
+muc_cancel_configure_non_existent(Config) ->
+ Room = muc_room_jid(Config),
+ #iq{type = result, sub_els = []} =
+ send_recv(Config,
+ #iq{to = Room, type = set,
+ sub_els = [#muc_owner{config = #xdata{type = cancel}}]}),
+ disconnect(Config).
+
+muc_service_subscriptions(Config) ->
+ MUC = #jid{server = Service} = muc_jid(Config),
+ Rooms = lists:sort(
+ lists:map(
+ fun(I) ->
+ RoomName = integer_to_binary(I),
+ jid:make(RoomName, Service)
+ end, lists:seq(1, 5))),
+ lists:foreach(
+ fun(Room) ->
+ ok = muc_join_new(Config, Room),
+ [104] = muc_set_config(Config, [{allow_subscription, true}], Room),
+ [] = muc_subscribe(Config, [], Room)
+ end, Rooms),
+ #iq{type = result, sub_els = [#muc_subscriptions{list = JIDs}]} =
+ send_recv(Config, #iq{type = get, to = MUC,
+ sub_els = [#muc_subscriptions{}]}),
+ Rooms = lists:sort(JIDs),
+ lists:foreach(
+ fun(Room) ->
+ ok = muc_unsubscribe(Config, Room),
+ ok = muc_leave(Config, Room)
+ end, Rooms),
+ disconnect(Config).
+
+%%%===================================================================
+%%% Master-slave tests
+%%%===================================================================
+master_slave_cases() ->
+ {muc_master_slave, [sequence],
+ [master_slave_test(muc_register),
+ master_slave_test(muc_groupchat_msg),
+ master_slave_test(muc_private_msg),
+ master_slave_test(muc_set_subject),
+ master_slave_test(muc_history),
+ master_slave_test(muc_invite),
+ master_slave_test(muc_invite_members_only),
+ master_slave_test(muc_invite_password_protected),
+ master_slave_test(muc_voice_request),
+ master_slave_test(muc_change_role),
+ master_slave_test(muc_kick),
+ master_slave_test(muc_change_affiliation),
+ master_slave_test(muc_destroy),
+ master_slave_test(muc_vcard),
+ master_slave_test(muc_nick_change),
+ master_slave_test(muc_config_title_desc),
+ master_slave_test(muc_config_public_list),
+ master_slave_test(muc_config_password),
+ master_slave_test(muc_config_whois),
+ master_slave_test(muc_config_members_only),
+ master_slave_test(muc_config_moderated),
+ master_slave_test(muc_config_private_messages),
+ master_slave_test(muc_config_query),
+ master_slave_test(muc_config_allow_invites),
+ master_slave_test(muc_config_visitor_status),
+ master_slave_test(muc_config_allow_voice_requests),
+ master_slave_test(muc_config_voice_request_interval),
+ master_slave_test(muc_config_visitor_nickchange),
+ master_slave_test(muc_join_conflict)]}.
+
+muc_join_conflict_master(Config) ->
+ ok = muc_join_new(Config),
+ put_event(Config, join),
+ ct:comment("Waiting for 'leave' command from the slave"),
+ leave = get_event(Config),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_join_conflict_slave(Config) ->
+ NewConfig = set_opt(nick, ?config(peer_nick, Config), Config),
+ ct:comment("Waiting for 'join' command from the master"),
+ join = get_event(Config),
+ ct:comment("Fail trying to join the room with conflicting nick"),
+ #stanza_error{reason = 'conflict'} = muc_join(NewConfig),
+ put_event(Config, leave),
+ disconnect(NewConfig).
+
+muc_groupchat_msg_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(slave, Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ MyNick = ?config(nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ ok = muc_master_join(Config),
+ lists:foreach(
+ fun(I) ->
+ Body = xmpp:mk_text(integer_to_binary(I)),
+ send(Config, #message{type = groupchat, to = Room,
+ body = Body}),
+ #message{type = groupchat, from = MyNickJID,
+ body = Body} = recv_message(Config)
+ end, lists:seq(1, 5)),
+ #muc_user{items = [#muc_item{jid = PeerJID,
+ role = none,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_groupchat_msg_slave(Config) ->
+ Room = muc_room_jid(Config),
+ PeerNick = ?config(master_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ {[], _, _} = muc_slave_join(Config),
+ lists:foreach(
+ fun(I) ->
+ Body = xmpp:mk_text(integer_to_binary(I)),
+ #message{type = groupchat, from = PeerNickJID,
+ body = Body} = recv_message(Config)
+ end, lists:seq(1, 5)),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_private_msg_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(slave, Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ ok = muc_master_join(Config),
+ lists:foreach(
+ fun(I) ->
+ Body = xmpp:mk_text(integer_to_binary(I)),
+ send(Config, #message{type = chat, to = PeerNickJID,
+ body = Body})
+ end, lists:seq(1, 5)),
+ #muc_user{items = [#muc_item{jid = PeerJID,
+ role = none,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ct:comment("Fail trying to send a private message to non-existing occupant"),
+ send(Config, #message{type = chat, to = PeerNickJID}),
+ #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config),
+ #stanza_error{reason = 'item-not-found'} = xmpp:get_error(ErrMsg),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_private_msg_slave(Config) ->
+ Room = muc_room_jid(Config),
+ PeerNick = ?config(master_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ {[], _, _} = muc_slave_join(Config),
+ lists:foreach(
+ fun(I) ->
+ Body = xmpp:mk_text(integer_to_binary(I)),
+ #message{type = chat, from = PeerNickJID,
+ body = Body} = recv_message(Config)
+ end, lists:seq(1, 5)),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_set_subject_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(slave, Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ Subject1 = xmpp:mk_text(?config(room_subject, Config)),
+ Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>),
+ ok = muc_master_join(Config),
+ ct:comment("Setting 1st subject"),
+ send(Config, #message{type = groupchat, to = Room,
+ subject = Subject1}),
+ #message{type = groupchat, from = MyNickJID,
+ subject = Subject1} = recv_message(Config),
+ ct:comment("Waiting for the slave to leave"),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ct:comment("Setting 2nd subject"),
+ send(Config, #message{type = groupchat, to = Room,
+ subject = Subject2}),
+ #message{type = groupchat, from = MyNickJID,
+ subject = Subject2} = recv_message(Config),
+ ct:comment("Asking the slave to join"),
+ put_event(Config, join),
+ recv_muc_presence(Config, PeerNickJID, available),
+ ct:comment("Receiving 1st subject set by the slave"),
+ #message{type = groupchat, from = PeerNickJID,
+ subject = Subject1} = recv_message(Config),
+ ct:comment("Disallow subject change"),
+ [104] = muc_set_config(Config, [{changesubject, false}]),
+ ct:comment("Waiting for the slave to leave"),
+ #muc_user{items = [#muc_item{jid = PeerJID,
+ role = none,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_set_subject_slave(Config) ->
+ Room = muc_room_jid(Config),
+ MyNickJID = my_muc_jid(Config),
+ PeerNick = ?config(master_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ Subject1 = xmpp:mk_text(?config(room_subject, Config)),
+ Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>),
+ {[], _, _} = muc_slave_join(Config),
+ ct:comment("Receiving 1st subject set by the master"),
+ #message{type = groupchat, from = PeerNickJID,
+ subject = Subject1} = recv_message(Config),
+ ok = muc_leave(Config),
+ ct:comment("Waiting for 'join' command from the master"),
+ join = get_event(Config),
+ {[], SubjMsg2, _} = muc_join(Config),
+ ct:comment("Checking if the master has set 2nd subject during our absence"),
+ #message{type = groupchat, from = PeerNickJID,
+ subject = Subject2} = SubjMsg2,
+ ct:comment("Setting 1st subject"),
+ send(Config, #message{to = Room, type = groupchat, subject = Subject1}),
+ #message{type = groupchat, from = MyNickJID,
+ subject = Subject1} = recv_message(Config),
+ ct:comment("Waiting for the master to disallow subject change"),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Fail trying to change the subject"),
+ send(Config, #message{to = Room, type = groupchat, subject = Subject2}),
+ #message{from = Room, type = error} = ErrMsg = recv_message(Config),
+ #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_history_master(Config) ->
+ Room = muc_room_jid(Config),
+ ServerHost = ?config(server_host, Config),
+ MyNick = ?config(nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ Size = gen_mod:get_module_opt(ServerHost, mod_muc, history_size,
+ fun(I) when is_integer(I), I>=0 -> I end,
+ 20),
+ ok = muc_join_new(Config),
+ ct:comment("Putting ~p+1 messages in the history", [Size]),
+ %% Only Size messages will be stored
+ lists:foreach(
+ fun(I) ->
+ Body = xmpp:mk_text(integer_to_binary(I)),
+ send(Config, #message{to = Room, type = groupchat,
+ body = Body}),
+ #message{type = groupchat, from = MyNickJID,
+ body = Body} = recv_message(Config)
+ end, lists:seq(0, Size)),
+ wait_for_slave(Config),
+ wait_for_slave(Config),
+ flush(Config),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_history_slave(Config) ->
+ Room = muc_room_jid(Config),
+ PeerNick = ?config(peer_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ ServerHost = ?config(server_host, Config),
+ Size = gen_mod:get_module_opt(ServerHost, mod_muc, history_size,
+ fun(I) when is_integer(I), I>=0 -> I end,
+ 20),
+ {History, _, _} = muc_slave_join(Config),
+ ct:comment("Checking ordering of history events"),
+ BodyList = [binary_to_integer(xmpp:get_text(Body))
+ || #message{type = groupchat, from = From,
+ body = Body} <- History,
+ From == PeerNickJID],
+ BodyList = lists:seq(1, Size),
+ ok = muc_leave(Config),
+ %% If the client wishes to receive no history, it MUST set the 'maxchars'
+ %% attribute to a value of "0" (zero)
+ %% (http://xmpp.org/extensions/xep-0045.html#enter-managehistory)
+ ct:comment("Checking if maxchars=0 yields to no history"),
+ {[], _, _} = muc_join(Config, #muc{history = #muc_history{maxchars = 0}}),
+ ok = muc_leave(Config),
+ ct:comment("Receiving only 10 last stanzas"),
+ {History10, _, _} = muc_join(Config,
+ #muc{history = #muc_history{maxstanzas = 10}}),
+ BodyList10 = [binary_to_integer(xmpp:get_text(Body))
+ || #message{type = groupchat, from = From,
+ body = Body} <- History10,
+ From == PeerNickJID],
+ BodyList10 = lists:nthtail(Size-10, lists:seq(1, Size)),
+ ok = muc_leave(Config),
+ #delay{stamp = TS} = xmpp:get_subtag(hd(History), #delay{}),
+ ct:comment("Receiving all history without the very first element"),
+ {HistoryWithoutFirst, _, _} = muc_join(Config,
+ #muc{history = #muc_history{since = TS}}),
+ BodyListWithoutFirst = [binary_to_integer(xmpp:get_text(Body))
+ || #message{type = groupchat, from = From,
+ body = Body} <- HistoryWithoutFirst,
+ From == PeerNickJID],
+ BodyListWithoutFirst = lists:nthtail(1, lists:seq(1, Size)),
+ ok = muc_leave(Config),
+ wait_for_master(Config),
+ disconnect(Config).
+
+muc_invite_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(peer, Config),
+ ok = muc_join_new(Config),
+ wait_for_slave(Config),
+ %% Inviting the peer
+ send(Config, #message{to = Room, type = normal,
+ sub_els =
+ [#muc_user{
+ invites =
+ [#muc_invite{to = PeerJID}]}]}),
+ #message{from = Room} = DeclineMsg = recv_message(Config),
+ #muc_user{decline = #muc_decline{from = PeerJID}} =
+ xmpp:get_subtag(DeclineMsg, #muc_user{}),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_invite_slave(Config) ->
+ Room = muc_room_jid(Config),
+ wait_for_master(Config),
+ PeerJID = ?config(master, Config),
+ #message{from = Room, type = normal} = Msg = recv_message(Config),
+ #muc_user{invites = [#muc_invite{from = PeerJID}]} =
+ xmpp:get_subtag(Msg, #muc_user{}),
+ %% Decline invitation
+ send(Config,
+ #message{to = Room,
+ sub_els = [#muc_user{
+ decline = #muc_decline{to = PeerJID}}]}),
+ disconnect(Config).
+
+muc_invite_members_only_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(slave, Config),
+ ok = muc_join_new(Config),
+ %% Setting the room to members-only
+ [_|_] = muc_set_config(Config, [{membersonly, true}]),
+ wait_for_slave(Config),
+ %% Inviting the peer
+ send(Config, #message{to = Room, type = normal,
+ sub_els =
+ [#muc_user{
+ invites =
+ [#muc_invite{to = PeerJID}]}]}),
+ #message{from = Room, type = normal} = AffMsg = recv_message(Config),
+ #muc_user{items = [#muc_item{jid = PeerJID, affiliation = member}]} =
+ xmpp:get_subtag(AffMsg, #muc_user{}),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_invite_members_only_slave(Config) ->
+ Room = muc_room_jid(Config),
+ wait_for_master(Config),
+ %% Receiving invitation
+ #message{from = Room, type = normal} = recv_message(Config),
+ disconnect(Config).
+
+muc_invite_password_protected_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(slave, Config),
+ Password = randoms:get_string(),
+ ok = muc_join_new(Config),
+ [104] = muc_set_config(Config, [{passwordprotectedroom, true},
+ {roomsecret, Password}]),
+ put_event(Config, Password),
+ %% Inviting the peer
+ send(Config, #message{to = Room, type = normal,
+ sub_els =
+ [#muc_user{
+ invites =
+ [#muc_invite{to = PeerJID}]}]}),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_invite_password_protected_slave(Config) ->
+ Room = muc_room_jid(Config),
+ Password = get_event(Config),
+ %% Receiving invitation
+ #message{from = Room, type = normal} = Msg = recv_message(Config),
+ #muc_user{password = Password} = xmpp:get_subtag(Msg, #muc_user{}),
+ disconnect(Config).
+
+muc_voice_request_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(slave, Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ ok = muc_join_new(Config),
+ [104] = muc_set_config(Config, [{members_by_default, false}]),
+ wait_for_slave(Config),
+ #muc_user{
+ items = [#muc_item{role = visitor,
+ jid = PeerJID,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ ct:comment("Receiving voice request"),
+ #message{from = Room, type = normal} = VoiceReq = recv_message(Config),
+ #xdata{type = form, fields = Fs} = xmpp:get_subtag(VoiceReq, #xdata{}),
+ [{jid, PeerJID},
+ {request_allow, false},
+ {role, participant},
+ {roomnick, PeerNick}] = lists:sort(muc_request:decode(Fs)),
+ ct:comment("Approving voice request"),
+ ApprovalFs = muc_request:encode([{jid, PeerJID}, {role, participant},
+ {nick, PeerNick}, {request_allow, true}]),
+ send(Config, #message{to = Room, sub_els = [#xdata{type = submit,
+ fields = ApprovalFs}]}),
+ #muc_user{
+ items = [#muc_item{role = participant,
+ jid = PeerJID,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ ct:comment("Waiting for the slave to leave"),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_voice_request_slave(Config) ->
+ Room = muc_room_jid(Config),
+ MyJID = my_jid(Config),
+ MyNick = ?config(nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ wait_for_master(Config),
+ {[], _, _} = muc_join(Config, visitor),
+ ct:comment("Requesting voice"),
+ Fs = muc_request:encode([{role, participant}]),
+ X = #xdata{type = submit, fields = Fs},
+ send(Config, #message{to = Room, sub_els = [X]}),
+ ct:comment("Waiting to become a participant"),
+ #muc_user{
+ items = [#muc_item{role = participant,
+ jid = MyJID,
+ affiliation = none}]} =
+ recv_muc_presence(Config, MyNickJID, available),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_change_role_master(Config) ->
+ Room = muc_room_jid(Config),
+ MyJID = my_jid(Config),
+ MyNick = ?config(nick, Config),
+ PeerJID = ?config(slave, Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ ok = muc_join_new(Config),
+ ct:comment("Waiting for the slave to join"),
+ wait_for_slave(Config),
+ #muc_user{items = [#muc_item{role = participant,
+ jid = PeerJID,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ lists:foreach(
+ fun(Role) ->
+ ct:comment("Checking if the slave is not in the roles list"),
+ case muc_get_role(Config, Role) of
+ [#muc_item{jid = MyJID, affiliation = owner,
+ role = moderator, nick = MyNick}] when Role == moderator ->
+ ok;
+ [] ->
+ ok
+ end,
+ Reason = randoms:get_string(),
+ put_event(Config, {Role, Reason}),
+ ok = muc_set_role(Config, Role, Reason),
+ ct:comment("Receiving role change to ~s", [Role]),
+ #muc_user{
+ items = [#muc_item{role = Role,
+ affiliation = none,
+ reason = Reason}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ [#muc_item{role = Role, affiliation = none,
+ nick = PeerNick}|_] = muc_get_role(Config, Role)
+ end, [visitor, participant, moderator]),
+ put_event(Config, disconnect),
+ wait_for_slave(Config),
+ flush(Config),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_change_role_slave(Config) ->
+ wait_for_master(Config),
+ {[], _, _} = muc_join(Config),
+ muc_change_role_slave(Config, get_event(Config)).
+
+muc_change_role_slave(Config, {Role, Reason}) ->
+ Room = muc_room_jid(Config),
+ MyNick = ?config(slave_nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ ct:comment("Receiving role change to ~s", [Role]),
+ #muc_user{status_codes = Codes,
+ items = [#muc_item{role = Role,
+ affiliation = none,
+ reason = Reason}]} =
+ recv_muc_presence(Config, MyNickJID, available),
+ true = lists:member(110, Codes),
+ muc_change_role_slave(Config, get_event(Config));
+muc_change_role_slave(Config, disconnect) ->
+ ok = muc_leave(Config),
+ wait_for_master(Config),
+ disconnect(Config).
+
+muc_change_affiliation_master(Config) ->
+ Room = muc_room_jid(Config),
+ MyJID = my_jid(Config),
+ MyBareJID = jid:remove_resource(MyJID),
+ MyNick = ?config(nick, Config),
+ PeerJID = ?config(slave, Config),
+ PeerBareJID = jid:remove_resource(PeerJID),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ ok = muc_join_new(Config),
+ ct:comment("Waiting for the slave to join"),
+ wait_for_slave(Config),
+ #muc_user{items = [#muc_item{role = participant,
+ jid = PeerJID,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ lists:foreach(
+ fun({Aff, Role, Status}) ->
+ ct:comment("Checking if slave is not in affiliation list"),
+ case muc_get_affiliation(Config, Aff) of
+ [#muc_item{jid = MyBareJID,
+ affiliation = owner}] when Aff == owner ->
+ ok;
+ [] ->
+ ok
+ end,
+ Reason = randoms:get_string(),
+ put_event(Config, {Aff, Role, Status, Reason}),
+ ok = muc_set_affiliation(Config, Aff, Reason),
+ ct:comment("Receiving affiliation change to ~s", [Aff]),
+ #muc_user{
+ items = [#muc_item{role = Role,
+ affiliation = Aff,
+ actor = Actor,
+ reason = Reason}]} =
+ recv_muc_presence(Config, PeerNickJID, Status),
+ if Aff == outcast ->
+ ct:comment("Checking if actor is set"),
+ #muc_actor{nick = MyNick} = Actor;
+ true ->
+ ok
+ end,
+ Affs = muc_get_affiliation(Config, Aff),
+ ct:comment("Checking if the affiliation was correctly set"),
+ case lists:keyfind(PeerBareJID, #muc_item.jid, Affs) of
+ false when Aff == none ->
+ ok;
+ #muc_item{affiliation = Aff} ->
+ ok
+ end
+ end, [{member, participant, available}, {none, participant, available},
+ {admin, moderator, available}, {owner, moderator, available},
+ {outcast, none, unavailable}]),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_change_affiliation_slave(Config) ->
+ wait_for_master(Config),
+ {[], _, _} = muc_join(Config),
+ muc_change_affiliation_slave(Config, get_event(Config)).
+
+muc_change_affiliation_slave(Config, {Aff, Role, Status, Reason}) ->
+ Room = muc_room_jid(Config),
+ PeerNick = ?config(master_nick, Config),
+ MyNick = ?config(nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ ct:comment("Receiving affiliation change to ~s", [Aff]),
+ #muc_user{status_codes = Codes,
+ items = [#muc_item{role = Role,
+ actor = Actor,
+ affiliation = Aff,
+ reason = Reason}]} =
+ recv_muc_presence(Config, MyNickJID, Status),
+ true = lists:member(110, Codes),
+ if Aff == outcast ->
+ ct:comment("Checking for status code '301' (banned)"),
+ true = lists:member(301, Codes),
+ ct:comment("Checking if actor is set"),
+ #muc_actor{nick = PeerNick} = Actor,
+ disconnect(Config);
+ true ->
+ muc_change_affiliation_slave(Config, get_event(Config))
+ end.
+
+muc_kick_master(Config) ->
+ Room = muc_room_jid(Config),
+ MyNick = ?config(nick, Config),
+ PeerJID = ?config(slave, Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ Reason = <<"Testing">>,
+ ok = muc_join_new(Config),
+ ct:comment("Waiting for the slave to join"),
+ wait_for_slave(Config),
+ #muc_user{items = [#muc_item{role = participant,
+ jid = PeerJID,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ [#muc_item{role = participant, affiliation = none,
+ nick = PeerNick}|_] = muc_get_role(Config, participant),
+ ct:comment("Kicking slave"),
+ ok = muc_set_role(Config, none, Reason),
+ ct:comment("Receiving role change to 'none'"),
+ #muc_user{
+ status_codes = Codes,
+ items = [#muc_item{role = none,
+ affiliation = none,
+ actor = #muc_actor{nick = MyNick},
+ reason = Reason}]} =
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ [] = muc_get_role(Config, participant),
+ ct:comment("Checking if the code is '307' (kicked)"),
+ true = lists:member(307, Codes),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_kick_slave(Config) ->
+ Room = muc_room_jid(Config),
+ PeerNick = ?config(master_nick, Config),
+ MyNick = ?config(nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ Reason = <<"Testing">>,
+ wait_for_master(Config),
+ {[], _, _} = muc_join(Config),
+ ct:comment("Receiving role change to 'none'"),
+ #muc_user{status_codes = Codes,
+ items = [#muc_item{role = none,
+ affiliation = none,
+ actor = #muc_actor{nick = PeerNick},
+ reason = Reason}]} =
+ recv_muc_presence(Config, MyNickJID, unavailable),
+ ct:comment("Checking if codes '110' (self-presence) "
+ "and '307' (kicked) are present"),
+ true = lists:member(110, Codes),
+ true = lists:member(307, Codes),
+ disconnect(Config).
+
+muc_destroy_master(Config) ->
+ Reason = <<"Testing">>,
+ Room = muc_room_jid(Config),
+ AltRoom = alt_room_jid(Config),
+ PeerJID = ?config(peer, Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ MyNick = ?config(nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ ok = muc_join_new(Config),
+ ct:comment("Waiting for slave to join"),
+ wait_for_slave(Config),
+ #muc_user{items = [#muc_item{role = participant,
+ jid = PeerJID,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ wait_for_slave(Config),
+ ok = muc_destroy(Config, Reason),
+ ct:comment("Receiving destruction presence"),
+ #muc_user{items = [#muc_item{role = none,
+ affiliation = none}],
+ destroy = #muc_destroy{jid = AltRoom,
+ reason = Reason}} =
+ recv_muc_presence(Config, MyNickJID, unavailable),
+ disconnect(Config).
+
+muc_destroy_slave(Config) ->
+ Reason = <<"Testing">>,
+ Room = muc_room_jid(Config),
+ AltRoom = alt_room_jid(Config),
+ MyNick = ?config(nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ wait_for_master(Config),
+ {[], _, _} = muc_join(Config),
+ #stanza_error{reason = 'forbidden'} = muc_destroy(Config, Reason),
+ wait_for_master(Config),
+ ct:comment("Receiving destruction presence"),
+ #muc_user{items = [#muc_item{role = none,
+ affiliation = none}],
+ destroy = #muc_destroy{jid = AltRoom,
+ reason = Reason}} =
+ recv_muc_presence(Config, MyNickJID, unavailable),
+ disconnect(Config).
+
+muc_vcard_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ FN = randoms:get_string(),
+ VCard = #vcard_temp{fn = FN},
+ ok = muc_join_new(Config),
+ ct:comment("Waiting for slave to join"),
+ wait_for_slave(Config),
+ #muc_user{items = [#muc_item{role = participant,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ #stanza_error{reason = 'item-not-found'} = muc_get_vcard(Config),
+ ok = muc_set_vcard(Config, VCard),
+ VCard = muc_get_vcard(Config),
+ put_event(Config, VCard),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ leave = get_event(Config),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_vcard_slave(Config) ->
+ wait_for_master(Config),
+ {[], _, _} = muc_join(Config),
+ VCard = get_event(Config),
+ VCard = muc_get_vcard(Config),
+ #stanza_error{reason = 'forbidden'} = muc_set_vcard(Config, VCard),
+ ok = muc_leave(Config),
+ VCard = muc_get_vcard(Config),
+ put_event(Config, leave),
+ disconnect(Config).
+
+muc_nick_change_master(Config) ->
+ NewNick = randoms:get_string(),
+ PeerJID = ?config(peer, Config),
+ PeerNickJID = peer_muc_jid(Config),
+ ok = muc_master_join(Config),
+ put_event(Config, {new_nick, NewNick}),
+ ct:comment("Waiting for nickchange presence from the slave"),
+ #muc_user{status_codes = Codes,
+ items = [#muc_item{jid = PeerJID,
+ nick = NewNick}]} =
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ct:comment("Checking if code '303' (nick change) is set"),
+ true = lists:member(303, Codes),
+ ct:comment("Waiting for updated presence from the slave"),
+ PeerNewNickJID = jid:replace_resource(PeerNickJID, NewNick),
+ recv_muc_presence(Config, PeerNewNickJID, available),
+ ct:comment("Waiting for the slave to leave"),
+ recv_muc_presence(Config, PeerNewNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_nick_change_slave(Config) ->
+ MyJID = my_jid(Config),
+ MyNickJID = my_muc_jid(Config),
+ {[], _, _} = muc_slave_join(Config),
+ {new_nick, NewNick} = get_event(Config),
+ MyNewNickJID = jid:replace_resource(MyNickJID, NewNick),
+ ct:comment("Sending new presence"),
+ send(Config, #presence{to = MyNewNickJID}),
+ ct:comment("Receiving nickchange self-presence"),
+ #muc_user{status_codes = Codes1,
+ items = [#muc_item{role = participant,
+ jid = MyJID,
+ nick = NewNick}]} =
+ recv_muc_presence(Config, MyNickJID, unavailable),
+ ct:comment("Checking if codes '110' (self-presence) and "
+ "'303' (nickchange) are present"),
+ lists:member(110, Codes1),
+ lists:member(303, Codes1),
+ ct:comment("Receiving self-presence update"),
+ #muc_user{status_codes = Codes2,
+ items = [#muc_item{jid = MyJID,
+ role = participant}]} =
+ recv_muc_presence(Config, MyNewNickJID, available),
+ ct:comment("Checking if code '110' (self-presence) is set"),
+ lists:member(110, Codes2),
+ NewConfig = set_opt(nick, NewNick, Config),
+ ok = muc_leave(NewConfig),
+ disconnect(NewConfig).
+
+muc_config_title_desc_master(Config) ->
+ Title = randoms:get_string(),
+ Desc = randoms:get_string(),
+ Room = muc_room_jid(Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ ok = muc_master_join(Config),
+ [104] = muc_set_config(Config, [{roomname, Title}, {roomdesc, Desc}]),
+ RoomCfg = muc_get_config(Config),
+ Title = proplists:get_value(roomname, RoomCfg),
+ Desc = proplists:get_value(roomdesc, RoomCfg),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_title_desc_slave(Config) ->
+ {[], _, _} = muc_slave_join(Config),
+ [104] = muc_recv_config_change_message(Config),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_public_list_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ ok = muc_join_new(Config),
+ wait_for_slave(Config),
+ recv_muc_presence(Config, PeerNickJID, available),
+ lists:member(<<"muc_public">>, get_features(Config, Room)),
+ [104] = muc_set_config(Config, [{public_list, false},
+ {publicroom, false}]),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ lists:member(<<"muc_hidden">>, get_features(Config, Room)),
+ wait_for_slave(Config),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_public_list_slave(Config) ->
+ Room = muc_room_jid(Config),
+ wait_for_master(Config),
+ PeerNick = ?config(peer_nick, Config),
+ PeerNickJID = peer_muc_jid(Config),
+ [#disco_item{jid = Room}] = muc_disco_items(Config),
+ [#disco_item{jid = PeerNickJID,
+ name = PeerNick}] = muc_disco_room_items(Config),
+ {[], _, _} = muc_join(Config),
+ [104] = muc_recv_config_change_message(Config),
+ ok = muc_leave(Config),
+ [] = muc_disco_items(Config),
+ [] = muc_disco_room_items(Config),
+ wait_for_master(Config),
+ disconnect(Config).
+
+muc_config_password_master(Config) ->
+ Password = randoms:get_string(),
+ Room = muc_room_jid(Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ ok = muc_join_new(Config),
+ lists:member(<<"muc_unsecured">>, get_features(Config, Room)),
+ [104] = muc_set_config(Config, [{passwordprotectedroom, true},
+ {roomsecret, Password}]),
+ lists:member(<<"muc_passwordprotected">>, get_features(Config, Room)),
+ put_event(Config, Password),
+ recv_muc_presence(Config, PeerNickJID, available),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_password_slave(Config) ->
+ Password = get_event(Config),
+ #stanza_error{reason = 'not-authorized'} = muc_join(Config),
+ #stanza_error{reason = 'not-authorized'} =
+ muc_join(Config, #muc{password = randoms:get_string()}),
+ {[], _, _} = muc_join(Config, #muc{password = Password}),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_whois_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerNickJID = peer_muc_jid(Config),
+ MyNickJID = my_muc_jid(Config),
+ ok = muc_master_join(Config),
+ lists:member(<<"muc_semianonymous">>, get_features(Config, Room)),
+ [172] = muc_set_config(Config, [{whois, anyone}]),
+ lists:member(<<"muc_nonanonymous">>, get_features(Config, Room)),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ recv_muc_presence(Config, PeerNickJID, available),
+ send(Config, #presence{to = Room}),
+ recv_muc_presence(Config, MyNickJID, available),
+ [173] = muc_set_config(Config, [{whois, moderators}]),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_whois_slave(Config) ->
+ PeerJID = ?config(peer, Config),
+ PeerNickJID = peer_muc_jid(Config),
+ {[], _, _} = muc_slave_join(Config),
+ ct:comment("Checking if the room becomes non-anonymous (code '172')"),
+ [172] = muc_recv_config_change_message(Config),
+ ct:comment("Re-joining in order to check status codes"),
+ ok = muc_leave(Config),
+ {[], _, Codes} = muc_join(Config),
+ ct:comment("Checking if code '100' (non-anonymous) present"),
+ true = lists:member(100, Codes),
+ ct:comment("Receiving presence from peer with JID exposed"),
+ #muc_user{items = [#muc_item{jid = PeerJID}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ ct:comment("Waiting for the room to become anonymous again (code '173')"),
+ [173] = muc_recv_config_change_message(Config),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_members_only_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(peer, Config),
+ PeerBareJID = jid:remove_resource(PeerJID),
+ PeerNickJID = peer_muc_jid(Config),
+ ok = muc_master_join(Config),
+ lists:member(<<"muc_open">>, get_features(Config, Room)),
+ [104] = muc_set_config(Config, [{membersonly, true}]),
+ #muc_user{status_codes = Codes,
+ items = [#muc_item{jid = PeerJID,
+ affiliation = none,
+ role = none}]} =
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ct:comment("Checking if code '322' (non-member) is set"),
+ true = lists:member(322, Codes),
+ lists:member(<<"muc_membersonly">>, get_features(Config, Room)),
+ ct:comment("Waiting for slave to fail joining the room"),
+ set_member = get_event(Config),
+ ok = muc_set_affiliation(Config, member, randoms:get_string()),
+ #message{from = Room, type = normal} = Msg = recv_message(Config),
+ #muc_user{items = [#muc_item{jid = PeerBareJID,
+ affiliation = member}]} =
+ xmpp:get_subtag(Msg, #muc_user{}),
+ ct:comment("Asking peer to join"),
+ put_event(Config, join),
+ ct:comment("Waiting for peer to join"),
+ recv_muc_presence(Config, PeerNickJID, available),
+ ok = muc_set_affiliation(Config, none, randoms:get_string()),
+ ct:comment("Waiting for peer to be kicked"),
+ #muc_user{status_codes = NewCodes,
+ items = [#muc_item{affiliation = none,
+ role = none}]} =
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ct:comment("Checking if code '321' (became non-member in "
+ "members-only room) is set"),
+ true = lists:member(321, NewCodes),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_members_only_slave(Config) ->
+ MyJID = my_jid(Config),
+ MyNickJID = my_muc_jid(Config),
+ {[], _, _} = muc_slave_join(Config),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Getting kicked because the room has become members-only"),
+ #muc_user{status_codes = Codes,
+ items = [#muc_item{jid = MyJID,
+ role = none,
+ affiliation = none}]} =
+ recv_muc_presence(Config, MyNickJID, unavailable),
+ ct:comment("Checking if the code '110' (self-presence) "
+ "and '322' (non-member) is set"),
+ true = lists:member(110, Codes),
+ true = lists:member(322, Codes),
+ ct:comment("Fail trying to join members-only room"),
+ #stanza_error{reason = 'registration-required'} = muc_join(Config),
+ ct:comment("Asking the peer to set us member"),
+ put_event(Config, set_member),
+ ct:comment("Waiting for the peer to ask for join"),
+ join = get_event(Config),
+ {[], _, _} = muc_join(Config, participant, member),
+ #muc_user{status_codes = NewCodes,
+ items = [#muc_item{jid = MyJID,
+ role = none,
+ affiliation = none}]} =
+ recv_muc_presence(Config, MyNickJID, unavailable),
+ ct:comment("Checking if the code '110' (self-presence) "
+ "and '321' (became non-member in members-only room) is set"),
+ true = lists:member(110, NewCodes),
+ true = lists:member(321, NewCodes),
+ disconnect(Config).
+
+muc_config_moderated_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerNickJID = peer_muc_jid(Config),
+ ok = muc_master_join(Config),
+ lists:member(<<"muc_moderated">>, get_features(Config, Room)),
+ ok = muc_set_role(Config, visitor, randoms:get_string()),
+ #muc_user{items = [#muc_item{role = visitor}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ set_unmoderated = get_event(Config),
+ [104] = muc_set_config(Config, [{moderatedroom, false}]),
+ #message{from = PeerNickJID, type = groupchat} = recv_message(Config),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ lists:member(<<"muc_unmoderated">>, get_features(Config, Room)),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_moderated_slave(Config) ->
+ Room = muc_room_jid(Config),
+ MyNickJID = my_muc_jid(Config),
+ {[], _, _} = muc_slave_join(Config),
+ #muc_user{items = [#muc_item{role = visitor}]} =
+ recv_muc_presence(Config, MyNickJID, available),
+ send(Config, #message{to = Room, type = groupchat}),
+ ErrMsg = #message{from = Room, type = error} = recv_message(Config),
+ #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg),
+ put_event(Config, set_unmoderated),
+ [104] = muc_recv_config_change_message(Config),
+ send(Config, #message{to = Room, type = groupchat}),
+ #message{from = MyNickJID, type = groupchat} = recv_message(Config),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_private_messages_master(Config) ->
+ PeerNickJID = peer_muc_jid(Config),
+ ok = muc_master_join(Config),
+ ct:comment("Waiting for a private message from the slave"),
+ #message{from = PeerNickJID, type = chat} = recv_message(Config),
+ ok = muc_set_role(Config, visitor, <<>>),
+ ct:comment("Waiting for the peer to become a visitor"),
+ recv_muc_presence(Config, PeerNickJID, available),
+ ct:comment("Waiting for a private message from the slave"),
+ #message{from = PeerNickJID, type = chat} = recv_message(Config),
+ [104] = muc_set_config(Config, [{allow_private_messages_from_visitors, moderators}]),
+ ct:comment("Waiting for a private message from the slave"),
+ #message{from = PeerNickJID, type = chat} = recv_message(Config),
+ [104] = muc_set_config(Config, [{allow_private_messages_from_visitors, nobody}]),
+ wait_for_slave(Config),
+ [104] = muc_set_config(Config, [{allow_private_messages_from_visitors, anyone},
+ {allow_private_messages, false}]),
+ ct:comment("Fail trying to send a private message"),
+ send(Config, #message{to = PeerNickJID, type = chat}),
+ #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config),
+ #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg),
+ ok = muc_set_role(Config, participant, <<>>),
+ ct:comment("Waiting for the peer to become a participant"),
+ recv_muc_presence(Config, PeerNickJID, available),
+ ct:comment("Waiting for the peer to leave"),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_private_messages_slave(Config) ->
+ MyNickJID = my_muc_jid(Config),
+ PeerNickJID = peer_muc_jid(Config),
+ {[], _, _} = muc_slave_join(Config),
+ ct:comment("Sending a private message"),
+ send(Config, #message{to = PeerNickJID, type = chat}),
+ ct:comment("Waiting to become a visitor"),
+ #muc_user{items = [#muc_item{role = visitor}]} =
+ recv_muc_presence(Config, MyNickJID, available),
+ ct:comment("Sending a private message"),
+ send(Config, #message{to = PeerNickJID, type = chat}),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Sending a private message"),
+ send(Config, #message{to = PeerNickJID, type = chat}),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Fail trying to send a private message"),
+ send(Config, #message{to = PeerNickJID, type = chat}),
+ #message{from = PeerNickJID, type = error} = ErrMsg1 = recv_message(Config),
+ #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg1),
+ wait_for_master(Config),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Waiting to become a participant again"),
+ #muc_user{items = [#muc_item{role = participant}]} =
+ recv_muc_presence(Config, MyNickJID, available),
+ ct:comment("Fail trying to send a private message"),
+ send(Config, #message{to = PeerNickJID, type = chat}),
+ #message{from = PeerNickJID, type = error} = ErrMsg2 = recv_message(Config),
+ #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg2),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_query_master(Config) ->
+ PeerNickJID = peer_muc_jid(Config),
+ ok = muc_join_new(Config),
+ wait_for_slave(Config),
+ recv_muc_presence(Config, PeerNickJID, available),
+ ct:comment("Receiving IQ query from the slave"),
+ #iq{type = get, from = PeerNickJID, id = I,
+ sub_els = [#ping{}]} = recv_iq(Config),
+ send(Config, #iq{type = result, to = PeerNickJID, id = I}),
+ [104] = muc_set_config(Config, [{allow_query_users, false}]),
+ ct:comment("Fail trying to send IQ"),
+ #iq{type = error, from = PeerNickJID} = Err =
+ send_recv(Config, #iq{type = get, to = PeerNickJID,
+ sub_els = [#ping{}]}),
+ #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_query_slave(Config) ->
+ PeerNickJID = peer_muc_jid(Config),
+ wait_for_master(Config),
+ ct:comment("Checking if IQ queries are denied from non-occupants"),
+ #iq{type = error, from = PeerNickJID} = Err1 =
+ send_recv(Config, #iq{type = get, to = PeerNickJID,
+ sub_els = [#ping{}]}),
+ #stanza_error{reason = 'not-acceptable'} = xmpp:get_error(Err1),
+ {[], _, _} = muc_join(Config),
+ ct:comment("Sending IQ to the master"),
+ #iq{type = result, from = PeerNickJID, sub_els = []} =
+ send_recv(Config, #iq{to = PeerNickJID, type = get, sub_els = [#ping{}]}),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Fail trying to send IQ"),
+ #iq{type = error, from = PeerNickJID} = Err2 =
+ send_recv(Config, #iq{type = get, to = PeerNickJID,
+ sub_els = [#ping{}]}),
+ #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err2),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_allow_invites_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(peer, Config),
+ PeerNickJID = peer_muc_jid(Config),
+ ok = muc_master_join(Config),
+ [104] = muc_set_config(Config, [{allowinvites, true}]),
+ ct:comment("Receiving an invitation from the slave"),
+ #message{from = Room, type = normal} = recv_message(Config),
+ [104] = muc_set_config(Config, [{allowinvites, false}]),
+ send_invitation = get_event(Config),
+ ct:comment("Sending an invitation"),
+ send(Config, #message{to = Room, type = normal,
+ sub_els =
+ [#muc_user{
+ invites =
+ [#muc_invite{to = PeerJID}]}]}),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_allow_invites_slave(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(peer, Config),
+ InviteMsg = #message{to = Room, type = normal,
+ sub_els =
+ [#muc_user{
+ invites =
+ [#muc_invite{to = PeerJID}]}]},
+ {[], _, _} = muc_slave_join(Config),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Sending an invitation"),
+ send(Config, InviteMsg),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Fail sending an invitation"),
+ send(Config, InviteMsg),
+ #message{from = Room, type = error} = Err = recv_message(Config),
+ #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err),
+ ct:comment("Checking if the master is still able to send invitations"),
+ put_event(Config, send_invitation),
+ #message{from = Room, type = normal} = recv_message(Config),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_visitor_status_master(Config) ->
+ PeerNickJID = peer_muc_jid(Config),
+ Status = xmpp:mk_text(randoms:get_string()),
+ ok = muc_join_new(Config),
+ [104] = muc_set_config(Config, [{members_by_default, false}]),
+ ct:comment("Asking the slave to join as a visitor"),
+ put_event(Config, {join, Status}),
+ #muc_user{items = [#muc_item{role = visitor}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ ct:comment("Receiving status change from the visitor"),
+ #presence{from = PeerNickJID, status = Status} = recv_presence(Config),
+ [104] = muc_set_config(Config, [{allow_visitor_status, false}]),
+ ct:comment("Receiving status change with <status/> stripped"),
+ #presence{from = PeerNickJID, status = []} = recv_presence(Config),
+ ct:comment("Waiting for the slave to leave"),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_visitor_status_slave(Config) ->
+ Room = muc_room_jid(Config),
+ MyNickJID = my_muc_jid(Config),
+ ct:comment("Waiting for 'join' command from the master"),
+ {join, Status} = get_event(Config),
+ {[], _, _} = muc_join(Config, visitor, none),
+ ct:comment("Sending status change"),
+ send(Config, #presence{to = Room, status = Status}),
+ #presence{from = MyNickJID, status = Status} = recv_presence(Config),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Sending status change again"),
+ send(Config, #presence{to = Room, status = Status}),
+ #presence{from = MyNickJID, status = []} = recv_presence(Config),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_allow_voice_requests_master(Config) ->
+ PeerNickJID = peer_muc_jid(Config),
+ ok = muc_join_new(Config),
+ [104] = muc_set_config(Config, [{members_by_default, false}]),
+ ct:comment("Asking the slave to join as a visitor"),
+ put_event(Config, join),
+ #muc_user{items = [#muc_item{role = visitor}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ [104] = muc_set_config(Config, [{allow_voice_requests, false}]),
+ ct:comment("Waiting for the slave to leave"),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_allow_voice_requests_slave(Config) ->
+ Room = muc_room_jid(Config),
+ ct:comment("Waiting for 'join' command from the master"),
+ join = get_event(Config),
+ {[], _, _} = muc_join(Config, visitor),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Fail sending voice request"),
+ Fs = muc_request:encode([{role, participant}]),
+ X = #xdata{type = submit, fields = Fs},
+ send(Config, #message{to = Room, sub_els = [X]}),
+ #message{from = Room, type = error} = Err = recv_message(Config),
+ #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_voice_request_interval_master(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(peer, Config),
+ PeerNick = ?config(peer_nick, Config),
+ PeerNickJID = peer_muc_jid(Config),
+ ok = muc_join_new(Config),
+ [104] = muc_set_config(Config, [{members_by_default, false}]),
+ ct:comment("Asking the slave to join as a visitor"),
+ put_event(Config, join),
+ #muc_user{items = [#muc_item{role = visitor}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ [104] = muc_set_config(Config, [{voice_request_min_interval, 5}]),
+ ct:comment("Receiving a voice request from slave"),
+ #message{from = Room, type = normal} = recv_message(Config),
+ ct:comment("Deny voice request at first"),
+ Fs = muc_request:encode([{jid, PeerJID}, {role, participant},
+ {nick, PeerNick}, {request_allow, false}]),
+ send(Config, #message{to = Room, sub_els = [#xdata{type = submit,
+ fields = Fs}]}),
+ put_event(Config, denied),
+ ct:comment("Waiting for repeated voice request from the slave"),
+ #message{from = Room, type = normal} = recv_message(Config),
+ ct:comment("Waiting for the slave to leave"),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_voice_request_interval_slave(Config) ->
+ Room = muc_room_jid(Config),
+ Fs = muc_request:encode([{role, participant}]),
+ X = #xdata{type = submit, fields = Fs},
+ ct:comment("Waiting for 'join' command from the master"),
+ join = get_event(Config),
+ {[], _, _} = muc_join(Config, visitor),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Sending voice request"),
+ send(Config, #message{to = Room, sub_els = [X]}),
+ ct:comment("Waiting for the master to deny our voice request"),
+ denied = get_event(Config),
+ ct:comment("Requesting voice again"),
+ send(Config, #message{to = Room, sub_els = [X]}),
+ ct:comment("Receving voice request error because we're sending to fast"),
+ #message{from = Room, type = error} = Err = recv_message(Config),
+ #stanza_error{reason = 'resource-constraint'} = xmpp:get_error(Err),
+ ct:comment("Waiting for 5 seconds"),
+ timer:sleep(timer:seconds(5)),
+ ct:comment("Repeating again"),
+ send(Config, #message{to = Room, sub_els = [X]}),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_visitor_nickchange_master(Config) ->
+ PeerNickJID = peer_muc_jid(Config),
+ ok = muc_join_new(Config),
+ [104] = muc_set_config(Config, [{members_by_default, false}]),
+ ct:comment("Asking the slave to join as a visitor"),
+ put_event(Config, join),
+ ct:comment("Waiting for the slave to join"),
+ #muc_user{items = [#muc_item{role = visitor}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ [104] = muc_set_config(Config, [{allow_visitor_nickchange, false}]),
+ ct:comment("Waiting for the slave to leave"),
+ recv_muc_presence(Config, PeerNickJID, unavailable),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_config_visitor_nickchange_slave(Config) ->
+ NewNick = randoms:get_string(),
+ MyNickJID = my_muc_jid(Config),
+ MyNewNickJID = jid:replace_resource(MyNickJID, NewNick),
+ ct:comment("Waiting for 'join' command from the master"),
+ join = get_event(Config),
+ {[], _, _} = muc_join(Config, visitor),
+ [104] = muc_recv_config_change_message(Config),
+ ct:comment("Fail trying to change nickname"),
+ send(Config, #presence{to = MyNewNickJID}),
+ #presence{from = MyNewNickJID, type = error} = Err = recv_presence(Config),
+ #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err),
+ ok = muc_leave(Config),
+ disconnect(Config).
+
+muc_register_master(Config) ->
+ MUC = muc_jid(Config),
+ %% Register nick "master1"
+ muc_register_nick(Config, MUC, <<"">>, <<"master1">>),
+ %% Unregister nick "master1" via jabber:register
+ #iq{type = result, sub_els = []} =
+ send_recv(Config, #iq{type = set, to = MUC,
+ sub_els = [#register{remove = true}]}),
+ %% Register nick "master2"
+ muc_register_nick(Config, MUC, <<"">>, <<"master2">>),
+ %% Now register nick "master"
+ muc_register_nick(Config, MUC, <<"master2">>, <<"master">>),
+ %% Wait for slave to fail trying to register nick "master"
+ wait_for_slave(Config),
+ wait_for_slave(Config),
+ %% Now register empty ("") nick, which means we're unregistering
+ muc_register_nick(Config, MUC, <<"master">>, <<"">>),
+ disconnect(Config).
+
+muc_register_slave(Config) ->
+ MUC = muc_jid(Config),
+ wait_for_master(Config),
+ %% Trying to register occupied nick "master"
+ Fs = muc_register:encode([{roomnick, <<"master">>}]),
+ X = #xdata{type = submit, fields = Fs},
+ #iq{type = error} =
+ send_recv(Config, #iq{type = set, to = MUC,
+ sub_els = [#register{xdata = X}]}),
+ wait_for_master(Config),
+ disconnect(Config).
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+master_slave_test(T) ->
+ {T, [parallel], [list_to_atom(atom_to_list(T) ++ "_master"),
+ list_to_atom(atom_to_list(T) ++ "_slave")]}.
+
+recv_muc_presence(Config, From, Type) ->
+ Pres = #presence{from = From, type = Type} = recv_presence(Config),
+ xmpp:get_subtag(Pres, #muc_user{}).
+
+muc_join_new(Config) ->
+ muc_join_new(Config, muc_room_jid(Config)).
+
+muc_join_new(Config, Room) ->
+ MyJID = my_jid(Config),
+ MyNick = ?config(nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ ct:comment("Joining new room"),
+ send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}),
+ %% As per XEP-0045 we MUST receive stanzas in the following order:
+ %% 1. In-room presence from other occupants
+ %% 2. In-room presence from the joining entity itself (so-called "self-presence")
+ %% 3. Room history (if any)
+ %% 4. The room subject
+ %% 5. Live messages, presence updates, new user joins, etc.
+ %% As this is the newly created room, we receive only the 2nd and 4th stanza.
+ #muc_user{
+ status_codes = Codes,
+ items = [#muc_item{role = moderator,
+ jid = MyJID,
+ affiliation = owner}]} =
+ xmpp:get_subtag(?recv1(#presence{from = MyNickJID}), #muc_user{}),
+ ct:comment("Checking if codes '110' (self-presence) and "
+ "'201' (new room) is set"),
+ true = lists:member(110, Codes),
+ true = lists:member(201, Codes),
+ ct:comment("Receiving empty room subject"),
+ #message{from = Room, type = groupchat, body = [],
+ subject = [#text{data = <<>>}]} = recv_message(Config),
+ case ?config(persistent_room, Config) of
+ true ->
+ [104] = muc_set_config(Config, [{persistentroom, true}], Room),
+ ok;
+ false ->
+ ok
+ end.
+
+muc_recv_history_and_subject(Config) ->
+ ct:comment("Receiving room history and/or subject"),
+ muc_recv_history_and_subject(Config, []).
+
+muc_recv_history_and_subject(Config, History) ->
+ Room = muc_room_jid(Config),
+ #message{type = groupchat, subject = Subj,
+ body = Body, thread = Thread} = Msg = recv_message(Config),
+ case xmpp:get_subtag(Msg, #delay{}) of
+ #delay{from = Room} ->
+ muc_recv_history_and_subject(Config, [Msg|History]);
+ false when Subj /= [], Body == [], Thread == undefined ->
+ {lists:reverse(History), Msg}
+ end.
+
+muc_join(Config) ->
+ muc_join(Config, participant, none, #muc{}).
+
+muc_join(Config, Role) when is_atom(Role) ->
+ muc_join(Config, Role, none, #muc{});
+muc_join(Config, #muc{} = SubEl) ->
+ muc_join(Config, participant, none, SubEl).
+
+muc_join(Config, Role, Aff) when is_atom(Role), is_atom(Aff) ->
+ muc_join(Config, Role, Aff, #muc{});
+muc_join(Config, Role, #muc{} = SubEl) when is_atom(Role) ->
+ muc_join(Config, Role, none, SubEl).
+
+muc_join(Config, Role, Aff, SubEl) ->
+ ct:comment("Joining existing room as ~s/~s", [Aff, Role]),
+ MyJID = my_jid(Config),
+ Room = muc_room_jid(Config),
+ MyNick = ?config(nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ PeerNick = ?config(peer_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ send(Config, #presence{to = MyNickJID, sub_els = [SubEl]}),
+ case recv_presence(Config) of
+ #presence{type = error, from = MyNickJID} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{});
+ #presence{type = available, from = PeerNickJID} = Pres ->
+ #muc_user{items = [#muc_item{role = moderator,
+ affiliation = owner}]} =
+ xmpp:get_subtag(Pres, #muc_user{}),
+ ct:comment("Receiving initial self-presence"),
+ #muc_user{status_codes = Codes,
+ items = [#muc_item{role = Role,
+ jid = MyJID,
+ affiliation = Aff}]} =
+ recv_muc_presence(Config, MyNickJID, available),
+ ct:comment("Checking if code '110' (self-presence) is set"),
+ true = lists:member(110, Codes),
+ {History, Subj} = muc_recv_history_and_subject(Config),
+ {History, Subj, Codes};
+ #presence{type = available, from = MyNickJID} = Pres ->
+ #muc_user{status_codes = Codes,
+ items = [#muc_item{role = Role,
+ jid = MyJID,
+ affiliation = Aff}]} =
+ xmpp:get_subtag(Pres, #muc_user{}),
+ ct:comment("Checking if code '110' (self-presence) is set"),
+ true = lists:member(110, Codes),
+ {History, Subj} = muc_recv_history_and_subject(Config),
+ {empty, History, Subj, Codes}
+ end.
+
+muc_leave(Config) ->
+ muc_leave(Config, muc_room_jid(Config)).
+
+muc_leave(Config, Room) ->
+ MyJID = my_jid(Config),
+ MyNick = ?config(nick, Config),
+ MyNickJID = jid:replace_resource(Room, MyNick),
+ Mode = ?config(mode, Config),
+ IsPersistent = ?config(persistent_room, Config),
+ if Mode /= slave, IsPersistent ->
+ [104] = muc_set_config(Config, [{persistentroom, false}], Room);
+ true ->
+ ok
+ end,
+ ct:comment("Leaving the room"),
+ send(Config, #presence{to = MyNickJID, type = unavailable}),
+ #muc_user{
+ status_codes = Codes,
+ items = [#muc_item{role = none, jid = MyJID}]} =
+ xmpp:get_subtag(?recv1(#presence{from = MyNickJID,
+ type = unavailable}), #muc_user{}),
+ ct:comment("Checking if code '110' (self-presence) is set"),
+ true = lists:member(110, Codes),
+ ok.
+
+muc_get_config(Config) ->
+ ct:comment("Get room config"),
+ Room = muc_room_jid(Config),
+ case send_recv(Config,
+ #iq{type = get, to = Room,
+ sub_els = [#muc_owner{}]}) of
+ #iq{type = result,
+ sub_els = [#muc_owner{config = #xdata{type = form} = X}]} ->
+ muc_roomconfig:decode(X#xdata.fields);
+ #iq{type = error} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{})
+ end.
+
+muc_set_config(Config, RoomConfig) ->
+ muc_set_config(Config, RoomConfig, muc_room_jid(Config)).
+
+muc_set_config(Config, RoomConfig, Room) ->
+ ct:comment("Set room config: ~p", [RoomConfig]),
+ Fs = case RoomConfig of
+ [] -> [];
+ _ -> muc_roomconfig:encode(RoomConfig)
+ end,
+ case send_recv(Config,
+ #iq{type = set, to = Room,
+ sub_els = [#muc_owner{config = #xdata{type = submit,
+ fields = Fs}}]}) of
+ #iq{type = result, sub_els = []} ->
+ #message{from = Room, type = groupchat} = Msg = recv_message(Config),
+ #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}),
+ lists:sort(Codes);
+ #iq{type = error} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{})
+ end.
+
+muc_create_persistent(Config) ->
+ [_|_] = muc_get_config(Config),
+ [] = muc_set_config(Config, [{persistentroom, true}], false),
+ ok.
+
+muc_destroy(Config) ->
+ muc_destroy(Config, <<>>).
+
+muc_destroy(Config, Reason) ->
+ Room = muc_room_jid(Config),
+ AltRoom = alt_room_jid(Config),
+ ct:comment("Destroying a room"),
+ case send_recv(Config,
+ #iq{type = set, to = Room,
+ sub_els = [#muc_owner{destroy = #muc_destroy{
+ reason = Reason,
+ jid = AltRoom}}]}) of
+ #iq{type = result, sub_els = []} ->
+ ok;
+ #iq{type = error} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{})
+ end.
+
+muc_disco_items(Config) ->
+ MUC = muc_jid(Config),
+ ct:comment("Performing disco#items request to ~s", [jid:to_string(MUC)]),
+ #iq{type = result, from = MUC, sub_els = [DiscoItems]} =
+ send_recv(Config, #iq{type = get, to = MUC,
+ sub_els = [#disco_items{}]}),
+ lists:keysort(#disco_item.jid, DiscoItems#disco_items.items).
+
+muc_disco_room_items(Config) ->
+ Room = muc_room_jid(Config),
+ #iq{type = result, from = Room, sub_els = [DiscoItems]} =
+ send_recv(Config, #iq{type = get, to = Room,
+ sub_els = [#disco_items{}]}),
+ DiscoItems#disco_items.items.
+
+muc_get_affiliations(Config, Aff) ->
+ Room = muc_room_jid(Config),
+ case send_recv(Config,
+ #iq{type = get, to = Room,
+ sub_els = [#muc_admin{items = [#muc_item{affiliation = Aff}]}]}) of
+ #iq{type = result, sub_els = [#muc_admin{items = Items}]} ->
+ Items;
+ #iq{type = error} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{})
+ end.
+
+muc_master_join(Config) ->
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(slave, Config),
+ PeerNick = ?config(slave_nick, Config),
+ PeerNickJID = jid:replace_resource(Room, PeerNick),
+ ok = muc_join_new(Config),
+ wait_for_slave(Config),
+ #muc_user{items = [#muc_item{jid = PeerJID,
+ role = participant,
+ affiliation = none}]} =
+ recv_muc_presence(Config, PeerNickJID, available),
+ ok.
+
+muc_slave_join(Config) ->
+ wait_for_master(Config),
+ muc_join(Config).
+
+muc_set_role(Config, Role, Reason) ->
+ ct:comment("Changing role to ~s", [Role]),
+ Room = muc_room_jid(Config),
+ PeerNick = ?config(slave_nick, Config),
+ case send_recv(
+ Config,
+ #iq{type = set, to = Room,
+ sub_els =
+ [#muc_admin{
+ items = [#muc_item{role = Role,
+ reason = Reason,
+ nick = PeerNick}]}]}) of
+ #iq{type = result, sub_els = []} ->
+ ok;
+ #iq{type = error} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{})
+ end.
+
+muc_get_role(Config, Role) ->
+ ct:comment("Requesting list for role '~s'", [Role]),
+ Room = muc_room_jid(Config),
+ case send_recv(
+ Config,
+ #iq{type = get, to = Room,
+ sub_els = [#muc_admin{
+ items = [#muc_item{role = Role}]}]}) of
+ #iq{type = result, sub_els = [#muc_admin{items = Items}]} ->
+ lists:keysort(#muc_item.affiliation, Items);
+ #iq{type = error} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{})
+ end.
+
+muc_set_affiliation(Config, Aff, Reason) ->
+ ct:comment("Changing affiliation to ~s", [Aff]),
+ Room = muc_room_jid(Config),
+ PeerJID = ?config(slave, Config),
+ PeerBareJID = jid:remove_resource(PeerJID),
+ case send_recv(
+ Config,
+ #iq{type = set, to = Room,
+ sub_els =
+ [#muc_admin{
+ items = [#muc_item{affiliation = Aff,
+ reason = Reason,
+ jid = PeerBareJID}]}]}) of
+ #iq{type = result, sub_els = []} ->
+ ok;
+ #iq{type = error} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{})
+ end.
+
+muc_get_affiliation(Config, Aff) ->
+ ct:comment("Requesting list for affiliation '~s'", [Aff]),
+ Room = muc_room_jid(Config),
+ case send_recv(
+ Config,
+ #iq{type = get, to = Room,
+ sub_els = [#muc_admin{
+ items = [#muc_item{affiliation = Aff}]}]}) of
+ #iq{type = result, sub_els = [#muc_admin{items = Items}]} ->
+ Items;
+ #iq{type = error} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{})
+ end.
+
+muc_set_vcard(Config, VCard) ->
+ Room = muc_room_jid(Config),
+ ct:comment("Setting vCard for ~s", [jid:to_string(Room)]),
+ case send_recv(Config, #iq{type = set, to = Room,
+ sub_els = [VCard]}) of
+ #iq{type = result, sub_els = []} ->
+ ok;
+ #iq{type = error} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{})
+ end.
+
+muc_get_vcard(Config) ->
+ Room = muc_room_jid(Config),
+ ct:comment("Retreiving vCard from ~s", [jid:to_string(Room)]),
+ case send_recv(Config, #iq{type = get, to = Room,
+ sub_els = [#vcard_temp{}]}) of
+ #iq{type = result, sub_els = [VCard]} ->
+ VCard;
+ #iq{type = error} = Err ->
+ xmpp:get_subtag(Err, #stanza_error{})
+ end.
+
+muc_recv_config_change_message(Config) ->
+ ct:comment("Receiving configuration change notification message"),
+ Room = muc_room_jid(Config),
+ #message{type = groupchat, from = Room} = Msg = recv_message(Config),
+ #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}),
+ lists:sort(Codes).
+
+muc_register_nick(Config, MUC, PrevNick, Nick) ->
+ PrevRegistered = if PrevNick /= <<"">> -> true;
+ true -> false
+ end,
+ NewRegistered = if Nick /= <<"">> -> true;
+ true -> false
+ end,
+ ct:comment("Requesting registration form"),
+ #iq{type = result,
+ sub_els = [#register{registered = PrevRegistered,
+ xdata = #xdata{type = form,
+ fields = FsWithoutNick}}]} =
+ send_recv(Config, #iq{type = get, to = MUC,
+ sub_els = [#register{}]}),
+ ct:comment("Checking if previous nick is registered"),
+ PrevNick = proplists:get_value(
+ roomnick, muc_register:decode(FsWithoutNick)),
+ X = #xdata{type = submit, fields = muc_register:encode([{roomnick, Nick}])},
+ ct:comment("Submitting registration form"),
+ #iq{type = result, sub_els = []} =
+ send_recv(Config, #iq{type = set, to = MUC,
+ sub_els = [#register{xdata = X}]}),
+ ct:comment("Checking if new nick was registered"),
+ #iq{type = result,
+ sub_els = [#register{registered = NewRegistered,
+ xdata = #xdata{type = form,
+ fields = FsWithNick}}]} =
+ send_recv(Config, #iq{type = get, to = MUC,
+ sub_els = [#register{}]}),
+ Nick = proplists:get_value(
+ roomnick, muc_register:decode(FsWithNick)).
+
+muc_subscribe(Config, Events, Room) ->
+ MyNick = ?config(nick, Config),
+ case send_recv(Config,
+ #iq{type = set, to = Room,
+ sub_els = [#muc_subscribe{nick = MyNick,
+ events = Events}]}) of
+ #iq{type = result, sub_els = [#muc_subscribe{events = ResEvents}]} ->
+ lists:sort(ResEvents);
+ #iq{type = error} = Err ->
+ xmpp:get_error(Err)
+ end.
+
+muc_unsubscribe(Config, Room) ->
+ case send_recv(Config, #iq{type = set, to = Room,
+ sub_els = [#muc_unsubscribe{}]}) of
+ #iq{type = result, sub_els = []} ->
+ ok;
+ #iq{type = error} = Err ->
+ xmpp:get_error(Err)
+ end.