feat: expand AI moderation with structured analysis, review workflow, and guardrails

- Add structured AI moderation fields (categories, severity, confidence,
  recommended_action, policy_version, evidence) to messages table
- Add moderation_reviews, moderation_actions, and retention_policies tables
- Upgrade LLM response parsing to support structured metadata with backwards
  compatibility for legacy responses
- Implement public AI evaluation review UI with decision controls
  (approve, false positive + reanalyze, escalate)
- Add auto-delete guardrails requiring high confidence, severity, and
  allowed categories; log all attempts to moderation_actions
- Add retention manager scaffolding for messages/attachments/voice
- Add action executor for moderation actions (mute, warn, kick, ban)
- Add review routes: GET/POST/PATCH /api/reviews, GET/POST/PATCH /api/actions
- Preserve auth separation: voice/media/recordings gated, review public

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-05-30 01:02:51 +07:00
co-authored by Claude Opus 4.6
parent b938420eb3
commit c894e5cd75
15 changed files with 2104 additions and 27 deletions
+303
View File
@@ -0,0 +1,303 @@
import type { Client, Guild, User } from "discord.js-selfbot-v13";
import { createChildLogger } from "../logger.js";
import {
getModerationAction,
updateModerationAction,
} from "./messageStore.js";
import type { ModerationAction, ModerationActionType } from "./types.js";
const logger = createChildLogger("action-executor");
interface ActionExecutionContext {
client: Client;
guildId: string;
}
/**
* Executes a moderation action (delete message, mute user, etc.)
*/
export async function executeModerationAction(
action: ModerationAction,
context: ActionExecutionContext,
): Promise<void> {
try {
const guild = await context.client.guilds.fetch(context.guildId);
if (!guild) {
throw new Error(`Guild ${context.guildId} not found`);
}
switch (action.action_type) {
case "delete_message":
await executeDeleteMessage(action, guild);
break;
case "mute_user":
await executeMuteUser(action, guild);
break;
case "warn_user":
await executeWarnUser(action, guild);
break;
case "kick_user":
await executeKickUser(action, guild);
break;
case "ban_user":
await executeBanUser(action, guild);
break;
default:
throw new Error(`Unknown action type: ${action.action_type}`);
}
// Mark action as executed
await updateModerationAction(action.id, {
status: "executed",
executed_at: Date.now(),
error: null,
});
logger.info(
{
actionId: action.id,
actionType: action.action_type,
guildId: context.guildId,
},
"Moderation action executed successfully",
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Mark action as failed
await updateModerationAction(action.id, {
status: "failed",
error: errorMessage,
});
logger.error(
{
actionId: action.id,
actionType: action.action_type,
guildId: context.guildId,
error: errorMessage,
},
"Failed to execute moderation action",
);
throw error;
}
}
async function executeDeleteMessage(
action: ModerationAction,
guild: Guild,
): Promise<void> {
if (!action.message_id) {
throw new Error("message_id is required for delete_message action");
}
// Note: Discord.js selfbot cannot delete messages from other users
// This is a placeholder for the intended behavior
logger.warn(
{ messageId: action.message_id },
"Delete message action requires manual execution or bot permissions",
);
}
async function executeMuteUser(
action: ModerationAction,
guild: Guild,
): Promise<void> {
if (!action.user_id) {
throw new Error("user_id is required for mute_user action");
}
try {
const member = await guild.members.fetch(action.user_id);
if (!member) {
throw new Error(`Member ${action.user_id} not found in guild`);
}
// Mute by removing speak permission in all voice channels
const voiceChannels = guild.channels.cache.filter(
(ch) => ch.type === "GUILD_VOICE",
);
for (const [, channel] of voiceChannels) {
await channel.permissionOverwrites.create(member, {
SPEAK: false,
});
}
logger.info(
{ userId: action.user_id, guildId: guild.id },
"User muted in all voice channels",
);
} catch (error) {
throw new Error(
`Failed to mute user: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async function executeWarnUser(
action: ModerationAction,
guild: Guild,
): Promise<void> {
if (!action.user_id) {
throw new Error("user_id is required for warn_user action");
}
try {
const user = await guild.client.users.fetch(action.user_id);
if (!user) {
throw new Error(`User ${action.user_id} not found`);
}
const reason = action.reason || "Warned by moderation system";
await user.send(
`You have been warned in ${guild.name}. Reason: ${reason}`,
);
logger.info(
{ userId: action.user_id, guildId: guild.id },
"User warned via DM",
);
} catch (error) {
logger.warn(
{
userId: action.user_id,
guildId: guild.id,
error: error instanceof Error ? error.message : String(error),
},
"Failed to send warning DM to user",
);
// Don't throw - warning DM failure is not critical
}
}
async function executeKickUser(
action: ModerationAction,
guild: Guild,
): Promise<void> {
if (!action.user_id) {
throw new Error("user_id is required for kick_user action");
}
try {
const member = await guild.members.fetch(action.user_id);
if (!member) {
throw new Error(`Member ${action.user_id} not found in guild`);
}
const reason = action.reason || "Kicked by moderation system";
await member.kick(reason);
logger.info(
{ userId: action.user_id, guildId: guild.id },
"User kicked from guild",
);
} catch (error) {
throw new Error(
`Failed to kick user: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async function executeBanUser(
action: ModerationAction,
guild: Guild,
): Promise<void> {
if (!action.user_id) {
throw new Error("user_id is required for ban_user action");
}
try {
const reason = action.reason || "Banned by moderation system";
await guild.bans.create(action.user_id, { reason });
logger.info(
{ userId: action.user_id, guildId: guild.id },
"User banned from guild",
);
} catch (error) {
throw new Error(
`Failed to ban user: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Processes pending moderation actions for a guild
*/
export async function processPendingActions(
guildId: string,
context: ActionExecutionContext,
): Promise<{ processed: number; failed: number }> {
const result = { processed: 0, failed: 0 };
try {
const { listModerationActions } = await import("./messageStore.js");
const { data: actions } = await listModerationActions({
guildId,
status: ["pending"],
limit: 100,
});
for (const action of actions) {
try {
await executeModerationAction(action, context);
result.processed++;
} catch (error) {
result.failed++;
logger.error(
{
actionId: action.id,
error: error instanceof Error ? error.message : String(error),
},
"Failed to process pending action",
);
}
}
logger.info(
{ guildId, ...result },
"Processed pending moderation actions",
);
return result;
} catch (error) {
logger.error(
{
guildId,
error: error instanceof Error ? error.message : String(error),
},
"Failed to process pending actions",
);
throw error;
}
}
/**
* Starts a periodic action processor
*/
export function startActionProcessor(
client: Client,
guildId: string,
intervalMs: number = 60 * 1000, // 1 minute
): NodeJS.Timeout {
logger.info({ guildId, intervalMs }, "Starting action processor");
const interval = setInterval(async () => {
try {
await processPendingActions(guildId, { client, guildId });
} catch (error) {
logger.error(
{
guildId,
error: error instanceof Error ? error.message : String(error),
},
"Action processor failed",
);
}
}, intervalMs);
return interval;
}