mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
633 lines
25 KiB
Erlang
633 lines
25 KiB
Erlang
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
-module(guild).
|
|
-feature(maybe_expr, enable).
|
|
-typing([eqwalizer]).
|
|
-behaviour(gen_server).
|
|
|
|
-export([start_link/1, update_counts/1]).
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
|
|
|
-define(HIBERNATE_TIMEOUT, 60000).
|
|
|
|
-type guild_state() :: map().
|
|
-type call_reply() ::
|
|
{reply, term(), guild_state()}
|
|
| {noreply, guild_state()}
|
|
| {stop, term(), term(), guild_state()}.
|
|
-type cast_reply() :: {noreply, guild_state()}.
|
|
-type info_reply() ::
|
|
{noreply, guild_state()}
|
|
| {noreply, guild_state(), timeout() | hibernate}
|
|
| {stop, term(), guild_state()}.
|
|
|
|
-spec start_link(map()) -> gen_server:start_ret().
|
|
start_link(GuildState) -> gen_server:start_link(?MODULE, GuildState, []).
|
|
|
|
-spec update_counts(guild_state()) -> guild_state().
|
|
update_counts(State) -> guild_maintenance:update_counts(State).
|
|
|
|
-spec init(map()) -> {ok, guild_state(), timeout()}.
|
|
init(GuildState) ->
|
|
process_flag(trap_exit, true),
|
|
erlang:process_flag(fullsweep_after, 10),
|
|
State0 = guild_init:init_base_state(GuildState),
|
|
State1 = guild_init:init_member_list(State0),
|
|
State2 = guild_init:init_counts(State1),
|
|
State3 = guild_init:init_caches_and_timers(State2),
|
|
State4 = guild_init:init_voice_server(State3),
|
|
erlang:garbage_collect(),
|
|
{ok, State4, ?HIBERNATE_TIMEOUT}.
|
|
|
|
-spec handle_call(term(), gen_server:from(), guild_state()) -> call_reply().
|
|
handle_call({session_connect, Request}, {CallerPid, _}, State) ->
|
|
handle_session_connect_call(Request, CallerPid, State);
|
|
handle_call(export_handoff_state, _From, State) ->
|
|
{reply, {ok, guild_handoff:export_handoff_state(State)}, State};
|
|
handle_call({get_cached_voice_state_by_connection, ConnectionId}, _From, State) ->
|
|
handle_cached_voice_state_call(ConnectionId, State);
|
|
handle_call({dispatch, Request}, _From, State) ->
|
|
handle_dispatch_call(Request, State);
|
|
handle_call({reload, NewData}, _From, State) ->
|
|
handle_reload_call(NewData, State);
|
|
handle_call(get_voice_server_pid, _From, State) ->
|
|
guild_voice_lifecycle:reply_voice_server_pid(State);
|
|
handle_call({terminate}, _From, State) ->
|
|
{stop, normal, ok, State};
|
|
handle_call(Msg, From, State) when is_tuple(Msg) ->
|
|
route_call(element(1, Msg), Msg, From, State);
|
|
handle_call(_, _From, State) ->
|
|
{reply, ok, State}.
|
|
|
|
-spec route_call(atom(), term(), gen_server:from(), guild_state()) -> call_reply().
|
|
route_call(Tag, Msg, From, State) ->
|
|
case call_handler(Tag) of
|
|
query -> guild_query_handler:handle_call(Msg, From, State);
|
|
voice -> guild_voice_handler:handle_call(Msg, From, State);
|
|
subscription -> guild_subscription_handler:handle_call(Msg, From, State);
|
|
undefined -> {reply, ok, State}
|
|
end.
|
|
|
|
-spec call_handler(atom()) -> query | voice | subscription | undefined.
|
|
call_handler(Tag) -> query_call_handler(Tag).
|
|
|
|
-spec query_call_handler(atom()) -> query | voice | subscription | undefined.
|
|
query_call_handler(get_counts) -> query;
|
|
query_call_handler(get_user_counts) -> query;
|
|
query_call_handler(get_channel_member_counts) -> query;
|
|
query_call_handler(get_large_guild_metadata) -> query;
|
|
query_call_handler(get_users_to_mention_by_roles) -> query;
|
|
query_call_handler(get_users_to_mention_by_user_ids) -> query;
|
|
query_call_handler(get_all_users_to_mention) -> query;
|
|
query_call_handler(resolve_all_mentions) -> query;
|
|
query_call_handler(resolve_mention_sources) -> query;
|
|
query_call_handler(resolve_mention_sources_page) -> query;
|
|
query_call_handler(resolve_channel_mentions) -> query;
|
|
query_call_handler(get_members_with_role) -> query;
|
|
query_call_handler(check_permission) -> query;
|
|
query_call_handler(get_user_permissions) -> query;
|
|
query_call_handler(can_manage_roles) -> query;
|
|
query_call_handler(can_manage_role) -> query;
|
|
query_call_handler(get_guild_data) -> query;
|
|
query_call_handler(get_assignable_roles) -> query;
|
|
query_call_handler(get_user_max_role_position) -> query;
|
|
query_call_handler(check_target_member) -> query;
|
|
query_call_handler(get_viewable_channels) -> query;
|
|
query_call_handler(get_guild_member) -> query;
|
|
query_call_handler(get_guild_members_batch) -> query;
|
|
query_call_handler(Tag) -> query_call_handler_more(Tag).
|
|
|
|
-spec query_call_handler_more(atom()) -> query | voice | subscription | undefined.
|
|
query_call_handler_more(has_member) -> query;
|
|
query_call_handler_more(list_guild_members) -> query;
|
|
query_call_handler_more(search_guild_members) -> query;
|
|
query_call_handler_more(list_guild_members_cursor) -> query;
|
|
query_call_handler_more(get_vanity_url_channel) -> query;
|
|
query_call_handler_more(get_first_viewable_text_channel) -> query;
|
|
query_call_handler_more(get_category_channel_count) -> query;
|
|
query_call_handler_more(get_channel_count) -> query;
|
|
query_call_handler_more(get_sessions) -> query;
|
|
query_call_handler_more(get_push_base_state) -> query;
|
|
query_call_handler_more(get_cluster_merge_state) -> query;
|
|
query_call_handler_more(Tag) -> voice_call_handler(Tag).
|
|
|
|
-spec voice_call_handler(atom()) -> voice | subscription | undefined.
|
|
voice_call_handler(voice_state_update) -> voice;
|
|
voice_call_handler(get_voice_state) -> voice;
|
|
voice_call_handler(update_member_voice) -> voice;
|
|
voice_call_handler(disconnect_voice_user) -> voice;
|
|
voice_call_handler(disconnect_voice_user_if_in_channel) -> voice;
|
|
voice_call_handler(disconnect_all_voice_users_in_channel) -> voice;
|
|
voice_call_handler(confirm_voice_connection_from_livekit) -> voice;
|
|
voice_call_handler(move_member) -> voice;
|
|
voice_call_handler(switch_voice_region) -> voice;
|
|
voice_call_handler(add_virtual_channel_access) -> voice;
|
|
voice_call_handler(store_pending_connection) -> voice;
|
|
voice_call_handler(get_voice_states_for_channel) -> voice;
|
|
voice_call_handler(get_pending_joins_for_channel) -> voice;
|
|
voice_call_handler(Tag) -> subscription_call_handler(Tag).
|
|
|
|
-spec subscription_call_handler(atom()) -> subscription | undefined.
|
|
subscription_call_handler(lazy_subscribe) -> subscription;
|
|
subscription_call_handler(_) -> undefined.
|
|
|
|
-spec handle_cast(term(), guild_state()) -> cast_reply().
|
|
handle_cast({dispatch, Request}, State) ->
|
|
handle_dispatch_cast(Request, State);
|
|
handle_cast(
|
|
{session_connect_async,
|
|
#{guild_id := GuildId, attempt := Attempt, request := Request} = Msg},
|
|
State
|
|
) ->
|
|
handle_session_connect_async_cast(GuildId, Attempt, Request, Msg, State);
|
|
handle_cast({session_connect_worker_done, SessionId, Attempt, Result0, Computed}, State) ->
|
|
handle_session_connect_worker_done_cast(SessionId, Attempt, Result0, Computed, State);
|
|
handle_cast({set_session_active, SessionId}, State) ->
|
|
handle_set_session_active_cast(SessionId, State);
|
|
handle_cast({set_session_passive, SessionId}, State) ->
|
|
handle_set_session_passive_cast(SessionId, State);
|
|
handle_cast({drop_session_member_lists, SessionId}, State) when is_binary(SessionId) ->
|
|
{noreply, guild_member_list:unsubscribe_session(SessionId, State)};
|
|
handle_cast({set_session_typing_override, SessionId, TypingFlag}, State) ->
|
|
handle_set_session_typing_override_cast(SessionId, TypingFlag, State);
|
|
handle_cast({send_guild_sync, SessionId}, State) ->
|
|
handle_send_guild_sync_cast(SessionId, State);
|
|
handle_cast({send_members_chunk, SessionId, ChunkData}, State) ->
|
|
handle_send_members_chunk_cast(SessionId, ChunkData, State);
|
|
handle_cast({patch_everyone_perms, Bit}, State) when is_integer(Bit), Bit > 0 ->
|
|
{noreply, guild_maintenance:apply_everyone_perm_bit(Bit, State)};
|
|
handle_cast(Msg, State) when is_tuple(Msg) ->
|
|
route_cast(element(1, Msg), Msg, State);
|
|
handle_cast(_, State) ->
|
|
{noreply, State}.
|
|
|
|
-spec route_cast(atom(), term(), guild_state()) -> cast_reply().
|
|
route_cast(Tag, Msg, State) ->
|
|
case cast_handler(Tag) of
|
|
voice -> guild_voice_handler:handle_cast(Msg, State);
|
|
subscription -> guild_subscription_handler:handle_cast(Msg, State);
|
|
undefined -> {noreply, State}
|
|
end.
|
|
|
|
-spec cast_handler(atom()) -> voice | subscription | undefined.
|
|
cast_handler(relay_voice_state_update) -> voice;
|
|
cast_handler(relay_voice_server_update) -> voice;
|
|
cast_handler(store_pending_connection) -> voice;
|
|
cast_handler(add_virtual_channel_access) -> voice;
|
|
cast_handler(remove_virtual_channel_access) -> voice;
|
|
cast_handler(cleanup_virtual_access_for_user) -> voice;
|
|
cast_handler(update_member_subscriptions) -> subscription;
|
|
cast_handler(_) -> undefined.
|
|
|
|
-spec handle_info(term(), guild_state()) -> info_reply().
|
|
handle_info({presence, UserId, Payload}, State) ->
|
|
handle_presence_info(UserId, Payload, State);
|
|
handle_info({'EXIT', Pid, Reason}, State) ->
|
|
handle_exit_info(Pid, Reason, State);
|
|
handle_info({'DOWN', Ref, process, _Pid, Reason}, State) ->
|
|
handle_down_info(Ref, Reason, State);
|
|
handle_info(count_cache_refresh, State) ->
|
|
State1 = update_counts(State),
|
|
_ = guild_maintenance:schedule_count_cache_refresh(State1),
|
|
{noreply, State1};
|
|
handle_info(availability_recheck, State) ->
|
|
{noreply, guild_availability:handle_availability_recheck(State)};
|
|
handle_info(passive_sync, State) ->
|
|
guild_passive_sync:handle_passive_sync(State);
|
|
handle_info(presence_reconcile, State) ->
|
|
guild_presence_reconcile:start_async(State),
|
|
_ = guild_presence_reconcile:schedule(),
|
|
{noreply, State};
|
|
handle_info({presence_reconcile_apply, PresenceById}, State) when is_map(PresenceById) ->
|
|
{noreply, guild_presence_reconcile:apply_reconcile_result(PresenceById, State)};
|
|
handle_info({reconcile_user_presence, UserId}, State) ->
|
|
{noreply, guild_presence_reconcile:reconcile_user(UserId, State)};
|
|
handle_info({clear_stale_cached_voice_states, ConnectionIds}, State) ->
|
|
handle_clear_stale_cached_voice_states_info(ConnectionIds, State);
|
|
handle_info(flush_lazy_subscribe_buffer, State) ->
|
|
guild_subscription_handler:handle_info(flush_lazy_subscribe_buffer, State);
|
|
handle_info(flush_member_list_sync_batch, State) ->
|
|
{noreply, guild_member_list:flush_pending_member_list_syncs(State)};
|
|
handle_info({check_auto_stop_empty, Token}, State) ->
|
|
handle_auto_stop_info(Token, State);
|
|
handle_info(check_auto_stop_empty, State) ->
|
|
{noreply, State};
|
|
handle_info(timeout, State) ->
|
|
{noreply, State, hibernate};
|
|
handle_info(_, State) ->
|
|
{noreply, State}.
|
|
|
|
-spec handle_session_connect_call(term(), pid(), guild_state()) -> call_reply().
|
|
handle_session_connect_call(Request, CallerPid, State) when is_map(Request) ->
|
|
guild_sessions:handle_session_connect(
|
|
Request, session_connect_pid(Request, CallerPid), State
|
|
).
|
|
|
|
-spec session_connect_pid(map(), pid()) -> pid().
|
|
session_connect_pid(#{session_pid := Pid}, _CallerPid) when is_pid(Pid) ->
|
|
Pid;
|
|
session_connect_pid(#{session_pid := Pid}, _CallerPid) ->
|
|
erlang:error({bad_session_pid, Pid});
|
|
session_connect_pid(_Request, CallerPid) ->
|
|
CallerPid.
|
|
|
|
-spec handle_cached_voice_state_call(term(), guild_state()) -> call_reply().
|
|
handle_cached_voice_state_call(ConnectionId, State) when is_binary(ConnectionId) ->
|
|
guild_voice_lifecycle:reply_cached_voice_state(ConnectionId, State).
|
|
|
|
-spec handle_reload_call(term(), guild_state()) -> call_reply().
|
|
handle_reload_call(NewData, State) when is_map(NewData) ->
|
|
guild_init:handle_reload(NewData, State).
|
|
|
|
-spec handle_dispatch_cast(term(), guild_state()) -> cast_reply().
|
|
handle_dispatch_cast(#{event := Event, data := EventData}, State) ->
|
|
{noreply, dispatch_event(Event, EventData, State)}.
|
|
|
|
-spec handle_session_connect_async_cast(term(), term(), term(), map(), guild_state()) ->
|
|
cast_reply().
|
|
handle_session_connect_async_cast(GuildId, Attempt, Request, Msg, State) when
|
|
is_integer(GuildId), is_integer(Attempt), Attempt >= 0, is_map(Request)
|
|
->
|
|
NewState = guild_connect_async:enqueue_session_connect_async(
|
|
GuildId, Attempt, Request, Msg, State
|
|
),
|
|
{noreply, NewState}.
|
|
|
|
-spec handle_session_connect_worker_done_cast(term(), term(), term(), term(), guild_state()) ->
|
|
cast_reply().
|
|
handle_session_connect_worker_done_cast(SessionId, Attempt, Result0, Computed, State) when
|
|
is_binary(SessionId), is_integer(Attempt), Attempt >= 0, is_map(Computed)
|
|
->
|
|
finalize_session_connect_worker_done(SessionId, Attempt, Result0, Computed, State);
|
|
handle_session_connect_worker_done_cast(undefined, Attempt, Result0, Computed, State) when
|
|
is_integer(Attempt), Attempt >= 0, is_map(Computed)
|
|
->
|
|
finalize_session_connect_worker_done(undefined, Attempt, Result0, Computed, State).
|
|
|
|
-spec finalize_session_connect_worker_done(
|
|
binary() | undefined, non_neg_integer(), term(), map(), guild_state()
|
|
) -> cast_reply().
|
|
finalize_session_connect_worker_done(SessionId, Attempt, Result0, Computed, State) ->
|
|
NewState = guild_connect_async:finalize_session_connect_async(
|
|
SessionId, Attempt, session_connect_result(Result0), Computed, State
|
|
),
|
|
{noreply, NewState}.
|
|
|
|
-spec session_connect_result(term()) ->
|
|
{ok, map()} | {ok_unavailable, map()} | {error, term()}.
|
|
session_connect_result({ok, Result}) when is_map(Result) ->
|
|
{ok, Result};
|
|
session_connect_result({ok_unavailable, Result}) when is_map(Result) ->
|
|
{ok_unavailable, Result};
|
|
session_connect_result({error, _Reason} = Error) ->
|
|
Error.
|
|
|
|
-spec handle_set_session_active_cast(term(), guild_state()) -> cast_reply().
|
|
handle_set_session_active_cast(SessionId, State) when is_binary(SessionId) ->
|
|
{noreply, guild_sessions:set_session_active_guild(SessionId, state_guild_id(State), State)}.
|
|
|
|
-spec handle_set_session_passive_cast(term(), guild_state()) -> cast_reply().
|
|
handle_set_session_passive_cast(SessionId, State) when is_binary(SessionId) ->
|
|
{noreply,
|
|
guild_sessions:set_session_passive_guild(SessionId, state_guild_id(State), State)}.
|
|
|
|
-spec handle_set_session_typing_override_cast(term(), term(), guild_state()) -> cast_reply().
|
|
handle_set_session_typing_override_cast(SessionId, TypingFlag, State) when
|
|
is_binary(SessionId), is_boolean(TypingFlag)
|
|
->
|
|
{noreply, guild_sessions:handle_set_typing_override(SessionId, TypingFlag, State)}.
|
|
|
|
-spec handle_send_guild_sync_cast(term(), guild_state()) -> cast_reply().
|
|
handle_send_guild_sync_cast(SessionId, State) when is_binary(SessionId) ->
|
|
{noreply, guild_sessions:handle_send_guild_sync(SessionId, State)}.
|
|
|
|
-spec handle_send_members_chunk_cast(term(), term(), guild_state()) -> cast_reply().
|
|
handle_send_members_chunk_cast(SessionId, ChunkData, State) when
|
|
is_binary(SessionId), is_map(ChunkData)
|
|
->
|
|
guild_sessions:handle_send_members_chunk(SessionId, ChunkData, State),
|
|
{noreply, State}.
|
|
|
|
-spec handle_presence_info(term(), term(), guild_state()) -> info_reply().
|
|
handle_presence_info(UserId, Payload, State) when is_integer(UserId), is_map(Payload) ->
|
|
guild_presence:handle_bus_presence(UserId, Payload, State).
|
|
|
|
-spec handle_exit_info(term(), term(), guild_state()) -> info_reply().
|
|
handle_exit_info(Pid, Reason, State) when is_pid(Pid) ->
|
|
handle_exit(Pid, Reason, State).
|
|
|
|
-spec handle_down_info(term(), term(), guild_state()) -> info_reply().
|
|
handle_down_info(Ref, Reason, State) when is_reference(Ref) ->
|
|
handle_down(Ref, Reason, State).
|
|
|
|
-spec handle_clear_stale_cached_voice_states_info(term(), guild_state()) -> info_reply().
|
|
handle_clear_stale_cached_voice_states_info(ConnectionIds, State) when is_list(ConnectionIds) ->
|
|
{noreply,
|
|
guild_voice_lifecycle:clear_stale_cached_voice_states(binary_ids(ConnectionIds), State)}.
|
|
|
|
-spec binary_ids([term()]) -> [binary()].
|
|
binary_ids(Ids) ->
|
|
[Id || Id <- Ids, is_binary(Id)].
|
|
|
|
-spec handle_auto_stop_info(term(), guild_state()) -> info_reply().
|
|
handle_auto_stop_info(Token, State) when is_reference(Token) ->
|
|
handle_auto_stop(Token, State).
|
|
|
|
-spec handle_exit(pid(), term(), guild_state()) -> info_reply().
|
|
handle_exit(Pid, Reason, State) ->
|
|
case maps:get(voice_server_pid, State, undefined) of
|
|
Pid -> {noreply, guild_voice_lifecycle:handle_voice_server_exit(Pid, Reason, State)};
|
|
_ -> handle_non_voice_exit(Pid, Reason, State)
|
|
end.
|
|
|
|
-spec handle_non_voice_exit(pid(), term(), guild_state()) -> info_reply().
|
|
handle_non_voice_exit(Pid, Reason, State) ->
|
|
case maps:get(broadcaster_pid, State, undefined) of
|
|
Pid ->
|
|
{noreply, maps:remove(broadcaster_pid, State)};
|
|
_ ->
|
|
{stop, linked_process_exit_reason(Pid, Reason), State}
|
|
end.
|
|
|
|
-spec handle_down(reference(), term(), guild_state()) -> info_reply().
|
|
handle_down(Ref, Reason, State) ->
|
|
WorkerRefs = session_connect_worker_refs(State),
|
|
case maps:is_key(Ref, WorkerRefs) of
|
|
true ->
|
|
handle_session_connect_worker_down(Ref, Reason, WorkerRefs, State);
|
|
false ->
|
|
guild_sessions:handle_session_down(Ref, State)
|
|
end.
|
|
|
|
-spec handle_session_connect_worker_down(reference(), term(), map(), guild_state()) ->
|
|
info_reply().
|
|
handle_session_connect_worker_down(Ref, Reason, WorkerRefs, State) ->
|
|
State1 = State#{session_connect_worker_refs => maps:remove(Ref, WorkerRefs)},
|
|
handle_session_connect_worker_down_reason(Reason, State1).
|
|
|
|
-spec handle_session_connect_worker_down_reason(term(), guild_state()) -> info_reply().
|
|
handle_session_connect_worker_down_reason(normal, State) ->
|
|
{noreply, State};
|
|
handle_session_connect_worker_down_reason(_Reason, State) ->
|
|
State1 = guild_connect_async:decrement_session_connect_inflight(State),
|
|
{noreply, guild_connect_async:maybe_start_session_connect_workers(State1)}.
|
|
|
|
-spec handle_auto_stop(reference(), guild_state()) ->
|
|
{noreply, guild_state()} | {stop, normal, guild_state()}.
|
|
handle_auto_stop(Token, State) ->
|
|
case maps:get(auto_stop_pending, State, undefined) of
|
|
#{token := Token} ->
|
|
auto_stop_pending_reply(State);
|
|
_ ->
|
|
{noreply, State}
|
|
end.
|
|
|
|
-spec terminate(term(), guild_state() | term()) -> ok.
|
|
terminate(Reason, State) when is_map(State) ->
|
|
safe_cleanup(
|
|
fun() ->
|
|
PresenceSubs = presence_subscriptions(State),
|
|
lists:foreach(fun safe_unsubscribe_presence/1, maps:keys(PresenceSubs))
|
|
end,
|
|
"presence_unsubscribe"
|
|
),
|
|
safe_cleanup(
|
|
fun() ->
|
|
guild_maintenance:maybe_delete_permission_cache(
|
|
maps:get(id, State, undefined), State
|
|
)
|
|
end,
|
|
"permission_cache_delete"
|
|
),
|
|
safe_cleanup(
|
|
fun() ->
|
|
cleanup_per_guild_ets(State)
|
|
end,
|
|
"ets_cleanup"
|
|
),
|
|
safe_cleanup(
|
|
fun() ->
|
|
cleanup_voice_server(State)
|
|
end,
|
|
"voice_cleanup"
|
|
),
|
|
safe_cleanup(
|
|
fun() ->
|
|
cleanup_member_list_subs(State)
|
|
end,
|
|
"member_list_subs_cleanup"
|
|
),
|
|
safe_cleanup(
|
|
fun() ->
|
|
cleanup_member_list_engine(State)
|
|
end,
|
|
"member_list_engine_cleanup"
|
|
),
|
|
maybe_report_crash(Reason, State),
|
|
ok;
|
|
terminate(Reason, State) ->
|
|
maybe_report_crash(Reason, State),
|
|
ok.
|
|
|
|
-spec code_change(term(), guild_state(), term()) -> {ok, guild_state()}.
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
erlang:process_flag(fullsweep_after, 10),
|
|
erlang:garbage_collect(),
|
|
{ok, State}.
|
|
|
|
-spec safe_unsubscribe_presence(integer()) -> ok.
|
|
safe_unsubscribe_presence(UserId) ->
|
|
try presence_bus:unsubscribe(UserId) of
|
|
_ -> ok
|
|
catch
|
|
throw:_Reason -> ok;
|
|
error:_Reason -> ok;
|
|
exit:_Reason -> ok
|
|
end.
|
|
|
|
-spec safe_cleanup(fun(() -> term()), string()) -> ok.
|
|
safe_cleanup(Fun, Label) ->
|
|
try Fun() of
|
|
_ -> ok
|
|
catch
|
|
Class:Reason ->
|
|
logger:warning(
|
|
"guild_terminate_cleanup_failed: step=~s class=~p reason=~p",
|
|
[Label, Class, Reason]
|
|
),
|
|
ok
|
|
end.
|
|
|
|
-spec cleanup_per_guild_ets(guild_state()) -> ok.
|
|
cleanup_per_guild_ets(State) ->
|
|
Data = maps:get(data, State, #{}),
|
|
safe_delete_ets(maps:get(members_ets, Data, undefined)),
|
|
safe_delete_ets(maps:get(member_presence, State, undefined)),
|
|
safe_delete_ets(maps:get(viewable_channels_cache, State, undefined)),
|
|
ok.
|
|
|
|
-spec cleanup_voice_server(guild_state()) -> ok.
|
|
cleanup_voice_server(State) ->
|
|
case maps:get(voice_server_pid, State, undefined) of
|
|
Pid when is_pid(Pid) ->
|
|
stop_voice_server_if_alive(Pid);
|
|
_ ->
|
|
ok
|
|
end.
|
|
|
|
-spec stop_voice_server_if_alive(pid()) -> ok.
|
|
stop_voice_server_if_alive(Pid) ->
|
|
case process_liveness:is_alive(Pid) of
|
|
true -> safe_stop_voice_server(Pid);
|
|
false -> ok
|
|
end.
|
|
|
|
-spec safe_stop_voice_server(pid()) -> ok.
|
|
safe_stop_voice_server(Pid) ->
|
|
try gen_server:stop(Pid, shutdown, 5000) of
|
|
_ -> ok
|
|
catch
|
|
exit:_Reason -> ok
|
|
end.
|
|
|
|
-spec cleanup_member_list_subs(guild_state()) -> ok.
|
|
cleanup_member_list_subs(State) ->
|
|
case maps:get(member_list_subscriptions, State, undefined) of
|
|
Tab when Tab =/= undefined ->
|
|
guild_member_list_subs:destroy(Tab);
|
|
_ ->
|
|
ok
|
|
end.
|
|
|
|
-spec cleanup_member_list_engine(guild_state()) -> ok.
|
|
cleanup_member_list_engine(State) ->
|
|
_ = guild_member_list_channel_engine:destroy_all(State),
|
|
case maps:get(member_list_engine, State, undefined) of
|
|
Ref when Ref =/= undefined ->
|
|
guild_member_list_engine:destroy(Ref);
|
|
_ ->
|
|
ok
|
|
end.
|
|
|
|
-spec safe_delete_ets(term()) -> ok.
|
|
safe_delete_ets(undefined) ->
|
|
ok;
|
|
safe_delete_ets(Tab) ->
|
|
try ets:delete(eqwalizer:dynamic_cast(Tab)) of
|
|
_ -> ok
|
|
catch
|
|
error:badarg -> ok
|
|
end.
|
|
|
|
-spec linked_process_exit_reason(pid(), term()) -> term().
|
|
linked_process_exit_reason(_Pid, normal) -> normal;
|
|
linked_process_exit_reason(_Pid, shutdown) -> shutdown;
|
|
linked_process_exit_reason(_Pid, {shutdown, _} = Reason) -> Reason;
|
|
linked_process_exit_reason(Pid, Reason) -> {linked_process_exit, Pid, Reason}.
|
|
|
|
-spec handle_dispatch_call(term(), guild_state()) -> {reply, ok, guild_state()}.
|
|
handle_dispatch_call(#{event := Event, data := EventData}, State) ->
|
|
{reply, ok, dispatch_event(Event, EventData, State)}.
|
|
|
|
-spec dispatch_event(term(), term(), guild_state()) -> guild_state().
|
|
dispatch_event(Event, EventData, State) ->
|
|
ParsedEventData = parse_event_data(EventData),
|
|
{noreply, NewState} = guild_dispatch:handle_dispatch(
|
|
Event, ParsedEventData, State
|
|
),
|
|
StateAfterPrune = guild_maintenance:maybe_prune_invalid_member_subscriptions(
|
|
Event, NewState
|
|
),
|
|
ok = maybe_refresh_permission_cache(Event, ParsedEventData, State, StateAfterPrune),
|
|
StateAfterPrune.
|
|
|
|
-spec parse_event_data(term()) -> map().
|
|
parse_event_data(D) when is_binary(D) -> require_map(json:decode(D));
|
|
parse_event_data(D) when is_map(D) -> D.
|
|
|
|
-spec maybe_refresh_permission_cache(term(), map(), guild_state(), guild_state()) -> ok.
|
|
maybe_refresh_permission_cache(Event, EventData, OldState, NewState) ->
|
|
case event_mutates_guild_data(Event) of
|
|
true ->
|
|
guild_maintenance:maybe_put_permission_cache(Event, EventData, OldState, NewState);
|
|
false ->
|
|
ok
|
|
end.
|
|
|
|
-spec event_mutates_guild_data(term()) -> boolean().
|
|
event_mutates_guild_data(E) ->
|
|
lists:member(E, [
|
|
guild_member_add,
|
|
guild_member_update,
|
|
guild_member_remove,
|
|
guild_role_create,
|
|
guild_role_update,
|
|
guild_role_update_bulk,
|
|
guild_role_delete,
|
|
channel_create,
|
|
channel_update,
|
|
channel_update_bulk,
|
|
channel_delete,
|
|
guild_update
|
|
]).
|
|
|
|
-spec state_guild_id(guild_state()) -> integer().
|
|
state_guild_id(#{id := GuildId}) when is_integer(GuildId) ->
|
|
GuildId.
|
|
|
|
-spec session_connect_worker_refs(guild_state()) -> map().
|
|
session_connect_worker_refs(State) ->
|
|
require_map(maps:get(session_connect_worker_refs, State, #{})).
|
|
|
|
-spec auto_stop_pending_reply(guild_state()) ->
|
|
{noreply, guild_state()} | {stop, normal, guild_state()}.
|
|
auto_stop_pending_reply(State) ->
|
|
CleanState = maps:remove(auto_stop_pending, State),
|
|
case map_size(sessions_map(State)) of
|
|
0 -> {stop, normal, CleanState};
|
|
_ -> {noreply, CleanState}
|
|
end.
|
|
|
|
-spec sessions_map(guild_state()) -> map().
|
|
sessions_map(State) ->
|
|
require_map(maps:get(sessions, State, #{})).
|
|
|
|
-spec presence_subscriptions(guild_state()) -> map().
|
|
presence_subscriptions(State) ->
|
|
require_map(maps:get(presence_subscriptions, State, #{})).
|
|
|
|
-spec require_map(term()) -> map().
|
|
require_map(Value) when is_map(Value) ->
|
|
Value;
|
|
require_map(Value) ->
|
|
erlang:error({badmap, Value}).
|
|
|
|
-spec maybe_report_crash(term(), term()) -> ok.
|
|
maybe_report_crash(normal, _) ->
|
|
ok;
|
|
maybe_report_crash(shutdown, _) ->
|
|
ok;
|
|
maybe_report_crash({shutdown, _}, _) ->
|
|
ok;
|
|
maybe_report_crash(_Reason, _State) ->
|
|
ok.
|
|
|
|
-ifdef(TEST).
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
handle_non_voice_exit_broadcaster_keeps_guild_alive_test() ->
|
|
BPid = list_to_pid("<0.250.0>"),
|
|
State = #{id => 42, broadcaster_pid => BPid},
|
|
?assertEqual({noreply, #{id => 42}}, handle_non_voice_exit(BPid, killed, State)).
|
|
|
|
handle_non_voice_exit_other_linked_stops_guild_test() ->
|
|
BPid = list_to_pid("<0.250.0>"),
|
|
OtherPid = list_to_pid("<0.251.0>"),
|
|
State = #{id => 42, broadcaster_pid => BPid},
|
|
?assertMatch(
|
|
{stop, {linked_process_exit, OtherPid, boom}, State},
|
|
handle_non_voice_exit(OtherPid, boom, State)
|
|
).
|
|
|
|
-endif.
|