mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix: tighten edge cases across services (#3158)
This commit is contained in:
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
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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()].
|
||||
|
||||
@@ -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()) ->
|
||||
|
||||
@@ -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
|
||||
]).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">>).
|
||||
|
||||
|
||||
Reference in new issue
Block a user