feat(gateway): persistent voice auto-reconnect — rejoin same channel after restart/reboot or unexpected drop

This commit is contained in:
asepharyana
2026-08-30 14:03:12 +07:00
parent 7f45cbff2d
commit 7dfb4035b7
8 changed files with 422 additions and 10 deletions
@@ -277,6 +277,22 @@ export const pgVoiceRecordingsTable = pgTable(
export const voiceRecordingsTable = pgVoiceRecordingsTable;
/**
* Voice Auto-Reconnect Table (PostgreSQL)
* Persists the desired voice state per guild so the gateway can rejoin the
* same channel after a restart/reboot, or after an unexpected drop (kick /
* server move / voice server restart).
*/
export const pgVoiceAutoReconnectTable = pgTable("voice_auto_reconnect", {
guild_id: pgText("guild_id").primaryKey(),
channel_id: pgText("channel_id").notNull(),
channel_name: pgText("channel_name"),
connected_at: pgBigint("connected_at", { mode: "number" }).notNull(),
updated_at: pgBigint("updated_at", { mode: "number" }).notNull(),
});
export const voiceAutoReconnectTable = pgVoiceAutoReconnectTable;
// =============================================================================
// AI Analysis / Analytics
// =============================================================================
@@ -558,6 +574,11 @@ export type CorrectedModerationInsert =
export type VoiceRecording = typeof voiceRecordingsTable.$inferSelect;
export type VoiceRecordingInsert = typeof voiceRecordingsTable.$inferInsert;
// Voice Auto-Reconnect
export type VoiceAutoReconnect = typeof voiceAutoReconnectTable.$inferSelect;
export type VoiceAutoReconnectInsert =
typeof voiceAutoReconnectTable.$inferInsert;
// AI Analysis Runs
export type AIAnalysisRun = typeof aiAnalysisRunsTable.$inferSelect;
export type AIAnalysisRunInsert = typeof aiAnalysisRunsTable.$inferInsert;
@@ -0,0 +1,59 @@
import { eq } from "drizzle-orm";
import { getDatabase } from "./drizzle.js";
import {
type VoiceAutoReconnect,
type VoiceAutoReconnectInsert,
voiceAutoReconnectTable,
} from "./schema.js";
/**
* Upsert a guild's desired voice state.
* Called after a successful connect so the gateway can rejoin after a
* restart/reboot or unexpected drop.
*/
export async function upsertVoiceAutoReconnect(
record: VoiceAutoReconnectInsert,
): Promise<VoiceAutoReconnect | undefined> {
const db = getDatabase();
const [row] = await db
.insert(voiceAutoReconnectTable)
.values(record)
.onConflictDoUpdate({
target: voiceAutoReconnectTable.guild_id,
set: {
channel_id: record.channel_id,
channel_name: record.channel_name ?? null,
connected_at: record.connected_at,
updated_at: record.updated_at,
},
})
.returning();
return row;
}
/** List all persisted desired voice states (for startup auto-reconnect). */
export async function listVoiceAutoReconnects(): Promise<VoiceAutoReconnect[]> {
const db = getDatabase();
return db.select().from(voiceAutoReconnectTable);
}
/** Clear a guild's persisted voice state (explicit manual leave). */
export async function deleteVoiceAutoReconnect(guildId: string): Promise<void> {
const db = getDatabase();
await db
.delete(voiceAutoReconnectTable)
.where(eq(voiceAutoReconnectTable.guild_id, guildId));
}
/** Fetch a single guild's persisted state, if any. */
export async function getVoiceAutoReconnect(
guildId: string,
): Promise<VoiceAutoReconnect | undefined> {
const db = getDatabase();
const rows = await db
.select()
.from(voiceAutoReconnectTable)
.where(eq(voiceAutoReconnectTable.guild_id, guildId))
.limit(1);
return rows[0];
}