fix: resolve architecture disconnects and codebase weaknesses
- Add TEXT_CHANNEL_ID and TEXT_GUILD_ID to config schema (fix silent channel monitoring) - Remove dead files: message-capture/broadcaster.ts, voice-recording/index.ts - Fix WebSocket voice_command payload to forward from frontend - Implement moderation:action handler in commandHandler - Fix useMascotChat to use canonical request() wrapper - Fix useAudioPlayback userId hash collision (use string not parseInt) - Add catch blocks to useMediaControl.skip/stop - Add typed broadcast functions (messageAnalyzed, voicePcmData, voiceActiveUser) - Apply Biome formatting and lint fixes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
30c9e86edc
commit
3a7b005d95
@@ -2,6 +2,7 @@ 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";
|
||||
@@ -155,6 +156,9 @@ export class CommandHandler {
|
||||
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 = {
|
||||
@@ -437,6 +441,128 @@ export class CommandHandler {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import type { WebSocket } from "ws";
|
||||
import type {
|
||||
AnalysisQueueStatus,
|
||||
AttachmentRecord,
|
||||
MessageRecord,
|
||||
ModerationWsEvent,
|
||||
} from "../message-capture/types.js";
|
||||
import type { MediaState } from "../voice-recording/mediaTypes.js";
|
||||
|
||||
export type BroadcasterClient = Pick<WebSocket, "readyState" | "send">;
|
||||
|
||||
const log = createChildLogger("broadcaster");
|
||||
|
||||
function sendJson(
|
||||
clients: Set<BroadcasterClient>,
|
||||
event: ModerationWsEvent,
|
||||
): void {
|
||||
const payload = JSON.stringify({ ...event, timestamp: Date.now() });
|
||||
for (const client of clients) {
|
||||
if (client.readyState === 1) {
|
||||
try {
|
||||
client.send(payload);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
{ error, eventType: event.type },
|
||||
"Failed to send event to client",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createBroadcaster() {
|
||||
const clients = new Set<BroadcasterClient>();
|
||||
|
||||
return {
|
||||
addClient(client: BroadcasterClient) {
|
||||
clients.add(client);
|
||||
log.debug({ clientCount: clients.size }, "Client added");
|
||||
},
|
||||
removeClient(client: BroadcasterClient) {
|
||||
clients.delete(client);
|
||||
log.debug({ clientCount: clients.size }, "Client removed");
|
||||
},
|
||||
clientCount() {
|
||||
return clients.size;
|
||||
},
|
||||
getClients() {
|
||||
return Array.from(clients);
|
||||
},
|
||||
uiState(state: unknown) {
|
||||
sendJson(clients, { type: "ui_state", state });
|
||||
},
|
||||
userState(users: unknown[]) {
|
||||
sendJson(clients, { type: "user_state", users });
|
||||
},
|
||||
messageCreated(data: MessageRecord) {
|
||||
sendJson(clients, { type: "message_created", data });
|
||||
},
|
||||
messageUpdated(data: Partial<MessageRecord> & { id: string }) {
|
||||
sendJson(clients, { type: "message_updated", data });
|
||||
},
|
||||
messageDeleted(data: { id: string; deleted_at: number }) {
|
||||
sendJson(clients, { type: "message_deleted", data });
|
||||
},
|
||||
messageAnalyzed(data: MessageRecord) {
|
||||
sendJson(clients, { type: "message_analyzed", data });
|
||||
},
|
||||
attachmentCreated(data: AttachmentRecord) {
|
||||
sendJson(clients, { type: "attachment_created", data });
|
||||
},
|
||||
analysisQueueStatus(data: AnalysisQueueStatus) {
|
||||
sendJson(clients, { type: "analysis_queue_status", data });
|
||||
},
|
||||
mediaState(state: MediaState) {
|
||||
sendJson(clients, { type: "media_state", state });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type ModerationBroadcaster = ReturnType<typeof createBroadcaster>;
|
||||
@@ -1,5 +0,0 @@
|
||||
export { OpusDecoder } from "./recorder/decoder.js";
|
||||
export { SegmentManager } from "./recorder/segment.js";
|
||||
export { startRecording, stopRecording } from "./recorder.js";
|
||||
export { voiceTransmitter } from "./transmitter.js";
|
||||
export { VoiceController } from "./voiceController.js";
|
||||
Reference in New Issue
Block a user