Files
fluxer/fluxer_gateway/src/guild/voice/voice_disconnect_common.erl
T
2026-02-17 12:22:36 +00:00

163 lines
5.7 KiB
Erlang

%% Copyright (C) 2026 Fluxer Contributors
%%
%% This file is part of Fluxer.
%%
%% Fluxer is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%%
%% Fluxer is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
-module(voice_disconnect_common).
-export([
find_session_by_user_id/2,
disconnect_user/4,
disconnect_user_if_in_channel/5,
channel_has_capacity/3
]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-type user_id() :: integer().
-type session_id() :: binary().
-type session_pid() :: pid().
-type monitor_ref() :: reference().
-type session_tuple() :: {user_id(), session_pid(), monitor_ref()}.
-type sessions_map() :: #{session_id() => session_tuple()}.
-type voice_states_map() :: #{user_id() => map()}.
-type cleanup_fun() :: fun((user_id(), session_id()) -> ok).
-spec find_session_by_user_id(user_id(), sessions_map()) ->
{ok, session_id(), session_pid(), monitor_ref()} | not_found.
find_session_by_user_id(UserId, Sessions) ->
maps:fold(
fun
(SessionId, {U, Pid, Ref}, _) when U =:= UserId ->
{ok, SessionId, Pid, Ref};
(_, _, Acc) ->
Acc
end,
not_found,
Sessions
).
-spec disconnect_user(user_id(), voice_states_map(), sessions_map(), cleanup_fun()) ->
{ok, voice_states_map(), sessions_map()} | {not_found, voice_states_map(), sessions_map()}.
disconnect_user(UserId, VoiceStates, Sessions, CleanupFun) ->
case find_session_by_user_id(UserId, Sessions) of
not_found ->
{not_found, VoiceStates, Sessions};
{ok, SessionId, _Pid, Ref} ->
demonitor(Ref, [flush]),
CleanupFun(UserId, SessionId),
NewVoiceStates = maps:remove(UserId, VoiceStates),
NewSessions = maps:remove(SessionId, Sessions),
{ok, NewVoiceStates, NewSessions}
end.
-spec disconnect_user_if_in_channel(
user_id(), integer(), voice_states_map(), sessions_map(), cleanup_fun()
) ->
{ok, voice_states_map(), sessions_map()}
| {not_found, voice_states_map(), sessions_map()}
| {channel_mismatch, voice_states_map(), sessions_map()}.
disconnect_user_if_in_channel(UserId, ExpectedChannelId, VoiceStates, Sessions, CleanupFun) ->
case maps:get(UserId, VoiceStates, undefined) of
undefined ->
{not_found, VoiceStates, Sessions};
VoiceState ->
ChannelIdBin = maps:get(<<"channel_id">>, VoiceState, undefined),
ExpectedBin = integer_to_binary(ExpectedChannelId),
case ChannelIdBin =:= ExpectedBin of
false ->
{channel_mismatch, VoiceStates, Sessions};
true ->
disconnect_user(UserId, VoiceStates, Sessions, CleanupFun)
end
end.
-spec channel_has_capacity(binary() | integer(), non_neg_integer(), voice_states_map()) ->
boolean().
channel_has_capacity(_ChannelId, 0, _VoiceStates) ->
true;
channel_has_capacity(ChannelId, UserLimit, VoiceStates) ->
ChannelIdBin = ensure_binary(ChannelId),
UsersInChannel = maps:fold(
fun(_UserId, VoiceState, Count) ->
case maps:get(<<"channel_id">>, VoiceState, undefined) of
ChannelIdBin -> Count + 1;
_ -> Count
end
end,
0,
VoiceStates
),
UsersInChannel < UserLimit.
-spec ensure_binary(binary() | integer()) -> binary().
ensure_binary(Value) when is_binary(Value) -> Value;
ensure_binary(Value) when is_integer(Value) -> integer_to_binary(Value).
-ifdef(TEST).
find_session_by_user_id_test() ->
Pid = self(),
Ref = make_ref(),
Sessions = #{
<<"session1">> => {100, Pid, Ref},
<<"session2">> => {200, Pid, make_ref()}
},
?assertMatch({ok, <<"session1">>, _, _}, find_session_by_user_id(100, Sessions)),
?assertEqual(not_found, find_session_by_user_id(999, Sessions)).
disconnect_user_not_found_test() ->
VoiceStates = #{},
Sessions = #{},
CleanupFun = fun(_, _) -> ok end,
?assertMatch({not_found, _, _}, disconnect_user(100, VoiceStates, Sessions, CleanupFun)).
disconnect_user_if_in_channel_mismatch_test() ->
VoiceStates = #{100 => #{<<"channel_id">> => <<"999">>}},
Sessions = #{},
CleanupFun = fun(_, _) -> ok end,
Result = disconnect_user_if_in_channel(100, 123, VoiceStates, Sessions, CleanupFun),
?assertMatch({channel_mismatch, _, _}, Result).
disconnect_user_if_in_channel_not_found_test() ->
VoiceStates = #{},
Sessions = #{},
CleanupFun = fun(_, _) -> ok end,
Result = disconnect_user_if_in_channel(100, 123, VoiceStates, Sessions, CleanupFun),
?assertMatch({not_found, _, _}, Result).
channel_has_capacity_unlimited_test() ->
VoiceStates = #{
1 => #{<<"channel_id">> => <<"100">>},
2 => #{<<"channel_id">> => <<"100">>}
},
?assert(channel_has_capacity(100, 0, VoiceStates)).
channel_has_capacity_limited_test() ->
VoiceStates = #{
1 => #{<<"channel_id">> => <<"100">>},
2 => #{<<"channel_id">> => <<"100">>}
},
?assertNot(channel_has_capacity(100, 2, VoiceStates)),
?assert(channel_has_capacity(100, 3, VoiceStates)).
ensure_binary_test() ->
?assertEqual(<<"123">>, ensure_binary(123)),
?assertEqual(<<"abc">>, ensure_binary(<<"abc">>)).
-endif.