Files
fx-test/fluxer_gateway/src/guild/voice/guild_voice_permissions.erl
Hampus Kraft 2f557eda8c initial commit
2026-01-01 21:05:54 +00:00

171 lines
6.4 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(guild_voice_permissions).
-export([check_voice_permissions_and_limits/6]).
-type guild_state() :: map().
-type voice_state_map() :: #{binary() => map()}.
-type channel() :: map().
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-import(utils, [parse_iso8601_to_unix_ms/1]).
-spec check_voice_permissions_and_limits(
integer(), integer(), channel(), voice_state_map(), guild_state(), boolean()
) ->
{ok, allowed} | {error, atom(), atom()}.
check_voice_permissions_and_limits(UserId, ChannelIdValue, Channel, VoiceStates, State, IsUpdate) ->
case is_member_timed_out(UserId, State) of
true ->
gateway_errors:error(voice_member_timed_out);
false ->
case has_view_and_connect_perms(UserId, ChannelIdValue, State) of
false ->
gateway_errors:error(voice_permission_denied);
true ->
case
channel_has_capacity(UserId, ChannelIdValue, Channel, VoiceStates, IsUpdate)
of
true -> {ok, allowed};
false -> gateway_errors:error(voice_channel_full)
end
end
end.
-spec has_view_and_connect_perms(integer(), integer(), guild_state()) -> boolean().
has_view_and_connect_perms(UserId, ChannelIdValue, State) ->
case guild_virtual_channel_access:has_virtual_access(UserId, ChannelIdValue, State) of
true ->
true;
false ->
Permissions = resolve_permissions(UserId, ChannelIdValue, State),
ViewPerm = constants:view_channel_permission(),
ConnectPerm = constants:connect_permission(),
(Permissions band ViewPerm) =:= ViewPerm andalso
(Permissions band ConnectPerm) =:= ConnectPerm
end.
-spec channel_has_capacity(integer(), integer(), channel(), voice_state_map(), boolean()) ->
boolean().
channel_has_capacity(UserId, ChannelIdValue, Channel, VoiceStates, IsUpdate) ->
UserLimit = maps:get(<<"user_limit">>, Channel, 0),
case UserLimit of
0 ->
true;
Limit when Limit > 0 ->
UsersInChannel = users_in_channel(ChannelIdValue, VoiceStates),
CurrentCount = sets:size(UsersInChannel),
AlreadyPresent = sets:is_element(UserId, UsersInChannel),
AdjustedCount =
case AlreadyPresent orelse IsUpdate of
true -> CurrentCount - 1;
false -> CurrentCount
end,
AdjustedCount < Limit;
_ ->
true
end.
-spec is_member_timed_out(integer(), guild_state()) -> boolean().
is_member_timed_out(UserId, State) ->
case guild_permissions:find_member_by_user_id(UserId, State) of
undefined ->
false;
Member ->
TimeoutMs = parse_iso8601_to_unix_ms(
maps:get(<<"communication_disabled_until">>, Member, undefined)
),
case TimeoutMs of
undefined ->
false;
Value when is_integer(Value) ->
Value > erlang:system_time(millisecond);
_ ->
false
end
end.
-spec users_in_channel(integer(), voice_state_map()) -> sets:set().
users_in_channel(ChannelIdValue, VoiceStates0) ->
VoiceStates = voice_state_utils:ensure_voice_states(VoiceStates0),
maps:fold(
fun(_ConnId, VState, Acc) ->
case voice_state_utils:voice_state_channel_id(VState) of
ChannelIdValue ->
case voice_state_utils:voice_state_user_id(VState) of
undefined -> Acc;
UserId -> sets:add_element(UserId, Acc)
end;
_ ->
Acc
end
end,
sets:new(),
VoiceStates
).
-spec resolve_permissions(integer(), integer(), guild_state()) -> integer().
resolve_permissions(UserId, ChannelIdValue, State) ->
case State of
#{test_perm_fun := Fun} when is_function(Fun, 1) ->
Fun(UserId);
_ ->
guild_permissions:get_member_permissions(UserId, ChannelIdValue, State)
end.
-ifdef(TEST).
voice_permissions_missing_view_test() ->
State = permission_test_state(0, fun(_) -> constants:view_channel_permission() end),
Result = check_voice_permissions_and_limits(1, 10, #{<<"user_limit">> => 0}, #{}, State, false),
?assertMatch({error, permission_denied, voice_permission_denied}, Result).
voice_permissions_full_channel_test() ->
State = permission_test_state(2, fun(_) -> required_voice_perms() end),
VoiceStates = #{
<<"conn1">> => #{<<"channel_id">> => <<"10">>, <<"user_id">> => <<"1">>},
<<"conn2">> => #{<<"channel_id">> => <<"10">>, <<"user_id">> => <<"2">>}
},
Result = check_voice_permissions_and_limits(
3, 10, #{<<"user_limit">> => 2}, VoiceStates, State, false
),
?assertMatch({error, permission_denied, voice_channel_full}, Result).
voice_permissions_existing_user_update_test() ->
State = permission_test_state(2, fun(_) -> required_voice_perms() end),
VoiceStates = #{
<<"conn1">> => #{<<"channel_id">> => <<"10">>, <<"user_id">> => <<"1">>},
<<"conn2">> => #{<<"channel_id">> => <<"10">>, <<"user_id">> => <<"2">>}
},
Result = check_voice_permissions_and_limits(
1, 10, #{<<"user_limit">> => 2}, VoiceStates, State, true
),
?assertEqual({ok, allowed}, Result).
required_voice_perms() ->
constants:view_channel_permission() bor constants:connect_permission().
permission_test_state(GuildId, PermFun) ->
#{id => GuildId, test_perm_fun => PermFun}.
-endif.