refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -21,123 +21,533 @@
is_guild_unavailable_for_user/2,
is_user_staff/2,
check_unavailability_transition/2,
handle_unavailability_transition/2
handle_unavailability_transition/2,
get_cached_unavailability_mode/1,
is_guild_unavailable_for_user_from_cache/2,
update_unavailability_cache_for_state/1
]).
-import(guild_permissions, [find_member_by_user_id/2]).
-import(guild_data, [get_guild_state/2]).
-type guild_state() :: map().
-type user_id() :: integer().
-type guild_id() :: integer().
-type unavailability_mode() ::
available
| unavailable_for_everyone
| unavailable_for_everyone_but_staff.
-type transition_result() :: {unavailable_enabled, boolean()} | unavailable_disabled | no_change.
-define(GUILD_UNAVAILABILITY_CACHE, guild_unavailability_cache).
-define(STAFF_USER_FLAG, 16#1).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-spec is_guild_unavailable_for_user(user_id(), guild_state()) -> boolean().
is_guild_unavailable_for_user(UserId, State) ->
Data = maps:get(data, State),
Guild = maps:get(<<"guild">>, Data),
Features = maps:get(<<"features">>, Guild, []),
HasUnavailableForEveryone = lists:member(<<"UNAVAILABLE_FOR_EVERYONE">>, Features),
HasUnavailableForEveryoneButStaff =
lists:member(<<"UNAVAILABLE_FOR_EVERYONE_BUT_STAFF">>, Features),
case {HasUnavailableForEveryone, HasUnavailableForEveryoneButStaff} of
{true, _} ->
case get_unavailability_mode_from_state(State) of
unavailable_for_everyone ->
true;
{false, true} ->
unavailable_for_everyone_but_staff ->
not is_user_staff(UserId, State);
{false, false} ->
available ->
false
end.
-spec is_user_staff(user_id(), guild_state()) -> boolean().
is_user_staff(UserId, State) ->
case find_member_by_user_id(UserId, State) of
undefined ->
case is_user_staff_from_sessions(UserId, State) of
true ->
true;
false ->
false;
Member ->
User = maps:get(<<"user">>, Member, #{}),
Flags = utils:binary_to_integer_safe(maps:get(<<"flags">>, User, <<"0">>)),
(Flags band 16#1) =:= 16#1
end.
check_unavailability_transition(OldState, NewState) ->
OldData = maps:get(data, OldState),
OldGuild = maps:get(<<"guild">>, OldData),
OldFeatures = maps:get(<<"features">>, OldGuild, []),
NewData = maps:get(data, NewState),
NewGuild = maps:get(<<"guild">>, NewData),
NewFeatures = maps:get(<<"features">>, NewGuild, []),
OldUnavailableForEveryone = lists:member(<<"UNAVAILABLE_FOR_EVERYONE">>, OldFeatures),
NewUnavailableForEveryone = lists:member(<<"UNAVAILABLE_FOR_EVERYONE">>, NewFeatures),
OldUnavailableForEveryoneButStaff =
lists:member(<<"UNAVAILABLE_FOR_EVERYONE_BUT_STAFF">>, OldFeatures),
NewUnavailableForEveryoneButStaff =
lists:member(<<"UNAVAILABLE_FOR_EVERYONE_BUT_STAFF">>, NewFeatures),
OldIsUnavailable = OldUnavailableForEveryone orelse OldUnavailableForEveryoneButStaff,
NewIsUnavailable = NewUnavailableForEveryone orelse NewUnavailableForEveryoneButStaff,
case {OldIsUnavailable, NewIsUnavailable} of
{false, true} ->
{unavailable_enabled, NewUnavailableForEveryoneButStaff};
{true, false} ->
unavailable_disabled;
_ ->
case
{OldUnavailableForEveryoneButStaff, NewUnavailableForEveryoneButStaff,
OldUnavailableForEveryone, NewUnavailableForEveryone}
of
{true, false, false, true} ->
{unavailable_enabled, false};
{false, true, true, false} ->
{unavailable_enabled, true};
_ ->
no_change
undefined ->
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
end.
handle_unavailability_transition(OldState, NewState) ->
GuildId = maps:get(id, NewState),
UnavailablePayload = #{
<<"id">> => integer_to_binary(GuildId),
<<"unavailable">> => true
},
-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) ->
case Acc of
undefined ->
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;
_ ->
Acc
end
end,
undefined,
Sessions
).
-spec get_cached_unavailability_mode(guild_id()) -> unavailability_mode().
get_cached_unavailability_mode(GuildId) ->
ensure_unavailability_cache_table(),
case ets:lookup(?GUILD_UNAVAILABILITY_CACHE, GuildId) of
[{GuildId, Mode}] ->
normalize_unavailability_mode(Mode);
[] ->
available
end.
-spec is_guild_unavailable_for_user_from_cache(guild_id(), map()) -> boolean().
is_guild_unavailable_for_user_from_cache(GuildId, UserData) ->
case get_cached_unavailability_mode(GuildId) of
unavailable_for_everyone ->
true;
unavailable_for_everyone_but_staff ->
not is_user_staff_from_user_data(UserData);
available ->
false
end.
-spec update_unavailability_cache_for_state(guild_state()) -> unavailability_mode().
update_unavailability_cache_for_state(State) ->
GuildId = maps:get(id, State),
Mode = get_unavailability_mode_from_state(State),
set_cached_unavailability_mode(GuildId, Mode),
Mode.
-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),
case {OldMode, NewMode} of
{available, unavailable_for_everyone} ->
{unavailable_enabled, false};
{available, unavailable_for_everyone_but_staff} ->
{unavailable_enabled, true};
{unavailable_for_everyone, available} ->
unavailable_disabled;
{unavailable_for_everyone_but_staff, available} ->
unavailable_disabled;
{unavailable_for_everyone_but_staff, unavailable_for_everyone} ->
{unavailable_enabled, false};
{unavailable_for_everyone, unavailable_for_everyone_but_staff} ->
{unavailable_enabled, true};
_ ->
no_change
end.
-spec handle_unavailability_transition(guild_state(), guild_state()) -> guild_state().
handle_unavailability_transition(OldState, NewState) ->
_ = update_unavailability_cache_for_state(NewState),
GuildId = maps:get(id, NewState),
case check_unavailability_transition(OldState, NewState) of
{unavailable_enabled, StaffOnly} ->
Sessions = maps:get(sessions, NewState, #{}),
lists:foreach(
fun({_SessionId, SessionData}) ->
UserId = maps:get(user_id, SessionData),
Pid = maps:get(pid, SessionData),
ShouldBeUnavailable =
case StaffOnly of
true -> not is_user_staff(UserId, NewState);
false -> true
end,
case ShouldBeUnavailable of
true ->
gen_server:cast(Pid, {dispatch, guild_delete, UnavailablePayload});
false ->
ok
end
end,
maps:to_list(Sessions)
);
disconnect_ineligible_sessions(StaffOnly, NewState, GuildId);
unavailable_disabled ->
Sessions = maps:get(sessions, NewState, #{}),
GuildId = maps:get(id, NewState),
BulkPresences = presence_utils:collect_guild_member_presences(NewState),
lists:foreach(
fun({_SessionId, SessionData}) ->
UserId = maps:get(user_id, SessionData),
Pid = maps:get(pid, SessionData),
GuildState = get_guild_state(UserId, NewState),
gen_server:cast(Pid, {dispatch, guild_create, GuildState}),
presence_utils:send_presence_bulk(Pid, GuildId, UserId, BulkPresences)
case maps:get(pending_connect, SessionData, false) of
true ->
ok;
false ->
UserId = maps:get(user_id, SessionData),
Pid = maps:get(pid, SessionData),
GuildState = guild_data:get_guild_state(UserId, NewState),
gen_server:cast(Pid, {dispatch, guild_create, GuildState}),
presence_utils:send_presence_bulk(Pid, GuildId, UserId, BulkPresences)
end
end,
maps:to_list(Sessions)
);
),
NewState;
no_change ->
NewState
end.
-spec disconnect_ineligible_sessions(boolean(), guild_state(), guild_id()) -> guild_state().
disconnect_ineligible_sessions(StaffOnly, State, GuildId) ->
Sessions = maps:get(sessions, State, #{}),
{FinalState, _DisconnectedUsers} = lists:foldl(
fun({SessionId, SessionData}, {AccState, ProcessedUsers}) ->
UserId = maps:get(user_id, SessionData),
case should_disconnect_user(UserId, StaffOnly, AccState) of
true ->
Pid = maps:get(pid, SessionData, undefined),
maybe_send_guild_leave(Pid, GuildId),
{VoiceState, UpdatedUsers} =
maybe_disconnect_voice_for_user(UserId, ProcessedUsers, AccState),
NewState = guild_sessions:remove_session(SessionId, VoiceState),
{NewState, UpdatedUsers};
false ->
{AccState, ProcessedUsers}
end
end,
{State, sets:new()},
maps:to_list(Sessions)
),
FinalState.
-spec should_disconnect_user(user_id(), boolean(), guild_state()) -> boolean().
should_disconnect_user(UserId, true, State) ->
not is_user_staff(UserId, State);
should_disconnect_user(_UserId, false, _State) ->
true.
-spec maybe_send_guild_leave(pid() | undefined, guild_id()) -> ok.
maybe_send_guild_leave(Pid, GuildId) when is_pid(Pid) ->
gen_server:cast(Pid, {guild_leave, GuildId, forced_unavailable}),
ok;
maybe_send_guild_leave(_Pid, _GuildId) ->
ok.
-spec maybe_disconnect_voice_for_user(user_id(), sets:set(user_id()), guild_state()) ->
{guild_state(), sets:set(user_id())}.
maybe_disconnect_voice_for_user(UserId, ProcessedUsers, State) ->
case sets:is_element(UserId, ProcessedUsers) of
true ->
{State, ProcessedUsers};
false ->
{reply, _Result, VoiceState} = guild_voice_disconnect:disconnect_voice_user(
#{user_id => UserId, connection_id => null},
State
),
{VoiceState, sets:add_element(UserId, ProcessedUsers)}
end.
-spec ensure_unavailability_cache_table() -> ok.
ensure_unavailability_cache_table() ->
case ets:whereis(?GUILD_UNAVAILABILITY_CACHE) of
undefined ->
try ets:new(?GUILD_UNAVAILABILITY_CACHE, [named_table, public, set, {read_concurrency, true}]) of
_ -> ok
catch
error:badarg -> ok
end;
_ ->
ok
end.
-spec set_cached_unavailability_mode(guild_id(), unavailability_mode()) -> ok.
set_cached_unavailability_mode(GuildId, available) ->
ensure_unavailability_cache_table(),
ets:delete(?GUILD_UNAVAILABILITY_CACHE, GuildId),
ok;
set_cached_unavailability_mode(GuildId, Mode) ->
ensure_unavailability_cache_table(),
ets:insert(?GUILD_UNAVAILABILITY_CACHE, {GuildId, Mode}),
ok.
-spec normalize_unavailability_mode(term()) -> unavailability_mode().
normalize_unavailability_mode(unavailable_for_everyone) ->
unavailable_for_everyone;
normalize_unavailability_mode(unavailable_for_everyone_but_staff) ->
unavailable_for_everyone_but_staff;
normalize_unavailability_mode(_) ->
available.
-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) ->
HasUnavailableForEveryone = lists:member(<<"UNAVAILABLE_FOR_EVERYONE">>, Features),
HasUnavailableForEveryoneButStaff =
lists:member(<<"UNAVAILABLE_FOR_EVERYONE_BUT_STAFF">>, Features),
case {HasUnavailableForEveryone, HasUnavailableForEveryoneButStaff} of
{true, _} ->
unavailable_for_everyone;
{false, true} ->
unavailable_for_everyone_but_staff;
{false, false} ->
available
end;
get_unavailability_mode_from_features(_) ->
available.
-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) ->
FlagsValue = maps:get(<<"flags">>, UserData, 0),
Flags = type_conv:to_integer(FlagsValue),
case Flags of
undefined ->
false;
Value when is_integer(Value) ->
(Value band ?STAFF_USER_FLAG) =:= ?STAFF_USER_FLAG
end.
-ifdef(TEST).
-spec cleanup_unavailability_cache(guild_id()) -> ok.
cleanup_unavailability_cache(GuildId) ->
set_cached_unavailability_mode(GuildId, available).
disconnect_ineligible_sessions_staff_only_test() ->
Parent = self(),
GuildId = 99001,
NonStaffPid = start_session_capture(non_staff, Parent),
StaffPid = start_session_capture(staff, Parent),
try
BaseState = state_for_unavailability_transition_test(GuildId, NonStaffPid, StaffPid),
OldState = BaseState,
NewState = BaseState#{
data => #{
<<"guild">> => #{
<<"features">> => [<<"UNAVAILABLE_FOR_EVERYONE_BUT_STAFF">>]
},
<<"members">> => [
#{<<"user">> => #{<<"id">> => <<"1001">>, <<"flags">> => <<"0">>}},
#{<<"user">> => #{<<"id">> => <<"1002">>, <<"flags">> => <<"1">>}}
]
}
},
UpdatedState = handle_unavailability_transition(OldState, NewState),
Sessions = maps:get(sessions, UpdatedState, #{}),
?assertEqual(1, map_size(Sessions)),
?assert(maps:is_key(<<"staff">>, Sessions)),
?assertEqual(unavailable_for_everyone_but_staff, get_cached_unavailability_mode(GuildId)),
receive
{non_staff, {'$gen_cast', {guild_leave, GuildId, forced_unavailable}}} -> ok
after 1000 ->
?assert(false)
end,
receive
{staff, {'$gen_cast', {guild_leave, GuildId, forced_unavailable}}} ->
?assert(false)
after 200 ->
ok
end
after
cleanup_unavailability_cache(GuildId),
NonStaffPid ! stop,
StaffPid ! stop
end.
disconnect_ineligible_sessions_everyone_test() ->
Parent = self(),
GuildId = 99002,
UserOnePid = start_session_capture(user_one, Parent),
UserTwoPid = start_session_capture(user_two, Parent),
try
BaseState = state_for_unavailability_transition_test(GuildId, UserOnePid, UserTwoPid),
OldState = BaseState,
NewState = BaseState#{
data => #{
<<"guild">> => #{
<<"features">> => [<<"UNAVAILABLE_FOR_EVERYONE">>]
},
<<"members">> => [
#{<<"user">> => #{<<"id">> => <<"1001">>, <<"flags">> => <<"0">>}},
#{<<"user">> => #{<<"id">> => <<"1002">>, <<"flags">> => <<"1">>}}
]
}
},
UpdatedState = handle_unavailability_transition(OldState, NewState),
?assertEqual(#{}, maps:get(sessions, UpdatedState, #{})),
?assertEqual(unavailable_for_everyone, get_cached_unavailability_mode(GuildId)),
receive
{user_one, {'$gen_cast', {guild_leave, GuildId, forced_unavailable}}} -> ok
after 1000 ->
?assert(false)
end,
receive
{user_two, {'$gen_cast', {guild_leave, GuildId, forced_unavailable}}} -> ok
after 1000 ->
?assert(false)
end
after
cleanup_unavailability_cache(GuildId),
UserOnePid ! stop,
UserTwoPid ! stop
end.
is_guild_unavailable_for_user_from_cache_is_staff_test() ->
GuildId = 99003,
try
set_cached_unavailability_mode(GuildId, unavailable_for_everyone_but_staff),
?assertEqual(
true,
is_guild_unavailable_for_user_from_cache(
GuildId,
#{<<"is_staff">> => false}
)
),
?assertEqual(
false,
is_guild_unavailable_for_user_from_cache(
GuildId,
#{<<"is_staff">> => true}
)
)
after
cleanup_unavailability_cache(GuildId)
end.
-spec start_session_capture(atom(), pid()) -> pid().
start_session_capture(Tag, Parent) ->
spawn(fun() -> session_capture_loop(Tag, Parent) end).
-spec session_capture_loop(atom(), pid()) -> ok.
session_capture_loop(Tag, Parent) ->
receive
stop ->
ok;
{'$gen_cast', Msg} ->
Parent ! {Tag, {'$gen_cast', Msg}},
session_capture_loop(Tag, Parent);
_Other ->
session_capture_loop(Tag, Parent)
end.
-spec state_for_unavailability_transition_test(guild_id(), pid(), pid()) -> guild_state().
state_for_unavailability_transition_test(GuildId, NonStaffPid, StaffPid) ->
#{
id => GuildId,
sessions => #{
<<"non_staff">> => #{
session_id => <<"non_staff">>,
user_id => 1001,
pid => NonStaffPid,
mref => make_ref(),
active_guilds => sets:new(),
user_roles => [],
bot => false,
is_staff => false,
previous_passive_updates => #{},
previous_passive_channel_versions => #{},
previous_passive_voice_states => #{}
},
<<"staff">> => #{
session_id => <<"staff">>,
user_id => 1002,
pid => StaffPid,
mref => make_ref(),
active_guilds => sets:new(),
user_roles => [],
bot => false,
is_staff => true,
previous_passive_updates => #{},
previous_passive_channel_versions => #{},
previous_passive_voice_states => #{}
}
},
presence_subscriptions => #{},
member_list_subscriptions => #{},
member_subscriptions => guild_subscriptions:init_state(),
data => #{
<<"guild">> => #{
<<"features">> => []
},
<<"members">> => [
#{<<"user">> => #{<<"id">> => <<"1001">>, <<"flags">> => <<"0">>}},
#{<<"user">> => #{<<"id">> => <<"1002">>, <<"flags">> => <<"1">>}}
]
},
voice_states => #{},
pending_voice_connections => #{}
}.
is_guild_unavailable_for_user_unavailable_for_everyone_test() ->
State = #{
data => #{
<<"guild">> => #{
<<"features">> => [<<"UNAVAILABLE_FOR_EVERYONE">>]
},
<<"members">> => []
}
},
?assertEqual(true, is_guild_unavailable_for_user(123, State)).
is_guild_unavailable_for_user_available_test() ->
State = #{
data => #{
<<"guild">> => #{
<<"features">> => []
},
<<"members">> => []
}
},
?assertEqual(false, is_guild_unavailable_for_user(123, State)).
check_unavailability_transition_no_change_test() ->
State = #{
data => #{
<<"guild">> => #{
<<"features">> => []
}
}
},
?assertEqual(no_change, check_unavailability_transition(State, State)).
check_unavailability_transition_enabled_test() ->
OldState = #{
data => #{
<<"guild">> => #{
<<"features">> => []
}
}
},
NewState = #{
data => #{
<<"guild">> => #{
<<"features">> => [<<"UNAVAILABLE_FOR_EVERYONE">>]
}
}
},
?assertEqual({unavailable_enabled, false}, check_unavailability_transition(OldState, NewState)).
check_unavailability_transition_disabled_test() ->
OldState = #{
data => #{
<<"guild">> => #{
<<"features">> => [<<"UNAVAILABLE_FOR_EVERYONE">>]
}
}
},
NewState = #{
data => #{
<<"guild">> => #{
<<"features">> => []
}
}
},
?assertEqual(unavailable_disabled, check_unavailability_transition(OldState, NewState)).
-endif.