fix: tighten edge cases across services (#3158)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-03 13:04:29 +02:00
1 parent a9f7a23c0d
commit 4e6b837ccc
170 files changed
+2028 -421

No files matched your search

@@ -322,7 +322,7 @@ parse_page_params_defaults_optional_fields_test() ->
maps:get(limit, Req)
).
mention_guild_calls_carry_the_caller_deadline_test() ->
mention_guild_calls_pass_the_caller_deadline_test() ->
Cases = [
{
fun(Pid) -> guild_call_sources_page(Pid, #{limit => 5}) end,
@@ -350,9 +350,9 @@ mention_guild_calls_carry_the_caller_deadline_test() ->
#{<<"user_ids">> => [<<"7">>]}
}
],
lists:foreach(fun assert_call_carries_deadline/1, Cases).
lists:foreach(fun assert_call_passes_deadline/1, Cases).
assert_call_carries_deadline({Call, GuildReply, Expected}) ->
assert_call_passes_deadline({Call, GuildReply, Expected}) ->
Self = self(),
Guild = spawn(fun() ->
receive
+4 -1
View File
@@ -741,6 +741,7 @@ voice_guild_state_keys() ->
virtual_channel_access_pending,
virtual_channel_access_preserve,
virtual_channel_access_move_pending,
virtual_channel_access_view_only,
test_perm_fun,
test_force_disconnect_fun,
test_livekit_fun,
@@ -824,7 +825,8 @@ voice_guild_state_pins_projected_key_set_test() ->
virtual_channel_access,
virtual_channel_access_pending,
virtual_channel_access_preserve,
virtual_channel_access_move_pending
virtual_channel_access_move_pending,
virtual_channel_access_view_only
]),
lists:sort(maps:keys(Projected))
),
@@ -1093,6 +1095,7 @@ voice_projection_state(Tab) ->
virtual_channel_access_pending => #{},
virtual_channel_access_preserve => #{},
virtual_channel_access_move_pending => #{},
virtual_channel_access_view_only => #{},
presence_subscriptions => #{},
member_list_subscriptions => #{},
connected_user_ids => sets:new(),
@@ -1475,7 +1475,7 @@ released_push_holds_treat_a_dead_guild_as_nothing_released_test() ->
end,
?assertEqual([], released_push_holds(GuildPid, [{1, <<"s1">>}])).
push_states_carry_the_guild_pid_for_the_grace_recheck_test() ->
push_states_include_the_guild_pid_for_the_grace_recheck_test() ->
?assertEqual({self(), self()}, push_state_guild_pids()).
push_states_skip_the_grace_recheck_while_presence_eligibility_is_off_test() ->
@@ -1798,7 +1798,7 @@ compact_push_state_drops_members_and_keeps_member_count_test() ->
ets:delete(Tab)
end.
legacy_push_state_carries_member_count_test() ->
legacy_push_state_includes_member_count_test() ->
UpdatedState = #{id => 7, data => #{}, sessions => #{}, member_count => 1234},
Legacy = legacy_push_state(7, UpdatedState),
?assertEqual(1234, maps:get(member_count, Legacy)),
@@ -1869,7 +1869,7 @@ spawn_push_without_members_table_falls_back_to_legacy_push_test() ->
reset_push_worker_state()
end.
compact_push_carries_large_guild_metadata_test() ->
compact_push_includes_large_guild_metadata_test() ->
Self = self(),
Tab = ets:new(test_members, [set, public]),
ok = meck:new(push, [passthrough, no_link]),
+3 -1
View File
@@ -28,7 +28,9 @@ export_handoff_state(State) ->
virtual_channel_access_preserve, State, #{}
),
virtual_channel_access_move_pending =>
maps:get(virtual_channel_access_move_pending, State, #{})
maps:get(virtual_channel_access_move_pending, State, #{}),
virtual_channel_access_view_only =>
maps:get(virtual_channel_access_view_only, State, #{})
}.
-spec derived_data_keys() -> [atom() | binary()].
+55 -55
View File
@@ -13,7 +13,7 @@
handle_reload/2
]).
-define(MAX_RELOAD_CARRY_WARN_MEMBERS, 100000).
-define(MAX_RELOAD_REUSE_WARN_MEMBERS, 100000).
-type guild_state() :: map().
-type user_id() :: integer().
@@ -130,7 +130,7 @@ handle_reload(NewData, State) ->
NewData, ExistingVoiceStates
),
NormalizedNewData0 = guild_data_index:normalize_map(ReloadData),
NormalizedNewData = carry_members_table(OldData, NormalizedNewData0),
NormalizedNewData = reuse_members_table(OldData, NormalizedNewData0),
NewState0 = guild_member_list_engine_inputs:forget_all(
State#{voice_states => ReloadVoiceStates, data => NormalizedNewData}
),
@@ -149,16 +149,16 @@ handle_reload(NewData, State) ->
ok = guild_maintenance:maybe_put_permission_cache(NewState),
{reply, ok, NewState}.
-spec carry_members_table(term(), term()) -> term().
carry_members_table(OldData, NewData) when is_map(OldData), is_map(NewData) ->
carry_healthy_members_table(OldData, NewData);
carry_members_table(_OldData, NewData) ->
-spec reuse_members_table(term(), term()) -> term().
reuse_members_table(OldData, NewData) when is_map(OldData), is_map(NewData) ->
reuse_healthy_members_table(OldData, NewData);
reuse_members_table(_OldData, NewData) ->
NewData.
-spec carry_healthy_members_table(map(), map()) -> map().
carry_healthy_members_table(OldData, NewData) ->
-spec reuse_healthy_members_table(map(), map()) -> map().
reuse_healthy_members_table(OldData, NewData) ->
case members_ets_table(OldData) of
Tab when is_reference(Tab) -> carry_live_members_table(Tab, OldData, NewData);
Tab when is_reference(Tab) -> reuse_live_members_table(Tab, OldData, NewData);
undefined -> maps:remove(members_ets, NewData)
end.
@@ -168,8 +168,8 @@ members_ets_table(#{members_ets := Tab}) ->
members_ets_table(_Data) ->
undefined.
-spec carry_live_members_table(ets:tid(), map(), map()) -> map().
carry_live_members_table(Tab, OldData, NewData) ->
-spec reuse_live_members_table(ets:tid(), map(), map()) -> map().
reuse_live_members_table(Tab, OldData, NewData) ->
case guild_members_table_repair:members_table_healthy(Tab) of
true -> apply_members_table_delta(Tab, OldData, NewData);
false -> maps:remove(members_ets, NewData)
@@ -179,7 +179,7 @@ carry_live_members_table(Tab, OldData, NewData) ->
apply_members_table_delta(Tab, OldData, NewData) ->
OldMap = guild_data_index_members:member_map(OldData),
NewMap = guild_data_index_members:member_map(NewData),
maybe_warn_large_carry(map_size(NewMap)),
maybe_warn_large_reuse(map_size(NewMap)),
try
ok = insert_changed_members(Tab, OldMap, NewMap),
ok = delete_removed_members(Tab, OldMap, NewMap),
@@ -223,13 +223,13 @@ delete_member_row(Tab, UserId) ->
true = ets:delete(Tab, UserId),
ok.
-spec maybe_warn_large_carry(non_neg_integer()) -> ok.
maybe_warn_large_carry(Size) when Size > ?MAX_RELOAD_CARRY_WARN_MEMBERS ->
-spec maybe_warn_large_reuse(non_neg_integer()) -> ok.
maybe_warn_large_reuse(Size) when Size > ?MAX_RELOAD_REUSE_WARN_MEMBERS ->
logger:warning(
"guild_reload_members_table_carry_large: members=~p threshold=~p",
[Size, ?MAX_RELOAD_CARRY_WARN_MEMBERS]
[Size, ?MAX_RELOAD_REUSE_WARN_MEMBERS]
);
maybe_warn_large_carry(_Size) ->
maybe_warn_large_reuse(_Size) ->
ok.
-spec collect_active_pid(term(), map(), [pid()]) -> [pid()].
@@ -265,66 +265,66 @@ guild_id(State) ->
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
carry_members_table_carries_healthy_tid_test() ->
Tab = ets:new(carry_members, [set, public]),
reuse_members_table_keeps_healthy_tid_test() ->
Tab = ets:new(reuse_members, [set, public]),
try
OldMap = #{1 => carry_member(1), 2 => carry_member(2)},
NewMap = #{1 => carry_member(1), 3 => carry_member(3)},
seed_carry_table(Tab, OldMap),
OldData = carry_data(OldMap, Tab),
Carried = carry_members_table(OldData, carry_data(NewMap, undefined)),
?assertEqual(Tab, maps:get(members_ets, Carried)),
?assertEqual([1, 3], carry_table_ids(Tab))
OldMap = #{1 => reuse_member(1), 2 => reuse_member(2)},
NewMap = #{1 => reuse_member(1), 3 => reuse_member(3)},
seed_reuse_table(Tab, OldMap),
OldData = reuse_data(OldMap, Tab),
Reused = reuse_members_table(OldData, reuse_data(NewMap, undefined)),
?assertEqual(Tab, maps:get(members_ets, Reused)),
?assertEqual([1, 3], reuse_table_ids(Tab))
after
ets:delete(Tab)
end.
carry_members_table_skips_dead_tid_test() ->
Tab = ets:new(dead_carry_members, [set, public]),
reuse_members_table_skips_dead_tid_test() ->
Tab = ets:new(dead_reuse_members, [set, public]),
true = ets:delete(Tab),
OldData = carry_data(#{1 => carry_member(1)}, Tab),
NewData = carry_data(#{1 => carry_member(1)}, undefined),
?assertEqual(NewData, carry_members_table(OldData, NewData)).
OldData = reuse_data(#{1 => reuse_member(1)}, Tab),
NewData = reuse_data(#{1 => reuse_member(1)}, undefined),
?assertEqual(NewData, reuse_members_table(OldData, NewData)).
carry_members_table_without_old_tid_leaves_new_data_test() ->
OldData = carry_data(#{1 => carry_member(1)}, undefined),
NewData = carry_data(#{2 => carry_member(2)}, undefined),
?assertEqual(NewData, carry_members_table(OldData, NewData)).
reuse_members_table_without_old_tid_leaves_new_data_test() ->
OldData = reuse_data(#{1 => reuse_member(1)}, undefined),
NewData = reuse_data(#{2 => reuse_member(2)}, undefined),
?assertEqual(NewData, reuse_members_table(OldData, NewData)).
carry_members_table_delta_failure_falls_back_to_new_data_test() ->
reuse_members_table_delta_failure_falls_back_to_new_data_test() ->
Tab = ets:new(failing_members, [set, public]),
true = ets:delete(Tab),
OldData = carry_data(#{}, undefined),
NewData = carry_data(#{1 => carry_member(1)}, undefined),
OldData = reuse_data(#{}, undefined),
NewData = reuse_data(#{1 => reuse_member(1)}, undefined),
?assertEqual(NewData, apply_members_table_delta(Tab, OldData, NewData)).
carry_delta_never_empties_table_test() ->
reuse_delta_never_empties_table_test() ->
Tab = ets:new(delta_members, [set, public]),
try
OldMap = #{1 => carry_member(1), 2 => carry_member(2), 3 => carry_member(3)},
OldMap = #{1 => reuse_member(1), 2 => reuse_member(2), 3 => reuse_member(3)},
NewMap = #{
1 => carry_member(1),
2 => carry_member(2, <<"changed">>),
4 => carry_member(4)
1 => reuse_member(1),
2 => reuse_member(2, <<"changed">>),
4 => reuse_member(4)
},
seed_carry_table(Tab, OldMap),
seed_reuse_table(Tab, OldMap),
?assertEqual(3, ets:info(Tab, size)),
ok = insert_changed_members(Tab, OldMap, NewMap),
?assertEqual(4, ets:info(Tab, size)),
?assertEqual([{1, carry_member(1)}], ets:lookup(Tab, 1)),
?assertEqual([{1, reuse_member(1)}], ets:lookup(Tab, 1)),
ok = delete_removed_members(Tab, OldMap, NewMap),
?assertEqual(3, ets:info(Tab, size)),
?assertEqual([{1, carry_member(1)}], ets:lookup(Tab, 1)),
?assertEqual([1, 2, 4], carry_table_ids(Tab)),
?assertEqual([{2, carry_member(2, <<"changed">>)}], ets:lookup(Tab, 2))
?assertEqual([{1, reuse_member(1)}], ets:lookup(Tab, 1)),
?assertEqual([1, 2, 4], reuse_table_ids(Tab)),
?assertEqual([{2, reuse_member(2, <<"changed">>)}], ets:lookup(Tab, 2))
after
ets:delete(Tab)
end.
carry_delta_skips_unchanged_members_test() ->
reuse_delta_skips_unchanged_members_test() ->
Tab = ets:new(unchanged_members, [set, public]),
try
MemberMap = #{1 => carry_member(1)},
MemberMap = #{1 => reuse_member(1)},
true = ets:insert(Tab, {1, sentinel}),
ok = insert_changed_members(Tab, MemberMap, MemberMap),
?assertEqual([{1, sentinel}], ets:tab2list(Tab))
@@ -332,21 +332,21 @@ carry_delta_skips_unchanged_members_test() ->
ets:delete(Tab)
end.
carry_data(MemberMap, undefined) ->
reuse_data(MemberMap, undefined) ->
#{<<"members">> => MemberMap, members_normalized => MemberMap};
carry_data(MemberMap, Tab) ->
reuse_data(MemberMap, Tab) ->
#{<<"members">> => MemberMap, members_normalized => MemberMap, members_ets => Tab}.
seed_carry_table(Tab, MemberMap) ->
seed_reuse_table(Tab, MemberMap) ->
maps:foreach(fun(UserId, Member) -> ets:insert(Tab, {UserId, Member}) end, MemberMap).
carry_member(UserId) ->
reuse_member(UserId) ->
#{<<"user">> => #{<<"id">> => UserId}}.
carry_table_ids(Tab) ->
reuse_table_ids(Tab) ->
lists:sort([Id || {Id, _} <- ets:tab2list(Tab)]).
carry_member(UserId, Nick) ->
reuse_member(UserId, Nick) ->
#{<<"user">> => #{<<"id">> => UserId}, <<"nick">> => Nick}.
-endif.
@@ -604,7 +604,7 @@ prune_overwrite(TargetId, Type) ->
<<"deny">> => <<"0">>
}.
%% Members 30, 31, 99 and 4242 all carry the same roles term and reach only channel 600
%% Members 30, 31, 99 and 4242 all have the same roles term and reach only channel 600
%% through it, but 99 holds virtual access to channel 500 and 4242 is a user-overwrite
%% target on 500, so neither may be answered from that term. Member 7 owns the guild,
%% 777 is subscribed without being a member, and session s3 has no cached viewable map.
@@ -101,7 +101,9 @@ normalize_transferred_guild_state(GuildId, TransferState) ->
virtual_channel_access_preserve, TransferState, #{}
),
virtual_channel_access_move_pending =>
maps:get(virtual_channel_access_move_pending, TransferState, #{})
maps:get(virtual_channel_access_move_pending, TransferState, #{}),
virtual_channel_access_view_only =>
maps:get(virtual_channel_access_view_only, TransferState, #{})
}.
-spec reply_start_transferred(guild_id(), map(), state()) ->
+1 -1
View File
@@ -16,7 +16,7 @@
-type list_sync() :: immediate | deferred.
%% members_sorted_ids trims with the member map it indexes: a snapshot that kept it would
%% answer sorted_member_ids/2 with ids for members the snapshot no longer carries.
%% answer sorted_member_ids/2 with ids for members the snapshot no longer holds.
-define(HEAVY_MEMBER_DATA_KEYS, [
<<"members">>, members_normalized, <<"member_role_index">>, members_sorted_ids
]).
+3 -1
View File
@@ -171,8 +171,10 @@ post_update_channel(channel_update, EventData, OldState, NewState) ->
post_update_channel(channel_update_bulk, EventData, OldState, NewState) ->
ChanIds = guild_state_channels:extract_channel_ids_from_channel_update_bulk(EventData),
resync_channels_after_permission_change(ChanIds, OldState, NewState);
post_update_channel(channel_delete, _EventData, _OldState, NewState) ->
post_update_channel(channel_delete, EventData, _OldState, NewState) ->
maybe_sync_member_list_permission_state(NewState),
ChannelId = snowflake_id:parse_optional(maps:get(<<"id">>, EventData, undefined)),
ok = guild_voice_lifecycle:cast_disconnect_all_voice_users_in_channel(ChannelId, NewState),
NewState.
-spec resync_channels_after_permission_change([integer()], guild_state(), guild_state()) ->
@@ -292,7 +292,7 @@ test_channel(ChannelId, Overwrites) ->
}.
%% Session user 10 and members 20 and 21 reach channel 500 through the viewer role.
%% Members 30, 31, 99 and 4242 all carry the same roles term and reach only channel
%% Members 30, 31, 99 and 4242 all have the same roles term and reach only channel
%% 600 by role, but 99 holds virtual access to 500 and 4242 is a user-overwrite
%% target on 500, so both must still come out true. Member 7 owns the guild.
test_state() ->
@@ -5,8 +5,11 @@
-export([
add_virtual_access/3,
add_view_only_access/3,
remove_virtual_access/3,
has_virtual_access/3,
has_voice_access/3,
is_view_only/3,
get_virtual_channels_for_user/2,
get_users_with_virtual_access/2,
dispatch_channel_visibility_change/4,
@@ -33,12 +36,34 @@ add_virtual_access(UserId, ChannelId, State) ->
VirtualAccess = maps:get(virtual_channel_access, State, #{}),
UserChannels = maps:get(UserId, VirtualAccess, sets:new()),
Updated = sets:add_element(ChannelId, UserChannels),
State1 = State#{virtual_channel_access => VirtualAccess#{UserId => Updated}},
State1 = clear_view_only(
UserId, ChannelId, State#{virtual_channel_access => VirtualAccess#{UserId => Updated}}
),
State2 = update_user_session_view_cache(UserId, ChannelId, add, State1),
mark_pending_join(
UserId, ChannelId, guild_member_list_engine_inputs:mark_stale(ChannelId, State2)
).
-spec clear_view_only(user_id(), channel_id(), guild_state()) -> guild_state().
clear_view_only(UserId, ChannelId, State) ->
case is_view_only(UserId, ChannelId, State) of
false ->
State;
true ->
ViewOnly = maps:get(virtual_channel_access_view_only, State),
State#{
virtual_channel_access_view_only =>
clear_from_user_set(UserId, ChannelId, ViewOnly)
}
end.
-spec add_view_only_access(user_id(), channel_id(), guild_state()) -> guild_state().
add_view_only_access(UserId, ChannelId, State) ->
State1 = add_virtual_access(UserId, ChannelId, State),
ViewOnly = maps:get(virtual_channel_access_view_only, State1, #{}),
UserViewOnly = sets:add_element(ChannelId, maps:get(UserId, ViewOnly, sets:new())),
State1#{virtual_channel_access_view_only => ViewOnly#{UserId => UserViewOnly}}.
-spec remove_virtual_access(user_id(), channel_id(), guild_state()) -> guild_state().
remove_virtual_access(UserId, ChannelId, State) ->
VirtualAccess = maps:get(virtual_channel_access, State, #{}),
@@ -65,13 +90,22 @@ remove_all_user_virtual_access(UserId, State) ->
VCP = maps:get(virtual_channel_access_pending, State, #{}),
VCPr = maps:get(virtual_channel_access_preserve, State, #{}),
VCM = maps:get(virtual_channel_access_move_pending, State, #{}),
State1 = State#{
State0 = State#{
virtual_channel_access => maps:remove(UserId, VCA),
virtual_channel_access_pending => maps:remove(UserId, VCP),
virtual_channel_access_preserve => maps:remove(UserId, VCPr),
virtual_channel_access_move_pending => maps:remove(UserId, VCM)
},
clear_user_session_view_cache(UserId, State1).
clear_user_session_view_cache(UserId, remove_user_view_only(UserId, State0)).
-spec remove_user_view_only(user_id(), guild_state()) -> guild_state().
remove_user_view_only(UserId, State) ->
case maps:find(virtual_channel_access_view_only, State) of
{ok, ViewOnly} ->
State#{virtual_channel_access_view_only => maps:remove(UserId, ViewOnly)};
error ->
State
end.
-spec update_user_virtual_access(user_id(), channel_id(), sets:set(), guild_state()) ->
guild_state().
@@ -80,12 +114,12 @@ update_user_virtual_access(UserId, ChannelId, UpdatedChans, State) ->
VCP = maps:get(virtual_channel_access_pending, State, #{}),
VCPr = maps:get(virtual_channel_access_preserve, State, #{}),
VCM = maps:get(virtual_channel_access_move_pending, State, #{}),
State1 = State#{
State1 = clear_view_only(UserId, ChannelId, State#{
virtual_channel_access => VCA#{UserId => UpdatedChans},
virtual_channel_access_pending => del_from_user_set(UserId, ChannelId, VCP),
virtual_channel_access_preserve => del_from_user_set(UserId, ChannelId, VCPr),
virtual_channel_access_move_pending => del_from_user_set(UserId, ChannelId, VCM)
},
}),
update_user_session_view_cache(UserId, ChannelId, remove, State1).
-spec del_from_user_set(user_id(), channel_id(), map()) -> map().
@@ -106,6 +140,15 @@ clear_from_user_set(UserId, ChannelId, Map) ->
has_virtual_access(UserId, ChannelId, State) ->
user_channel_check(UserId, ChannelId, virtual_channel_access, State).
-spec has_voice_access(user_id(), channel_id(), guild_state()) -> boolean().
has_voice_access(UserId, ChannelId, State) ->
has_virtual_access(UserId, ChannelId, State) andalso
not is_view_only(UserId, ChannelId, State).
-spec is_view_only(user_id(), channel_id(), guild_state()) -> boolean().
is_view_only(UserId, ChannelId, State) ->
user_channel_check(UserId, ChannelId, virtual_channel_access_view_only, State).
-spec get_virtual_channels_for_user(user_id(), guild_state()) -> [channel_id()].
get_virtual_channels_for_user(UserId, State) ->
VirtualAccess = maps:get(virtual_channel_access, State, #{}),
@@ -279,7 +279,9 @@ grant_virtual_access_if_needed(UserId, ChannelId, State) ->
true ->
State;
false ->
State1 = guild_virtual_channel_access:add_virtual_access(UserId, ChannelId, State),
State1 = guild_virtual_channel_access:add_view_only_access(
UserId, ChannelId, State
),
guild_virtual_channel_access:clear_pending_join(UserId, ChannelId, State1)
end.
@@ -325,7 +327,9 @@ maybe_grant_virtual_access(UserId, ChannelId, State) ->
true ->
{State, true};
false ->
State1 = guild_virtual_channel_access:add_virtual_access(UserId, ChannelId, State),
State1 = guild_virtual_channel_access:add_view_only_access(
UserId, ChannelId, State
),
State2 = guild_virtual_channel_access:clear_pending_join(UserId, ChannelId, State1),
{State2, true}
end.
@@ -9,7 +9,8 @@
reply_voice_server_pid/1,
clear_stale_cached_voice_states/2,
authoritative_voice_states/1,
cast_disconnect_voice_user/2
cast_disconnect_voice_user/2,
cast_disconnect_all_voice_users_in_channel/2
]).
-type guild_state() :: map().
@@ -188,6 +189,22 @@ cast_disconnect_voice_user(UserId, State) when is_integer(UserId), UserId > 0 ->
cast_disconnect_voice_user(_UserId, _State) ->
ok.
-spec cast_disconnect_all_voice_users_in_channel(integer() | undefined, guild_state()) -> ok.
cast_disconnect_all_voice_users_in_channel(ChannelId, State) when
is_integer(ChannelId), ChannelId > 0
->
case voice_server_pid(State) of
{ok, VoiceServerPid} ->
gen_server:cast(
VoiceServerPid,
{disconnect_all_voice_users_in_channel, #{channel_id => ChannelId}}
);
error ->
ok
end;
cast_disconnect_all_voice_users_in_channel(_ChannelId, _State) ->
ok.
-spec voice_server_pid(guild_state()) -> {ok, pid()} | error.
voice_server_pid(State) ->
case maps:get(voice_server_pid, State, undefined) of
@@ -168,7 +168,7 @@ maybe_clear_self_stream(_ChId, _VoiceState, _VoicePermissions, _State) ->
-spec user_has_base_voice_access(user_id(), channel_id(), guild_state()) -> boolean().
user_has_base_voice_access(UserId, ChannelId, State) ->
case guild_virtual_channel_access:has_virtual_access(UserId, ChannelId, State) of
case guild_virtual_channel_access:has_voice_access(UserId, ChannelId, State) of
true ->
true;
false ->
@@ -87,7 +87,7 @@ voice_connection_limit_allowed(UserId, ChannelIdValue, Channel, Stats, State, Is
-spec has_view_and_connect_perms(integer(), integer(), guild_state()) -> boolean().
has_view_and_connect_perms(UserId, ChannelIdValue, State) ->
guild_virtual_channel_access:has_virtual_access(UserId, ChannelIdValue, State) orelse
guild_virtual_channel_access:has_voice_access(UserId, ChannelIdValue, State) orelse
guild_virtual_channel_access:is_move_pending(UserId, ChannelIdValue, State) orelse
has_resolved_view_and_connect_perms(UserId, ChannelIdValue, State).
@@ -237,6 +237,11 @@ handle_cast({store_pending_connection, ConnId, Meta}, State) ->
{noreply, State#{pending_voice_connections => NewPending}};
handle_cast({disconnect_voice_user, Request}, State) when is_map(Request) ->
{noreply, delegate_voice_cast(fun guild_voice:disconnect_voice_user/2, Request, State)};
handle_cast({disconnect_all_voice_users_in_channel, Request}, State) when is_map(Request) ->
{noreply,
delegate_voice_cast(
fun guild_voice:disconnect_all_voice_users_in_channel/2, Request, State
)};
handle_cast({cleanup_virtual_access_for_user, UserId}, State) when is_integer(UserId) ->
GS = guild_voice_server_state:build_guild_state(State),
NewGS = guild_voice_disconnect:cleanup_virtual_channel_access_for_user(UserId, GS),
@@ -231,7 +231,7 @@ compute_voice_permissions(UserId, ChannelId, State) ->
IsAdmin = permission_bits:has(Permissions, AdminPerm),
CanSpeak = IsAdmin orelse permission_bits:has(Permissions, SpeakPerm),
CanStream = IsAdmin orelse permission_bits:has(Permissions, StreamPerm),
HasVirtualAccess = guild_virtual_channel_access:has_virtual_access(
HasVirtualAccess = guild_virtual_channel_access:has_voice_access(
UserId, ChannelId, State
),
FinalCanSpeak = CanSpeak orelse HasVirtualAccess,
+1 -1
View File
@@ -1325,7 +1325,7 @@ push_loss_counters_keep_a_genuine_zero_distinct_from_absent_test() ->
?assertEqual(0, maps:get(worker_pool_dropped, push_loss_counters()))
end).
cache_stats_with_counters_carries_the_loss_surface_test() ->
cache_stats_with_counters_includes_the_loss_surface_test() ->
push_ets_cache:init(),
with_counter_table(fun() ->
Stats = cache_stats_with_counters(),
@@ -355,7 +355,7 @@ caller_fields_caps_the_caller_name_test() ->
?MAX_CALLER_NAME_BYTES, byte_size(maps:get(<<"caller_name">>, Fields))
).
notification_fields_carry_title_body_and_tags_test() ->
notification_fields_include_title_body_and_tags_test() ->
Fields = test_notification_fields(
#{<<"content">> => <<"Hello world">>, <<"mentions">> => []},
123,
@@ -110,7 +110,7 @@ repair(WindowSeconds) when
repair(_WindowSeconds) ->
#{error => invalid_window_seconds}.
%% Only presences whose cached payload actually carries an expires_at are worth
%% Only presences whose cached payload actually has an expires_at are worth
%% nudging. Nudging every local presence would cost one payload rebuild and one
%% replicated cache write each, thousands per node, to correct a few dozen.
%% Filtering on "has an expiry" rather than "is expired" is deliberate: it also
@@ -129,7 +129,7 @@ expiring_presence_pids(UserIds) ->
has_expires_at(UserId) ->
try presence_cache:get(UserId) of
{ok, Presence} when is_map(Presence) ->
presence_carries_expiry(Presence);
presence_has_expiry(Presence);
_ ->
false
catch
@@ -139,8 +139,8 @@ has_expires_at(UserId) ->
%% presence_cache:get/1 returns {ok, map()} | not_found, never a bare map, and
%% expires_at_ms/1 takes the timestamp value rather than the custom_status map.
-spec presence_carries_expiry(map()) -> boolean().
presence_carries_expiry(Presence) ->
-spec presence_has_expiry(map()) -> boolean().
presence_has_expiry(Presence) ->
case maps:get(<<"custom_status">>, Presence, null) of
CustomStatus when is_map(CustomStatus) ->
expires_at_ms(maps:get(<<"expires_at">>, CustomStatus, null)) =/= none;
@@ -198,15 +198,15 @@ spread_ms(Index, Total, WindowMs) -> (Index * WindowMs) div Total.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
presence_carries_expiry_detects_an_expiry_test() ->
presence_has_expiry_detects_an_expiry_test() ->
?assert(
presence_carries_expiry(#{
presence_has_expiry(#{
<<"custom_status">> => #{<<"expires_at">> => <<"2026-05-13T13:02:27.497Z">>}
})
),
?assertNot(presence_carries_expiry(#{<<"custom_status">> => #{<<"text">> => <<"hi">>}})),
?assertNot(presence_carries_expiry(#{<<"custom_status">> => null})),
?assertNot(presence_carries_expiry(#{})).
?assertNot(presence_has_expiry(#{<<"custom_status">> => #{<<"text">> => <<"hi">>}})),
?assertNot(presence_has_expiry(#{<<"custom_status">> => null})),
?assertNot(presence_has_expiry(#{})).
-define(LIVE_EXPIRES_AT, <<"2026-05-13T13:02:27.497Z">>).