fix(gateway): resync permissions and validate voice leaves (#2492)

This commit is contained in:
Hampus
2026-09-06 15:24:23 +02:00
committed by GitHub
parent d3170fc320
commit 214d19d45a
10 changed files with 590 additions and 21 deletions
+33 -15
View File
@@ -96,6 +96,7 @@ handle_post_update(guild_member_update, EventData, OldState, NewState) ->
guild_state_member:sync_member(EventData, NewState),
UserId = guild_state_member:extract_user_id(EventData),
State1 = dispatch_member_update_visibility(UserId, OldState, NewState),
ok = guild_voice_permission_sync:maybe_sync_permissions_on_member_update(EventData, State1),
refresh_member_session_cache(UserId, State1);
handle_post_update(Event, EventData, OldState, NewState) when
Event =:= guild_role_create;
@@ -122,15 +123,25 @@ post_update_role(guild_role_create, _EventData, _OldState, NewState) ->
post_update_role(guild_role_update, EventData, OldState, NewState) ->
NewState1 = guild_state_roles:sync_hoisted_roles(NewState),
RoleIds = guild_state_roles:extract_role_ids_from_role_update(EventData),
guild_state_roles:recompute_visibility_for_roles(RoleIds, OldState, NewState1);
resync_roles_after_permission_change(RoleIds, OldState, NewState1);
post_update_role(guild_role_update_bulk, EventData, OldState, NewState) ->
NewState1 = guild_state_roles:sync_hoisted_roles(NewState),
RoleIds = guild_state_roles:extract_role_ids_from_role_update_bulk(EventData),
guild_state_roles:recompute_visibility_for_roles(RoleIds, OldState, NewState1);
resync_roles_after_permission_change(RoleIds, OldState, NewState1);
post_update_role(guild_role_delete, EventData, OldState, NewState) ->
NewState1 = guild_state_roles:sync_hoisted_roles(NewState),
RoleIds = guild_state_roles:extract_role_ids_from_role_delete(EventData),
guild_state_roles:recompute_visibility_for_roles(RoleIds, OldState, NewState1).
resync_roles_after_permission_change(RoleIds, OldState, NewState1).
-spec resync_roles_after_permission_change([integer()], guild_state(), guild_state()) ->
guild_state().
resync_roles_after_permission_change(RoleIds, OldState, NewState) ->
Recomputed = guild_state_roles:recompute_visibility_for_roles(RoleIds, OldState, NewState),
lists:foreach(
fun(RoleId) -> guild_voice_permission_sync:sync_users_with_role(RoleId, Recomputed) end,
RoleIds
),
Recomputed.
-spec post_update_channel(event(), event_data(), guild_state(), guild_state()) -> guild_state().
post_update_channel(channel_create, _EventData, _OldState, NewState) ->
@@ -138,24 +149,31 @@ post_update_channel(channel_create, _EventData, _OldState, NewState) ->
NewState;
post_update_channel(channel_update, EventData, OldState, NewState) ->
ChanIds = guild_state_channels:extract_channel_ids_from_channel_update(EventData),
NewState1 = guild_member_list_write:rebuild_channels_for_permission_change(
ChanIds, NewState
),
guild_visibility:compute_and_dispatch_visibility_changes_for_channels(
ChanIds, OldState, NewState1
);
resync_channels_after_permission_change(ChanIds, OldState, NewState);
post_update_channel(channel_update_bulk, EventData, OldState, NewState) ->
ChanIds = guild_state_channels:extract_channel_ids_from_channel_update_bulk(EventData),
NewState1 = guild_member_list_write:rebuild_channels_for_permission_change(
ChanIds, NewState
),
guild_visibility:compute_and_dispatch_visibility_changes_for_channels(
ChanIds, OldState, NewState1
);
resync_channels_after_permission_change(ChanIds, OldState, NewState);
post_update_channel(channel_delete, _EventData, _OldState, NewState) ->
maybe_sync_member_list_permission_state(NewState),
NewState.
-spec resync_channels_after_permission_change([integer()], guild_state(), guild_state()) ->
guild_state().
resync_channels_after_permission_change(ChanIds, OldState, NewState) ->
Rebuilt = guild_member_list_write:rebuild_channels_for_permission_change(ChanIds, NewState),
Dispatched = guild_visibility:compute_and_dispatch_visibility_changes_for_channels(
ChanIds, OldState, Rebuilt
),
lists:foreach(
fun(ChanId) ->
guild_voice_permission_sync:sync_all_voice_permissions_for_channel(
ChanId, Dispatched
)
end,
ChanIds
),
Dispatched.
-spec post_update_member_remove(event_data(), guild_state()) -> guild_state().
post_update_member_remove(EventData, NewState) ->
UserId = guild_state_member:extract_user_id(EventData),
@@ -104,7 +104,7 @@ handle_member_voice(Context, Member, VoiceStates, State) ->
voice_reply().
handle_disconnect(Context, VoiceStates, State) ->
guild_voice_disconnect:handle_voice_disconnect(
maps:get(raw_connection_id, Context),
maps:get(connection_id, Context),
maps:get(session_id, Context),
maps:get(user_id, Context),
VoiceStates,
@@ -43,7 +43,6 @@ build_context(Request0) ->
Request = map_utils:ensure_map(Request0),
Norm = fun guild_voice_connection_normalize:normalize_boolean/1,
Coord = fun guild_voice_connection_normalize:normalize_coordinate/1,
RawConnectionId = maps:get(connection_id, Request, undefined),
#{
user_id => guild_voice_connection_normalize:normalize_user_id(
maps:get(user_id, Request, undefined)
@@ -53,9 +52,8 @@ build_context(Request0) ->
),
session_id => maps:get(session_id, Request, undefined),
connection_id => guild_voice_connection_normalize:normalize_connection_id(
RawConnectionId
maps:get(connection_id, Request, undefined)
),
raw_connection_id => RawConnectionId,
self_mute => Norm(maps:get(self_mute, Request, false)),
self_deaf => Norm(maps:get(self_deaf, Request, false)),
self_video => Norm(maps:get(self_video, Request, false)),
@@ -6,6 +6,7 @@
-export([
sync_user_voice_permissions/2,
sync_all_voice_permissions_for_channel/2,
sync_users_with_role/2,
maybe_sync_permissions_on_role_update/2,
maybe_sync_permissions_on_member_update/2
]).
@@ -129,6 +130,7 @@ sync_validated_voice_state(GuildId, UserId, ChId, ConnId, VoiceState, State) ->
VoicePermissions = VoicePermissions0#{
deaf => maps:get(<<"deaf">>, VoiceState, false)
},
ok = maybe_clear_self_stream(ChId, VoiceState, VoicePermissions, State),
dispatch_permission_update(
GuildId, ChId, UserId, ConnId, VoicePermissions, State
);
@@ -136,6 +138,27 @@ sync_validated_voice_state(GuildId, UserId, ChId, ConnId, VoiceState, State) ->
dispatch_force_disconnect(GuildId, ChId, UserId, ConnId, State)
end.
-spec maybe_clear_self_stream(
channel_id(), voice_state(), voice_utils:voice_permissions(), guild_state()
) -> ok.
maybe_clear_self_stream(ChId, VoiceState, #{can_stream := false}, State) ->
SelfStream = maps:get(<<"self_stream">>, VoiceState, false),
SelfVideo = maps:get(<<"self_video">>, VoiceState, false),
case SelfStream =:= true orelse SelfVideo =:= true of
false ->
ok;
true ->
Cleared = VoiceState#{
<<"self_stream">> => false,
<<"self_video">> => false
},
guild_voice_broadcast:broadcast_voice_state_update(
Cleared, State, integer_to_binary(ChId)
)
end;
maybe_clear_self_stream(_ChId, _VoiceState, _VoicePermissions, _State) ->
ok.
-spec user_has_base_voice_access(user_id(), channel_id(), guild_state()) -> boolean().
user_has_base_voice_access(UserId, ChannelId, State) ->
case guild_virtual_channel_access:has_virtual_access(UserId, ChannelId, State) of
@@ -319,6 +342,7 @@ build_sync_test_data(GuildId, RoleId, UserId, ChannelId, Permissions) ->
}.
sync_user_voice_permissions_syncs_connected_user_test() ->
ok = drain_mailbox(),
TestFun = make_sync_test_fun(),
State = build_sync_test_state(TestFun),
ok = sync_user_voice_permissions(10, State),
@@ -393,6 +417,7 @@ maybe_sync_permissions_on_role_update_uses_role_index_test() ->
end.
sync_disconnects_when_connect_permission_lost_test() ->
ok = drain_mailbox(),
TestFun = make_sync_test_fun(),
UserId = 10,
ChannelId = 500,
@@ -410,6 +435,80 @@ sync_disconnects_when_connect_permission_lost_test() ->
?assert(false)
end.
sync_clears_self_stream_when_stream_permission_lost_test() ->
ok = drain_mailbox(),
TestFun = make_sync_test_fun(),
UserId = 10,
ChannelId = 500,
GuildId = 42,
RoleId = 999,
WithoutStream =
constants:view_channel_permission() bor
constants:connect_permission() bor
constants:speak_permission(),
State = build_streaming_sync_test_state(
TestFun, GuildId, RoleId, UserId, ChannelId, WithoutStream
),
ok = sync_user_voice_permissions(UserId, State),
Broadcast = receive_voice_state_dispatch(),
?assertEqual(false, maps:get(<<"self_stream">>, Broadcast)),
?assertEqual(false, maps:get(<<"self_video">>, Broadcast)),
?assertEqual(<<"test-conn">>, maps:get(<<"connection_id">>, Broadcast)).
sync_keeps_self_stream_when_stream_permission_held_test() ->
ok = drain_mailbox(),
TestFun = make_sync_test_fun(),
UserId = 10,
ChannelId = 500,
GuildId = 42,
RoleId = 999,
WithStream =
constants:view_channel_permission() bor
constants:connect_permission() bor
constants:speak_permission() bor
constants:stream_permission(),
State = build_streaming_sync_test_state(
TestFun, GuildId, RoleId, UserId, ChannelId, WithStream
),
ok = sync_user_voice_permissions(UserId, State),
?assertEqual(undefined, receive_optional_voice_state_dispatch()).
build_streaming_sync_test_state(TestFun, GuildId, RoleId, UserId, ChannelId, Permissions) ->
VoiceState = #{
<<"user_id">> => integer_to_binary(UserId),
<<"channel_id">> => integer_to_binary(ChannelId),
<<"connection_id">> => <<"test-conn">>,
<<"self_stream">> => true,
<<"self_video">> => true
},
#{
id => GuildId,
voice_states => #{<<"conn">> => VoiceState},
sessions => #{
<<"s1">> => #{
pid => self(),
user_id => UserId,
viewable_channels => #{ChannelId => true}
}
},
test_permission_sync_fun => TestFun,
data => build_sync_test_data(GuildId, RoleId, UserId, ChannelId, Permissions)
}.
receive_voice_state_dispatch() ->
case receive_optional_voice_state_dispatch() of
undefined -> error(voice_state_update_not_received);
Payload -> Payload
end.
receive_optional_voice_state_dispatch() ->
receive
{'$gen_cast', {dispatch, voice_state_update, {pre_encoded, Bin}}} -> json:decode(Bin);
{'$gen_cast', {dispatch, voice_state_update, Payload}} -> Payload
after 200 ->
undefined
end.
make_sync_test_fun() ->
Self = self(),
fun(GId, ChId, UId, ConnId, Perms) ->
@@ -461,4 +560,11 @@ role_sync_test_data(
]
}).
drain_mailbox() ->
receive
_ -> drain_mailbox()
after 0 ->
ok
end.
-endif.
+15 -1
View File
@@ -40,7 +40,21 @@ apply_voice_permissions_to_flags(Flags, VoicePermissions) ->
false -> true;
_ -> false
end,
Flags#{suppress => Suppress}.
SelfStream =
case maps:get(can_stream, VoicePermissions, true) of
false -> false;
_ -> maps:get(self_stream, Flags, false) =:= true
end,
SelfVideo =
case maps:get(can_video, VoicePermissions, true) of
false -> false;
_ -> maps:get(self_video, Flags, false) =:= true
end,
Flags#{
suppress => Suppress,
self_stream => SelfStream,
self_video => SelfVideo
}.
-spec build_voice_token_rpc_request(
integer() | null,
@@ -0,0 +1,164 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(guild_state_channel_resync_tests).
-typing([eqwalizer]).
-include_lib("eunit/include/eunit.hrl").
-define(GUILD_ID, 9100).
-define(CHANNEL_ID, 9500).
-define(ROLE_ID, 9200).
-define(USER_A, 9010).
-define(USER_B, 9020).
-define(USER_C, 9030).
channel_update_syncs_every_subscriber_after_permission_change_test() ->
with_relay_mock(fun run_channel_update_syncs_every_subscriber/0).
channel_update_bulk_syncs_every_subscriber_after_permission_change_test() ->
with_relay_mock(fun run_channel_update_bulk_syncs_every_subscriber/0).
run_channel_update_syncs_every_subscriber() ->
run_resync_case(channel_update, denied_channel()).
run_channel_update_bulk_syncs_every_subscriber() ->
run_resync_case(channel_update_bulk, #{<<"channels">> => [denied_channel()]}).
run_resync_case(Event, EventData) ->
PidA = spawn(fun idle/0),
PidB = spawn(fun idle/0),
SubsTab = guild_member_list_subs:new(),
State0 = base_state(SubsTab, PidA, PidB),
{State1, _SyncA, _RangesA} = guild_member_list:subscribe_ranges(
<<"s_a">>, list_id(), [{0, 99}], State0
),
{State2, _SyncB, _RangesB} = guild_member_list:subscribe_ranges(
<<"s_b">>, list_id(), [{0, 99}], State1
),
try
?assertEqual({3, 0}, guild_member_list:get_counts(list_id(), State2)),
{noreply, UpdatedState} = guild_dispatch:handle_dispatch(Event, EventData, State2),
?assertEqual({2, 0}, guild_member_list:get_counts(list_id(), UpdatedState)),
{Pids, Raw, Payload} = receive_member_list_dispatch(),
?assertEqual(lists:sort([PidA, PidB]), lists:sort(Pids)),
?assertEqual(list_id(), maps:get(<<"channel_id">>, Payload)),
?assertEqual(list_id(), maps:get(<<"id">>, Payload)),
?assertEqual(2, maps:get(<<"member_count">>, Payload)),
?assertEqual(nomatch, binary:match(Raw, integer_to_binary(?USER_C))),
assert_no_further_dispatch(),
guild_member_list_channel_engine:destroy_all(UpdatedState)
after
PidA ! stop,
PidB ! stop,
ets:delete(SubsTab)
end.
base_state(SubsTab, PidA, PidB) ->
#{
id => ?GUILD_ID,
data => #{
<<"guild">> => #{<<"owner_id">> => <<"999">>},
<<"roles">> => [
#{
<<"id">> => integer_to_binary(?GUILD_ID),
<<"permissions">> => integer_to_binary(viewer_permissions())
},
#{<<"id">> => integer_to_binary(?ROLE_ID), <<"permissions">> => <<"0">>}
],
<<"members">> => [
member(?USER_A, []),
member(?USER_B, []),
member(?USER_C, [integer_to_binary(?ROLE_ID)])
],
<<"channels">> => [visible_channel()]
},
sessions => #{
<<"s_a">> => session(<<"s_a">>, ?USER_A, PidA),
<<"s_b">> => session(<<"s_b">>, ?USER_B, PidB)
},
member_presence => #{},
member_list_subscriptions => SubsTab
}.
session(SessionId, UserId, Pid) ->
#{
session_id => SessionId,
user_id => UserId,
pid => Pid,
viewable_channels => #{?CHANNEL_ID => true}
}.
member(UserId, Roles) ->
#{
<<"user">> => #{
<<"id">> => integer_to_binary(UserId),
<<"username">> => <<"u", (integer_to_binary(UserId))/binary>>
},
<<"roles">> => Roles
}.
visible_channel() ->
#{<<"id">> => list_id(), <<"type">> => 0, <<"permission_overwrites">> => []}.
denied_channel() ->
#{
<<"id">> => list_id(),
<<"type">> => 0,
<<"permission_overwrites">> => [
#{
<<"id">> => integer_to_binary(?ROLE_ID),
<<"type">> => 0,
<<"allow">> => <<"0">>,
<<"deny">> => integer_to_binary(constants:view_channel_permission())
}
]
}.
viewer_permissions() ->
constants:view_channel_permission() bor constants:view_channel_members_permission().
list_id() ->
integer_to_binary(?CHANNEL_ID).
with_relay_mock(Fun) ->
meck:new(gateway_dispatch_relay, [passthrough, no_link]),
Parent = self(),
meck:expect(
gateway_dispatch_relay,
dispatch_many,
fun
(Pids, guild_member_list_update, {pre_encoded, Bin}, GuildId) when is_binary(Bin) ->
Parent ! {member_list_dispatch, Pids, guild_member_list_update, GuildId, Bin},
ok;
(_Pids, _Event, _Payload, _GuildId) ->
ok
end
),
try
Fun()
after
meck:unload(gateway_dispatch_relay)
end.
receive_member_list_dispatch() ->
receive
{member_list_dispatch, Pids, guild_member_list_update, ?GUILD_ID, Bin} ->
{Pids, Bin, json:decode(Bin)}
after 1000 ->
?assert(false, no_member_list_sync_dispatched),
{[], <<>>, #{}}
end.
assert_no_further_dispatch() ->
receive
{member_list_dispatch, _Pids, _Event, _GuildId, _Bin} = Msg ->
?assert(false, {unexpected_member_list_dispatch, Msg})
after 0 ->
ok
end.
idle() ->
receive
stop -> ok
after 30000 -> ok
end.
@@ -102,6 +102,15 @@ build_context_normalizes_fields_test() ->
?assertEqual(false, maps:get(self_stream, Context)),
?assertEqual(false, maps:get(is_mobile, Context)).
build_context_normalizes_null_connection_id_test() ->
Context = guild_voice_connection_util:build_context(#{
user_id => 42,
channel_id => null,
connection_id => null
}),
?assertEqual(undefined, maps:get(connection_id, Context)),
?assertNot(maps:is_key(raw_connection_id, Context)).
build_context_does_not_accept_zero_ids_test() ->
Context = guild_voice_connection_util:build_context(#{
user_id => 0,
@@ -55,6 +55,30 @@ voice_state_update_invalid_channel_id_test() ->
#{user_id => 10, channel_id => undefined}, State
).
voice_state_update_guild_leave_null_connection_id_test() ->
State = connected_state(<<"10">>),
{reply, {error, validation_error, voice_missing_connection_id}, NewState} =
guild_voice_connection:voice_state_update(
#{user_id => 10, channel_id => null, connection_id => null}, State
),
?assertEqual(connected_voice_states(<<"10">>), maps:get(voice_states, NewState)).
voice_state_update_guild_leave_omitted_connection_id_test() ->
State = connected_state(<<"10">>),
{reply, {error, validation_error, voice_missing_connection_id}, NewState} =
guild_voice_connection:voice_state_update(
#{user_id => 10, channel_id => null}, State
),
?assertEqual(connected_voice_states(<<"10">>), maps:get(voice_states, NewState)).
voice_state_update_guild_leave_blank_connection_id_test() ->
State = connected_state(<<"10">>),
assert_voice_state_update_error(
#{user_id => 10, channel_id => null, connection_id => <<>>},
State,
{error, validation_error, voice_missing_connection_id}
).
voice_state_update_channel_not_found_test() ->
State = replace_channels(base_test_state(), []),
{reply, {error, not_found, voice_channel_not_found}, _} =
@@ -300,6 +324,28 @@ voice_state_update_stress_many_watch_unwatch_updates_test() ->
),
?assertMatch(#{<<"conn-1">> := #{}}, maps:get(voice_states, FinalState)).
voice_state_update_clears_self_stream_without_stream_permission_test() ->
State = connected_state(<<"10">>),
{reply, #{success := true}, NewState} = guild_voice_connection:voice_state_update(
update_request(#{self_stream => true, self_video => true}), State
),
Updated = updated_voice_state(NewState),
?assertEqual(false, maps:get(<<"self_stream">>, Updated)),
?assertEqual(false, maps:get(<<"self_video">>, Updated)).
voice_state_update_keeps_self_stream_with_stream_permission_test() ->
State = maps:put(
voice_states,
connected_voice_states(<<"10">>),
with_stream_permission(base_test_state())
),
{reply, #{success := true}, NewState} = guild_voice_connection:voice_state_update(
update_request(#{self_stream => true, self_video => true}), State
),
Updated = updated_voice_state(NewState),
?assertEqual(true, maps:get(<<"self_stream">>, Updated)),
?assertEqual(true, maps:get(<<"self_video">>, Updated)).
voice_state_update_allows_twenty_fifth_camera_sharer_test() ->
State = camera_state(24),
{reply, #{success := true}, NewState} = guild_voice_connection:voice_state_update(
@@ -379,10 +425,20 @@ camera_base_state(SharerCount) ->
Members = [
base_test_member(10) | [base_test_member(200 + N) || N <- lists:seq(1, SharerCount)]
],
State = base_test_state(),
State = with_stream_permission(base_test_state()),
Data0 = maps:get(data, State, #{}),
maps:put(data, maps:put(<<"members">>, Members, Data0), State).
with_stream_permission(State) ->
Roles = [
#{
<<"id">> => <<"999">>,
<<"permissions">> => integer_to_binary(constants:stream_permission())
}
],
Data0 = maps:get(data, State, #{}),
maps:put(data, maps:put(<<"roles">>, Roles, Data0), State).
camera_state(SharerCount) ->
maps:put(
voice_states, camera_test_voice_states(SharerCount), camera_base_state(SharerCount)
@@ -135,6 +135,7 @@ users_in_channel_test() ->
?assertNot(sets:is_element(3, Result)).
permission_sync_disconnects_when_view_channel_lost_test() ->
ok = drain_mailbox(),
ConnectOnly = constants:connect_permission(),
{State, UserId, ChannelId, GuildId} = build_perm_sync_state(ConnectOnly),
ok = guild_voice_permission_sync:sync_user_voice_permissions(UserId, State),
@@ -146,6 +147,7 @@ permission_sync_disconnects_when_view_channel_lost_test() ->
end.
permission_sync_does_not_disconnect_with_full_perms_test() ->
ok = drain_mailbox(),
FullPerms =
constants:view_channel_permission() bor
constants:connect_permission() bor
@@ -160,6 +162,123 @@ permission_sync_does_not_disconnect_with_full_perms_test() ->
?assert(false)
end.
permission_sync_runs_on_role_permission_update_test() ->
ok = drain_mailbox(),
{State, UserId, ChannelId, GuildId, RoleId} = build_sync_wiring_state(),
_ = guild_state:update_state(
guild_role_update,
#{
<<"role">> => #{
<<"id">> => integer_to_binary(RoleId),
<<"permissions">> => <<"0">>
}
},
State
),
receive
{synced, GuildId, ChannelId, UserId, <<"test-conn">>, Perms} ->
?assertEqual(false, maps:get(can_speak, Perms)),
?assertEqual(false, maps:get(can_stream, Perms))
after 200 ->
?assert(false)
end.
permission_sync_runs_on_channel_overwrite_update_test() ->
ok = drain_mailbox(),
{State, UserId, ChannelId, GuildId, RoleId} = build_sync_wiring_state(),
_ = guild_state:update_state(
channel_update,
#{
<<"id">> => integer_to_binary(ChannelId),
<<"type">> => 2,
<<"permission_overwrites">> => [
#{
<<"id">> => integer_to_binary(RoleId),
<<"type">> => 0,
<<"allow">> => <<"0">>,
<<"deny">> => integer_to_binary(constants:stream_permission())
}
]
},
State
),
receive
{synced, GuildId, ChannelId, UserId, <<"test-conn">>, Perms} ->
?assertEqual(true, maps:get(can_speak, Perms)),
?assertEqual(false, maps:get(can_stream, Perms))
after 200 ->
?assert(false)
end.
permission_sync_runs_on_member_role_removal_test() ->
ok = drain_mailbox(),
{State, UserId, ChannelId, GuildId, _RoleId} = build_sync_wiring_state(),
_ = guild_state:update_state(
guild_member_update,
#{
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
<<"roles">> => []
},
State
),
receive
{synced, GuildId, ChannelId, UserId, <<"test-conn">>, Perms} ->
?assertEqual(false, maps:get(can_speak, Perms)),
?assertEqual(false, maps:get(can_stream, Perms))
after 200 ->
?assert(false)
end.
build_sync_wiring_state() ->
Self = self(),
TestFun = fun(GId, ChId, UId, ConnId, Perms) ->
Self ! {synced, GId, ChId, UId, ConnId, Perms}
end,
UserId = 10,
ChannelId = 500,
GuildId = 42,
RoleId = 999,
RoleIdBin = integer_to_binary(RoleId),
EveryonePerms = required_voice_perms(),
RolePerms = constants:speak_permission() bor constants:stream_permission(),
State = #{
id => GuildId,
voice_states => #{
<<"conn">> => #{
<<"user_id">> => integer_to_binary(UserId),
<<"channel_id">> => integer_to_binary(ChannelId),
<<"connection_id">> => <<"test-conn">>,
<<"deaf">> => false
}
},
member_list_subscriptions => guild_member_list_subs:new(),
test_permission_sync_fun => TestFun,
data => #{
<<"guild">> => #{<<"owner_id">> => <<"1">>},
<<"roles">> => [
#{<<"id">> => RoleIdBin, <<"permissions">> => integer_to_binary(RolePerms)},
#{
<<"id">> => integer_to_binary(GuildId),
<<"permissions">> => integer_to_binary(EveryonePerms)
}
],
<<"members">> => [
#{
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
<<"roles">> => [RoleIdBin]
}
],
<<"channels">> => [
#{
<<"id">> => integer_to_binary(ChannelId),
<<"type">> => 2,
<<"permission_overwrites">> => []
}
]
}
},
{State, UserId, ChannelId, GuildId, RoleId}.
build_perm_sync_state(Permissions) ->
Self = self(),
TestFun = fun(GId, ChId, UId, ConnId, Perms) ->
@@ -221,3 +340,10 @@ camera_limited_voice_states(Count, ChannelId) ->
#{},
lists:seq(1, Count)
).
drain_mailbox() ->
receive
_ -> drain_mailbox()
after 0 ->
ok
end.
+78
View File
@@ -103,6 +103,84 @@ apply_voice_permissions_to_flags_suppresses_without_speak_test() ->
?assertEqual(false, maps:get(self_mute, Result)),
?assertEqual(true, maps:get(suppress, Result)).
apply_voice_permissions_to_flags_clears_self_stream_without_stream_test() ->
Flags = #{self_stream => true, self_video => true, suppress => false},
VoicePerms = #{
can_speak => true,
can_stream => false,
can_video => false
},
Result = voice_utils:apply_voice_permissions_to_flags(Flags, VoicePerms),
?assertEqual(false, maps:get(self_stream, Result)),
?assertEqual(false, maps:get(self_video, Result)),
?assertEqual(false, maps:get(suppress, Result)).
apply_voice_permissions_to_flags_keeps_self_stream_with_stream_test() ->
Flags = #{self_stream => true, self_video => true, suppress => false},
VoicePerms = #{
can_speak => true,
can_stream => true,
can_video => true
},
Result = voice_utils:apply_voice_permissions_to_flags(Flags, VoicePerms),
?assertEqual(true, maps:get(self_stream, Result)),
?assertEqual(true, maps:get(self_video, Result)).
apply_voice_permissions_to_flags_clears_self_stream_denied_by_overwrite_test() ->
UserId = 10,
ChannelId = 500,
State = stream_denied_overwrite_state(UserId, ChannelId),
VoicePerms = voice_utils:compute_voice_permissions(UserId, ChannelId, State),
?assertEqual(true, maps:get(can_speak, VoicePerms)),
?assertEqual(false, maps:get(can_stream, VoicePerms)),
Result = voice_utils:apply_voice_permissions_to_flags(
#{self_stream => true, self_video => true, suppress => false}, VoicePerms
),
?assertEqual(false, maps:get(self_stream, Result)),
?assertEqual(false, maps:get(self_video, Result)),
?assertEqual(false, maps:get(suppress, Result)).
stream_denied_overwrite_state(UserId, ChannelId) ->
GuildId = 90,
RoleId = 300,
RolePerms =
constants:view_channel_permission() bor
constants:connect_permission() bor
constants:speak_permission() bor
constants:stream_permission(),
#{
id => GuildId,
data => #{
<<"guild">> => #{<<"owner_id">> => <<"999">>},
<<"roles">> => [
#{<<"id">> => integer_to_binary(GuildId), <<"permissions">> => <<"0">>},
#{
<<"id">> => integer_to_binary(RoleId),
<<"permissions">> => integer_to_binary(RolePerms)
}
],
<<"members">> => [
#{
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
<<"roles">> => [integer_to_binary(RoleId)]
}
],
<<"channels">> => [
#{
<<"id">> => integer_to_binary(ChannelId),
<<"permission_overwrites">> => [
#{
<<"id">> => integer_to_binary(RoleId),
<<"type">> => 0,
<<"allow">> => <<"0">>,
<<"deny">> => integer_to_binary(constants:stream_permission())
}
]
}
]
}
}.
generate_token_nonce_format_test() ->
Nonce = voice_utils:generate_token_nonce(),
?assert(is_binary(Nonce)),