refactor: atomic, DRY, and logging improvements

- Shared Redis channel constants as single source of truth (redis-channels.ts)
- commandHandler.ts split into VoiceHandler, MediaHandler, GuildHandler,
  ModerationHandler with handler-registry.ts dispatch
- messageStore.ts (1322 lines) split into domain-specific DB files:
  messages.db.ts, attachments.db.ts, reviews.db.ts,
  moderation-actions.db.ts, retention.db.ts
- recorder.ts startSpeaking callback extracted into speakingHandler.ts,
  streamSetup.ts, segmentFinalizer.ts
- autoDeleteManager.ts split into autoDeleteEligibility.ts,
  autoDeleteNotify.ts, autoDeleteLogger.ts
- Added createChildLogger() logging across 8 service files
- Backend messages.repository.ts migrated from raw SQL to Drizzle ORM
- Fixed biome.json to exclude packages/**/dist/* from lint
- Fixed config.ts GUILD_ID pre-existing type error

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-09 17:34:18 +07:00
co-authored by Claude Opus 4.8
parent 7108f6bb47
commit b68789fffc
37 changed files with 3620 additions and 2345 deletions
@@ -1,11 +1,23 @@
import {
BACKEND_COMMAND,
type CommandMessage,
type CommandReply,
MEDIA_STATUS_KEY,
VOICE_STATUS_KEY,
} from "@bete/shared";
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 { createModerationAction } from "../message-capture/messageStore.js";
import { discordPlayer } from "../voice-recording/player.js";
import { voiceTransmitter } from "../voice-recording/transmitter.js";
import type { VoiceController } from "../voice-recording/voiceController.js";
import { GuildHandler } from "./guild.handler.js";
import {
type CommandHandlerFn,
createHandlerRegistry,
} from "./handler-registry.js";
import { MediaHandler } from "./media.handler.js";
import { ModerationHandler } from "./moderation.handler.js";
import { VoiceHandler } from "./voice.handler.js";
const logger = createChildLogger("command-handler");
@@ -13,20 +25,6 @@ const logger = createChildLogger("command-handler");
// Types
// ---------------------------------------------------------------------------
interface BackendCommand {
id: string;
type: string;
payload: Record<string, unknown>;
replyChannel: string;
}
interface CommandReply {
id: string;
success: boolean;
data: unknown;
error?: string;
}
interface VoiceStatusPayload {
connected: boolean;
activeGuildId: string | null;
@@ -34,21 +32,6 @@ interface VoiceStatusPayload {
activeChannelName: string | null;
}
interface MediaStatusPayload {
playing: boolean;
musicVolume: number;
current: unknown;
queue: unknown[];
}
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const COMMAND_CHANNEL = "backend:command";
const VOICE_STATUS_KEY = "voice:status";
const MEDIA_STATUS_KEY = "media:status";
// ---------------------------------------------------------------------------
// CommandHandler
// ---------------------------------------------------------------------------
@@ -56,8 +39,12 @@ const MEDIA_STATUS_KEY = "media:status";
export class CommandHandler {
private redisSub: Redis;
private redisPub: Redis;
private client: Client | null = null;
private voiceController: VoiceController | null = null;
private registry: Map<string, CommandHandlerFn> = new Map();
private voiceHandler!: VoiceHandler;
private mediaHandler!: MediaHandler;
private guildHandler!: GuildHandler;
private moderationHandler!: ModerationHandler;
constructor() {
this.redisSub = new Redis(config.REDIS_URL);
@@ -79,9 +66,22 @@ export class CommandHandler {
* command channel. Must be called *after* the Discord client is created.
*/
start(client: Client, voiceController: VoiceController): void {
this.client = client;
this.voiceController = voiceController;
// Create domain-specific handlers with their dependencies
this.voiceHandler = new VoiceHandler(client, voiceController);
this.mediaHandler = new MediaHandler();
this.guildHandler = new GuildHandler(client);
this.moderationHandler = new ModerationHandler(client);
// Build the command registry
this.registry = createHandlerRegistry(
this.voiceHandler,
this.mediaHandler,
this.guildHandler,
this.moderationHandler,
);
this.redisSub.on("message", (_channel, message) => {
this.handleCommand(message).catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
@@ -89,11 +89,11 @@ export class CommandHandler {
});
});
this.redisSub.subscribe(COMMAND_CHANNEL, (err) => {
this.redisSub.subscribe(BACKEND_COMMAND, (err) => {
if (err) {
logger.error({ error: err }, "Failed to subscribe to command channel");
} else {
logger.info(`Subscribed to Redis channel "${COMMAND_CHANNEL}"`);
logger.info(`Subscribed to Redis channel "${BACKEND_COMMAND}"`);
}
});
@@ -109,9 +109,9 @@ export class CommandHandler {
// ---- Command dispatch ----
private async handleCommand(raw: string): Promise<void> {
let cmd: BackendCommand;
let cmd: CommandMessage;
try {
cmd = JSON.parse(raw) as BackendCommand;
cmd = JSON.parse(raw) as CommandMessage;
} catch {
logger.warn({ raw }, "Received invalid JSON on command channel");
return;
@@ -119,54 +119,20 @@ export class CommandHandler {
logger.info({ commandId: cmd.id, type: cmd.type }, "Received command");
let reply: CommandReply;
let reply: CommandReply<unknown>;
try {
switch (cmd.type) {
case "voice:connect":
reply = await this.handleVoiceConnect(cmd);
break;
case "voice:disconnect":
reply = await this.handleVoiceDisconnect(cmd);
break;
case "voice:channels":
reply = await this.handleVoiceChannels(cmd);
break;
case "voice:transmit:start":
reply = await this.handleVoiceTransmitStart(cmd);
break;
case "voice:transmit:stop":
reply = await this.handleVoiceTransmitStop(cmd);
break;
case "guilds:list":
reply = await this.handleListGuilds(cmd);
break;
case "guilds:text-channels":
reply = await this.handleTextChannels(cmd);
break;
case "media:queue":
reply = await this.handleMediaQueue(cmd);
break;
case "media:skip":
reply = await this.handleMediaSkip(cmd);
break;
case "media:stop":
reply = await this.handleMediaStop(cmd);
break;
case "media:volume":
reply = await this.handleMediaVolume(cmd);
break;
case "moderation:action":
reply = await this.handleModerationAction(cmd);
break;
default:
logger.warn({ type: cmd.type }, "Unknown command type");
reply = {
id: cmd.id,
success: false,
data: null,
error: `Unknown command type: ${cmd.type}`,
};
const handler = this.registry.get(cmd.type);
if (handler) {
reply = await handler(cmd);
} else {
logger.warn({ type: cmd.type }, "Unknown command type");
reply = {
id: cmd.id,
success: false,
data: null,
error: `Unknown command type: ${cmd.type}`,
};
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
@@ -195,383 +161,6 @@ export class CommandHandler {
this.publishMediaStatus();
}
// ---- Command handlers ----
private async handleVoiceConnect(cmd: BackendCommand): Promise<CommandReply> {
if (!this.client || !this.voiceController) {
return {
id: cmd.id,
success: false,
data: null,
error: "Gateway not initialized",
};
}
const guildId = String(cmd.payload.guildId ?? "");
const channelId = String(cmd.payload.channelId ?? "");
if (!guildId || !channelId) {
return {
id: cmd.id,
success: false,
data: null,
error: "guildId and channelId are required",
};
}
const status = await this.voiceController.connect(guildId, channelId);
return { id: cmd.id, success: true, data: status };
}
private async handleVoiceDisconnect(
cmd: BackendCommand,
): Promise<CommandReply> {
if (!this.voiceController) {
return {
id: cmd.id,
success: false,
data: null,
error: "Gateway not initialized",
};
}
const status = await this.voiceController.disconnect();
return { id: cmd.id, success: true, data: status };
}
private async handleVoiceChannels(
cmd: BackendCommand,
): Promise<CommandReply> {
if (!this.client) {
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",
};
}
try {
const guild = await this.client.guilds.fetch(guildId);
const channels = await guild.channels.fetch();
const voiceChannels = channels
.filter((c) => c?.type === "GUILD_VOICE")
.map((c) => ({
id: c.id,
name: c.name,
type: "voice" as const,
}));
return { id: cmd.id, success: true, data: voiceChannels };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { id: cmd.id, success: false, data: null, error: msg };
}
}
private async handleListGuilds(cmd: BackendCommand): Promise<CommandReply> {
if (!this.client) {
return {
id: cmd.id,
success: false,
data: null,
error: "Gateway not initialized",
};
}
try {
const guilds = this.client.guilds.cache.map((g) => ({
id: g.id,
name: g.name,
icon: g.iconURL() ?? null,
}));
return { id: cmd.id, success: true, data: guilds };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { id: cmd.id, success: false, data: null, error: msg };
}
}
private async handleTextChannels(cmd: BackendCommand): Promise<CommandReply> {
if (!this.client) {
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",
};
}
try {
const guild = await this.client.guilds.fetch(guildId);
const channels = await guild.channels.fetch();
const textChannels = channels
.filter((c) => c?.type === "GUILD_TEXT")
.map((c) => ({
id: c.id,
name: c.name,
type: "text" as const,
}));
return { id: cmd.id, success: true, data: textChannels };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { id: cmd.id, success: false, data: null, error: msg };
}
}
private getCurrentMediaStatus(): MediaStatusPayload {
return {
playing: discordPlayer.getStatus() === "playing",
musicVolume: discordPlayer.getMusicVolume(),
current: null,
queue: [],
};
}
private async handleMediaQueue(cmd: BackendCommand): Promise<CommandReply> {
// Media queueing is handled at a higher level (frontend / backend streams
// audio directly). Log the request for now.
logger.info("media:queue received — media queueing is handled externally");
return {
id: cmd.id,
success: true,
data: this.getCurrentMediaStatus(),
};
}
private async handleMediaSkip(cmd: BackendCommand): Promise<CommandReply> {
discordPlayer.stop("music");
return { id: cmd.id, success: true, data: this.getCurrentMediaStatus() };
}
private async handleMediaStop(cmd: BackendCommand): Promise<CommandReply> {
discordPlayer.stop("music");
return { id: cmd.id, success: true, data: this.getCurrentMediaStatus() };
}
private async handleMediaVolume(cmd: BackendCommand): Promise<CommandReply> {
const volume = Number(cmd.payload.volume);
if (!Number.isFinite(volume)) {
return {
id: cmd.id,
success: false,
data: null,
error: "volume must be a number",
};
}
discordPlayer.setMusicVolume(volume);
return {
id: cmd.id,
success: true,
data: this.getCurrentMediaStatus(),
};
}
private async handleVoiceTransmitStart(
cmd: BackendCommand,
): Promise<CommandReply> {
if (!discordPlayer.isConnected()) {
return {
id: cmd.id,
success: false,
data: null,
error: "Not connected to voice channel",
};
}
try {
// Create a new Redis connection for the transmitter
const transmitRedis = new Redis(config.REDIS_URL);
await voiceTransmitter.start(transmitRedis);
const status = voiceTransmitter.getStatus();
logger.info({ status }, "Voice transmit started");
return {
id: cmd.id,
success: true,
data: status,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error({ error: message }, "Failed to start voice transmit");
return {
id: cmd.id,
success: false,
data: null,
error: message,
};
}
}
private async handleVoiceTransmitStop(
cmd: BackendCommand,
): Promise<CommandReply> {
try {
await voiceTransmitter.stop();
logger.info("Voice transmit stopped");
return {
id: cmd.id,
success: true,
data: { status: "stopped" },
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error({ error: message }, "Failed to stop voice transmit");
return {
id: cmd.id,
success: false,
data: null,
error: message,
};
}
}
private async handleModerationAction(
cmd: BackendCommand,
): Promise<CommandReply> {
const payload = cmd.payload as {
message_id?: string;
user_id?: string;
guild_id?: string;
channel_id?: string;
action_type?: string;
reason?: string;
executed_by?: string;
};
if (
!payload.message_id ||
!payload.user_id ||
!payload.guild_id ||
!payload.action_type
) {
return {
id: cmd.id,
success: false,
data: null,
error: "message_id, user_id, guild_id, and action_type are required",
};
}
const validActions = [
"delete_message",
"mute_user",
"warn_user",
"kick_user",
"ban_user",
] as const;
if (
!validActions.includes(
payload.action_type as (typeof validActions)[number],
)
) {
return {
id: cmd.id,
success: false,
data: null,
error: `Invalid action_type: ${payload.action_type}. Must be one of: ${validActions.join(", ")}`,
};
}
try {
// For delete_message, also actually delete via Discord if client is available
if (payload.action_type === "delete_message" && this.client) {
try {
const channelId = String(cmd.payload.channel_id ?? "");
if (channelId) {
const channel = await this.client.channels.fetch(channelId);
if (channel?.isText()) {
const msg = await channel.messages
.fetch(payload.message_id)
.catch(() => null);
if (msg) {
await msg.delete().catch((err: unknown) => {
logger.warn(
{ error: err, messageId: payload.message_id },
"Failed to delete message via Discord",
);
});
}
}
}
} catch (err) {
logger.warn(
{ error: err, messageId: payload.message_id },
"Failed to fetch channel/message for deletion",
);
}
}
const action = await createModerationAction({
message_id: payload.message_id,
user_id: payload.user_id,
guild_id: payload.guild_id,
action_type: payload.action_type as
| "delete_message"
| "mute_user"
| "warn_user"
| "kick_user"
| "ban_user",
reason: payload.reason ?? null,
executed_by: payload.executed_by ?? "command-handler",
status: "executed",
error: null,
executed_at: Date.now(),
});
logger.info(
{
actionId: action.id,
actionType: payload.action_type,
userId: payload.user_id,
},
"Moderation action executed",
);
return {
id: cmd.id,
success: true,
data: action,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error(
{ error: message, commandId: cmd.id },
"Failed to execute moderation action",
);
return {
id: cmd.id,
success: false,
data: null,
error: message,
};
}
}
// ---- Status publishing ----
private publishVoiceStatus(): void {
@@ -588,7 +177,10 @@ export class CommandHandler {
}
private publishMediaStatus(): void {
this.setKey(MEDIA_STATUS_KEY, JSON.stringify(this.getCurrentMediaStatus()));
this.setKey(
MEDIA_STATUS_KEY,
JSON.stringify(this.mediaHandler.getCurrentMediaStatus()),
);
}
/**
@@ -0,0 +1,86 @@
import { type CommandMessage, type CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import type { Client } from "discord.js-selfbot-v13";
// ---------------------------------------------------------------------------
// GuildHandler
// ---------------------------------------------------------------------------
export class GuildHandler {
private logger = createChildLogger("guild-handler");
constructor(private client: Client | null) {}
setClient(client: Client): void {
this.client = client;
}
async handleListGuilds(cmd: CommandMessage): Promise<CommandReply<unknown>> {
if (!this.client) {
return {
id: cmd.id,
success: false,
data: null,
error: "Gateway not initialized",
};
}
try {
const guilds = this.client.guilds.cache.map((g) => ({
id: g.id,
name: g.name,
icon: g.iconURL() ?? null,
}));
return { id: cmd.id, success: true, data: guilds };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.logger.error({ error: msg }, "Failed to list guilds");
return { id: cmd.id, success: false, data: null, error: msg };
}
}
async handleTextChannels(
cmd: CommandMessage,
): Promise<CommandReply<unknown>> {
if (!this.client) {
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",
};
}
try {
const guild = await this.client.guilds.fetch(guildId);
const channels = await guild.channels.fetch();
const textChannels = channels
.filter((c) => c?.type === "GUILD_TEXT")
.map((c) => ({
id: c.id,
name: c.name,
type: "text" as const,
}));
return { id: cmd.id, success: true, data: textChannels };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.logger.error(
{ error: msg, guildId },
"Failed to list text channels",
);
return { id: cmd.id, success: false, data: null, error: msg };
}
}
}
@@ -0,0 +1,83 @@
import {
COMMAND_GUILDS_LIST,
COMMAND_GUILDS_TEXT_CHANNELS,
COMMAND_MEDIA_QUEUE,
COMMAND_MEDIA_SKIP,
COMMAND_MEDIA_STOP,
COMMAND_MEDIA_VOLUME,
COMMAND_MODERATION_ACTION,
COMMAND_VOICE_CHANNELS,
COMMAND_VOICE_CONNECT,
COMMAND_VOICE_DISCONNECT,
COMMAND_VOICE_TRANSMIT_START,
COMMAND_VOICE_TRANSMIT_STOP,
type CommandMessage,
type CommandReply,
} from "@bete/shared";
import type { GuildHandler } from "./guild.handler.js";
import type { MediaHandler } from "./media.handler.js";
import type { ModerationHandler } from "./moderation.handler.js";
import type { VoiceHandler } from "./voice.handler.js";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type CommandHandlerFn = (
cmd: CommandMessage,
) => Promise<CommandReply<unknown>>;
// ---------------------------------------------------------------------------
// Registry factory
// ---------------------------------------------------------------------------
export function createHandlerRegistry(
voiceHandler: VoiceHandler,
mediaHandler: MediaHandler,
guildHandler: GuildHandler,
moderationHandler: ModerationHandler,
): Map<string, CommandHandlerFn> {
const registry = new Map<string, CommandHandlerFn>();
// Voice commands
registry.set(COMMAND_VOICE_CONNECT, (cmd) =>
voiceHandler.handleVoiceConnect(cmd),
);
registry.set(COMMAND_VOICE_DISCONNECT, (cmd) =>
voiceHandler.handleVoiceDisconnect(cmd),
);
registry.set(COMMAND_VOICE_CHANNELS, (cmd) =>
voiceHandler.handleVoiceChannels(cmd),
);
registry.set(COMMAND_VOICE_TRANSMIT_START, (cmd) =>
voiceHandler.handleVoiceTransmitStart(cmd),
);
registry.set(COMMAND_VOICE_TRANSMIT_STOP, (cmd) =>
voiceHandler.handleVoiceTransmitStop(cmd),
);
// Media commands
registry.set(COMMAND_MEDIA_QUEUE, (cmd) =>
mediaHandler.handleMediaQueue(cmd),
);
registry.set(COMMAND_MEDIA_SKIP, (cmd) => mediaHandler.handleMediaSkip(cmd));
registry.set(COMMAND_MEDIA_STOP, (cmd) => mediaHandler.handleMediaStop(cmd));
registry.set(COMMAND_MEDIA_VOLUME, (cmd) =>
mediaHandler.handleMediaVolume(cmd),
);
// Guild commands
registry.set(COMMAND_GUILDS_LIST, (cmd) =>
guildHandler.handleListGuilds(cmd),
);
registry.set(COMMAND_GUILDS_TEXT_CHANNELS, (cmd) =>
guildHandler.handleTextChannels(cmd),
);
// Moderation commands
registry.set(COMMAND_MODERATION_ACTION, (cmd) =>
moderationHandler.handleModerationAction(cmd),
);
return registry;
}
@@ -0,0 +1,78 @@
import { type CommandMessage, type CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import { discordPlayer } from "../voice-recording/player.js";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface MediaStatusPayload {
playing: boolean;
musicVolume: number;
current: unknown;
queue: unknown[];
}
// ---------------------------------------------------------------------------
// MediaHandler
// ---------------------------------------------------------------------------
export class MediaHandler {
private logger = createChildLogger("media-handler");
getCurrentMediaStatus(): MediaStatusPayload {
return {
playing: discordPlayer.getStatus() === "playing",
musicVolume: discordPlayer.getMusicVolume(),
current: null,
queue: [],
};
}
async handleMediaQueue(cmd: CommandMessage): Promise<CommandReply<unknown>> {
this.logger.info(
"media:queue received — media queueing is handled externally",
);
return {
id: cmd.id,
success: true,
data: this.getCurrentMediaStatus(),
};
}
async handleMediaSkip(cmd: CommandMessage): Promise<CommandReply<unknown>> {
discordPlayer.stop("music");
return {
id: cmd.id,
success: true,
data: this.getCurrentMediaStatus(),
};
}
async handleMediaStop(cmd: CommandMessage): Promise<CommandReply<unknown>> {
discordPlayer.stop("music");
return {
id: cmd.id,
success: true,
data: this.getCurrentMediaStatus(),
};
}
async handleMediaVolume(cmd: CommandMessage): Promise<CommandReply<unknown>> {
const volume = Number(cmd.payload.volume);
if (!Number.isFinite(volume)) {
return {
id: cmd.id,
success: false,
data: null,
error: "volume must be a number",
};
}
discordPlayer.setMusicVolume(volume);
return {
id: cmd.id,
success: true,
data: this.getCurrentMediaStatus(),
};
}
}
@@ -0,0 +1,140 @@
import { type CommandMessage, type CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import type { Client } from "discord.js-selfbot-v13";
import { createModerationAction } from "../message-capture/messageStore.js";
// ---------------------------------------------------------------------------
// ModerationHandler
// ---------------------------------------------------------------------------
export class ModerationHandler {
private logger = createChildLogger("moderation-handler");
constructor(private client: Client | null) {}
setClient(client: Client): void {
this.client = client;
}
async handleModerationAction(
cmd: CommandMessage,
): Promise<CommandReply<unknown>> {
const payload = cmd.payload as {
message_id?: string;
user_id?: string;
guild_id?: string;
channel_id?: string;
action_type?: string;
reason?: string;
executed_by?: string;
};
if (
!payload.message_id ||
!payload.user_id ||
!payload.guild_id ||
!payload.action_type
) {
return {
id: cmd.id,
success: false,
data: null,
error: "message_id, user_id, guild_id, and action_type are required",
};
}
const validActions = [
"delete_message",
"mute_user",
"warn_user",
"kick_user",
"ban_user",
] as const;
if (
!validActions.includes(
payload.action_type as (typeof validActions)[number],
)
) {
return {
id: cmd.id,
success: false,
data: null,
error: `Invalid action_type: ${payload.action_type}. Must be one of: ${validActions.join(", ")}`,
};
}
try {
// For delete_message, also actually delete via Discord if client is available
if (payload.action_type === "delete_message" && this.client) {
try {
const channelId = String(cmd.payload.channel_id ?? "");
if (channelId) {
const channel = await this.client.channels.fetch(channelId);
if (channel?.isText()) {
const msg = await channel.messages
.fetch(payload.message_id)
.catch(() => null);
if (msg) {
await msg.delete().catch((err: unknown) => {
this.logger.warn(
{ error: err, messageId: payload.message_id },
"Failed to delete message via Discord",
);
});
}
}
}
} catch (err) {
this.logger.warn(
{ error: err, messageId: payload.message_id },
"Failed to fetch channel/message for deletion",
);
}
}
const action = await createModerationAction({
message_id: payload.message_id,
user_id: payload.user_id,
guild_id: payload.guild_id,
action_type: payload.action_type as
| "delete_message"
| "mute_user"
| "warn_user"
| "kick_user"
| "ban_user",
reason: payload.reason ?? null,
executed_by: payload.executed_by ?? "command-handler",
status: "executed",
error: null,
executed_at: Date.now(),
});
this.logger.info(
{
actionId: action.id,
actionType: payload.action_type,
userId: payload.user_id,
},
"Moderation action executed",
);
return {
id: cmd.id,
success: true,
data: action,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.logger.error(
{ error: message, commandId: cmd.id },
"Failed to execute moderation action",
);
return {
id: cmd.id,
success: false,
data: null,
error: message,
};
}
}
}
@@ -0,0 +1,174 @@
import { type CommandMessage, type CommandReply } from "@bete/shared";
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 { discordPlayer } from "../voice-recording/player.js";
import { voiceTransmitter } from "../voice-recording/transmitter.js";
import type { VoiceController } from "../voice-recording/voiceController.js";
// ---------------------------------------------------------------------------
// VoiceHandler
// ---------------------------------------------------------------------------
export class VoiceHandler {
private logger = createChildLogger("voice-handler");
constructor(
private client: Client | null,
private voiceController: VoiceController | null,
) {}
setClient(client: Client): void {
this.client = client;
}
setVoiceController(voiceController: VoiceController): void {
this.voiceController = voiceController;
}
async handleVoiceConnect(
cmd: CommandMessage,
): Promise<CommandReply<unknown>> {
if (!this.client || !this.voiceController) {
return {
id: cmd.id,
success: false,
data: null,
error: "Gateway not initialized",
};
}
const guildId = String(cmd.payload.guildId ?? "");
const channelId = String(cmd.payload.channelId ?? "");
if (!guildId || !channelId) {
return {
id: cmd.id,
success: false,
data: null,
error: "guildId and channelId are required",
};
}
const status = await this.voiceController.connect(guildId, channelId);
return { id: cmd.id, success: true, data: status };
}
async handleVoiceDisconnect(
cmd: CommandMessage,
): Promise<CommandReply<unknown>> {
if (!this.voiceController) {
return {
id: cmd.id,
success: false,
data: null,
error: "Gateway not initialized",
};
}
const status = await this.voiceController.disconnect();
return { id: cmd.id, success: true, data: status };
}
async handleVoiceChannels(
cmd: CommandMessage,
): Promise<CommandReply<unknown>> {
if (!this.client) {
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",
};
}
try {
const guild = await this.client.guilds.fetch(guildId);
const channels = await guild.channels.fetch();
const voiceChannels = channels
.filter((c) => c?.type === "GUILD_VOICE")
.map((c) => ({
id: c.id,
name: c.name,
type: "voice" as const,
}));
return { id: cmd.id, success: true, data: voiceChannels };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { id: cmd.id, success: false, data: null, error: msg };
}
}
async handleVoiceTransmitStart(
cmd: CommandMessage,
): Promise<CommandReply<unknown>> {
if (!discordPlayer.isConnected()) {
return {
id: cmd.id,
success: false,
data: null,
error: "Not connected to voice channel",
};
}
try {
// Create a new Redis connection for the transmitter
const transmitRedis = new Redis(config.REDIS_URL);
await voiceTransmitter.start(transmitRedis);
const status = voiceTransmitter.getStatus();
this.logger.info({ status }, "Voice transmit started");
return {
id: cmd.id,
success: true,
data: status,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.logger.error({ error: message }, "Failed to start voice transmit");
return {
id: cmd.id,
success: false,
data: null,
error: message,
};
}
}
async handleVoiceTransmitStop(
cmd: CommandMessage,
): Promise<CommandReply<unknown>> {
try {
await voiceTransmitter.stop();
this.logger.info("Voice transmit stopped");
return {
id: cmd.id,
success: true,
data: { status: "stopped" },
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.logger.error({ error: message }, "Failed to stop voice transmit");
return {
id: cmd.id,
success: false,
data: null,
error: message,
};
}
}
}