diff --git a/fluxer_gateway/src/guild/guild_state.erl b/fluxer_gateway/src/guild/guild_state.erl index f9870afdf..ceefffa69 100644 --- a/fluxer_gateway/src/guild/guild_state.erl +++ b/fluxer_gateway/src/guild/guild_state.erl @@ -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), diff --git a/fluxer_gateway/src/guild/voice/guild_voice_connection.erl b/fluxer_gateway/src/guild/voice/guild_voice_connection.erl index 21eec8959..cb3bf5907 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_connection.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_connection.erl @@ -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, diff --git a/fluxer_gateway/src/guild/voice/guild_voice_connection_util.erl b/fluxer_gateway/src/guild/voice/guild_voice_connection_util.erl index 17a855ed3..795982fc5 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_connection_util.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_connection_util.erl @@ -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)), diff --git a/fluxer_gateway/src/guild/voice/guild_voice_permission_sync.erl b/fluxer_gateway/src/guild/voice/guild_voice_permission_sync.erl index ceab9e348..b59944c78 100644 --- a/fluxer_gateway/src/guild/voice/guild_voice_permission_sync.erl +++ b/fluxer_gateway/src/guild/voice/guild_voice_permission_sync.erl @@ -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. diff --git a/fluxer_gateway/src/guild/voice/voice_utils.erl b/fluxer_gateway/src/guild/voice/voice_utils.erl index 324a4b07e..f81bb0b71 100644 --- a/fluxer_gateway/src/guild/voice/voice_utils.erl +++ b/fluxer_gateway/src/guild/voice/voice_utils.erl @@ -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, diff --git a/fluxer_gateway/test/guild_state_channel_resync_tests.erl b/fluxer_gateway/test/guild_state_channel_resync_tests.erl new file mode 100644 index 000000000..869e678f6 --- /dev/null +++ b/fluxer_gateway/test/guild_state_channel_resync_tests.erl @@ -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. diff --git a/fluxer_gateway/test/guild_voice_connection_pending_tests.erl b/fluxer_gateway/test/guild_voice_connection_pending_tests.erl index 964035b7c..c3bf076a5 100644 --- a/fluxer_gateway/test/guild_voice_connection_pending_tests.erl +++ b/fluxer_gateway/test/guild_voice_connection_pending_tests.erl @@ -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, diff --git a/fluxer_gateway/test/guild_voice_connection_tests.erl b/fluxer_gateway/test/guild_voice_connection_tests.erl index 7144175b0..2809ec761 100644 --- a/fluxer_gateway/test/guild_voice_connection_tests.erl +++ b/fluxer_gateway/test/guild_voice_connection_tests.erl @@ -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) diff --git a/fluxer_gateway/test/guild_voice_permissions_tests.erl b/fluxer_gateway/test/guild_voice_permissions_tests.erl index 3fbdce2d7..0b7504cf1 100644 --- a/fluxer_gateway/test/guild_voice_permissions_tests.erl +++ b/fluxer_gateway/test/guild_voice_permissions_tests.erl @@ -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. diff --git a/fluxer_gateway/test/voice_utils_tests.erl b/fluxer_gateway/test/voice_utils_tests.erl index 57108689c..6ace24088 100644 --- a/fluxer_gateway/test/voice_utils_tests.erl +++ b/fluxer_gateway/test/voice_utils_tests.erl @@ -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)),