mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-08 03:32:27 +09:00
perf(gateway): make channel moves cheap in large guilds (#3008)
This commit is contained in:
@@ -8,6 +8,10 @@
|
||||
-export([start_link/1, update_counts/1]).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-export([dispatch_event/3]).
|
||||
-endif.
|
||||
|
||||
-define(HIBERNATE_TIMEOUT, 60000).
|
||||
-define(VOICE_MEMBERS_TABLE_WARNED, {?MODULE, voice_members_table_unavailable}).
|
||||
|
||||
|
||||
@@ -43,12 +43,12 @@ dispatch_member_list_event(channel_create, _EventData, _OldState, UpdatedState)
|
||||
broadcast_all_updates(UpdatedState);
|
||||
dispatch_member_list_event(channel_delete, _EventData, _OldState, UpdatedState) ->
|
||||
broadcast_all_updates(UpdatedState);
|
||||
dispatch_member_list_event(channel_update, EventData, OldState, UpdatedState) ->
|
||||
broadcast_channel_update(EventData, OldState, UpdatedState);
|
||||
dispatch_member_list_event(channel_update_bulk, EventData, OldState, UpdatedState) ->
|
||||
dispatch_member_list_event(channel_update, EventData, _OldState, UpdatedState) ->
|
||||
broadcast_channel_update(EventData, UpdatedState);
|
||||
dispatch_member_list_event(channel_update_bulk, EventData, _OldState, UpdatedState) ->
|
||||
Channels = maps:get(<<"channels">>, EventData, []),
|
||||
lists:foldl(
|
||||
fun(Channel, AccState) -> broadcast_channel_update(Channel, OldState, AccState) end,
|
||||
fun(Channel, AccState) -> broadcast_channel_update(Channel, AccState) end,
|
||||
UpdatedState,
|
||||
Channels
|
||||
);
|
||||
@@ -73,25 +73,25 @@ broadcast_all_updates(UpdatedState) ->
|
||||
{ok, NewState} = guild_member_list:broadcast_all_member_list_updates(UpdatedState),
|
||||
NewState.
|
||||
|
||||
-spec broadcast_channel_update(event_data(), guild_state(), guild_state()) -> guild_state().
|
||||
broadcast_channel_update(EventData, OldState, UpdatedState) ->
|
||||
-spec broadcast_channel_update(event_data(), guild_state()) -> guild_state().
|
||||
broadcast_channel_update(EventData, UpdatedState) ->
|
||||
ChannelIdBin = maps:get(<<"id">>, EventData, undefined),
|
||||
case guild_dispatch_decorate:parse_snowflake(<<"id">>, ChannelIdBin) of
|
||||
undefined ->
|
||||
UpdatedState;
|
||||
ChannelId ->
|
||||
broadcast_channel_update_for_id(ChannelId, OldState, UpdatedState)
|
||||
broadcast_channel_update_for_id(ChannelId, UpdatedState)
|
||||
end.
|
||||
|
||||
%% guild_state:post_update_channel/2 has already rebuilt this channel's engine
|
||||
%% earlier in the same dispatch, through
|
||||
%% earlier in the same dispatch when its permission inputs changed, through
|
||||
%% guild_member_list_write:rebuild_channels_for_permission_change/2. Rebuilding
|
||||
%% again here is a second full O(members) build over the same state, so when the
|
||||
%% engine already exists and nothing in the window can have changed what a
|
||||
%% rebuild would see, queue the sync and skip the rebuild.
|
||||
-spec broadcast_channel_update_for_id(integer(), guild_state(), guild_state()) -> guild_state().
|
||||
broadcast_channel_update_for_id(ChannelId, OldState, UpdatedState) ->
|
||||
case rebuild_is_redundant(ChannelId, OldState, UpdatedState) of
|
||||
-spec broadcast_channel_update_for_id(integer(), guild_state()) -> guild_state().
|
||||
broadcast_channel_update_for_id(ChannelId, UpdatedState) ->
|
||||
case rebuild_is_redundant(ChannelId, UpdatedState) of
|
||||
true ->
|
||||
sync_channel_list_without_rebuild(ChannelId, UpdatedState);
|
||||
false ->
|
||||
@@ -102,17 +102,15 @@ broadcast_channel_update_for_id(ChannelId, OldState, UpdatedState) ->
|
||||
end.
|
||||
|
||||
%% The engine must already exist, because the earlier pass only rebuilds
|
||||
%% ALREADY-LOADED engines while this pass would build a missing one. Virtual
|
||||
%% channel access is compared across the whole dispatch, which is a conservative
|
||||
%% superset of the window between the two rebuilds: guild_visibility can grant a
|
||||
%% user virtual access to a channel they just lost permission for, and that does
|
||||
%% change what a rebuild sees.
|
||||
-spec rebuild_is_redundant(integer(), guild_state(), guild_state()) -> boolean().
|
||||
rebuild_is_redundant(ChannelId, OldState, UpdatedState) ->
|
||||
guild_member_list_channel_engine:ref(integer_to_binary(ChannelId), UpdatedState) =/=
|
||||
undefined andalso
|
||||
maps:get(virtual_channel_access, OldState, #{}) =:=
|
||||
maps:get(virtual_channel_access, UpdatedState, #{}).
|
||||
%% ALREADY-LOADED engines while this pass would build a missing one. guild_visibility
|
||||
%% can grant a user virtual access to a channel they just lost permission for, and
|
||||
%% that does change what a rebuild sees, so the engine must also still match the
|
||||
%% permission inputs it was built from.
|
||||
-spec rebuild_is_redundant(integer(), guild_state()) -> boolean().
|
||||
rebuild_is_redundant(ChannelId, UpdatedState) ->
|
||||
ListId = integer_to_binary(ChannelId),
|
||||
guild_member_list_channel_engine:ref(ListId, UpdatedState) =/= undefined andalso
|
||||
not guild_member_list_engine_inputs:is_stale(ListId, UpdatedState).
|
||||
|
||||
%% Mirrors guild_member_list_write:broadcast_list_by_id/4 with the rebuild
|
||||
%% removed: a list nobody is subscribed to is left alone, otherwise the sync is
|
||||
|
||||
@@ -128,7 +128,9 @@ handle_reload(NewData, State) ->
|
||||
),
|
||||
NormalizedNewData0 = guild_data_index:normalize_map(ReloadData),
|
||||
NormalizedNewData = carry_members_table(OldData, NormalizedNewData0),
|
||||
NewState0 = State#{voice_states => ReloadVoiceStates, data => NormalizedNewData},
|
||||
NewState0 = guild_member_list_engine_inputs:forget_all(
|
||||
State#{voice_states => ReloadVoiceStates, data => NormalizedNewData}
|
||||
),
|
||||
NewState1 = guild_availability:handle_unavailability_transition(State, NewState0),
|
||||
NewState2 = guild_sessions:refresh_all_viewable_channels(NewState1),
|
||||
GuildId = maps:get(id, State),
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
schedule_count_cache_refresh/1,
|
||||
maybe_prune_invalid_member_subscriptions/2,
|
||||
cleanup_removed_member_subscriptions/3,
|
||||
apply_everyone_perm_bit/2
|
||||
apply_everyone_perm_bit/2,
|
||||
viewable_exceptions/1
|
||||
]).
|
||||
-export_type([guild_state/0]).
|
||||
|
||||
@@ -126,8 +127,12 @@ apply_everyone_perm_bit(Bit, State) ->
|
||||
Roles = guild_data_index:role_list(Data),
|
||||
{Updated, Changed} = update_everyone_role(Roles, GuildId, Bit),
|
||||
case Changed of
|
||||
false -> State;
|
||||
true -> State#{data => guild_data_index:put_roles(Updated, Data)}
|
||||
false ->
|
||||
State;
|
||||
true ->
|
||||
guild_member_list_engine_inputs:latch_stale(
|
||||
State#{data => guild_data_index:put_roles(Updated, Data)}
|
||||
)
|
||||
end.
|
||||
|
||||
-spec event_requires_prune(term()) -> boolean().
|
||||
|
||||
@@ -71,7 +71,7 @@ replace_engine(ListId, ChannelId, OldRef, State) ->
|
||||
NewRef = load_engine(ChannelId, State),
|
||||
State1 = put_engines(maps:put(ListId, NewRef, engines(State)), State),
|
||||
guild_member_list_engine:destroy(OldRef),
|
||||
State1.
|
||||
guild_member_list_engine_inputs:record(ListId, ChannelId, State, State1).
|
||||
|
||||
-spec rebuild_all(guild_state()) -> guild_state().
|
||||
rebuild_all(State) ->
|
||||
@@ -105,7 +105,9 @@ drop(ListId, State) ->
|
||||
State;
|
||||
Ref ->
|
||||
guild_member_list_engine:destroy(Ref),
|
||||
put_engines(maps:remove(ListId, Engines), State)
|
||||
guild_member_list_engine_inputs:forget(
|
||||
ListId, put_engines(maps:remove(ListId, Engines), State)
|
||||
)
|
||||
end.
|
||||
|
||||
-spec destroy_all(guild_state()) -> guild_state().
|
||||
@@ -114,7 +116,7 @@ destroy_all(State) ->
|
||||
fun(_ListId, Ref) -> guild_member_list_engine:destroy(Ref) end,
|
||||
engines(State)
|
||||
),
|
||||
put_engines(#{}, State).
|
||||
guild_member_list_engine_inputs:forget_all(put_engines(#{}, State)).
|
||||
|
||||
-spec sync_online(integer(), boolean(), guild_state()) -> ok.
|
||||
sync_online(UserId, IsOnline, State) ->
|
||||
@@ -229,7 +231,12 @@ build(ListId, State) ->
|
||||
State;
|
||||
ChannelId ->
|
||||
Ref = load_engine(ChannelId, State),
|
||||
guild_member_list_engine_inputs:record(
|
||||
ListId,
|
||||
ChannelId,
|
||||
State,
|
||||
put_engines(maps:put(ListId, Ref, engines(State)), State)
|
||||
)
|
||||
end.
|
||||
|
||||
-spec load_engine(pos_integer(), guild_state()) -> engine_ref().
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(guild_member_list_engine_inputs).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([
|
||||
is_stale/2,
|
||||
record/4,
|
||||
forget/2,
|
||||
forget_all/1,
|
||||
mark_stale/2,
|
||||
latch_stale/1
|
||||
]).
|
||||
|
||||
-type guild_state() :: map().
|
||||
-type list_id() :: binary().
|
||||
-type channel_id() :: pos_integer().
|
||||
-type inputs() :: {term(), [term()]}.
|
||||
|
||||
-export_type([guild_state/0, list_id/0]).
|
||||
|
||||
-define(INPUTS_KEY, channel_member_list_engine_inputs).
|
||||
|
||||
-spec is_stale(list_id(), guild_state()) -> boolean().
|
||||
is_stale(ListId, State) ->
|
||||
case {channel_id(ListId), maps:find(ListId, recorded(State))} of
|
||||
{ChannelId, {ok, Recorded}} when is_integer(ChannelId) ->
|
||||
Recorded =/= inputs(ChannelId, State);
|
||||
_ ->
|
||||
true
|
||||
end.
|
||||
|
||||
-spec record(list_id(), channel_id(), guild_state(), guild_state()) -> guild_state().
|
||||
record(ListId, ChannelId, BuiltFrom, State) ->
|
||||
put_recorded(maps:put(ListId, inputs(ChannelId, BuiltFrom), recorded(State)), State).
|
||||
|
||||
-spec forget(list_id(), guild_state()) -> guild_state().
|
||||
forget(ListId, State) ->
|
||||
put_recorded(maps:remove(ListId, recorded(State)), State).
|
||||
|
||||
-spec forget_all(guild_state()) -> guild_state().
|
||||
forget_all(State) ->
|
||||
put_recorded(#{}, State).
|
||||
|
||||
-spec mark_stale(channel_id(), guild_state()) -> guild_state().
|
||||
mark_stale(ChannelId, State) ->
|
||||
forget(integer_to_binary(ChannelId), State).
|
||||
|
||||
-spec latch_stale(guild_state()) -> guild_state().
|
||||
latch_stale(State) ->
|
||||
AccessByChannel = virtual_access_by_channel(State),
|
||||
put_recorded(
|
||||
maps:filter(
|
||||
fun(ListId, Recorded) ->
|
||||
case channel_id(ListId) of
|
||||
undefined -> false;
|
||||
ChannelId -> Recorded =:= inputs(ChannelId, State, AccessByChannel)
|
||||
end
|
||||
end,
|
||||
recorded(State)
|
||||
),
|
||||
State
|
||||
).
|
||||
|
||||
-spec inputs(channel_id(), guild_state()) -> inputs().
|
||||
inputs(ChannelId, State) ->
|
||||
inputs(ChannelId, State, virtual_access_by_channel(State)).
|
||||
|
||||
-spec inputs(channel_id(), guild_state(), #{term() => [term()]}) -> inputs().
|
||||
inputs(ChannelId, State, AccessByChannel) ->
|
||||
{guild_permissions:view_inputs(ChannelId, State), maps:get(ChannelId, AccessByChannel, [])}.
|
||||
|
||||
-spec virtual_access_by_channel(guild_state()) -> #{term() => [term()]}.
|
||||
virtual_access_by_channel(State) ->
|
||||
ByChannel = maps:fold(
|
||||
fun(UserId, Channels, Acc) ->
|
||||
sets:fold(
|
||||
fun(ChannelId, Inner) ->
|
||||
maps:update_with(
|
||||
ChannelId, fun(Users) -> [UserId | Users] end, [UserId], Inner
|
||||
)
|
||||
end,
|
||||
Acc,
|
||||
Channels
|
||||
)
|
||||
end,
|
||||
#{},
|
||||
map_utils:ensure_map(maps:get(virtual_channel_access, State, #{}))
|
||||
),
|
||||
maps:map(fun(_ChannelId, Users) -> lists:sort(Users) end, ByChannel).
|
||||
|
||||
-spec recorded(guild_state()) -> #{list_id() => inputs()}.
|
||||
recorded(State) ->
|
||||
case maps:get(?INPUTS_KEY, State, #{}) of
|
||||
Map when is_map(Map) -> Map;
|
||||
_ -> #{}
|
||||
end.
|
||||
|
||||
-spec put_recorded(#{list_id() => inputs()}, guild_state()) -> guild_state().
|
||||
put_recorded(Map, State) when map_size(Map) =:= 0 ->
|
||||
maps:remove(?INPUTS_KEY, State);
|
||||
put_recorded(Map, State) ->
|
||||
State#{?INPUTS_KEY => Map}.
|
||||
|
||||
-spec channel_id(list_id()) -> channel_id() | undefined.
|
||||
channel_id(ListId) ->
|
||||
case snowflake_id:parse_maybe(ListId) of
|
||||
Id when is_integer(Id), Id > 0 -> Id;
|
||||
_ -> undefined
|
||||
end.
|
||||
@@ -16,6 +16,7 @@
|
||||
find_member_by_user_id/2,
|
||||
find_role_by_id/2,
|
||||
find_channel_by_id/2,
|
||||
view_inputs/2,
|
||||
aggregate_role_permissions_cached/4
|
||||
]).
|
||||
|
||||
@@ -114,6 +115,10 @@ find_role_by_id(RoleId, Roles) ->
|
||||
find_channel_by_id(ChannelId, State) ->
|
||||
guild_permissions_check:find_channel_by_id(ChannelId, State).
|
||||
|
||||
-spec view_inputs(integer(), guild_state()) -> term().
|
||||
view_inputs(ChannelId, State) ->
|
||||
guild_permissions_check:view_inputs(ChannelId, State).
|
||||
|
||||
-spec compute_non_owner_permissions(
|
||||
user_id(), maybe_channel_id(), maybe_member(), guild_state(), guild_data()
|
||||
) -> permission().
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
get_max_role_position/2,
|
||||
find_member_by_user_id/2,
|
||||
find_role_by_id/2,
|
||||
find_channel_by_id/2
|
||||
find_channel_by_id/2,
|
||||
view_inputs/2
|
||||
]).
|
||||
|
||||
-export_type([
|
||||
@@ -283,6 +284,51 @@ is_viewable_child(Channel, UserId, CategoryId, Member, State) ->
|
||||
false
|
||||
end.
|
||||
|
||||
-spec view_inputs(channel_id(), guild_state()) -> term().
|
||||
view_inputs(ChannelId, State) ->
|
||||
Data = map_utils:ensure_map(guild_permissions_common:resolve_data_map(State)),
|
||||
Guild = map_utils:ensure_map(maps:get(<<"guild">>, Data, #{})),
|
||||
Index = guild_data_index:channel_index(Data),
|
||||
Cache = map_utils:ensure_map(maps:get(overwrite_perms_cache, Data, #{})),
|
||||
{
|
||||
maps:get(<<"owner_id">>, Guild, undefined),
|
||||
maps:get(<<"roles">>, Data, undefined),
|
||||
maps:get(<<"role_index">>, Data, undefined),
|
||||
maps:get(role_perms_cache, Data, undefined),
|
||||
channel_permission_inputs(ChannelId, Index, Cache),
|
||||
child_permission_inputs(ChannelId, Index, Cache, Data)
|
||||
}.
|
||||
|
||||
-spec channel_permission_inputs(integer(), map(), map()) -> term().
|
||||
channel_permission_inputs(ChannelId, Index, Cache) ->
|
||||
case maps:get(ChannelId, Index, undefined) of
|
||||
Channel when is_map(Channel) ->
|
||||
{
|
||||
maps:get(<<"type">>, Channel, undefined),
|
||||
maps:get(<<"permission_overwrites">>, Channel, undefined),
|
||||
maps:get(ChannelId, Cache, undefined)
|
||||
};
|
||||
_ ->
|
||||
missing
|
||||
end.
|
||||
|
||||
-spec child_permission_inputs(channel_id(), map(), map(), map()) -> [term()].
|
||||
child_permission_inputs(ChannelId, Index, Cache, Data) ->
|
||||
case maps:get(ChannelId, Index, undefined) of
|
||||
#{<<"type">> := 4} ->
|
||||
[
|
||||
{ChildId, channel_permission_inputs(ChildId, Index, Cache)}
|
||||
|| Child <- map_utils:ensure_list(maps:get(<<"channels">>, Data, [])),
|
||||
is_map(Child),
|
||||
snowflake_id:parse_maybe(maps:get(<<"parent_id">>, Child, undefined)) =:=
|
||||
ChannelId,
|
||||
ChildId <- [snowflake_id:parse_maybe(maps:get(<<"id">>, Child, undefined))],
|
||||
is_integer(ChildId)
|
||||
];
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec role_position(role()) -> integer().
|
||||
role_position(Role) ->
|
||||
case guild_data_normalize_schema:int(maps:get(<<"position">>, Role, undefined)) of
|
||||
|
||||
@@ -23,7 +23,25 @@ update_state(Event, EventData, State) ->
|
||||
StateWithUpdatedUser = StateWithUpdatedUser0#{data => Data},
|
||||
UpdatedData = update_data_for_event(Event, EventData, Data, State),
|
||||
UpdatedState = StateWithUpdatedUser#{data => UpdatedData},
|
||||
handle_post_update(Event, EventData, StateWithUpdatedUser, UpdatedState).
|
||||
maybe_latch_stale_engines(
|
||||
Event, handle_post_update(Event, EventData, StateWithUpdatedUser, UpdatedState)
|
||||
).
|
||||
|
||||
-spec maybe_latch_stale_engines(event(), guild_state()) -> guild_state().
|
||||
maybe_latch_stale_engines(Event, State) when
|
||||
Event =:= guild_update;
|
||||
Event =:= guild_role_create;
|
||||
Event =:= guild_role_update;
|
||||
Event =:= guild_role_update_bulk;
|
||||
Event =:= guild_role_delete;
|
||||
Event =:= channel_create;
|
||||
Event =:= channel_update;
|
||||
Event =:= channel_update_bulk;
|
||||
Event =:= channel_delete
|
||||
->
|
||||
guild_member_list_engine_inputs:latch_stale(State);
|
||||
maybe_latch_stale_engines(_Event, State) ->
|
||||
State.
|
||||
|
||||
-spec ensure_normalized(guild_data()) -> guild_data().
|
||||
ensure_normalized(Data) ->
|
||||
@@ -160,17 +178,19 @@ post_update_channel(channel_delete, _EventData, _OldState, NewState) ->
|
||||
-spec resync_channels_after_permission_change([integer()], guild_state(), guild_state()) ->
|
||||
guild_state().
|
||||
resync_channels_after_permission_change(ChanIds, OldState, NewState) ->
|
||||
Rebuilt = guild_member_list_write:rebuild_channels_for_permission_change(ChanIds, NewState),
|
||||
Rebuilt = guild_member_list_write:rebuild_channels_for_permission_change(
|
||||
[
|
||||
Id
|
||||
|| Id <- ChanIds,
|
||||
guild_member_list_engine_inputs:is_stale(integer_to_binary(Id), NewState)
|
||||
],
|
||||
NewState
|
||||
),
|
||||
Dispatched = guild_visibility:compute_and_dispatch_visibility_changes_for_channels(
|
||||
ChanIds, OldState, Rebuilt
|
||||
),
|
||||
lists:foreach(
|
||||
fun(ChanId) ->
|
||||
guild_voice_permission_sync:sync_all_voice_permissions_for_channel(
|
||||
ChanId, Dispatched
|
||||
)
|
||||
end,
|
||||
ChanIds
|
||||
ok = guild_voice_permission_sync:sync_all_voice_permissions_for_channels(
|
||||
ChanIds, Dispatched
|
||||
),
|
||||
Dispatched.
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ add_virtual_access(UserId, ChannelId, State) ->
|
||||
Updated = sets:add_element(ChannelId, UserChannels),
|
||||
State1 = State#{virtual_channel_access => VirtualAccess#{UserId => Updated}},
|
||||
State2 = update_user_session_view_cache(UserId, ChannelId, add, State1),
|
||||
mark_pending_join(UserId, ChannelId, State2).
|
||||
mark_pending_join(
|
||||
UserId, ChannelId, guild_member_list_engine_inputs:mark_stale(ChannelId, State2)
|
||||
).
|
||||
|
||||
-spec remove_virtual_access(user_id(), channel_id(), guild_state()) -> guild_state().
|
||||
remove_virtual_access(UserId, ChannelId, State) ->
|
||||
@@ -49,8 +51,9 @@ remove_virtual_access(UserId, ChannelId, State) ->
|
||||
|
||||
-spec remove_channel_from_user(user_id(), channel_id(), sets:set(), guild_state()) ->
|
||||
guild_state().
|
||||
remove_channel_from_user(UserId, ChannelId, UserChannels, State) ->
|
||||
remove_channel_from_user(UserId, ChannelId, UserChannels, State0) ->
|
||||
Updated = sets:del_element(ChannelId, UserChannels),
|
||||
State = guild_member_list_engine_inputs:mark_stale(ChannelId, State0),
|
||||
case sets:size(Updated) of
|
||||
0 -> remove_all_user_virtual_access(UserId, State);
|
||||
_ -> update_user_virtual_access(UserId, ChannelId, Updated, State)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(guild_visibility_memo).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([new/3, pairs/5]).
|
||||
|
||||
-export_type([memo/0, pairs/0]).
|
||||
|
||||
-type guild_state() :: map().
|
||||
-type user_id() :: integer().
|
||||
-type channel_id() :: integer().
|
||||
-type visibility() :: #{channel_id() => {boolean(), boolean()}}.
|
||||
-type settled() :: {#{channel_id() => true}, [channel_id()]} | unsettled.
|
||||
-type pairs() :: {visibility(), settled()}.
|
||||
-type memo() ::
|
||||
disabled
|
||||
| #{
|
||||
exceptions := sets:set(user_id()),
|
||||
unchanged := #{channel_id() => true},
|
||||
pairs := #{term() => pairs()}
|
||||
}.
|
||||
|
||||
-spec new([channel_id()], guild_state(), guild_state()) -> memo().
|
||||
new(ChannelIds, OldState, NewState) ->
|
||||
#{
|
||||
exceptions => sets:union(
|
||||
guild_maintenance:viewable_exceptions(OldState),
|
||||
guild_maintenance:viewable_exceptions(NewState)
|
||||
),
|
||||
unchanged => maps:from_keys(
|
||||
[
|
||||
Id
|
||||
|| Id <- ChannelIds,
|
||||
guild_permissions:view_inputs(Id, OldState) =:=
|
||||
guild_permissions:view_inputs(Id, NewState)
|
||||
],
|
||||
true
|
||||
),
|
||||
pairs => #{}
|
||||
}.
|
||||
|
||||
-spec pairs([channel_id()], guild_state(), guild_state(), map(), memo()) ->
|
||||
{pairs() | none, memo()}.
|
||||
pairs(_ChannelIds, _OldState, _NewState, _ChangeContext, disabled) ->
|
||||
{none, disabled};
|
||||
pairs(ChannelIds, OldState, NewState, ChangeContext, Memo) ->
|
||||
#{user_id := UserId, old_member := OldMember, new_member := NewMember} = ChangeContext,
|
||||
#{exceptions := Exceptions, unchanged := Unchanged, pairs := Cache} = Memo,
|
||||
case {sets:is_element(UserId, Exceptions), OldMember, NewMember} of
|
||||
{false, #{}, #{}} ->
|
||||
Key = {maps:get(<<"roles">>, OldMember, []), maps:get(<<"roles">>, NewMember, [])},
|
||||
case maps:find(Key, Cache) of
|
||||
{ok, Found} ->
|
||||
{Found, Memo};
|
||||
error ->
|
||||
Reusable =
|
||||
case Key of
|
||||
{Same, Same} -> Unchanged;
|
||||
_ -> #{}
|
||||
end,
|
||||
Computed = compute(ChannelIds, OldState, NewState, Reusable, ChangeContext),
|
||||
{Computed, Memo#{pairs := Cache#{Key => Computed}}}
|
||||
end;
|
||||
_ ->
|
||||
{none, Memo}
|
||||
end.
|
||||
|
||||
-spec compute([channel_id()], guild_state(), guild_state(), #{channel_id() => true}, map()) ->
|
||||
pairs().
|
||||
compute(ChannelIds, OldState, NewState, Reusable, ChangeContext) ->
|
||||
Visibility = maps:from_list([
|
||||
{ChannelId, visibility(ChannelId, OldState, NewState, Reusable, ChangeContext)}
|
||||
|| ChannelId <- ChannelIds
|
||||
]),
|
||||
{Visibility, settle(Visibility)}.
|
||||
|
||||
-spec visibility(channel_id(), guild_state(), guild_state(), #{channel_id() => true}, map()) ->
|
||||
{boolean(), boolean()}.
|
||||
visibility(ChannelId, OldState, NewState, Reusable, ChangeContext) ->
|
||||
#{user_id := UserId, old_member := OldMember, new_member := NewMember} = ChangeContext,
|
||||
OldVisible = guild_visibility_channels:channel_is_visible(
|
||||
UserId, ChannelId, OldMember, OldState
|
||||
),
|
||||
case maps:is_key(ChannelId, Reusable) of
|
||||
true ->
|
||||
{OldVisible, OldVisible};
|
||||
false ->
|
||||
{OldVisible,
|
||||
guild_visibility_channels:channel_is_visible(
|
||||
UserId, ChannelId, NewMember, NewState
|
||||
)}
|
||||
end.
|
||||
|
||||
-spec settle(visibility()) -> settled().
|
||||
settle(Visibility) ->
|
||||
case lists:all(fun({Old, New}) -> Old =:= New end, maps:values(Visibility)) of
|
||||
true ->
|
||||
{
|
||||
maps:from_keys([Id || {Id, {_, true}} <- maps:to_list(Visibility)], true),
|
||||
[Id || {Id, {_, false}} <- maps:to_list(Visibility)]
|
||||
};
|
||||
false ->
|
||||
unsettled
|
||||
end.
|
||||
@@ -39,7 +39,12 @@ compute_and_dispatch_visibility_changes_for_channels(ChannelIds, OldState, NewSt
|
||||
NewState
|
||||
),
|
||||
dispatch_channel_changes_for_sessions(
|
||||
SessionEntries, ValidChannelIds, OldState, NewState, ConnectedVoiceByUser
|
||||
SessionEntries,
|
||||
ValidChannelIds,
|
||||
OldState,
|
||||
NewState,
|
||||
ConnectedVoiceByUser,
|
||||
guild_visibility_memo:new(ValidChannelIds, OldState, NewState)
|
||||
)
|
||||
end.
|
||||
|
||||
@@ -195,15 +200,22 @@ dispatch_added_channels(Added, Pid, SessionId, SessionData, StateWithCache, Guil
|
||||
[channel_id()],
|
||||
guild_state(),
|
||||
guild_state(),
|
||||
#{user_id() => sets:set(channel_id())}
|
||||
#{user_id() => sets:set(channel_id())},
|
||||
guild_visibility_memo:memo()
|
||||
) -> guild_state().
|
||||
dispatch_channel_changes_for_sessions(
|
||||
SessionEntries, ChannelIds, OldState, NewState, ConnectedVoiceByUser
|
||||
SessionEntries, ChannelIds, OldState, NewState, ConnectedVoiceByUser, Memo
|
||||
) ->
|
||||
case guild_id(NewState) of
|
||||
GuildId when is_integer(GuildId), GuildId > 0 ->
|
||||
dispatch_channel_changes_with_guild_id(
|
||||
SessionEntries, ChannelIds, OldState, NewState, GuildId, ConnectedVoiceByUser
|
||||
SessionEntries,
|
||||
ChannelIds,
|
||||
OldState,
|
||||
NewState,
|
||||
GuildId,
|
||||
ConnectedVoiceByUser,
|
||||
Memo
|
||||
);
|
||||
_ ->
|
||||
NewState
|
||||
@@ -215,13 +227,14 @@ dispatch_channel_changes_for_sessions(
|
||||
guild_state(),
|
||||
guild_state(),
|
||||
integer(),
|
||||
#{user_id() => sets:set(channel_id())}
|
||||
#{user_id() => sets:set(channel_id())},
|
||||
guild_visibility_memo:memo()
|
||||
) -> guild_state().
|
||||
dispatch_channel_changes_with_guild_id(
|
||||
SessionEntries, ChannelIds, OldState, NewState, GuildId, ConnectedVoiceByUser
|
||||
SessionEntries, ChannelIds, OldState, NewState, GuildId, ConnectedVoiceByUser, Memo
|
||||
) ->
|
||||
lists:foldl(
|
||||
fun({SessionId, SessionData}, AccState) ->
|
||||
{FinalState, _Memo} = lists:foldl(
|
||||
fun({SessionId, SessionData}, {AccState, AccMemo}) ->
|
||||
dispatch_per_channel_changes(
|
||||
SessionId,
|
||||
SessionData,
|
||||
@@ -229,12 +242,14 @@ dispatch_channel_changes_with_guild_id(
|
||||
OldState,
|
||||
AccState,
|
||||
GuildId,
|
||||
ConnectedVoiceByUser
|
||||
ConnectedVoiceByUser,
|
||||
AccMemo
|
||||
)
|
||||
end,
|
||||
NewState,
|
||||
{NewState, Memo},
|
||||
SessionEntries
|
||||
).
|
||||
),
|
||||
FinalState.
|
||||
|
||||
-spec dispatch_per_channel_changes(
|
||||
binary(),
|
||||
@@ -243,15 +258,16 @@ dispatch_channel_changes_with_guild_id(
|
||||
guild_state(),
|
||||
guild_state(),
|
||||
integer(),
|
||||
#{user_id() => sets:set(channel_id())}
|
||||
) -> guild_state().
|
||||
#{user_id() => sets:set(channel_id())},
|
||||
guild_visibility_memo:memo()
|
||||
) -> {guild_state(), guild_visibility_memo:memo()}.
|
||||
dispatch_per_channel_changes(
|
||||
SessionId, SessionData, ChannelIds, OldState, NewState, GuildId, ConnectedVoiceByUser
|
||||
SessionId, SessionData, ChannelIds, OldState, NewState, GuildId, ConnectedVoiceByUser, Memo
|
||||
) ->
|
||||
UserId = maps:get(user_id, SessionData, undefined),
|
||||
case is_integer(UserId) of
|
||||
false ->
|
||||
NewState;
|
||||
{NewState, Memo};
|
||||
true ->
|
||||
Pid = maps:get(pid, SessionData, undefined),
|
||||
OldMember = guild_permissions:find_member_by_user_id(UserId, OldState),
|
||||
@@ -271,25 +287,58 @@ dispatch_per_channel_changes(
|
||||
old_state => OldState,
|
||||
guild_id => GuildId
|
||||
},
|
||||
{FinalMap, StateAfter} = process_channel_list(
|
||||
ChannelIds, InitialViewableMap, NewState, ChangeContext
|
||||
{Memoised, Memo1} = guild_visibility_memo:pairs(
|
||||
ChannelIds, OldState, NewState, ChangeContext, Memo
|
||||
),
|
||||
guild_sessions:set_session_viewable_channels(
|
||||
SessionId, FinalMap, StateAfter
|
||||
)
|
||||
{FinalMap, StateAfter} = process_channel_list(
|
||||
ChannelIds, Memoised, InitialViewableMap, NewState, ChangeContext
|
||||
),
|
||||
{
|
||||
guild_sessions:set_session_viewable_channels(SessionId, FinalMap, StateAfter),
|
||||
Memo1
|
||||
}
|
||||
end.
|
||||
|
||||
-spec process_channel_list(
|
||||
[channel_id()], map(), guild_state(), map()
|
||||
[channel_id()], guild_visibility_memo:pairs() | none, map(), guild_state(), map()
|
||||
) -> {map(), guild_state()}.
|
||||
process_channel_list(ChannelIds, InitialViewableMap, NewState, ChangeContext) ->
|
||||
process_channel_list(ChannelIds, none, InitialViewableMap, NewState, ChangeContext) ->
|
||||
lists:foldl(
|
||||
fun(ChannelId, Acc) ->
|
||||
apply_channel_change(ChannelId, Acc, ChangeContext)
|
||||
end,
|
||||
{InitialViewableMap, NewState},
|
||||
ChannelIds
|
||||
).
|
||||
);
|
||||
process_channel_list(ChannelIds, {Pairs, Settled}, InitialViewableMap, NewState, ChangeContext) ->
|
||||
#{connected_set := ConnectedSet} = ChangeContext,
|
||||
case {Settled, connected_channels(ChannelIds, ConnectedSet)} of
|
||||
{{Visible, Hidden}, []} ->
|
||||
{maps:merge(maps:without(Hidden, InitialViewableMap), Visible), NewState};
|
||||
{_, Connected} ->
|
||||
lists:foldl(
|
||||
fun(ChannelId, Acc) ->
|
||||
case lists:member(ChannelId, Connected) of
|
||||
true ->
|
||||
apply_channel_change(ChannelId, Acc, ChangeContext);
|
||||
false ->
|
||||
{OldVisible, NewVisible} = maps:get(ChannelId, Pairs),
|
||||
apply_visibility(
|
||||
ChannelId, OldVisible, NewVisible, Acc, ChangeContext
|
||||
)
|
||||
end
|
||||
end,
|
||||
{InitialViewableMap, NewState},
|
||||
ChannelIds
|
||||
)
|
||||
end.
|
||||
|
||||
-spec connected_channels([channel_id()], sets:set(channel_id())) -> [channel_id()].
|
||||
connected_channels(ChannelIds, ConnectedSet) ->
|
||||
case sets:is_empty(ConnectedSet) of
|
||||
true -> [];
|
||||
false -> [Id || Id <- ChannelIds, sets:is_element(Id, ConnectedSet)]
|
||||
end.
|
||||
|
||||
-spec apply_channel_change(
|
||||
channel_id(), {map(), guild_state()}, map()
|
||||
@@ -297,14 +346,10 @@ process_channel_list(ChannelIds, InitialViewableMap, NewState, ChangeContext) ->
|
||||
apply_channel_change(ChannelId, {ViewableMapAcc, StateAcc}, ChangeContext) ->
|
||||
#{
|
||||
user_id := UserId,
|
||||
pid := Pid,
|
||||
session_id := SessionId,
|
||||
session_data := SessionData,
|
||||
old_member := OldMember,
|
||||
new_member := NewMember,
|
||||
connected_set := ConnectedSet,
|
||||
old_state := OldState,
|
||||
guild_id := GuildId
|
||||
old_state := OldState
|
||||
} = ChangeContext,
|
||||
OldVisible = guild_visibility_channels:channel_is_visible(
|
||||
UserId, ChannelId, OldMember, OldState
|
||||
@@ -313,6 +358,21 @@ apply_channel_change(ChannelId, {ViewableMapAcc, StateAcc}, ChangeContext) ->
|
||||
guild_visibility_channels:ensure_new_channel_visibility(
|
||||
UserId, ChannelId, ConnectedSet, NewMember, StateAcc
|
||||
),
|
||||
apply_visibility(
|
||||
ChannelId, OldVisible, NewVisible, {ViewableMapAcc, StateAfterPreserve}, ChangeContext
|
||||
).
|
||||
|
||||
-spec apply_visibility(
|
||||
channel_id(), boolean(), boolean(), {map(), guild_state()}, map()
|
||||
) -> {map(), guild_state()}.
|
||||
apply_visibility(ChannelId, OldVisible, NewVisible, {ViewableMapAcc, StateAcc}, ChangeContext) ->
|
||||
#{
|
||||
pid := Pid,
|
||||
session_id := SessionId,
|
||||
session_data := SessionData,
|
||||
old_state := OldState,
|
||||
guild_id := GuildId
|
||||
} = ChangeContext,
|
||||
UpdatedViewableMap = guild_visibility_channels:update_viewable_map_for_channel(
|
||||
ViewableMapAcc, ChannelId, NewVisible
|
||||
),
|
||||
@@ -324,7 +384,7 @@ apply_channel_change(ChannelId, {ViewableMapAcc, StateAcc}, ChangeContext) ->
|
||||
SessionId,
|
||||
SessionData,
|
||||
OldState,
|
||||
StateAfterPreserve,
|
||||
StateAcc,
|
||||
GuildId,
|
||||
UpdatedViewableMap
|
||||
).
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
-export([
|
||||
sync_user_voice_permissions/2,
|
||||
sync_all_voice_permissions_for_channel/2,
|
||||
sync_all_voice_permissions_for_channels/2,
|
||||
sync_users_with_role/2,
|
||||
maybe_sync_permissions_on_role_update/2,
|
||||
maybe_sync_permissions_on_member_update/2
|
||||
@@ -54,14 +54,21 @@ maybe_sync_user_voice_state(GuildId, UserId, VoiceState, State) ->
|
||||
_ -> ok
|
||||
end.
|
||||
|
||||
-spec sync_all_voice_permissions_for_channel(channel_id(), guild_state()) -> ok.
|
||||
sync_all_voice_permissions_for_channel(ChannelId, State) ->
|
||||
-spec sync_all_voice_permissions_for_channels([channel_id()], guild_state()) -> ok.
|
||||
sync_all_voice_permissions_for_channels([], _State) ->
|
||||
ok;
|
||||
sync_all_voice_permissions_for_channels(ChannelIds, State) ->
|
||||
VoiceStates = guild_voice_lifecycle:authoritative_voice_states(State),
|
||||
case state_guild_id(State) of
|
||||
undefined ->
|
||||
ok;
|
||||
GuildId ->
|
||||
lists:foreach(
|
||||
fun(ChannelId) ->
|
||||
do_sync_channel_permissions(GuildId, ChannelId, VoiceStates, State)
|
||||
end,
|
||||
ChannelIds
|
||||
)
|
||||
end.
|
||||
|
||||
-spec do_sync_channel_permissions(integer(), channel_id(), map(), guild_state()) -> ok.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user