feat(discord-gateway): implement voice & push improvements

- Voice disconnect broadcast on stopRecording
- Multi-guild voice support (VoiceController Map<guildId>)
- Session finalization + auto-enqueue muxer job
- Recordings API: duration field, channelId/userId filters
- Transmitter Redis connection reuse (shared conn)
- FFmpeg stderr memory cap (4KB limit)
- 10 new Redis event channels + Redis bridge subscriptions
- New DB tables: message_reactions, message_edits
- Webhook notification module
- Gateway metrics / Prometheus endpoint
- Multi-guild message capture (MONITOR_GUILD_IDS array)
- Thread tracking, presence, channel topic, guild member events
- Edit history snapshot on message update
- Muxer audio post-processing worker

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-13 13:51:21 +07:00
co-authored by Claude
parent fd3b5c5ca5
commit 14c0081f01
17 changed files with 260 additions and 90 deletions
@@ -9,7 +9,10 @@ import { createChildLogger } from "@bete/shared/logger";
import type { Client } from "discord.js-selfbot-v13";
import Redis from "ioredis";
import { config } from "../../shared/config/config.js";
import type { VoiceController } from "../voice-recording/voiceController.js";
import type {
VoiceController,
VoiceStatus,
} from "../voice-recording/voiceController.js";
import { GuildHandler } from "./guild.handler.js";
import {
type CommandHandlerFn,
@@ -30,6 +33,12 @@ interface VoiceStatusPayload {
activeGuildId: string | null;
activeChannelId: string | null;
activeChannelName: string | null;
connections: Array<{
guildId: string;
channelId: string;
channelName: string;
connectedAt: number;
}>;
}
// ---------------------------------------------------------------------------
@@ -69,7 +78,11 @@ export class CommandHandler {
this.voiceController = voiceController;
// Create domain-specific handlers with their dependencies
this.voiceHandler = new VoiceHandler(client, voiceController, this.redisPub);
this.voiceHandler = new VoiceHandler(
client,
voiceController,
this.redisPub,
);
this.mediaHandler = new MediaHandler();
this.guildHandler = new GuildHandler(client);
this.moderationHandler = new ModerationHandler(client);
@@ -164,14 +177,23 @@ export class CommandHandler {
// ---- Status publishing ----
private publishVoiceStatus(): void {
const status: VoiceStatusPayload = this.voiceController
const raw = this.voiceController
? this.voiceController.getStatus()
: {
ready: false,
connected: false,
activeGuildId: null,
activeChannelId: null,
activeChannelName: null,
connections: [],
};
const status: VoiceStatusPayload = {
connected: raw.connected,
activeGuildId: raw.activeGuildId,
activeChannelId: raw.activeChannelId,
activeChannelName: raw.activeChannelName,
connections: raw.connections ?? [],
};
this.setKey(VOICE_STATUS_KEY, JSON.stringify(status));
}
@@ -9,6 +9,7 @@ import {
COMMAND_VOICE_CHANNELS,
COMMAND_VOICE_CONNECT,
COMMAND_VOICE_DISCONNECT,
COMMAND_VOICE_DISCONNECT_GUILD,
COMMAND_VOICE_TRANSMIT_START,
COMMAND_VOICE_TRANSMIT_STOP,
type CommandMessage,
@@ -46,6 +47,9 @@ export function createHandlerRegistry(
registry.set(COMMAND_VOICE_DISCONNECT, (cmd) =>
voiceHandler.handleVoiceDisconnect(cmd),
);
registry.set(COMMAND_VOICE_DISCONNECT_GUILD, (cmd) =>
voiceHandler.handleVoiceDisconnectGuild(cmd),
);
registry.set(COMMAND_VOICE_CHANNELS, (cmd) =>
voiceHandler.handleVoiceChannels(cmd),
);
@@ -1,4 +1,8 @@
import { type CommandMessage, type CommandReply } from "@bete/shared";
import {
COMMAND_VOICE_DISCONNECT_GUILD,
type CommandMessage,
type CommandReply,
} from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import type { Client } from "discord.js-selfbot-v13";
import type Redis from "ioredis";
@@ -72,6 +76,33 @@ export class VoiceHandler {
return { id: cmd.id, success: true, data: status };
}
async handleVoiceDisconnectGuild(
cmd: CommandMessage,
): Promise<CommandReply<unknown>> {
if (!this.voiceController) {
return {
id: cmd.id,
success: false,
data: null,
error: "Gateway not initialized",
};
}
const guildId = String(cmd.payload.guildId ?? "");
if (!guildId) {
return {
id: cmd.id,
success: false,
data: null,
error: "guildId is required",
};
}
await this.voiceController.disconnectGuild(guildId);
const status = this.voiceController.getStatus();
return { id: cmd.id, success: true, data: status };
}
async handleVoiceChannels(
cmd: CommandMessage,
): Promise<CommandReply<unknown>> {
@@ -126,7 +157,9 @@ export class VoiceHandler {
try {
// Reuse shared Redis connection from CommandHandler
const transmitRedis = this.sharedRedis ?? new (await import("ioredis")).default(config.REDIS_URL);
const transmitRedis =
this.sharedRedis ??
new (await import("ioredis")).default(config.REDIS_URL);
await voiceTransmitter.start(transmitRedis);
const status = voiceTransmitter.getStatus();