25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-22 16:20:52 +01:00

Merge branch 'muc-self-presence'

This commit is contained in:
Evgeniy Khramtsov 2018-04-14 18:32:12 +03:00
commit a5284229cb
5 changed files with 161 additions and 52 deletions

View File

@ -63,6 +63,7 @@
max_users = ?MAX_USERS_DEFAULT :: non_neg_integer() | none, max_users = ?MAX_USERS_DEFAULT :: non_neg_integer() | none,
logging = false :: boolean(), logging = false :: boolean(),
vcard = <<"">> :: binary(), vcard = <<"">> :: binary(),
vcard_xupdate = undefined :: undefined | external | binary(),
captcha_whitelist = (?SETS):empty() :: ?TGB_SET, captcha_whitelist = (?SETS):empty() :: ?TGB_SET,
mam = false :: boolean(), mam = false :: boolean(),
pubsub = <<"">> :: binary() pubsub = <<"">> :: binary()

View File

@ -38,7 +38,8 @@
-export([read_caps/1, list_features/1, caps_stream_features/2, -export([read_caps/1, list_features/1, caps_stream_features/2,
disco_features/5, disco_identity/5, disco_info/5, disco_features/5, disco_identity/5, disco_info/5,
get_features/2, export/1, import_info/0, import/5, get_features/2, export/1, import_info/0, import/5,
get_user_caps/2, import_start/2, import_stop/2]). get_user_caps/2, import_start/2, import_stop/2,
compute_disco_hash/2, is_valid_node/1]).
%% gen_mod callbacks %% gen_mod callbacks
-export([start/2, stop/1, reload/3, depends/2]). -export([start/2, stop/1, reload/3, depends/2]).
@ -412,13 +413,13 @@ make_my_disco_hash(Host) ->
DiscoInfo = #disco_info{identities = Identities, DiscoInfo = #disco_info{identities = Identities,
features = Feats, features = Feats,
xdata = Info}, xdata = Info},
make_disco_hash(DiscoInfo, sha); compute_disco_hash(DiscoInfo, sha);
_Err -> <<"">> _Err -> <<"">>
end. end.
-type digest_type() :: md5 | sha | sha224 | sha256 | sha384 | sha512. -type digest_type() :: md5 | sha | sha224 | sha256 | sha384 | sha512.
-spec make_disco_hash(disco_info(), digest_type()) -> binary(). -spec compute_disco_hash(disco_info(), digest_type()) -> binary().
make_disco_hash(DiscoInfo, Algo) -> compute_disco_hash(DiscoInfo, Algo) ->
Concat = list_to_binary([concat_identities(DiscoInfo), Concat = list_to_binary([concat_identities(DiscoInfo),
concat_features(DiscoInfo), concat_info(DiscoInfo)]), concat_features(DiscoInfo), concat_info(DiscoInfo)]),
base64:encode(case Algo of base64:encode(case Algo of
@ -434,17 +435,17 @@ make_disco_hash(DiscoInfo, Algo) ->
check_hash(Caps, DiscoInfo) -> check_hash(Caps, DiscoInfo) ->
case Caps#caps.hash of case Caps#caps.hash of
<<"md5">> -> <<"md5">> ->
Caps#caps.version == make_disco_hash(DiscoInfo, md5); Caps#caps.version == compute_disco_hash(DiscoInfo, md5);
<<"sha-1">> -> <<"sha-1">> ->
Caps#caps.version == make_disco_hash(DiscoInfo, sha); Caps#caps.version == compute_disco_hash(DiscoInfo, sha);
<<"sha-224">> -> <<"sha-224">> ->
Caps#caps.version == make_disco_hash(DiscoInfo, sha224); Caps#caps.version == compute_disco_hash(DiscoInfo, sha224);
<<"sha-256">> -> <<"sha-256">> ->
Caps#caps.version == make_disco_hash(DiscoInfo, sha256); Caps#caps.version == compute_disco_hash(DiscoInfo, sha256);
<<"sha-384">> -> <<"sha-384">> ->
Caps#caps.version == make_disco_hash(DiscoInfo, sha384); Caps#caps.version == compute_disco_hash(DiscoInfo, sha384);
<<"sha-512">> -> <<"sha-512">> ->
Caps#caps.version == make_disco_hash(DiscoInfo, sha512); Caps#caps.version == compute_disco_hash(DiscoInfo, sha512);
_ -> true _ -> true
end. end.

View File

@ -290,7 +290,7 @@ normal_state({route, <<"">>,
process_iq_admin(From, IQ, StateData); process_iq_admin(From, IQ, StateData);
?NS_MUC_OWNER -> ?NS_MUC_OWNER ->
process_iq_owner(From, IQ, StateData); process_iq_owner(From, IQ, StateData);
?NS_DISCO_INFO when SubEl#disco_info.node == <<>> -> ?NS_DISCO_INFO ->
process_iq_disco_info(From, IQ, StateData); process_iq_disco_info(From, IQ, StateData);
?NS_DISCO_ITEMS -> ?NS_DISCO_ITEMS ->
process_iq_disco_items(From, IQ, StateData); process_iq_disco_items(From, IQ, StateData);
@ -2068,12 +2068,30 @@ presence_broadcast_allowed(JID, StateData) ->
-spec send_initial_presences_and_messages( -spec send_initial_presences_and_messages(
jid(), binary(), presence(), state(), state()) -> ok. jid(), binary(), presence(), state(), state()) -> ok.
send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) -> send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) ->
send_self_presence(From, NewState),
send_existing_presences(From, NewState), send_existing_presences(From, NewState),
send_initial_presence(From, NewState, OldState), send_initial_presence(From, NewState, OldState),
History = get_history(Nick, Presence, NewState), History = get_history(Nick, Presence, NewState),
send_history(From, History, NewState), send_history(From, History, NewState),
send_subject(From, OldState). send_subject(From, OldState).
-spec send_self_presence(jid(), state()) -> ok.
send_self_presence(JID, State) ->
AvatarHash = (State#state.config)#config.vcard_xupdate,
DiscoInfo = make_disco_info(JID, State),
DiscoHash = mod_caps:compute_disco_hash(DiscoInfo, sha),
Els1 = [#caps{hash = <<"sha-1">>,
node = ?EJABBERD_URI,
version = DiscoHash}],
Els2 = if is_binary(AvatarHash) ->
[#vcard_xupdate{hash = AvatarHash}|Els1];
true ->
Els1
end,
ejabberd_router:route(#presence{from = State#state.jid, to = JID,
id = randoms:get_string(),
sub_els = Els2}).
-spec send_initial_presence(jid(), state(), state()) -> ok. -spec send_initial_presence(jid(), state(), state()) -> ok.
send_initial_presence(NJID, StateData, OldStateData) -> send_initial_presence(NJID, StateData, OldStateData) ->
send_new_presence1(NJID, <<"">>, true, StateData, OldStateData). send_new_presence1(NJID, <<"">>, true, StateData, OldStateData).
@ -3344,12 +3362,15 @@ send_config_change_info(New, #state{config = Old} = StateData) ->
end end
++ ++
case Old#config{anonymous = New#config.anonymous, case Old#config{anonymous = New#config.anonymous,
vcard = New#config.vcard,
logging = New#config.logging} of logging = New#config.logging} of
New -> []; New -> [];
_ -> [104] _ -> [104]
end, end,
if Codes /= [] -> if Codes /= [] ->
lists:foreach(
fun({_LJID, #user{jid = JID}}) ->
send_self_presence(JID, StateData#state{config = New})
end, ?DICT:to_list(StateData#state.users)),
Message = #message{type = groupchat, Message = #message{type = groupchat,
id = randoms:get_string(), id = randoms:get_string(),
sub_els = [#muc_user{status_codes = Codes}]}, sub_els = [#muc_user{status_codes = Codes}]},
@ -3377,7 +3398,8 @@ remove_nonmembers(StateData) ->
StateData, (?DICT):to_list(get_users_and_subscribers(StateData))). StateData, (?DICT):to_list(get_users_and_subscribers(StateData))).
-spec set_opts([{atom(), any()}], state()) -> state(). -spec set_opts([{atom(), any()}], state()) -> state().
set_opts([], StateData) -> StateData; set_opts([], StateData) ->
set_vcard_xupdate(StateData);
set_opts([{Opt, Val} | Opts], StateData) -> set_opts([{Opt, Val} | Opts], StateData) ->
NSD = case Opt of NSD = case Opt of
title -> title ->
@ -3492,6 +3514,10 @@ set_opts([{Opt, Val} | Opts], StateData) ->
StateData#state{config = StateData#state{config =
(StateData#state.config)#config{vcard = (StateData#state.config)#config{vcard =
Val}}; Val}};
vcard_xupdate ->
StateData#state{config =
(StateData#state.config)#config{vcard_xupdate =
Val}};
pubsub -> pubsub ->
StateData#state{config = StateData#state{config =
(StateData#state.config)#config{pubsub = Val}}; (StateData#state.config)#config{pubsub = Val}};
@ -3526,6 +3552,20 @@ set_opts([{Opt, Val} | Opts], StateData) ->
end, end,
set_opts(Opts, NSD). set_opts(Opts, NSD).
set_vcard_xupdate(#state{config =
#config{vcard = VCardRaw,
vcard_xupdate = undefined} = Config} = State)
when VCardRaw /= <<"">> ->
case fxml_stream:parse_element(VCardRaw) of
{error, _} ->
State;
El ->
Hash = mod_vcard_xupdate:compute_hash(El),
State#state{config = Config#config{vcard_xupdate = Hash}}
end;
set_vcard_xupdate(State) ->
State.
-define(MAKE_CONFIG_OPT(Opt), -define(MAKE_CONFIG_OPT(Opt),
{get_config_opt_name(Opt), element(Opt, Config)}). {get_config_opt_name(Opt), element(Opt, Config)}).
@ -3561,6 +3601,7 @@ make_opts(StateData) ->
?MAKE_CONFIG_OPT(#config.presence_broadcast), ?MAKE_CONFIG_OPT(#config.presence_broadcast),
?MAKE_CONFIG_OPT(#config.voice_request_min_interval), ?MAKE_CONFIG_OPT(#config.voice_request_min_interval),
?MAKE_CONFIG_OPT(#config.vcard), ?MAKE_CONFIG_OPT(#config.vcard),
?MAKE_CONFIG_OPT(#config.vcard_xupdate),
?MAKE_CONFIG_OPT(#config.pubsub), ?MAKE_CONFIG_OPT(#config.pubsub),
{captcha_whitelist, {captcha_whitelist,
(?SETS):to_list((StateData#state.config)#config.captcha_whitelist)}, (?SETS):to_list((StateData#state.config)#config.captcha_whitelist)},
@ -3633,12 +3674,8 @@ destroy_room(DEl, StateData) ->
false -> Fiffalse false -> Fiffalse
end). end).
-spec process_iq_disco_info(jid(), iq(), state()) -> -spec make_disco_info(jid(), state()) -> disco_info().
{result, disco_info()} | {error, stanza_error()}. make_disco_info(_From, StateData) ->
process_iq_disco_info(_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_info(_From, #iq{type = get, lang = Lang}, StateData) ->
Config = StateData#state.config, Config = StateData#state.config,
Feats = [?NS_VCARD, ?NS_MUC, Feats = [?NS_VCARD, ?NS_MUC,
?CONFIG_OPT_TO_FEATURE((Config#config.public), ?CONFIG_OPT_TO_FEATURE((Config#config.public),
@ -3664,11 +3701,35 @@ process_iq_disco_info(_From, #iq{type = get, lang = Lang}, StateData) ->
_ -> _ ->
[] []
end, end,
{result, #disco_info{xdata = [iq_disco_info_extras(Lang, StateData)], #disco_info{identities = [#identity{category = <<"conference">>,
identities = [#identity{category = <<"conference">>,
type = <<"text">>, type = <<"text">>,
name = get_title(StateData)}], name = get_title(StateData)}],
features = Feats}}. features = Feats}.
-spec process_iq_disco_info(jid(), iq(), state()) ->
{result, disco_info()} | {error, stanza_error()}.
process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) ->
Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
{error, xmpp:err_not_allowed(Txt, Lang)};
process_iq_disco_info(From, #iq{type = get, lang = Lang,
sub_els = [#disco_info{node = <<>>}]},
StateData) ->
DiscoInfo = make_disco_info(From, StateData),
Extras = iq_disco_info_extras(Lang, StateData),
{result, DiscoInfo#disco_info{xdata = [Extras]}};
process_iq_disco_info(From, #iq{type = get, lang = Lang,
sub_els = [#disco_info{node = Node}]},
StateData) ->
try
true = mod_caps:is_valid_node(Node),
DiscoInfo = make_disco_info(From, StateData),
Hash = mod_caps:compute_disco_hash(DiscoInfo, sha),
Node = <<(?EJABBERD_URI)/binary, $#, Hash/binary>>,
{result, DiscoInfo#disco_info{node = Node}}
catch _:{badmatch, _} ->
Txt = <<"Invalid node name">>,
{error, xmpp:err_item_not_found(Txt, Lang)}
end.
-spec iq_disco_info_extras(binary(), state()) -> xdata(). -spec iq_disco_info_extras(binary(), state()) -> xdata().
iq_disco_info_extras(Lang, StateData) -> iq_disco_info_extras(Lang, StateData) ->
@ -3734,13 +3795,15 @@ process_iq_vcard(_From, #iq{type = get}, StateData) ->
{error, _} -> {error, _} ->
{error, xmpp:err_item_not_found()} {error, xmpp:err_item_not_found()}
end; end;
process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [SubEl]}, process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [Pkt]},
StateData) -> StateData) ->
case get_affiliation(From, StateData) of case get_affiliation(From, StateData) of
owner -> owner ->
VCardRaw = fxml:element_to_binary(xmpp:encode(SubEl)), SubEl = xmpp:encode(Pkt),
VCardRaw = fxml:element_to_binary(SubEl),
Hash = mod_vcard_xupdate:compute_hash(SubEl),
Config = StateData#state.config, Config = StateData#state.config,
NewConfig = Config#config{vcard = VCardRaw}, NewConfig = Config#config{vcard = VCardRaw, vcard_xupdate = Hash},
change_config(NewConfig, StateData); change_config(NewConfig, StateData);
_ -> _ ->
ErrText = <<"Owner privileges required">>, ErrText = <<"Owner privileges required">>,
@ -4162,6 +4225,28 @@ send_wrapped(From, To, Packet, Node, State) ->
ok ok
end; end;
true -> true ->
case Packet of
#presence{type = unavailable} ->
case xmpp:get_subtag(Packet, #muc_user{}) of
#muc_user{destroy = Destroy,
status_codes = Codes} ->
case Destroy /= undefined orelse
(lists:member(110,Codes) andalso
not lists:member(303, Codes)) of
true ->
ejabberd_router:route(
#presence{from = State#state.jid, to = To,
id = randoms:get_string(),
type = unavailable});
false ->
ok
end;
_ ->
false
end;
_ ->
ok
end,
ejabberd_router:route(xmpp:set_from_to(Packet, From, To)) ejabberd_router:route(xmpp:set_from_to(Packet, From, To))
end. end.

View File

@ -33,6 +33,8 @@
-export([update_presence/1, vcard_set/1, remove_user/2, -export([update_presence/1, vcard_set/1, remove_user/2,
user_send_packet/1, mod_opt_type/1, mod_options/1, depends/2]). user_send_packet/1, mod_opt_type/1, mod_options/1, depends/2]).
%% API
-export([compute_hash/1]).
-include("ejabberd.hrl"). -include("ejabberd.hrl").
-include("logger.hrl"). -include("logger.hrl").

View File

@ -800,6 +800,11 @@ change_affiliation_slave(Config, {Aff, Role, Status, Reason}) ->
MyNick = ?config(nick, Config), MyNick = ?config(nick, Config),
MyNickJID = jid:replace_resource(Room, MyNick), MyNickJID = jid:replace_resource(Room, MyNick),
ct:comment("Receiving affiliation change to ~s", [Aff]), ct:comment("Receiving affiliation change to ~s", [Aff]),
if Aff == outcast ->
#presence{from = Room, type = unavailable} = recv_presence(Config);
true ->
ok
end,
#muc_user{status_codes = Codes, #muc_user{status_codes = Codes,
items = [#muc_item{role = Role, items = [#muc_item{role = Role,
actor = Actor, actor = Actor,
@ -858,6 +863,7 @@ kick_slave(Config) ->
wait_for_master(Config), wait_for_master(Config),
{[], _, _} = join(Config), {[], _, _} = join(Config),
ct:comment("Receiving role change to 'none'"), ct:comment("Receiving role change to 'none'"),
#presence{from = Room, type = unavailable} = recv_presence(Config),
#muc_user{status_codes = Codes, #muc_user{status_codes = Codes,
items = [#muc_item{role = none, items = [#muc_item{role = none,
affiliation = none, affiliation = none,
@ -889,6 +895,7 @@ destroy_master(Config) ->
wait_for_slave(Config), wait_for_slave(Config),
ok = destroy(Config, Reason), ok = destroy(Config, Reason),
ct:comment("Receiving destruction presence"), ct:comment("Receiving destruction presence"),
#presence{from = Room, type = unavailable} = recv_presence(Config),
#muc_user{items = [#muc_item{role = none, #muc_user{items = [#muc_item{role = none,
affiliation = none}], affiliation = none}],
destroy = #muc_destroy{jid = AltRoom, destroy = #muc_destroy{jid = AltRoom,
@ -907,6 +914,7 @@ destroy_slave(Config) ->
#stanza_error{reason = 'forbidden'} = destroy(Config, Reason), #stanza_error{reason = 'forbidden'} = destroy(Config, Reason),
wait_for_master(Config), wait_for_master(Config),
ct:comment("Receiving destruction presence"), ct:comment("Receiving destruction presence"),
#presence{from = Room, type = unavailable} = recv_presence(Config),
#muc_user{items = [#muc_item{role = none, #muc_user{items = [#muc_item{role = none,
affiliation = none}], affiliation = none}],
destroy = #muc_destroy{jid = AltRoom, destroy = #muc_destroy{jid = AltRoom,
@ -938,6 +946,7 @@ vcard_master(Config) ->
vcard_slave(Config) -> vcard_slave(Config) ->
wait_for_master(Config), wait_for_master(Config),
{[], _, _} = join(Config), {[], _, _} = join(Config),
[104] = recv_config_change_message(Config),
VCard = get_event(Config), VCard = get_event(Config),
VCard = get_vcard(Config), VCard = get_vcard(Config),
#stanza_error{reason = 'forbidden'} = set_vcard(Config, VCard), #stanza_error{reason = 'forbidden'} = set_vcard(Config, VCard),
@ -1150,11 +1159,13 @@ config_members_only_master(Config) ->
disconnect(Config). disconnect(Config).
config_members_only_slave(Config) -> config_members_only_slave(Config) ->
Room = muc_room_jid(Config),
MyJID = my_jid(Config), MyJID = my_jid(Config),
MyNickJID = my_muc_jid(Config), MyNickJID = my_muc_jid(Config),
{[], _, _} = slave_join(Config), {[], _, _} = slave_join(Config),
[104] = recv_config_change_message(Config), [104] = recv_config_change_message(Config),
ct:comment("Getting kicked because the room has become members-only"), ct:comment("Getting kicked because the room has become members-only"),
#presence{from = Room, type = unavailable} = recv_presence(Config),
#muc_user{status_codes = Codes, #muc_user{status_codes = Codes,
items = [#muc_item{jid = MyJID, items = [#muc_item{jid = MyJID,
role = none, role = none,
@ -1171,6 +1182,7 @@ config_members_only_slave(Config) ->
ct:comment("Waiting for the peer to ask for join"), ct:comment("Waiting for the peer to ask for join"),
join = get_event(Config), join = get_event(Config),
{[], _, _} = join(Config, participant, member), {[], _, _} = join(Config, participant, member),
#presence{from = Room, type = unavailable} = recv_presence(Config),
#muc_user{status_codes = NewCodes, #muc_user{status_codes = NewCodes,
items = [#muc_item{jid = MyJID, items = [#muc_item{jid = MyJID,
role = none, role = none,
@ -1555,8 +1567,9 @@ join_new(Config, Room) ->
MyJID = my_jid(Config), MyJID = my_jid(Config),
MyNick = ?config(nick, Config), MyNick = ?config(nick, Config),
MyNickJID = jid:replace_resource(Room, MyNick), MyNickJID = jid:replace_resource(Room, MyNick),
ct:comment("Joining new room"), ct:comment("Joining new room ~p", [Room]),
send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}),
#presence{from = Room, type = available} = recv_presence(Config),
%% As per XEP-0045 we MUST receive stanzas in the following order: %% As per XEP-0045 we MUST receive stanzas in the following order:
%% 1. In-room presence from other occupants %% 1. In-room presence from other occupants
%% 2. In-room presence from the joining entity itself (so-called "self-presence") %% 2. In-room presence from the joining entity itself (so-called "self-presence")
@ -1625,6 +1638,8 @@ join(Config, Role, Aff, SubEl) ->
case recv_presence(Config) of case recv_presence(Config) of
#presence{type = error, from = MyNickJID} = Err -> #presence{type = error, from = MyNickJID} = Err ->
xmpp:get_subtag(Err, #stanza_error{}); xmpp:get_subtag(Err, #stanza_error{});
#presence{from = Room, type = available} ->
case recv_presence(Config) of
#presence{type = available, from = PeerNickJID} = Pres -> #presence{type = available, from = PeerNickJID} = Pres ->
#muc_user{items = [#muc_item{role = moderator, #muc_user{items = [#muc_item{role = moderator,
affiliation = owner}]} = affiliation = owner}]} =
@ -1649,6 +1664,7 @@ join(Config, Role, Aff, SubEl) ->
true = lists:member(110, Codes), true = lists:member(110, Codes),
{History, Subj} = recv_history_and_subject(Config), {History, Subj} = recv_history_and_subject(Config),
{empty, History, Subj, Codes} {empty, History, Subj, Codes}
end
end. end.
leave(Config) -> leave(Config) ->
@ -1667,6 +1683,7 @@ leave(Config, Room) ->
end, end,
ct:comment("Leaving the room"), ct:comment("Leaving the room"),
send(Config, #presence{to = MyNickJID, type = unavailable}), send(Config, #presence{to = MyNickJID, type = unavailable}),
#presence{from = Room, type = unavailable} = recv_presence(Config),
#muc_user{ #muc_user{
status_codes = Codes, status_codes = Codes,
items = [#muc_item{role = none, jid = MyJID}]} = items = [#muc_item{role = none, jid = MyJID}]} =
@ -1702,6 +1719,7 @@ set_config(Config, RoomConfig, Room) ->
sub_els = [#muc_owner{config = #xdata{type = submit, sub_els = [#muc_owner{config = #xdata{type = submit,
fields = Fs}}]}) of fields = Fs}}]}) of
#iq{type = result, sub_els = []} -> #iq{type = result, sub_els = []} ->
#presence{from = Room, type = available} = recv_presence(Config),
#message{from = Room, type = groupchat} = Msg = recv_message(Config), #message{from = Room, type = groupchat} = Msg = recv_message(Config),
#muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}),
lists:sort(Codes); lists:sort(Codes);
@ -1846,6 +1864,7 @@ set_vcard(Config, VCard) ->
case send_recv(Config, #iq{type = set, to = Room, case send_recv(Config, #iq{type = set, to = Room,
sub_els = [VCard]}) of sub_els = [VCard]}) of
#iq{type = result, sub_els = []} -> #iq{type = result, sub_els = []} ->
[104] = recv_config_change_message(Config),
ok; ok;
#iq{type = error} = Err -> #iq{type = error} = Err ->
xmpp:get_subtag(Err, #stanza_error{}) xmpp:get_subtag(Err, #stanza_error{})
@ -1865,6 +1884,7 @@ get_vcard(Config) ->
recv_config_change_message(Config) -> recv_config_change_message(Config) ->
ct:comment("Receiving configuration change notification message"), ct:comment("Receiving configuration change notification message"),
Room = muc_room_jid(Config), Room = muc_room_jid(Config),
#presence{from = Room, type = available} = recv_presence(Config),
#message{type = groupchat, from = Room} = Msg = recv_message(Config), #message{type = groupchat, from = Room} = Msg = recv_message(Config),
#muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}),
lists:sort(Codes). lists:sort(Codes).