refactor progress
This commit is contained in:
@@ -23,15 +23,18 @@
|
||||
-export([disconnect_voice_user_if_in_channel/2]).
|
||||
-export([disconnect_all_voice_users_in_channel/2]).
|
||||
-export([cleanup_virtual_channel_access_for_user/2]).
|
||||
|
||||
-type guild_state() :: map().
|
||||
-type voice_state() :: map().
|
||||
-type voice_state_map() :: #{binary() => voice_state()}.
|
||||
-export([recently_disconnected_voice_states/1]).
|
||||
-export([clear_recently_disconnected/2]).
|
||||
-export([clear_recently_disconnected_for_channel/2]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-endif.
|
||||
|
||||
-type guild_state() :: map().
|
||||
-type voice_state() :: map().
|
||||
-type voice_state_map() :: #{binary() => voice_state()}.
|
||||
|
||||
-spec handle_voice_disconnect(
|
||||
binary() | undefined,
|
||||
term(),
|
||||
@@ -45,7 +48,10 @@ handle_voice_disconnect(ConnectionId, _SessionId, UserId, VoiceStates0, State) -
|
||||
VoiceStates = voice_state_utils:ensure_voice_states(VoiceStates0),
|
||||
case maps:get(ConnectionId, VoiceStates, undefined) of
|
||||
undefined ->
|
||||
{reply, #{success => true}, State};
|
||||
%% Voice state not in voice_states - check if it's still pending
|
||||
%% (user disconnected before LiveKit confirmation)
|
||||
State1 = clear_pending_voice_connection(ConnectionId, State),
|
||||
{reply, #{success => true}, State1};
|
||||
OldVoiceState ->
|
||||
case guild_voice_state:user_matches_voice_state(OldVoiceState, UserId) of
|
||||
false ->
|
||||
@@ -64,16 +70,43 @@ handle_voice_disconnect(ConnectionId, _SessionId, UserId, VoiceStates0, State) -
|
||||
{GuildId, ChannelId} ->
|
||||
maybe_force_disconnect(GuildId, ChannelId, UserId, ConnectionId, State),
|
||||
NewVoiceStates = maps:remove(ConnectionId, VoiceStates),
|
||||
NewState = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState0 = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState = clear_recently_disconnected(ConnectionId, NewState0),
|
||||
voice_state_utils:broadcast_disconnects(
|
||||
#{ConnectionId => OldVoiceState}, NewState
|
||||
),
|
||||
FinalState = cleanup_virtual_channel_access_for_user(UserId, NewState),
|
||||
FinalState = maybe_cleanup_after_disconnect(
|
||||
UserId, ChannelId, NewState
|
||||
),
|
||||
{reply, #{success => true}, FinalState}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
-spec clear_pending_voice_connection(binary(), guild_state()) -> guild_state().
|
||||
clear_pending_voice_connection(ConnectionId, State) ->
|
||||
PendingConnections = maps:get(pending_voice_connections, State, #{}),
|
||||
case maps:is_key(ConnectionId, PendingConnections) of
|
||||
false ->
|
||||
State;
|
||||
true ->
|
||||
NewPendingConnections = maps:remove(ConnectionId, PendingConnections),
|
||||
maps:put(pending_voice_connections, NewPendingConnections, State)
|
||||
end.
|
||||
|
||||
-spec maybe_cleanup_after_disconnect(integer(), integer(), guild_state()) -> guild_state().
|
||||
maybe_cleanup_after_disconnect(UserId, ChannelId, State) ->
|
||||
case
|
||||
guild_virtual_channel_access:is_pending_join(UserId, ChannelId, State) orelse
|
||||
guild_virtual_channel_access:has_preserve(UserId, ChannelId, State) orelse
|
||||
guild_virtual_channel_access:is_move_pending(UserId, ChannelId, State)
|
||||
of
|
||||
true ->
|
||||
State;
|
||||
false ->
|
||||
cleanup_virtual_channel_access_for_user(UserId, State)
|
||||
end.
|
||||
|
||||
-spec disconnect_voice_user(map(), guild_state()) -> {reply, map(), guild_state()}.
|
||||
disconnect_voice_user(#{user_id := UserId} = Request, State) ->
|
||||
ConnectionId = maps:get(connection_id, Request, null),
|
||||
@@ -85,12 +118,22 @@ disconnect_voice_user(#{user_id := UserId} = Request, State) ->
|
||||
end),
|
||||
case maps:size(UserVoiceStates) of
|
||||
0 ->
|
||||
{reply, #{success => true}, State};
|
||||
%% No active voice states - also clean up any pending connections
|
||||
State1 = clear_pending_voice_connections_for_user(UserId, State),
|
||||
{reply, #{success => true}, State1};
|
||||
_ ->
|
||||
maybe_force_disconnect_voice_states(UserVoiceStates, State),
|
||||
NewVoiceStates = voice_state_utils:drop_voice_states(
|
||||
UserVoiceStates, VoiceStates
|
||||
),
|
||||
NewState = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState0 = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState = maps:fold(
|
||||
fun(ConnId, _, AccState) ->
|
||||
clear_recently_disconnected(ConnId, AccState)
|
||||
end,
|
||||
NewState0,
|
||||
UserVoiceStates
|
||||
),
|
||||
voice_state_utils:broadcast_disconnects(UserVoiceStates, NewState),
|
||||
FinalState = cleanup_virtual_channel_access_for_user(UserId, NewState),
|
||||
{reply, #{success => true}, FinalState}
|
||||
@@ -98,14 +141,18 @@ disconnect_voice_user(#{user_id := UserId} = Request, State) ->
|
||||
SpecificConnection ->
|
||||
case maps:get(SpecificConnection, VoiceStates, undefined) of
|
||||
undefined ->
|
||||
{reply, #{success => true}, State};
|
||||
%% Not found in voice_states - also clean up pending connection
|
||||
State1 = clear_pending_voice_connection(SpecificConnection, State),
|
||||
{reply, #{success => true}, State1};
|
||||
VoiceState ->
|
||||
case voice_state_utils:voice_state_user_id(VoiceState) of
|
||||
undefined ->
|
||||
{reply, gateway_errors:error(voice_invalid_state), State};
|
||||
VoiceStateUserId when VoiceStateUserId =:= UserId ->
|
||||
maybe_force_disconnect_voice_state(SpecificConnection, VoiceState, State),
|
||||
NewVoiceStates = maps:remove(SpecificConnection, VoiceStates),
|
||||
NewState = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState0 = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState = clear_recently_disconnected(SpecificConnection, NewState0),
|
||||
voice_state_utils:broadcast_disconnects(
|
||||
#{SpecificConnection => VoiceState}, NewState
|
||||
),
|
||||
@@ -117,6 +164,19 @@ disconnect_voice_user(#{user_id := UserId} = Request, State) ->
|
||||
end
|
||||
end.
|
||||
|
||||
-spec clear_pending_voice_connections_for_user(integer(), guild_state()) -> guild_state().
|
||||
clear_pending_voice_connections_for_user(UserId, State) ->
|
||||
PendingConnections = maps:get(pending_voice_connections, State, #{}),
|
||||
FilteredPending = maps:filter(
|
||||
fun(_ConnId, PendingData) ->
|
||||
PendingUserId = maps:get(user_id, PendingData, undefined),
|
||||
PendingUserId =/= UserId
|
||||
end,
|
||||
PendingConnections
|
||||
),
|
||||
maps:put(pending_voice_connections, FilteredPending, State).
|
||||
|
||||
-spec disconnect_voice_user_if_in_channel(map(), guild_state()) -> {reply, map(), guild_state()}.
|
||||
disconnect_voice_user_if_in_channel(
|
||||
#{user_id := UserId, expected_channel_id := ExpectedChannelId} = Request,
|
||||
State
|
||||
@@ -131,27 +191,36 @@ disconnect_voice_user_if_in_channel(
|
||||
end),
|
||||
case maps:size(UserVoiceStates) of
|
||||
0 ->
|
||||
%% Not found in voice_states - also clean up any pending connections
|
||||
%% for this user/channel (user disconnected before LiveKit confirmation)
|
||||
State1 = clear_pending_voice_connections_for_user_channel(
|
||||
UserId, ExpectedChannelId, State
|
||||
),
|
||||
{reply,
|
||||
#{
|
||||
success => true,
|
||||
ignored => true,
|
||||
reason => <<"not_in_expected_channel">>
|
||||
},
|
||||
State};
|
||||
State1};
|
||||
_ ->
|
||||
NewVoiceStates = voice_state_utils:drop_voice_states(
|
||||
UserVoiceStates, VoiceStates
|
||||
),
|
||||
NewState = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState0 = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState = cache_recently_disconnected(UserVoiceStates, NewState0),
|
||||
voice_state_utils:broadcast_disconnects(UserVoiceStates, NewState),
|
||||
{reply, #{success => true}, NewState}
|
||||
end;
|
||||
ConnId ->
|
||||
case maps:get(ConnId, VoiceStates, undefined) of
|
||||
undefined ->
|
||||
%% Not found in voice_states - also clean up pending connection
|
||||
%% (user disconnected before LiveKit confirmation)
|
||||
State1 = clear_pending_voice_connection(ConnId, State),
|
||||
{reply,
|
||||
#{success => true, ignored => true, reason => <<"connection_not_found">>},
|
||||
State};
|
||||
State1};
|
||||
VoiceState ->
|
||||
case
|
||||
{
|
||||
@@ -161,7 +230,10 @@ disconnect_voice_user_if_in_channel(
|
||||
of
|
||||
{UserId, ExpectedChannelId} ->
|
||||
NewVoiceStates = maps:remove(ConnId, VoiceStates),
|
||||
NewState = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState0 = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState = cache_recently_disconnected(
|
||||
#{ConnId => VoiceState}, NewState0
|
||||
),
|
||||
voice_state_utils:broadcast_disconnects(
|
||||
#{ConnId => VoiceState}, NewState
|
||||
),
|
||||
@@ -178,105 +250,157 @@ disconnect_voice_user_if_in_channel(
|
||||
end
|
||||
end.
|
||||
|
||||
-spec clear_pending_voice_connections_for_user_channel(integer(), integer(), guild_state()) ->
|
||||
guild_state().
|
||||
clear_pending_voice_connections_for_user_channel(UserId, ChannelId, State) ->
|
||||
PendingConnections = maps:get(pending_voice_connections, State, #{}),
|
||||
FilteredPending = maps:filter(
|
||||
fun(_ConnId, PendingData) ->
|
||||
PendingUserId = maps:get(user_id, PendingData, undefined),
|
||||
PendingChannelId = maps:get(channel_id, PendingData, undefined),
|
||||
not (PendingUserId =:= UserId andalso PendingChannelId =:= ChannelId)
|
||||
end,
|
||||
PendingConnections
|
||||
),
|
||||
maps:put(pending_voice_connections, FilteredPending, State).
|
||||
|
||||
-spec clear_pending_voice_connections_for_channel(integer(), guild_state()) -> guild_state().
|
||||
clear_pending_voice_connections_for_channel(ChannelId, State) ->
|
||||
PendingConnections = maps:get(pending_voice_connections, State, #{}),
|
||||
FilteredPending = maps:filter(
|
||||
fun(_ConnId, PendingData) ->
|
||||
PendingChannelId = maps:get(channel_id, PendingData, undefined),
|
||||
PendingChannelId =/= ChannelId
|
||||
end,
|
||||
PendingConnections
|
||||
),
|
||||
maps:put(pending_voice_connections, FilteredPending, State).
|
||||
|
||||
-spec disconnect_all_voice_users_in_channel(map(), guild_state()) -> {reply, map(), guild_state()}.
|
||||
disconnect_all_voice_users_in_channel(#{channel_id := ChannelId}, State) ->
|
||||
VoiceStates = voice_state_utils:voice_states(State),
|
||||
ChannelVoiceStates = voice_state_utils:filter_voice_states(VoiceStates, fun(_, V) ->
|
||||
voice_state_utils:voice_state_channel_id(V) =:= ChannelId
|
||||
end),
|
||||
%% Also clean up any pending connections for this channel
|
||||
%% (users that requested tokens but haven't confirmed via LiveKit yet)
|
||||
State1 = clear_pending_voice_connections_for_channel(ChannelId, State),
|
||||
case maps:size(ChannelVoiceStates) of
|
||||
0 ->
|
||||
{reply, #{success => true, disconnected_count => 0}, State};
|
||||
{reply, #{success => true, disconnected_count => 0}, State1};
|
||||
Count ->
|
||||
maybe_force_disconnect_voice_states(ChannelVoiceStates, State1),
|
||||
NewVoiceStates = voice_state_utils:drop_voice_states(ChannelVoiceStates, VoiceStates),
|
||||
NewState = maps:put(voice_states, NewVoiceStates, State),
|
||||
NewState0 = maps:put(voice_states, NewVoiceStates, State1),
|
||||
NewState = clear_recently_disconnected_for_channel(ChannelId, NewState0),
|
||||
voice_state_utils:broadcast_disconnects(ChannelVoiceStates, NewState),
|
||||
{reply, #{success => true, disconnected_count => Count}, NewState}
|
||||
end.
|
||||
|
||||
-spec maybe_force_disconnect_voice_states(voice_state_map(), guild_state()) -> ok.
|
||||
maybe_force_disconnect_voice_states(VoiceStates, State) ->
|
||||
maps:foreach(
|
||||
fun(ConnId, VoiceState) ->
|
||||
maybe_force_disconnect_voice_state(ConnId, VoiceState, State)
|
||||
end,
|
||||
VoiceStates
|
||||
),
|
||||
ok.
|
||||
|
||||
-spec maybe_force_disconnect_voice_state(binary(), voice_state(), guild_state()) -> ok.
|
||||
maybe_force_disconnect_voice_state(ConnectionId, VoiceState, State) ->
|
||||
UserId = voice_state_utils:voice_state_user_id(VoiceState),
|
||||
ChannelId = voice_state_utils:voice_state_channel_id(VoiceState),
|
||||
GuildId = resolve_guild_id(VoiceState, State),
|
||||
case {GuildId, ChannelId, UserId} of
|
||||
{GId, CId, UId} when is_integer(GId), is_integer(CId), is_integer(UId) ->
|
||||
_ = maybe_force_disconnect(GId, CId, UId, ConnectionId, State),
|
||||
ok;
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec resolve_guild_id(voice_state(), guild_state()) -> integer() | undefined.
|
||||
resolve_guild_id(VoiceState, State) ->
|
||||
case voice_state_utils:voice_state_guild_id(VoiceState) of
|
||||
undefined ->
|
||||
map_utils:get_integer(State, id, undefined);
|
||||
GuildId ->
|
||||
GuildId
|
||||
end.
|
||||
|
||||
-spec force_disconnect_participant(integer(), integer(), integer(), binary()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
force_disconnect_participant(GuildId, ChannelId, UserId, ConnectionId) ->
|
||||
Req = voice_utils:build_force_disconnect_rpc_request(GuildId, ChannelId, UserId, ConnectionId),
|
||||
case rpc_client:call(Req) of
|
||||
{ok, _Data} ->
|
||||
logger:debug(
|
||||
"[guild_voice_disconnect] Force disconnected participant via RPC ~p",
|
||||
[
|
||||
[
|
||||
{guildId, GuildId},
|
||||
{channelId, ChannelId},
|
||||
{userId, UserId},
|
||||
{connectionId, ConnectionId}
|
||||
]
|
||||
]
|
||||
),
|
||||
{ok, #{success => true}};
|
||||
{error, Reason} ->
|
||||
logger:error(
|
||||
"[guild_voice_disconnect] Failed to force disconnect participant via RPC ~p",
|
||||
[
|
||||
[
|
||||
{guildId, GuildId},
|
||||
{channelId, ChannelId},
|
||||
{userId, UserId},
|
||||
{connectionId, ConnectionId},
|
||||
{error, Reason}
|
||||
]
|
||||
]
|
||||
),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec cleanup_virtual_channel_access_for_user(integer(), guild_state()) -> guild_state().
|
||||
cleanup_virtual_channel_access_for_user(UserId, State) ->
|
||||
VoiceStates = voice_state_utils:voice_states(State),
|
||||
HasVoiceConnection = maps:fold(
|
||||
VirtualChannels = guild_virtual_channel_access:get_virtual_channels_for_user(UserId, State),
|
||||
lists:foldl(
|
||||
fun(ChannelId, AccState) ->
|
||||
case user_has_voice_connection_in_channel(UserId, ChannelId, VoiceStates) of
|
||||
true ->
|
||||
AccState;
|
||||
false ->
|
||||
case
|
||||
guild_virtual_channel_access:is_pending_join(UserId, ChannelId, AccState) orelse
|
||||
guild_virtual_channel_access:has_preserve(UserId, ChannelId, AccState) orelse
|
||||
guild_virtual_channel_access:is_move_pending(
|
||||
UserId, ChannelId, AccState
|
||||
)
|
||||
of
|
||||
true ->
|
||||
AccState;
|
||||
false ->
|
||||
ok = maybe_dispatch_visibility_remove(UserId, ChannelId, AccState),
|
||||
guild_virtual_channel_access:remove_virtual_access(
|
||||
UserId, ChannelId, AccState
|
||||
)
|
||||
end
|
||||
end
|
||||
end,
|
||||
State,
|
||||
VirtualChannels
|
||||
).
|
||||
|
||||
-spec user_has_voice_connection_in_channel(integer(), integer(), voice_state_map()) -> boolean().
|
||||
user_has_voice_connection_in_channel(UserId, ChannelId, VoiceStates) ->
|
||||
ChannelIdBin = integer_to_binary(ChannelId),
|
||||
maps:fold(
|
||||
fun(_ConnId, VoiceState, Acc) ->
|
||||
case Acc of
|
||||
true -> true;
|
||||
false -> voice_state_utils:voice_state_user_id(VoiceState) =:= UserId
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
voice_state_utils:voice_state_user_id(VoiceState) =:= UserId andalso
|
||||
maps:get(<<"channel_id">>, VoiceState, null) =:= ChannelIdBin
|
||||
end
|
||||
end,
|
||||
false,
|
||||
VoiceStates
|
||||
),
|
||||
case HasVoiceConnection of
|
||||
).
|
||||
|
||||
-spec maybe_dispatch_visibility_remove(integer(), integer(), guild_state()) -> ok.
|
||||
maybe_dispatch_visibility_remove(UserId, ChannelId, State) ->
|
||||
case guild_virtual_channel_access:has_virtual_access(UserId, ChannelId, State) of
|
||||
true ->
|
||||
State;
|
||||
guild_virtual_channel_access:dispatch_channel_visibility_change(
|
||||
UserId, ChannelId, remove, State
|
||||
);
|
||||
false ->
|
||||
VirtualChannels = guild_virtual_channel_access:get_virtual_channels_for_user(
|
||||
UserId, State
|
||||
),
|
||||
lists:foldl(
|
||||
fun(ChannelId, AccState) ->
|
||||
Member = guild_permissions:find_member_by_user_id(UserId, AccState),
|
||||
case Member of
|
||||
undefined ->
|
||||
AccState;
|
||||
_ ->
|
||||
HasViewPermission = guild_permissions:can_view_channel_by_permissions(
|
||||
UserId, ChannelId, Member, AccState
|
||||
),
|
||||
case HasViewPermission of
|
||||
true ->
|
||||
guild_virtual_channel_access:remove_virtual_access(
|
||||
UserId, ChannelId, AccState
|
||||
);
|
||||
false ->
|
||||
guild_virtual_channel_access:dispatch_channel_visibility_change(
|
||||
UserId, ChannelId, remove, AccState
|
||||
),
|
||||
guild_virtual_channel_access:remove_virtual_access(
|
||||
UserId, ChannelId, AccState
|
||||
)
|
||||
end
|
||||
end
|
||||
end,
|
||||
State,
|
||||
VirtualChannels
|
||||
)
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec maybe_force_disconnect(integer(), integer(), integer(), binary(), guild_state()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
maybe_force_disconnect(GuildId, ChannelId, UserId, ConnectionId, State) ->
|
||||
case maps:get(test_force_disconnect_fun, State, undefined) of
|
||||
Fun when is_function(Fun, 4) ->
|
||||
@@ -285,6 +409,59 @@ maybe_force_disconnect(GuildId, ChannelId, UserId, ConnectionId, State) ->
|
||||
force_disconnect_participant(GuildId, ChannelId, UserId, ConnectionId)
|
||||
end.
|
||||
|
||||
-define(RECENTLY_DISCONNECTED_TTL_MS, 60000).
|
||||
|
||||
-spec recently_disconnected_voice_states(guild_state()) -> map().
|
||||
recently_disconnected_voice_states(State) ->
|
||||
case maps:get(recently_disconnected_voice_states, State, undefined) of
|
||||
Map when is_map(Map) -> Map;
|
||||
_ -> #{}
|
||||
end.
|
||||
|
||||
-spec cache_recently_disconnected(voice_state_map(), guild_state()) -> guild_state().
|
||||
cache_recently_disconnected(VoiceStatesToCache, State) ->
|
||||
Now = erlang:system_time(millisecond),
|
||||
Existing = recently_disconnected_voice_states(State),
|
||||
Swept = sweep_expired_recently_disconnected(Existing, Now),
|
||||
NewEntries = maps:fold(
|
||||
fun(ConnId, VoiceState, Acc) ->
|
||||
maps:put(ConnId, #{voice_state => VoiceState, disconnected_at => Now}, Acc)
|
||||
end,
|
||||
Swept,
|
||||
VoiceStatesToCache
|
||||
),
|
||||
maps:put(recently_disconnected_voice_states, NewEntries, State).
|
||||
|
||||
-spec sweep_expired_recently_disconnected(map(), integer()) -> map().
|
||||
sweep_expired_recently_disconnected(Cache, Now) ->
|
||||
maps:filter(
|
||||
fun(_ConnId, #{disconnected_at := DisconnectedAt}) ->
|
||||
(Now - DisconnectedAt) < ?RECENTLY_DISCONNECTED_TTL_MS;
|
||||
(_ConnId, _) ->
|
||||
false
|
||||
end,
|
||||
Cache
|
||||
).
|
||||
|
||||
-spec clear_recently_disconnected(binary(), guild_state()) -> guild_state().
|
||||
clear_recently_disconnected(ConnectionId, State) ->
|
||||
Cache = recently_disconnected_voice_states(State),
|
||||
NewCache = maps:remove(ConnectionId, Cache),
|
||||
maps:put(recently_disconnected_voice_states, NewCache, State).
|
||||
|
||||
-spec clear_recently_disconnected_for_channel(integer(), guild_state()) -> guild_state().
|
||||
clear_recently_disconnected_for_channel(ChannelId, State) ->
|
||||
Cache = recently_disconnected_voice_states(State),
|
||||
NewCache = maps:filter(
|
||||
fun(_ConnId, #{voice_state := VS}) ->
|
||||
voice_state_utils:voice_state_channel_id(VS) =/= ChannelId;
|
||||
(_ConnId, _) ->
|
||||
false
|
||||
end,
|
||||
Cache
|
||||
),
|
||||
maps:put(recently_disconnected_voice_states, NewCache, State).
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
disconnect_voice_user_removes_all_connections_test() ->
|
||||
@@ -292,7 +469,10 @@ disconnect_voice_user_removes_all_connections_test() ->
|
||||
<<"a">> => voice_state_fixture(5, 10, 20),
|
||||
<<"b">> => voice_state_fixture(5, 10, 21)
|
||||
},
|
||||
State = #{voice_states => VoiceStates},
|
||||
State = #{
|
||||
voice_states => VoiceStates,
|
||||
test_force_disconnect_fun => fun(_, _, _, _) -> {ok, #{success => true}} end
|
||||
},
|
||||
{reply, #{success := true}, #{voice_states := #{}}} =
|
||||
disconnect_voice_user(#{user_id => 5, connection_id => null}, State).
|
||||
|
||||
@@ -309,6 +489,353 @@ disconnect_voice_user_if_in_channel_ignored_test() ->
|
||||
{reply, #{ignored := true}, _} =
|
||||
disconnect_voice_user_if_in_channel(#{user_id => 5, expected_channel_id => 99}, State).
|
||||
|
||||
user_has_voice_connection_in_channel_test() ->
|
||||
VoiceStates = #{
|
||||
<<"conn">> => #{<<"user_id">> => <<"10">>, <<"channel_id">> => <<"100">>}
|
||||
},
|
||||
?assert(user_has_voice_connection_in_channel(10, 100, VoiceStates)),
|
||||
?assertNot(user_has_voice_connection_in_channel(10, 200, VoiceStates)),
|
||||
?assertNot(user_has_voice_connection_in_channel(20, 100, VoiceStates)).
|
||||
|
||||
recently_disconnected_voice_states_default_test() ->
|
||||
?assertEqual(#{}, recently_disconnected_voice_states(#{})).
|
||||
|
||||
recently_disconnected_voice_states_returns_map_test() ->
|
||||
Cache = #{<<"conn">> => #{voice_state => #{}, disconnected_at => 1000}},
|
||||
State = #{recently_disconnected_voice_states => Cache},
|
||||
?assertEqual(Cache, recently_disconnected_voice_states(State)).
|
||||
|
||||
cache_recently_disconnected_test() ->
|
||||
VS = voice_state_fixture(5, 10, 20),
|
||||
State = #{},
|
||||
NewState = cache_recently_disconnected(#{<<"conn">> => VS}, State),
|
||||
Cache = recently_disconnected_voice_states(NewState),
|
||||
?assert(maps:is_key(<<"conn">>, Cache)),
|
||||
#{<<"conn">> := #{voice_state := CachedVS}} = Cache,
|
||||
?assertEqual(VS, CachedVS).
|
||||
|
||||
clear_recently_disconnected_test() ->
|
||||
VS = voice_state_fixture(5, 10, 20),
|
||||
State0 = cache_recently_disconnected(#{<<"conn">> => VS}, #{}),
|
||||
State1 = clear_recently_disconnected(<<"conn">>, State0),
|
||||
?assertEqual(#{}, recently_disconnected_voice_states(State1)).
|
||||
|
||||
clear_recently_disconnected_for_channel_test() ->
|
||||
VS1 = voice_state_fixture(5, 10, 20),
|
||||
VS2 = voice_state_fixture(6, 10, 30),
|
||||
State0 = cache_recently_disconnected(#{<<"a">> => VS1, <<"b">> => VS2}, #{}),
|
||||
State1 = clear_recently_disconnected_for_channel(20, State0),
|
||||
Cache = recently_disconnected_voice_states(State1),
|
||||
?assertNot(maps:is_key(<<"a">>, Cache)),
|
||||
?assert(maps:is_key(<<"b">>, Cache)).
|
||||
|
||||
sweep_expired_recently_disconnected_test() ->
|
||||
Now = erlang:system_time(millisecond),
|
||||
Cache = #{
|
||||
<<"fresh">> => #{voice_state => #{}, disconnected_at => Now - 1000},
|
||||
<<"expired">> => #{voice_state => #{}, disconnected_at => Now - 70000}
|
||||
},
|
||||
Swept = sweep_expired_recently_disconnected(Cache, Now),
|
||||
?assert(maps:is_key(<<"fresh">>, Swept)),
|
||||
?assertNot(maps:is_key(<<"expired">>, Swept)).
|
||||
|
||||
disconnect_voice_user_if_in_channel_caches_by_connection_test() ->
|
||||
VS = voice_state_fixture(5, 10, 20),
|
||||
VoiceStates = #{<<"conn">> => VS},
|
||||
State = #{voice_states => VoiceStates},
|
||||
{reply, #{success := true}, NewState} =
|
||||
disconnect_voice_user_if_in_channel(
|
||||
#{user_id => 5, expected_channel_id => 20, connection_id => <<"conn">>},
|
||||
State
|
||||
),
|
||||
Cache = recently_disconnected_voice_states(NewState),
|
||||
?assert(maps:is_key(<<"conn">>, Cache)).
|
||||
|
||||
disconnect_voice_user_calls_force_disconnect_for_all_connections_test() ->
|
||||
Self = self(),
|
||||
TestFun = fun(GuildId, ChannelId, UserId, ConnectionId) ->
|
||||
Self ! {force_disconnect, GuildId, ChannelId, UserId, ConnectionId},
|
||||
{ok, #{success => true}}
|
||||
end,
|
||||
VoiceStates = #{
|
||||
<<"a">> => voice_state_fixture(5, 10, 20),
|
||||
<<"b">> => voice_state_fixture(5, 10, 21)
|
||||
},
|
||||
State = #{
|
||||
id => 10,
|
||||
voice_states => VoiceStates,
|
||||
test_force_disconnect_fun => TestFun
|
||||
},
|
||||
{reply, #{success := true}, #{voice_states := #{}}} =
|
||||
disconnect_voice_user(#{user_id => 5, connection_id => null}, State),
|
||||
Msgs = collect_force_disconnect_messages(2),
|
||||
?assertEqual(2, length(Msgs)),
|
||||
?assert(lists:member({force_disconnect, 10, 20, 5, <<"a">>}, Msgs)),
|
||||
?assert(lists:member({force_disconnect, 10, 21, 5, <<"b">>}, Msgs)).
|
||||
|
||||
disconnect_voice_user_calls_force_disconnect_for_specific_connection_test() ->
|
||||
Self = self(),
|
||||
TestFun = fun(GuildId, ChannelId, UserId, ConnectionId) ->
|
||||
Self ! {force_disconnect, GuildId, ChannelId, UserId, ConnectionId},
|
||||
{ok, #{success => true}}
|
||||
end,
|
||||
VoiceStates = #{
|
||||
<<"a">> => voice_state_fixture(5, 10, 20),
|
||||
<<"b">> => voice_state_fixture(5, 10, 21)
|
||||
},
|
||||
State = #{
|
||||
id => 10,
|
||||
voice_states => VoiceStates,
|
||||
test_force_disconnect_fun => TestFun
|
||||
},
|
||||
{reply, #{success := true}, NewState} =
|
||||
disconnect_voice_user(#{user_id => 5, connection_id => <<"a">>}, State),
|
||||
Msgs = collect_force_disconnect_messages(1),
|
||||
?assertEqual(1, length(Msgs)),
|
||||
?assert(lists:member({force_disconnect, 10, 20, 5, <<"a">>}, Msgs)),
|
||||
Remaining = maps:get(voice_states, NewState),
|
||||
?assert(maps:is_key(<<"b">>, Remaining)),
|
||||
?assertNot(maps:is_key(<<"a">>, Remaining)).
|
||||
|
||||
disconnect_all_voice_users_in_channel_calls_force_disconnect_test() ->
|
||||
Self = self(),
|
||||
TestFun = fun(GuildId, ChannelId, UserId, ConnectionId) ->
|
||||
Self ! {force_disconnect, GuildId, ChannelId, UserId, ConnectionId},
|
||||
{ok, #{success => true}}
|
||||
end,
|
||||
VoiceStates = #{
|
||||
<<"a">> => voice_state_fixture(5, 10, 20),
|
||||
<<"b">> => voice_state_fixture(6, 10, 20),
|
||||
<<"c">> => voice_state_fixture(7, 10, 30)
|
||||
},
|
||||
State = #{
|
||||
id => 10,
|
||||
voice_states => VoiceStates,
|
||||
test_force_disconnect_fun => TestFun
|
||||
},
|
||||
{reply, #{success := true, disconnected_count := 2}, NewState} =
|
||||
disconnect_all_voice_users_in_channel(#{channel_id => 20}, State),
|
||||
Msgs = collect_force_disconnect_messages(2),
|
||||
?assertEqual(2, length(Msgs)),
|
||||
?assert(lists:member({force_disconnect, 10, 20, 5, <<"a">>}, Msgs)),
|
||||
?assert(lists:member({force_disconnect, 10, 20, 6, <<"b">>}, Msgs)),
|
||||
Remaining = maps:get(voice_states, NewState),
|
||||
?assert(maps:is_key(<<"c">>, Remaining)),
|
||||
?assertNot(maps:is_key(<<"a">>, Remaining)),
|
||||
?assertNot(maps:is_key(<<"b">>, Remaining)).
|
||||
|
||||
disconnect_voice_user_if_in_channel_skips_force_disconnect_test() ->
|
||||
Self = self(),
|
||||
TestFun = fun(_, _, _, _) ->
|
||||
Self ! force_disconnect_called,
|
||||
{ok, #{success => true}}
|
||||
end,
|
||||
VoiceStates = #{<<"a">> => voice_state_fixture(5, 10, 20)},
|
||||
State = #{
|
||||
voice_states => VoiceStates,
|
||||
test_force_disconnect_fun => TestFun
|
||||
},
|
||||
{reply, #{success := true}, _} =
|
||||
disconnect_voice_user_if_in_channel(
|
||||
#{user_id => 5, expected_channel_id => 20, connection_id => <<"a">>},
|
||||
State
|
||||
),
|
||||
receive
|
||||
force_disconnect_called -> ?assert(false)
|
||||
after 0 ->
|
||||
ok
|
||||
end.
|
||||
|
||||
%% Tests for clear_pending_voice_connection/2
|
||||
|
||||
clear_pending_voice_connection_removes_connection_test() ->
|
||||
PendingConnections = #{
|
||||
<<"conn1">> => #{user_id => 1, channel_id => 100},
|
||||
<<"conn2">> => #{user_id => 2, channel_id => 200}
|
||||
},
|
||||
State = #{pending_voice_connections => PendingConnections},
|
||||
NewState = clear_pending_voice_connection(<<"conn1">>, State),
|
||||
NewPending = maps:get(pending_voice_connections, NewState),
|
||||
?assertNot(maps:is_key(<<"conn1">>, NewPending)),
|
||||
?assert(maps:is_key(<<"conn2">>, NewPending)).
|
||||
|
||||
clear_pending_voice_connection_ignores_missing_test() ->
|
||||
PendingConnections = #{<<"conn1">> => #{user_id => 1, channel_id => 100}},
|
||||
State = #{pending_voice_connections => PendingConnections},
|
||||
NewState = clear_pending_voice_connection(<<"missing">>, State),
|
||||
?assertEqual(State, NewState).
|
||||
|
||||
clear_pending_voice_connection_handles_empty_pending_test() ->
|
||||
State = #{voice_states => #{}},
|
||||
NewState = clear_pending_voice_connection(<<"conn">>, State),
|
||||
?assertEqual(#{}, maps:get(pending_voice_connections, NewState, #{})).
|
||||
|
||||
%% Tests for clear_pending_voice_connections_for_user/2
|
||||
|
||||
clear_pending_voice_connections_for_user_removes_all_user_connections_test() ->
|
||||
PendingConnections = #{
|
||||
<<"conn1">> => #{user_id => 5, channel_id => 100},
|
||||
<<"conn2">> => #{user_id => 5, channel_id => 200},
|
||||
<<"conn3">> => #{user_id => 6, channel_id => 100}
|
||||
},
|
||||
State = #{pending_voice_connections => PendingConnections},
|
||||
NewState = clear_pending_voice_connections_for_user(5, State),
|
||||
NewPending = maps:get(pending_voice_connections, NewState),
|
||||
?assertNot(maps:is_key(<<"conn1">>, NewPending)),
|
||||
?assertNot(maps:is_key(<<"conn2">>, NewPending)),
|
||||
?assert(maps:is_key(<<"conn3">>, NewPending)).
|
||||
|
||||
%% Tests for clear_pending_voice_connections_for_user_channel/3
|
||||
|
||||
clear_pending_voice_connections_for_user_channel_removes_matching_test() ->
|
||||
PendingConnections = #{
|
||||
<<"conn1">> => #{user_id => 5, channel_id => 100},
|
||||
<<"conn2">> => #{user_id => 5, channel_id => 200},
|
||||
<<"conn3">> => #{user_id => 6, channel_id => 100}
|
||||
},
|
||||
State = #{pending_voice_connections => PendingConnections},
|
||||
NewState = clear_pending_voice_connections_for_user_channel(5, 100, State),
|
||||
NewPending = maps:get(pending_voice_connections, NewState),
|
||||
?assertNot(maps:is_key(<<"conn1">>, NewPending)),
|
||||
?assert(maps:is_key(<<"conn2">>, NewPending)),
|
||||
?assert(maps:is_key(<<"conn3">>, NewPending)).
|
||||
|
||||
%% Tests for clear_pending_voice_connections_for_channel/2
|
||||
|
||||
clear_pending_voice_connections_for_channel_removes_all_channel_connections_test() ->
|
||||
PendingConnections = #{
|
||||
<<"conn1">> => #{user_id => 5, channel_id => 100},
|
||||
<<"conn2">> => #{user_id => 6, channel_id => 100},
|
||||
<<"conn3">> => #{user_id => 7, channel_id => 200}
|
||||
},
|
||||
State = #{pending_voice_connections => PendingConnections},
|
||||
NewState = clear_pending_voice_connections_for_channel(100, State),
|
||||
NewPending = maps:get(pending_voice_connections, NewState),
|
||||
?assertNot(maps:is_key(<<"conn1">>, NewPending)),
|
||||
?assertNot(maps:is_key(<<"conn2">>, NewPending)),
|
||||
?assert(maps:is_key(<<"conn3">>, NewPending)).
|
||||
|
||||
%% Tests for disconnect handlers cleaning up pending connections
|
||||
|
||||
handle_voice_disconnect_cleans_pending_when_not_in_voice_states_test() ->
|
||||
PendingConnections = #{<<"conn1">> => #{user_id => 5, channel_id => 100}},
|
||||
State = #{
|
||||
voice_states => #{},
|
||||
pending_voice_connections => PendingConnections
|
||||
},
|
||||
{reply, #{success := true}, NewState} =
|
||||
handle_voice_disconnect(<<"conn1">>, undefined, 5, #{}, State),
|
||||
NewPending = maps:get(pending_voice_connections, NewState),
|
||||
?assertNot(maps:is_key(<<"conn1">>, NewPending)).
|
||||
|
||||
disconnect_voice_user_cleans_pending_when_no_active_states_test() ->
|
||||
PendingConnections = #{
|
||||
<<"conn1">> => #{user_id => 5, channel_id => 100},
|
||||
<<"conn2">> => #{user_id => 6, channel_id => 100}
|
||||
},
|
||||
State = #{
|
||||
id => 10,
|
||||
voice_states => #{},
|
||||
pending_voice_connections => PendingConnections
|
||||
},
|
||||
{reply, #{success := true}, NewState} =
|
||||
disconnect_voice_user(#{user_id => 5}, State),
|
||||
NewPending = maps:get(pending_voice_connections, NewState),
|
||||
?assertNot(maps:is_key(<<"conn1">>, NewPending)),
|
||||
?assert(maps:is_key(<<"conn2">>, NewPending)).
|
||||
|
||||
disconnect_voice_user_cleans_pending_for_specific_connection_test() ->
|
||||
PendingConnections = #{
|
||||
<<"conn1">> => #{user_id => 5, channel_id => 100},
|
||||
<<"conn2">> => #{user_id => 5, channel_id => 200}
|
||||
},
|
||||
State = #{
|
||||
id => 10,
|
||||
voice_states => #{},
|
||||
pending_voice_connections => PendingConnections
|
||||
},
|
||||
{reply, #{success := true}, NewState} =
|
||||
disconnect_voice_user(#{user_id => 5, connection_id => <<"conn1">>}, State),
|
||||
NewPending = maps:get(pending_voice_connections, NewState),
|
||||
?assertNot(maps:is_key(<<"conn1">>, NewPending)),
|
||||
?assert(maps:is_key(<<"conn2">>, NewPending)).
|
||||
|
||||
disconnect_voice_user_if_in_channel_cleans_pending_when_not_found_test() ->
|
||||
PendingConnections = #{
|
||||
<<"conn1">> => #{user_id => 5, channel_id => 100},
|
||||
<<"conn2">> => #{user_id => 6, channel_id => 100}
|
||||
},
|
||||
State = #{
|
||||
id => 10,
|
||||
voice_states => #{},
|
||||
pending_voice_connections => PendingConnections
|
||||
},
|
||||
{reply, #{success := true, ignored := true}, NewState} =
|
||||
disconnect_voice_user_if_in_channel(
|
||||
#{user_id => 5, expected_channel_id => 100},
|
||||
State
|
||||
),
|
||||
NewPending = maps:get(pending_voice_connections, NewState),
|
||||
?assertNot(maps:is_key(<<"conn1">>, NewPending)),
|
||||
?assert(maps:is_key(<<"conn2">>, NewPending)).
|
||||
|
||||
disconnect_all_voice_users_in_channel_cleans_pending_test() ->
|
||||
Self = self(),
|
||||
TestFun = fun(GuildId, ChannelId, UserId, ConnectionId) ->
|
||||
Self ! {force_disconnect, GuildId, ChannelId, UserId, ConnectionId},
|
||||
{ok, #{success => true}}
|
||||
end,
|
||||
VoiceStates = #{
|
||||
<<"a">> => voice_state_fixture(5, 10, 20)
|
||||
},
|
||||
PendingConnections = #{
|
||||
<<"pending1">> => #{user_id => 6, channel_id => 20},
|
||||
<<"pending2">> => #{user_id => 7, channel_id => 30}
|
||||
},
|
||||
State = #{
|
||||
id => 10,
|
||||
voice_states => VoiceStates,
|
||||
pending_voice_connections => PendingConnections,
|
||||
test_force_disconnect_fun => TestFun
|
||||
},
|
||||
{reply, #{success := true, disconnected_count := 1}, NewState} =
|
||||
disconnect_all_voice_users_in_channel(#{channel_id => 20}, State),
|
||||
_ = collect_force_disconnect_messages(1),
|
||||
NewPending = maps:get(pending_voice_connections, NewState),
|
||||
?assertNot(maps:is_key(<<"pending1">>, NewPending)),
|
||||
?assert(maps:is_key(<<"pending2">>, NewPending)).
|
||||
|
||||
disconnect_all_voice_users_in_channel_cleans_pending_when_no_active_states_test() ->
|
||||
PendingConnections = #{
|
||||
<<"pending1">> => #{user_id => 5, channel_id => 20},
|
||||
<<"pending2">> => #{user_id => 6, channel_id => 30}
|
||||
},
|
||||
State = #{
|
||||
id => 10,
|
||||
voice_states => #{},
|
||||
pending_voice_connections => PendingConnections
|
||||
},
|
||||
{reply, #{success := true, disconnected_count := 0}, NewState} =
|
||||
disconnect_all_voice_users_in_channel(#{channel_id => 20}, State),
|
||||
NewPending = maps:get(pending_voice_connections, NewState),
|
||||
?assertNot(maps:is_key(<<"pending1">>, NewPending)),
|
||||
?assert(maps:is_key(<<"pending2">>, NewPending)).
|
||||
|
||||
collect_force_disconnect_messages(Count) ->
|
||||
collect_force_disconnect_messages(Count, []).
|
||||
|
||||
collect_force_disconnect_messages(0, Acc) ->
|
||||
lists:reverse(Acc);
|
||||
collect_force_disconnect_messages(Count, Acc) when Count > 0 ->
|
||||
receive
|
||||
{force_disconnect, _, _, _, _} = Msg ->
|
||||
collect_force_disconnect_messages(Count - 1, [Msg | Acc]);
|
||||
_Other ->
|
||||
collect_force_disconnect_messages(Count, Acc)
|
||||
after 200 ->
|
||||
lists:reverse(Acc)
|
||||
end.
|
||||
|
||||
voice_state_fixture(UserId, GuildId, ChannelId) ->
|
||||
#{
|
||||
<<"user_id">> => integer_to_binary(UserId),
|
||||
|
||||
Reference in New Issue
Block a user