diff --git a/src/commands/admin/case.ts b/src/commands/admin/case.ts new file mode 100644 index 0000000..b687f5e --- /dev/null +++ b/src/commands/admin/case.ts @@ -0,0 +1,54 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"; +import { ModerationService } from "@/modules/moderation/moderation.service"; +import { getCaseEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view"; + +export const moderationCase = createCommand({ + data: new SlashCommandBuilder() + .setName("case") + .setDescription("View details of a specific moderation case") + .addStringOption(option => + option + .setName("case_id") + .setDescription("The case ID (e.g., CASE-0001)") + .setRequired(true) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction) => { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const caseId = interaction.options.getString("case_id", true).toUpperCase(); + + // Validate case ID format + if (!caseId.match(/^CASE-\d+$/)) { + await interaction.editReply({ + embeds: [getModerationErrorEmbed("Invalid case ID format. Expected format: CASE-0001")] + }); + return; + } + + // Get the case + const moderationCase = await ModerationService.getCaseById(caseId); + + if (!moderationCase) { + await interaction.editReply({ + embeds: [getModerationErrorEmbed(`Case **${caseId}** not found.`)] + }); + return; + } + + // Display the case + await interaction.editReply({ + embeds: [getCaseEmbed(moderationCase)] + }); + + } catch (error) { + console.error("Case command error:", error); + await interaction.editReply({ + embeds: [getModerationErrorEmbed("An error occurred while fetching the case.")] + }); + } + } +}); diff --git a/src/commands/admin/cases.ts b/src/commands/admin/cases.ts new file mode 100644 index 0000000..cfbc0ca --- /dev/null +++ b/src/commands/admin/cases.ts @@ -0,0 +1,54 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"; +import { ModerationService } from "@/modules/moderation/moderation.service"; +import { getCasesListEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view"; + +export const cases = createCommand({ + data: new SlashCommandBuilder() + .setName("cases") + .setDescription("View all moderation cases for a user") + .addUserOption(option => + option + .setName("user") + .setDescription("The user to check cases for") + .setRequired(true) + ) + .addBooleanOption(option => + option + .setName("active_only") + .setDescription("Show only active cases (warnings)") + .setRequired(false) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction) => { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const targetUser = interaction.options.getUser("user", true); + const activeOnly = interaction.options.getBoolean("active_only") || false; + + // Get cases for the user + const userCases = await ModerationService.getUserCases(targetUser.id, activeOnly); + + const title = activeOnly + ? `⚠️ Active Cases for ${targetUser.username}` + : `📋 All Cases for ${targetUser.username}`; + + const description = userCases.length === 0 + ? undefined + : `Total cases: **${userCases.length}**`; + + // Display the cases + await interaction.editReply({ + embeds: [getCasesListEmbed(userCases, title, description)] + }); + + } catch (error) { + console.error("Cases command error:", error); + await interaction.editReply({ + embeds: [getModerationErrorEmbed("An error occurred while fetching cases.")] + }); + } + } +}); diff --git a/src/commands/admin/clearwarning.ts b/src/commands/admin/clearwarning.ts new file mode 100644 index 0000000..90c78ca --- /dev/null +++ b/src/commands/admin/clearwarning.ts @@ -0,0 +1,84 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"; +import { ModerationService } from "@/modules/moderation/moderation.service"; +import { getClearSuccessEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view"; + +export const clearwarning = createCommand({ + data: new SlashCommandBuilder() + .setName("clearwarning") + .setDescription("Clear/resolve a warning") + .addStringOption(option => + option + .setName("case_id") + .setDescription("The case ID to clear (e.g., CASE-0001)") + .setRequired(true) + ) + .addStringOption(option => + option + .setName("reason") + .setDescription("Reason for clearing the warning") + .setRequired(false) + .setMaxLength(500) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction) => { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const caseId = interaction.options.getString("case_id", true).toUpperCase(); + const reason = interaction.options.getString("reason") || "Cleared by moderator"; + + // Validate case ID format + if (!caseId.match(/^CASE-\d+$/)) { + await interaction.editReply({ + embeds: [getModerationErrorEmbed("Invalid case ID format. Expected format: CASE-0001")] + }); + return; + } + + // Check if case exists and is active + const existingCase = await ModerationService.getCaseById(caseId); + + if (!existingCase) { + await interaction.editReply({ + embeds: [getModerationErrorEmbed(`Case **${caseId}** not found.`)] + }); + return; + } + + if (!existingCase.active) { + await interaction.editReply({ + embeds: [getModerationErrorEmbed(`Case **${caseId}** is already resolved.`)] + }); + return; + } + + if (existingCase.type !== 'warn') { + await interaction.editReply({ + embeds: [getModerationErrorEmbed(`Case **${caseId}** is not a warning. Only warnings can be cleared.`)] + }); + return; + } + + // Clear the warning + await ModerationService.clearCase({ + caseId, + clearedBy: interaction.user.id, + clearedByName: interaction.user.username, + reason + }); + + // Send success message + await interaction.editReply({ + embeds: [getClearSuccessEmbed(caseId)] + }); + + } catch (error) { + console.error("Clear warning command error:", error); + await interaction.editReply({ + embeds: [getModerationErrorEmbed("An error occurred while clearing the warning.")] + }); + } + } +}); diff --git a/src/commands/admin/note.ts b/src/commands/admin/note.ts new file mode 100644 index 0000000..43d6335 --- /dev/null +++ b/src/commands/admin/note.ts @@ -0,0 +1,61 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"; +import { ModerationService } from "@/modules/moderation/moderation.service"; +import { getNoteSuccessEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view"; + +export const note = createCommand({ + data: new SlashCommandBuilder() + .setName("note") + .setDescription("Add a staff-only note about a user") + .addUserOption(option => + option + .setName("user") + .setDescription("The user to add a note for") + .setRequired(true) + ) + .addStringOption(option => + option + .setName("note") + .setDescription("The note to add") + .setRequired(true) + .setMaxLength(1000) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction) => { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const targetUser = interaction.options.getUser("user", true); + const noteText = interaction.options.getString("note", true); + + // Create the note case + const moderationCase = await ModerationService.createCase({ + type: 'note', + userId: targetUser.id, + username: targetUser.username, + moderatorId: interaction.user.id, + moderatorName: interaction.user.username, + reason: noteText, + }); + + if (!moderationCase) { + await interaction.editReply({ + embeds: [getModerationErrorEmbed("Failed to create note.")] + }); + return; + } + + // Send success message + await interaction.editReply({ + embeds: [getNoteSuccessEmbed(moderationCase.caseId, targetUser.username)] + }); + + } catch (error) { + console.error("Note command error:", error); + await interaction.editReply({ + embeds: [getModerationErrorEmbed("An error occurred while adding the note.")] + }); + } + } +}); diff --git a/src/commands/admin/notes.ts b/src/commands/admin/notes.ts new file mode 100644 index 0000000..41b7257 --- /dev/null +++ b/src/commands/admin/notes.ts @@ -0,0 +1,43 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"; +import { ModerationService } from "@/modules/moderation/moderation.service"; +import { getCasesListEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view"; + +export const notes = createCommand({ + data: new SlashCommandBuilder() + .setName("notes") + .setDescription("View all staff notes for a user") + .addUserOption(option => + option + .setName("user") + .setDescription("The user to check notes for") + .setRequired(true) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction) => { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const targetUser = interaction.options.getUser("user", true); + + // Get all notes for the user + const userNotes = await ModerationService.getUserNotes(targetUser.id); + + // Display the notes + await interaction.editReply({ + embeds: [getCasesListEmbed( + userNotes, + `📝 Staff Notes for ${targetUser.username}`, + userNotes.length === 0 ? undefined : `Total notes: **${userNotes.length}**` + )] + }); + + } catch (error) { + console.error("Notes command error:", error); + await interaction.editReply({ + embeds: [getModerationErrorEmbed("An error occurred while fetching notes.")] + }); + } + } +}); diff --git a/src/commands/admin/warn.ts b/src/commands/admin/warn.ts new file mode 100644 index 0000000..d6d4ca0 --- /dev/null +++ b/src/commands/admin/warn.ts @@ -0,0 +1,131 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"; +import { ModerationService } from "@/modules/moderation/moderation.service"; +import { + getWarnSuccessEmbed, + getModerationErrorEmbed, + getUserWarningEmbed +} from "@/modules/moderation/moderation.view"; +import { config } from "@/lib/config"; + +export const warn = createCommand({ + data: new SlashCommandBuilder() + .setName("warn") + .setDescription("Issue a warning to a user") + .addUserOption(option => + option + .setName("user") + .setDescription("The user to warn") + .setRequired(true) + ) + .addStringOption(option => + option + .setName("reason") + .setDescription("Reason for the warning") + .setRequired(true) + .setMaxLength(1000) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction) => { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const targetUser = interaction.options.getUser("user", true); + const reason = interaction.options.getString("reason", true); + + // Don't allow warning bots + if (targetUser.bot) { + await interaction.editReply({ + embeds: [getModerationErrorEmbed("You cannot warn bots.")] + }); + return; + } + + // Don't allow self-warnings + if (targetUser.id === interaction.user.id) { + await interaction.editReply({ + embeds: [getModerationErrorEmbed("You cannot warn yourself.")] + }); + return; + } + + // Create the warning case + const moderationCase = await ModerationService.createCase({ + type: 'warn', + userId: targetUser.id, + username: targetUser.username, + moderatorId: interaction.user.id, + moderatorName: interaction.user.username, + reason, + }); + + if (!moderationCase) { + await interaction.editReply({ + embeds: [getModerationErrorEmbed("Failed to create warning case.")] + }); + return; + } + + // Get total warning count for the user + const warningCount = await ModerationService.getActiveWarningCount(targetUser.id); + + // Send success message to moderator + await interaction.editReply({ + embeds: [getWarnSuccessEmbed(moderationCase.caseId, targetUser.username, reason)] + }); + + // Try to DM the user if configured + if (config.moderation.cases.dmOnWarn) { + try { + const serverName = interaction.guild?.name || 'this server'; + await targetUser.send({ + embeds: [getUserWarningEmbed(serverName, reason, moderationCase.caseId, warningCount)] + }); + } catch (error) { + // Silently fail if user has DMs disabled + console.log(`Could not DM warning to ${targetUser.username}: ${error}`); + } + } + + // Optional: Check for auto-timeout threshold + if (config.moderation.cases.autoTimeoutThreshold && + warningCount >= config.moderation.cases.autoTimeoutThreshold) { + + try { + const member = await interaction.guild?.members.fetch(targetUser.id); + if (member) { + // Auto-timeout for 24 hours (86400000 ms) + await member.timeout(86400000, `Automatic timeout: ${warningCount} warnings`); + + // Create a timeout case + await ModerationService.createCase({ + type: 'timeout', + userId: targetUser.id, + username: targetUser.username, + moderatorId: interaction.client.user!.id, + moderatorName: interaction.client.user!.username, + reason: `Automatic timeout: reached ${warningCount} warnings`, + metadata: { duration: '24h', automatic: true } + }); + + await interaction.followUp({ + embeds: [getModerationErrorEmbed( + `⚠️ User has reached ${warningCount} warnings and has been automatically timed out for 24 hours.` + )], + flags: MessageFlags.Ephemeral + }); + } + } catch (error) { + console.error('Failed to auto-timeout user:', error); + } + } + + } catch (error) { + console.error("Warn command error:", error); + await interaction.editReply({ + embeds: [getModerationErrorEmbed("An error occurred while issuing the warning.")] + }); + } + } +}); diff --git a/src/commands/admin/warnings.ts b/src/commands/admin/warnings.ts new file mode 100644 index 0000000..87662bc --- /dev/null +++ b/src/commands/admin/warnings.ts @@ -0,0 +1,39 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"; +import { ModerationService } from "@/modules/moderation/moderation.service"; +import { getWarningsEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view"; + +export const warnings = createCommand({ + data: new SlashCommandBuilder() + .setName("warnings") + .setDescription("View active warnings for a user") + .addUserOption(option => + option + .setName("user") + .setDescription("The user to check warnings for") + .setRequired(true) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction) => { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const targetUser = interaction.options.getUser("user", true); + + // Get active warnings for the user + const activeWarnings = await ModerationService.getUserWarnings(targetUser.id); + + // Display the warnings + await interaction.editReply({ + embeds: [getWarningsEmbed(activeWarnings, targetUser.username)] + }); + + } catch (error) { + console.error("Warnings command error:", error); + await interaction.editReply({ + embeds: [getModerationErrorEmbed("An error occurred while fetching warnings.")] + }); + } + } +}); diff --git a/src/modules/moderation/moderation.types.ts b/src/modules/moderation/moderation.types.ts index 82dd03b..98f3805 100644 --- a/src/modules/moderation/moderation.types.ts +++ b/src/modules/moderation/moderation.types.ts @@ -26,7 +26,7 @@ export interface ModerationCase { moderatorId: bigint; moderatorName: string; reason: string; - metadata: Record; + metadata: unknown; active: boolean; createdAt: Date; resolvedAt: Date | null;