perf(gateway): make channel moves cheap in large guilds (#3008)

This commit is contained in:
Hampus
2026-09-28 02:07:47 +02:00
committed by GitHub
parent 12417a6942
commit ed8c412415
14 changed files with 2416 additions and 75 deletions
+4
View File
@@ -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
+3 -1
View File
@@ -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),
put_engines(maps:put(ListId, Ref, engines(State)), 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
+29 -9
View File
@@ -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 ->
do_sync_channel_permissions(GuildId, ChannelId, VoiceStates, State)
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