Unconnected user is stuck inside the server

(#395) Bug Fixed voice

Thread

Comment by @cproudlock
RexSystem 1 vote Merged from #404 originally by @cproudlock on GitHub
I dug into the gateway code — the root cause is that session.erl's terminate/2 callback is a no-op:
terminate(_Reason, _State) ->
    ok.
When a WebSocket drops ungracefully (PC poweroff, network loss), the session flow is:
  1. handle_socket_down marks socket as undefined, starts 10s resume timer
  2. No reconnection → resume_timeout fires → {stop, normal, State}
  3. terminate/2 is called but does nothing
  4. Guild processes are never notified → voice states persist in KeyDB until TTL (up to 1 hour)
Graceful disconnect works fine — session_voice.erl:handle_voice_disconnect/1 calls dispatch_guild_voice_disconnects/2 which notifies all guilds. But that code path is never reached on ungraceful death. Suggested fix: implement cleanup in terminate/2:
terminate(_Reason, State) ->
    case maps:get(voice_channel_id, State, undefined) of
        undefined -> ok;
        _ChannelId ->
            %% Reuse the same disconnect logic as graceful disconnect
            Guilds = maps:get(guilds, State, #{}),
            UserId = maps:get(user_id, State),
            SessionId = maps:get(id, State),
            ConnectionId = maps:get(connection_id, State, null),
            Request = #{
                user_id => UserId,
                session_id => SessionId,
                connection_id => ConnectionId
            },
            dispatch_guild_voice_disconnects(Guilds, Request),
            dm_voice:disconnect_voice_user(UserId, State)
    end,
    ok.
This ensures the same cleanup runs whether the user explicitly disconnects or their connection drops.