%% Copyright (C) 2026 Fluxer Contributors %% %% This file is part of Fluxer. %% %% Fluxer is free software: you can redistribute it and/or modify %% it under the terms of the GNU Affero General Public License as published by %% the Free Software Foundation, either version 3 of the License, or %% (at your option) any later version. %% %% Fluxer is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU Affero General Public License for more details. %% %% You should have received a copy of the GNU Affero General Public License %% along with Fluxer. If not, see . -module(session_monitor). -export([ handle_process_down/3, find_guild_by_ref/2, find_call_by_ref/2 ]). -type session_state() :: session:session_state(). -type guild_id() :: session:guild_id(). -type channel_id() :: session:channel_id(). -type guild_ref() :: {pid(), reference()} | undefined | cached_unavailable. -type call_ref() :: {pid(), reference()}. -spec handle_process_down(reference(), term(), session_state()) -> {noreply, session_state()}. handle_process_down(Ref, Reason, State) -> SocketRef = maps:get(socket_mref, State, undefined), PresenceRef = maps:get(presence_mref, State, undefined), Guilds = maps:get(guilds, State), Calls = maps:get(calls, State, #{}), case Ref of SocketRef when Ref =:= SocketRef -> handle_socket_down(State); PresenceRef when Ref =:= PresenceRef -> handle_presence_down(State); _ -> case find_guild_by_ref(Ref, Guilds) of {ok, GuildId} -> handle_guild_down(GuildId, Reason, State, Guilds); not_found -> case find_call_by_ref(Ref, Calls) of {ok, ChannelId} -> handle_call_down(ChannelId, Reason, State, Calls); not_found -> {noreply, State} end end end. -spec handle_socket_down(session_state()) -> {noreply, session_state()}. handle_socket_down(State) -> self() ! {presence_update, #{status => offline}}, erlang:send_after(10000, self(), resume_timeout), {noreply, maps:merge(State, #{socket_pid => undefined, socket_mref => undefined})}. -spec handle_presence_down(session_state()) -> {noreply, session_state()}. handle_presence_down(State) -> self() ! {presence_connect, 0}, {noreply, maps:put(presence_pid, undefined, State)}. -spec handle_guild_down(guild_id(), term(), session_state(), #{guild_id() => guild_ref()}) -> {noreply, session_state()}. handle_guild_down(GuildId, killed, State, _Guilds) -> gen_server:cast(self(), {guild_leave, GuildId}), {noreply, State}; handle_guild_down(GuildId, normal, State, _Guilds) -> gen_server:cast(self(), {guild_leave, GuildId}), {noreply, State}; handle_guild_down(GuildId, _Reason, State, Guilds) -> GuildDeleteData = #{ <<"id">> => integer_to_binary(GuildId), <<"unavailable">> => true }, {noreply, UpdatedState} = session_dispatch:handle_dispatch( guild_delete, GuildDeleteData, State ), NewGuilds = maps:put(GuildId, undefined, Guilds), erlang:send_after(1000, self(), {guild_connect, GuildId, 0}), {noreply, maps:put(guilds, NewGuilds, UpdatedState)}. -spec handle_call_down(channel_id(), term(), session_state(), #{channel_id() => call_ref()}) -> {noreply, session_state()}. handle_call_down(ChannelId, killed, State, Calls) -> NewCalls = maps:remove(ChannelId, Calls), {noreply, maps:put(calls, NewCalls, State)}; handle_call_down(ChannelId, _Reason, State, Calls) -> CallDeleteData = #{ <<"channel_id">> => integer_to_binary(ChannelId), <<"unavailable">> => true }, {noreply, UpdatedState} = session_dispatch:handle_dispatch(call_delete, CallDeleteData, State), NewCalls = maps:put(ChannelId, undefined, Calls), erlang:send_after(1000, self(), {call_reconnect, ChannelId, 0}), {noreply, maps:put(calls, NewCalls, UpdatedState)}. -spec find_guild_by_ref(reference(), #{guild_id() => guild_ref()}) -> {ok, guild_id()} | not_found. find_guild_by_ref(Ref, Guilds) -> find_by_ref(Ref, Guilds). -spec find_call_by_ref(reference(), #{channel_id() => call_ref()}) -> {ok, channel_id()} | not_found. find_call_by_ref(Ref, Calls) -> find_by_ref(Ref, Calls). -spec find_by_ref(reference(), #{integer() => {pid(), reference()} | undefined}) -> {ok, integer()} | not_found. find_by_ref(Ref, Map) -> maps:fold( fun (Id, {_Pid, R}, _) when R =:= Ref -> {ok, Id}; (_, _, Acc) -> Acc end, not_found, Map ). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). find_by_ref_test() -> Ref1 = make_ref(), Ref2 = make_ref(), Ref3 = make_ref(), Map = #{ 123 => {self(), Ref1}, 456 => {self(), Ref2}, 789 => undefined }, ?assertEqual({ok, 123}, find_by_ref(Ref1, Map)), ?assertEqual({ok, 456}, find_by_ref(Ref2, Map)), ?assertEqual(not_found, find_by_ref(Ref3, Map)), ok. find_guild_by_ref_test() -> Ref = make_ref(), Guilds = #{100 => {self(), Ref}, 200 => undefined}, ?assertEqual({ok, 100}, find_guild_by_ref(Ref, Guilds)), ?assertEqual(not_found, find_guild_by_ref(make_ref(), Guilds)), ok. find_call_by_ref_test() -> Ref = make_ref(), Calls = #{300 => {self(), Ref}}, ?assertEqual({ok, 300}, find_call_by_ref(Ref, Calls)), ?assertEqual(not_found, find_call_by_ref(make_ref(), Calls)), ok. -endif.