Files
fluxer/fluxer_gateway/src/guild/guild_availability_check.erl
T

246 lines
9.4 KiB
Erlang

%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(guild_availability_check).
-typing([eqwalizer]).
-export([
get_unavailability_mode_from_state/1,
is_unavailable_hidden_enabled/1,
is_unavailable_hidden_enabled_from_features/1,
is_user_staff/2,
is_user_staff_from_user_data/1,
check_unavailability_transition/2
]).
-type guild_state() :: map().
-type user_id() :: integer().
-type unavailability_mode() ::
available
| unavailable_for_everyone
| unavailable_for_everyone_but_staff.
-type transition_result() ::
{unavailable_enabled, boolean()} | unavailable_disabled | no_change.
-export_type([guild_state/0, user_id/0, unavailability_mode/0, transition_result/0]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-spec get_unavailability_mode_from_state(guild_state()) -> unavailability_mode().
get_unavailability_mode_from_state(State) ->
Data = maps:get(data, State, #{}),
Guild = maps:get(<<"guild">>, Data, #{}),
Features = maps:get(<<"features">>, Guild, []),
get_unavailability_mode_from_features(Features).
-spec get_unavailability_mode_from_features(term()) -> unavailability_mode().
get_unavailability_mode_from_features(Features) when is_list(Features) ->
HasForEveryone = lists:member(<<"UNAVAILABLE_FOR_EVERYONE">>, Features),
HasForEveryoneButStaff =
lists:member(<<"UNAVAILABLE_FOR_EVERYONE_BUT_STAFF">>, Features),
case {HasForEveryone, HasForEveryoneButStaff} of
{true, _} -> unavailable_for_everyone;
{false, true} -> unavailable_for_everyone_but_staff;
{false, false} -> available
end;
get_unavailability_mode_from_features(_) ->
available.
-spec is_unavailable_hidden_enabled(guild_state()) -> boolean().
is_unavailable_hidden_enabled(State) ->
Data = maps:get(data, State, #{}),
Guild = maps:get(<<"guild">>, Data, #{}),
Features = maps:get(<<"features">>, Guild, []),
is_unavailable_hidden_enabled_from_features(Features).
-spec is_unavailable_hidden_enabled_from_features(term()) -> boolean().
is_unavailable_hidden_enabled_from_features(Features) when is_list(Features) ->
HasUnavailableHidden = lists:member(<<"UNAVAILABLE_HIDDEN">>, Features),
IsUnavailable = get_unavailability_mode_from_features(Features) =/= available,
HasUnavailableHidden andalso IsUnavailable;
is_unavailable_hidden_enabled_from_features(_) ->
false.
-spec is_user_staff(user_id(), guild_state()) -> boolean().
is_user_staff(UserId, State) ->
case is_user_staff_from_sessions(UserId, State) of
true -> true;
false -> false;
undefined -> is_user_staff_from_member(UserId, State)
end.
-spec is_user_staff_from_sessions(user_id(), guild_state()) -> boolean() | undefined.
is_user_staff_from_sessions(UserId, State) ->
Sessions = maps:get(sessions, State, #{}),
maps:fold(
fun(_SessionId, SessionData, Acc) ->
fold_check_staff(UserId, SessionData, Acc)
end,
undefined,
Sessions
).
-spec fold_check_staff(user_id(), map(), boolean() | undefined) -> boolean() | undefined.
fold_check_staff(_UserId, _SessionData, Acc) when Acc =/= undefined ->
Acc;
fold_check_staff(UserId, SessionData, undefined) ->
check_session_staff(UserId, SessionData).
-spec check_session_staff(user_id(), map()) -> boolean() | undefined.
check_session_staff(UserId, SessionData) ->
SessionUserId = maps:get(user_id, SessionData, undefined),
SessionIsStaff = maps:get(is_staff, SessionData, undefined),
case {SessionUserId =:= UserId, SessionIsStaff} of
{true, true} -> true;
{true, false} -> false;
_ -> undefined
end.
-spec is_user_staff_from_member(user_id(), guild_state()) -> boolean().
is_user_staff_from_member(UserId, State) ->
case guild_permissions:find_member_by_user_id(UserId, State) of
undefined ->
false;
Member ->
User = maps:get(<<"user">>, Member, #{}),
is_user_staff_from_user_data(User)
end.
-spec is_user_staff_from_user_data(map()) -> boolean().
is_user_staff_from_user_data(UserData) when is_map(UserData) ->
case parse_is_staff_value(maps:get(<<"is_staff">>, UserData, undefined)) of
undefined -> is_user_staff_from_flags(UserData);
IsStaff -> IsStaff
end;
is_user_staff_from_user_data(_) ->
false.
-spec parse_is_staff_value(term()) -> boolean() | undefined.
parse_is_staff_value(true) -> true;
parse_is_staff_value(false) -> false;
parse_is_staff_value(<<"true">>) -> true;
parse_is_staff_value(<<"false">>) -> false;
parse_is_staff_value(_) -> undefined.
-spec is_user_staff_from_flags(map()) -> boolean().
is_user_staff_from_flags(UserData) ->
user_flags:is_staff(maps:get(<<"flags">>, UserData, 0)).
-spec check_unavailability_transition(guild_state(), guild_state()) -> transition_result().
check_unavailability_transition(OldState, NewState) ->
OldMode = get_unavailability_mode_from_state(OldState),
NewMode = get_unavailability_mode_from_state(NewState),
transition_result(OldMode, NewMode).
-spec transition_result(unavailability_mode(), unavailability_mode()) -> transition_result().
transition_result(available, unavailable_for_everyone) ->
{unavailable_enabled, false};
transition_result(available, unavailable_for_everyone_but_staff) ->
{unavailable_enabled, true};
transition_result(unavailable_for_everyone, available) ->
unavailable_disabled;
transition_result(unavailable_for_everyone_but_staff, available) ->
unavailable_disabled;
transition_result(unavailable_for_everyone_but_staff, unavailable_for_everyone) ->
{unavailable_enabled, false};
transition_result(unavailable_for_everyone, unavailable_for_everyone_but_staff) ->
{unavailable_enabled, true};
transition_result(_, _) ->
no_change.
-ifdef(TEST).
get_unavailability_mode_available_test() ->
State = #{data => #{<<"guild">> => #{<<"features">> => []}}},
?assertEqual(available, get_unavailability_mode_from_state(State)).
get_unavailability_mode_for_everyone_test() ->
State = #{data => #{<<"guild">> => #{<<"features">> => [<<"UNAVAILABLE_FOR_EVERYONE">>]}}},
?assertEqual(unavailable_for_everyone, get_unavailability_mode_from_state(State)).
get_unavailability_mode_for_everyone_but_staff_test() ->
Features = [<<"UNAVAILABLE_FOR_EVERYONE_BUT_STAFF">>],
State = #{data => #{<<"guild">> => #{<<"features">> => Features}}},
?assertEqual(
unavailable_for_everyone_but_staff,
get_unavailability_mode_from_state(State)
).
is_unavailable_hidden_enabled_test() ->
HiddenFeatures = [<<"UNAVAILABLE_FOR_EVERYONE">>, <<"UNAVAILABLE_HIDDEN">>],
StateHidden = #{data => #{<<"guild">> => #{<<"features">> => HiddenFeatures}}},
NotHiddenFeatures = [<<"UNAVAILABLE_HIDDEN">>],
StateNotHidden = #{data => #{<<"guild">> => #{<<"features">> => NotHiddenFeatures}}},
?assertEqual(true, is_unavailable_hidden_enabled(StateHidden)),
?assertEqual(false, is_unavailable_hidden_enabled(StateNotHidden)).
check_unavailability_transition_no_change_test() ->
State = #{data => #{<<"guild">> => #{<<"features">> => []}}},
?assertEqual(no_change, check_unavailability_transition(State, State)).
check_unavailability_transition_enabled_test() ->
Old = #{data => #{<<"guild">> => #{<<"features">> => []}}},
New = #{data => #{<<"guild">> => #{<<"features">> => [<<"UNAVAILABLE_FOR_EVERYONE">>]}}},
?assertEqual({unavailable_enabled, false}, check_unavailability_transition(Old, New)).
check_unavailability_transition_disabled_test() ->
Old = #{data => #{<<"guild">> => #{<<"features">> => [<<"UNAVAILABLE_FOR_EVERYONE">>]}}},
New = #{data => #{<<"guild">> => #{<<"features">> => []}}},
?assertEqual(unavailable_disabled, check_unavailability_transition(Old, New)).
is_user_staff_from_user_data_test() ->
?assertEqual(true, is_user_staff_from_user_data(#{<<"is_staff">> => true})),
?assertEqual(false, is_user_staff_from_user_data(#{<<"is_staff">> => false})),
?assertEqual(true, is_user_staff_from_user_data(#{<<"flags">> => 1})),
?assertEqual(false, is_user_staff_from_user_data(#{<<"flags">> => 0})),
?assertEqual(false, is_user_staff_from_user_data(#{})).
parse_is_staff_value_test() ->
?assertEqual(true, parse_is_staff_value(true)),
?assertEqual(false, parse_is_staff_value(false)),
?assertEqual(true, parse_is_staff_value(<<"true">>)),
?assertEqual(false, parse_is_staff_value(<<"false">>)),
?assertEqual(undefined, parse_is_staff_value(something)).
is_user_staff_ignores_injected_member_data_test() ->
UserId = 42,
Sessions = #{
<<"sess1">> => #{
session_id => <<"sess1">>,
user_id => UserId,
is_staff => false
}
},
InjectedMember = #{
<<"user">> => #{<<"id">> => <<"42">>, <<"is_staff">> => true, <<"flags">> => <<"0">>}
},
State = #{
sessions => Sessions,
data => #{
<<"members">> => [InjectedMember],
<<"guild">> => #{<<"features">> => []}
}
},
?assertEqual(false, is_user_staff(UserId, State)).
is_user_staff_trusts_session_identity_test() ->
UserId = 99,
Sessions = #{
<<"sess2">> => #{
session_id => <<"sess2">>,
user_id => UserId,
is_staff => true
}
},
State = #{
sessions => Sessions,
data => #{
<<"members">> => [],
<<"guild">> => #{<<"features">> => []}
}
},
?assertEqual(true, is_user_staff(UserId, State)).
-endif.