refactor: resolve architecture disconnects and codebase weaknesses
- Consolidate eventTypes.ts as single source of truth for Redis channels: - Remove duplicate DiscordGatewayEvent interface from eventBroadcaster.ts - Replace all hardcoded channel strings with EventChannels constants - eventTypes.ts is no longer an orphan file - Remove dangerous moderation action feature (selfbot safety): - Remove /messages/:id/moderate endpoint from backend - Remove moderation:action handler from commandHandler.ts - Remove publishFireAndForgetCommand (wrong envelope format) - Verified no remaining references to moderation:action in code - Remove unused getCommandPublisher import from redis-bridge.ts Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3614d32701
commit
7d6612bb2d
@@ -124,64 +124,5 @@ export function createMessagesRouter(): Router {
|
||||
}),
|
||||
);
|
||||
|
||||
// POST /api/messages/:id/moderate — Trigger moderation action via DG
|
||||
router.post(
|
||||
"/messages/:id/moderate",
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const id = String(req.params.id ?? "");
|
||||
if (!id) {
|
||||
res.status(400).json({ error: "MISSING_ID" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { actionType, reason } = (req.body ?? {}) as {
|
||||
actionType?: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
const allowedActions = [
|
||||
"delete_message",
|
||||
"warn_user",
|
||||
"kick_user",
|
||||
"ban_user",
|
||||
"mute_user",
|
||||
];
|
||||
|
||||
if (!actionType || !allowedActions.includes(actionType)) {
|
||||
res.status(400).json({
|
||||
error: "INVALID_ACTION",
|
||||
message: `actionType must be one of: ${allowedActions.join(", ")}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the message to get guild/user context
|
||||
const msg = await messagesService.getMessageById(id).catch(() => null);
|
||||
if (!msg) {
|
||||
res.status(404).json({ error: "MESSAGE_NOT_FOUND" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Publish command to DG via Redis
|
||||
const { publishCommand } = await import("../../ws/redis-bridge.js");
|
||||
await publishCommand({
|
||||
id: crypto.randomUUID(),
|
||||
type: "moderation:action",
|
||||
payload: {
|
||||
messageId: id,
|
||||
guildId: msg.guild_id,
|
||||
channelId: msg.thread_id || msg.channel_id,
|
||||
userId: msg.user_id,
|
||||
actionType,
|
||||
reason: reason ?? "Manual moderation from dashboard",
|
||||
requestedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info({ id, actionType, reason }, "Moderation action dispatched");
|
||||
res.json({ ok: true, actionType, messageId: id });
|
||||
}),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import Redis from "ioredis";
|
||||
import { config } from "../shared/config/index.js";
|
||||
import { getCommandPublisher } from "../shared/redis/index.js";
|
||||
import { broadcastRaw } from "./broadcast.js";
|
||||
|
||||
const logger = createChildLogger("ws.redis-bridge");
|
||||
@@ -35,24 +34,6 @@ function createSubscriber(): Redis {
|
||||
return new Redis(config.REDIS_URL, { keyPrefix: "" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a command to the Discord Gateway via Redis.
|
||||
* The DG's commandHandler listens on "backend:command" channel.
|
||||
*/
|
||||
export async function publishCommand(
|
||||
payload: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const pub = getCommandPublisher();
|
||||
const envelope = {
|
||||
type: "command",
|
||||
data: payload,
|
||||
timestamp: Date.now(),
|
||||
source: "backend",
|
||||
};
|
||||
await pub.publish("backend:command", JSON.stringify(envelope));
|
||||
logger.debug({ payload }, "Published command to DG");
|
||||
}
|
||||
|
||||
function handleSubscriptionMessage(channel: string, message: string): void {
|
||||
const mapping = SUBSCRIPTIONS.find((m) => m.channel === channel);
|
||||
if (!mapping) {
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import type { CustomLogger } from "@bete/shared/logger";
|
||||
import Redis from "ioredis";
|
||||
|
||||
export interface DiscordGatewayEvent {
|
||||
type: string;
|
||||
data: unknown;
|
||||
timestamp: number;
|
||||
source: string;
|
||||
}
|
||||
import { type DiscordGatewayEvent, EventChannels } from "./eventTypes.js";
|
||||
|
||||
export class RedisEventPublisher {
|
||||
private redis: Redis;
|
||||
@@ -49,7 +43,7 @@ export class EventBroadcaster {
|
||||
}
|
||||
|
||||
async messageCreated(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:message:created", {
|
||||
await this.publisher.publish(EventChannels.MESSAGE_CREATED, {
|
||||
type: "message_created",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
@@ -58,7 +52,7 @@ export class EventBroadcaster {
|
||||
}
|
||||
|
||||
async messageUpdated(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:message:updated", {
|
||||
await this.publisher.publish(EventChannels.MESSAGE_UPDATED, {
|
||||
type: "message_updated",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
@@ -67,7 +61,7 @@ export class EventBroadcaster {
|
||||
}
|
||||
|
||||
async messageDeleted(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:message:deleted", {
|
||||
await this.publisher.publish(EventChannels.MESSAGE_DELETED, {
|
||||
type: "message_deleted",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
@@ -76,7 +70,7 @@ export class EventBroadcaster {
|
||||
}
|
||||
|
||||
async messageAnalyzed(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:message:analyzed", {
|
||||
await this.publisher.publish(EventChannels.MESSAGE_ANALYZED, {
|
||||
type: "message_analyzed",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
@@ -85,7 +79,7 @@ export class EventBroadcaster {
|
||||
}
|
||||
|
||||
async attachmentCreated(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:attachment:created", {
|
||||
await this.publisher.publish(EventChannels.ATTACHMENT_CREATED, {
|
||||
type: "attachment_created",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
@@ -94,7 +88,7 @@ export class EventBroadcaster {
|
||||
}
|
||||
|
||||
async attachmentUploaded(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:attachment:uploaded", {
|
||||
await this.publisher.publish(EventChannels.ATTACHMENT_UPLOADED, {
|
||||
type: "attachment_uploaded",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
@@ -103,7 +97,7 @@ export class EventBroadcaster {
|
||||
}
|
||||
|
||||
async voiceRecordingStarted(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:voice:started", {
|
||||
await this.publisher.publish(EventChannels.VOICE_STARTED, {
|
||||
type: "voice_recording_started",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
@@ -112,7 +106,7 @@ export class EventBroadcaster {
|
||||
}
|
||||
|
||||
async voiceRecordingStopped(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:voice:stopped", {
|
||||
await this.publisher.publish(EventChannels.VOICE_STOPPED, {
|
||||
type: "voice_recording_stopped",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
@@ -121,7 +115,7 @@ export class EventBroadcaster {
|
||||
}
|
||||
|
||||
async voiceRecordingUploaded(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:voice:uploaded", {
|
||||
await this.publisher.publish(EventChannels.VOICE_UPLOADED, {
|
||||
type: "voice_recording_uploaded",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
@@ -140,7 +134,7 @@ export class EventBroadcaster {
|
||||
userId: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
await this.publisher.publish("discord:voice:pcm", {
|
||||
await this.publisher.publish(EventChannels.VOICE_PCM, {
|
||||
type: "voice_pcm_data",
|
||||
data: {
|
||||
userId,
|
||||
@@ -161,7 +155,7 @@ export class EventBroadcaster {
|
||||
userId: string,
|
||||
data: { username: string; avatar: string; speaking: boolean },
|
||||
): Promise<void> {
|
||||
await this.publisher.publish("discord:voice:active_user", {
|
||||
await this.publisher.publish(EventChannels.VOICE_ACTIVE_USER, {
|
||||
type: "voice_active_user",
|
||||
data: {
|
||||
userId,
|
||||
@@ -173,7 +167,7 @@ export class EventBroadcaster {
|
||||
}
|
||||
|
||||
async analysisQueueStatus(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:analysis:queue_status", {
|
||||
await this.publisher.publish(EventChannels.ANALYSIS_QUEUE_STATUS, {
|
||||
type: "analysis_queue_status",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
|
||||
@@ -152,12 +152,12 @@ export function useDashboardSocket(handlers: WsHandlers) {
|
||||
onUserState: (u) => handlersRef.current.onUserState?.(u),
|
||||
onUiState: (s) => handlersRef.current.onUiState?.(s),
|
||||
onMediaState: (s) => handlersRef.current.onMediaState?.(s),
|
||||
onVoiceRecordingUploaded: (d) =>
|
||||
handlersRef.current.onVoiceRecordingUploaded?.(d),
|
||||
onVoiceRecordingStarted: (d) =>
|
||||
handlersRef.current.onVoiceRecordingStarted?.(d),
|
||||
onVoiceRecordingStopped: (d) =>
|
||||
handlersRef.current.onVoiceRecordingStopped?.(d),
|
||||
onVoiceRecordingUploaded: (d) =>
|
||||
handlersRef.current.onVoiceRecordingUploaded?.(d),
|
||||
onVoicePcmData: (d) => handlersRef.current.onVoicePcmData?.(d),
|
||||
onVoiceActiveUser: (d) => handlersRef.current.onVoiceActiveUser?.(d),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user