Update README and moderation commands for localization and migration support

- Revised README to include database migration steps and a new section for database management.
- Enhanced moderation command responses to utilize localized messages for unknown commands.
- Refactored moderation commands to use centralized command data definitions, improving maintainability and consistency.
- Added localization support for command descriptions and options in both German and English.
- Versioned Prisma migrations for better tracking of database changes.
This commit is contained in:
smueller
2026-07-22 12:06:16 +02:00
parent 405b70bdfc
commit a44f4d6641
12 changed files with 784 additions and 164 deletions

View File

@@ -0,0 +1,128 @@
-- CreateTable
CREATE TABLE "Guild" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Guild_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GuildSettings" (
"id" TEXT NOT NULL,
"guildId" TEXT NOT NULL,
"locale" TEXT NOT NULL DEFAULT 'de',
"moderationEnabled" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "GuildSettings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Case" (
"id" TEXT NOT NULL,
"caseNumber" INTEGER NOT NULL,
"guildId" TEXT NOT NULL,
"targetUserId" TEXT NOT NULL,
"moderatorId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"reason" TEXT,
"metadata" JSONB,
"deletedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Case_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Warning" (
"id" TEXT NOT NULL,
"guildId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"moderatorId" TEXT NOT NULL,
"reason" TEXT,
"caseId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Warning_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ModNote" (
"id" TEXT NOT NULL,
"guildId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"moderatorId" TEXT NOT NULL,
"note" TEXT NOT NULL,
"caseId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ModNote_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EscalationRule" (
"id" TEXT NOT NULL,
"guildId" TEXT NOT NULL,
"warnCount" INTEGER NOT NULL,
"action" TEXT NOT NULL,
"durationMs" INTEGER,
"reason" TEXT,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EscalationRule_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "GuildSettings_guildId_key" ON "GuildSettings"("guildId");
-- CreateIndex
CREATE INDEX "Case_guildId_targetUserId_idx" ON "Case"("guildId", "targetUserId");
-- CreateIndex
CREATE UNIQUE INDEX "Case_guildId_caseNumber_key" ON "Case"("guildId", "caseNumber");
-- CreateIndex
CREATE INDEX "Warning_guildId_userId_idx" ON "Warning"("guildId", "userId");
-- CreateIndex
CREATE INDEX "ModNote_guildId_userId_idx" ON "ModNote"("guildId", "userId");
-- CreateIndex
CREATE INDEX "EscalationRule_guildId_enabled_idx" ON "EscalationRule"("guildId", "enabled");
-- CreateIndex
CREATE UNIQUE INDEX "EscalationRule_guildId_warnCount_key" ON "EscalationRule"("guildId", "warnCount");
-- AddForeignKey
ALTER TABLE "GuildSettings" ADD CONSTRAINT "GuildSettings_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Case" ADD CONSTRAINT "Case_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Warning" ADD CONSTRAINT "Warning_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Warning" ADD CONSTRAINT "Warning_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "Case"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "Case"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EscalationRule" ADD CONSTRAINT "EscalationRule_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@@ -1,6 +1,8 @@
import { REST, Routes, type ChatInputCommandInteraction } from 'discord.js';
import { t } from '@nexumi/shared';
import { env } from './env.js';
import { logger } from './logger.js';
import { getGuildLocale } from './i18n.js';
import { moderationCommands } from './modules/moderation/commands.js';
import type { BotContext, SlashCommand } from './types.js';
@@ -17,8 +19,9 @@ export async function registerCommands() {
export async function routeCommand(interaction: ChatInputCommandInteraction, context: BotContext) {
const command = map.get(interaction.commandName);
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!command) {
await interaction.reply({ content: 'Unknown command.', ephemeral: true });
await interaction.reply({ content: t(locale, 'generic.unknownCommand'), ephemeral: true });
return;
}
await command.execute(interaction, context);

View File

@@ -0,0 +1,213 @@
import {
SlashCommandBuilder,
type SlashCommandStringOption,
type SlashCommandUserOption
} from 'discord.js';
import {
applyCommandDescription,
applyOptionDescription,
localizedChoice,
type CommandLocaleKey
} from '@nexumi/shared';
function userOpt(
o: SlashCommandUserOption,
key: CommandLocaleKey = 'moderation.common.options.user'
) {
return applyOptionDescription(o.setName('user').setRequired(true), key);
}
function reasonOpt(o: SlashCommandStringOption, required = false) {
const option = o.setName('reason');
if (required) {
option.setRequired(true);
}
return applyOptionDescription(option, 'moderation.common.options.reason');
}
export const banCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('ban'),
'moderation.ban.description'
)
.addUserOption((o) => userOpt(o, 'moderation.ban.options.user'))
.addStringOption((o) => reasonOpt(o))
.addStringOption((o) =>
applyOptionDescription(o.setName('duration'), 'moderation.ban.options.duration')
);
export const unbanCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('unban'),
'moderation.unban.description'
)
.addStringOption((o) =>
applyOptionDescription(o.setName('user_id').setRequired(true), 'moderation.unban.options.user_id')
)
.addStringOption((o) => reasonOpt(o));
export const kickCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('kick'),
'moderation.kick.description'
)
.addUserOption((o) => userOpt(o, 'moderation.ban.options.user'))
.addStringOption((o) => reasonOpt(o));
export const timeoutCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('timeout'),
'moderation.timeout.description'
)
.addUserOption((o) => userOpt(o, 'moderation.ban.options.user'))
.addStringOption((o) =>
applyOptionDescription(o.setName('duration').setRequired(true), 'moderation.timeout.options.duration')
)
.addStringOption((o) => reasonOpt(o));
export const untimeoutCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('untimeout'),
'moderation.untimeout.description'
)
.addUserOption((o) => userOpt(o, 'moderation.ban.options.user'))
.addStringOption((o) => reasonOpt(o));
export const warnCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('warn'),
'moderation.warn.description'
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('add'), 'moderation.warn.add.description')
.addUserOption((o) => userOpt(o, 'moderation.common.options.target'))
.addStringOption((o) => reasonOpt(o, true))
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('list'), 'moderation.warn.list.description').addUserOption((o) =>
userOpt(o, 'moderation.common.options.target')
)
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('remove'), 'moderation.warn.remove.description').addStringOption(
(o) =>
applyOptionDescription(o.setName('warning_id').setRequired(true), 'moderation.warn.options.warning_id')
)
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('clear'), 'moderation.warn.clear.description').addUserOption((o) =>
userOpt(o, 'moderation.common.options.target')
)
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('escalation-set'), 'moderation.warn.escalation_set.description')
.addIntegerOption((o) =>
applyOptionDescription(o.setName('count').setRequired(true).setMinValue(1).setMaxValue(50), 'moderation.warn.options.count')
)
.addStringOption((o) =>
applyOptionDescription(o.setName('action').setRequired(true), 'moderation.warn.options.action').addChoices(
localizedChoice('moderation.choice.timeout', 'TIMEOUT'),
localizedChoice('moderation.choice.kick', 'KICK'),
localizedChoice('moderation.choice.ban', 'BAN')
)
)
.addStringOption((o) =>
applyOptionDescription(o.setName('duration'), 'moderation.warn.options.escalation_duration')
)
.addStringOption((o) =>
applyOptionDescription(o.setName('reason'), 'moderation.warn.options.escalation_reason')
)
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('escalation-list'), 'moderation.warn.escalation_list.description')
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('escalation-remove'), 'moderation.warn.escalation_remove.description').addIntegerOption(
(o) =>
applyOptionDescription(o.setName('count').setRequired(true).setMinValue(1).setMaxValue(50), 'moderation.warn.options.count')
)
);
export const purgeCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('purge'),
'moderation.purge.description'
)
.addIntegerOption((o) =>
applyOptionDescription(o.setName('amount').setRequired(true).setMinValue(2).setMaxValue(100), 'moderation.purge.options.amount')
)
.addUserOption((o) => applyOptionDescription(o.setName('user'), 'moderation.purge.options.user'))
.addBooleanOption((o) => applyOptionDescription(o.setName('bots'), 'moderation.purge.options.bots'))
.addBooleanOption((o) => applyOptionDescription(o.setName('links'), 'moderation.purge.options.links'))
.addBooleanOption((o) =>
applyOptionDescription(o.setName('attachments'), 'moderation.purge.options.attachments')
)
.addStringOption((o) => applyOptionDescription(o.setName('regex'), 'moderation.purge.options.regex'));
export const slowmodeCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('slowmode'),
'moderation.slowmode.description'
).addIntegerOption((o) =>
applyOptionDescription(o.setName('seconds').setRequired(true).setMinValue(0).setMaxValue(21600), 'moderation.slowmode.options.seconds')
);
export const lockCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('lock'),
'moderation.lock.description'
);
export const unlockCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('unlock'),
'moderation.unlock.description'
);
export const nickCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('nick'),
'moderation.nick.description'
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('set'), 'moderation.nick.set.description')
.addUserOption((o) => userOpt(o, 'moderation.common.options.target'))
.addStringOption((o) =>
applyOptionDescription(o.setName('nickname').setRequired(true), 'moderation.nick.options.nickname')
)
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('reset'), 'moderation.nick.reset.description').addUserOption((o) =>
userOpt(o, 'moderation.common.options.target')
)
);
export const caseCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('case'),
'moderation.case.description'
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('view'), 'moderation.case.view.description').addIntegerOption((o) =>
applyOptionDescription(o.setName('id').setRequired(true), 'moderation.case.options.id')
)
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('edit'), 'moderation.case.edit.description')
.addIntegerOption((o) =>
applyOptionDescription(o.setName('id').setRequired(true), 'moderation.case.options.id')
)
.addStringOption((o) =>
applyOptionDescription(o.setName('reason').setRequired(true), 'moderation.case.options.reason')
)
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('delete'), 'moderation.case.delete.description').addIntegerOption((o) =>
applyOptionDescription(o.setName('id').setRequired(true), 'moderation.case.options.id')
)
);
export const modNoteCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('modnote'),
'moderation.modnote.description'
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('add'), 'moderation.modnote.add.description')
.addUserOption((o) => userOpt(o, 'moderation.common.options.target'))
.addStringOption((o) =>
applyOptionDescription(o.setName('note').setRequired(true), 'moderation.modnote.options.note')
)
)
.addSubcommand((s) =>
applyCommandDescription(s.setName('list'), 'moderation.modnote.list.description').addUserOption((o) =>
userOpt(o, 'moderation.common.options.target')
)
);

View File

@@ -1,7 +1,6 @@
import {
ChannelType,
PermissionFlagsBits,
SlashCommandBuilder,
TextChannel,
type ChatInputCommandInteraction,
type GuildMember
@@ -13,6 +12,21 @@ import { applyWarnEscalation, createCase } from './service.js';
import { parseDuration } from './duration.js';
import { getGuildLocale } from '../../i18n.js';
import { promptDestructiveConfirmation } from './confirmations.js';
import {
banCommandData,
caseCommandData,
kickCommandData,
lockCommandData,
modNoteCommandData,
nickCommandData,
purgeCommandData,
slowmodeCommandData,
timeoutCommandData,
unbanCommandData,
unlockCommandData,
untimeoutCommandData,
warnCommandData
} from './command-definitions.js';
function reasonFrom(i: ChatInputCommandInteraction, locale: 'de' | 'en'): string {
return i.options.getString('reason') ?? t(locale, 'moderation.reason.none');
@@ -38,12 +52,7 @@ async function ensureMod(
}
const banCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Ban a user')
.addUserOption((opt) => opt.setName('user').setDescription('Target user').setRequired(true))
.addStringOption((opt) => opt.setName('reason').setDescription('Reason'))
.addStringOption((opt) => opt.setName('duration').setDescription('Optional temp-ban duration (e.g. 7d)')),
data: banCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.BanMembers, locale))) return;
@@ -62,11 +71,7 @@ const banCommand: SlashCommand = {
};
const unbanCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('unban')
.setDescription('Unban a user')
.addStringOption((opt) => opt.setName('user_id').setDescription('Discord user id').setRequired(true))
.addStringOption((opt) => opt.setName('reason').setDescription('Reason')),
data: unbanCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.BanMembers, locale))) return;
@@ -86,11 +91,7 @@ const unbanCommand: SlashCommand = {
};
const kickCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('kick')
.setDescription('Kick a user')
.addUserOption((opt) => opt.setName('user').setDescription('Target user').setRequired(true))
.addStringOption((opt) => opt.setName('reason').setDescription('Reason')),
data: kickCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.KickMembers, locale))) return;
@@ -111,14 +112,7 @@ const kickCommand: SlashCommand = {
};
const timeoutCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('timeout')
.setDescription('Timeout a user')
.addUserOption((opt) => opt.setName('user').setDescription('Target user').setRequired(true))
.addStringOption((opt) =>
opt.setName('duration').setDescription('Duration (e.g. 15m)').setRequired(true)
)
.addStringOption((opt) => opt.setName('reason').setDescription('Reason')),
data: timeoutCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers, locale))) return;
@@ -141,11 +135,7 @@ const timeoutCommand: SlashCommand = {
};
const untimeoutCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('untimeout')
.setDescription('Remove timeout from a user')
.addUserOption((opt) => opt.setName('user').setDescription('Target user').setRequired(true))
.addStringOption((opt) => opt.setName('reason').setDescription('Reason')),
data: untimeoutCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers, locale))) return;
@@ -166,60 +156,7 @@ const untimeoutCommand: SlashCommand = {
};
const warnCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('warn')
.setDescription('Manage user warnings')
.addSubcommand((s) =>
s
.setName('add')
.setDescription('Add warning')
.addUserOption((o) => o.setName('user').setDescription('Target').setRequired(true))
.addStringOption((o) => o.setName('reason').setDescription('Reason').setRequired(true))
)
.addSubcommand((s) =>
s
.setName('list')
.setDescription('List warnings')
.addUserOption((o) => o.setName('user').setDescription('Target').setRequired(true))
)
.addSubcommand((s) =>
s
.setName('remove')
.setDescription('Remove warning')
.addStringOption((o) => o.setName('warning_id').setDescription('Warning ID').setRequired(true))
)
.addSubcommand((s) =>
s
.setName('clear')
.setDescription('Clear warnings for user')
.addUserOption((o) => o.setName('user').setDescription('Target').setRequired(true))
)
.addSubcommand((s) =>
s
.setName('escalation-set')
.setDescription('Create or update escalation rule')
.addIntegerOption((o) => o.setName('count').setDescription('Warning count threshold').setRequired(true).setMinValue(1).setMaxValue(50))
.addStringOption((o) =>
o
.setName('action')
.setDescription('Escalation action')
.setRequired(true)
.addChoices(
{ name: 'timeout', value: 'TIMEOUT' },
{ name: 'kick', value: 'KICK' },
{ name: 'ban', value: 'BAN' }
)
)
.addStringOption((o) => o.setName('duration').setDescription('Timeout duration (required for TIMEOUT)'))
.addStringOption((o) => o.setName('reason').setDescription('Optional escalation reason'))
)
.addSubcommand((s) => s.setName('escalation-list').setDescription('List escalation rules'))
.addSubcommand((s) =>
s
.setName('escalation-remove')
.setDescription('Remove escalation rule')
.addIntegerOption((o) => o.setName('count').setDescription('Warning count threshold').setRequired(true).setMinValue(1).setMaxValue(50))
),
data: warnCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers, locale))) return;
@@ -308,6 +245,7 @@ const warnCommand: SlashCommand = {
guildId: interaction.guildId!,
targetUserId: user.id,
moderatorId: interaction.user.id,
...auditFrom(interaction),
action: 'WARN_ADD',
reason
});
@@ -367,6 +305,7 @@ const warnCommand: SlashCommand = {
guildId: interaction.guildId!,
targetUserId: warning.userId,
moderatorId: interaction.user.id,
...auditFrom(interaction),
action: 'WARN_REMOVE',
reason: `Warning ${warningId} removed`
});
@@ -389,21 +328,7 @@ const warnCommand: SlashCommand = {
};
const purgeCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('purge')
.setDescription('Bulk delete messages')
.addIntegerOption((o) =>
o.setName('amount').setDescription('2-100').setRequired(true).setMinValue(2).setMaxValue(100)
)
.addUserOption((o) => o.setName('user').setDescription('Only purge this user messages'))
.addBooleanOption((o) => o.setName('bots').setDescription('Only purge bot messages'))
.addBooleanOption((o) => o.setName('links').setDescription('Only purge messages containing links'))
.addBooleanOption((o) =>
o.setName('attachments').setDescription('Only purge messages with attachments')
)
.addStringOption((o) =>
o.setName('regex').setDescription('Only purge messages matching regex')
),
data: purgeCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageMessages, locale))) return;
@@ -443,10 +368,7 @@ const purgeCommand: SlashCommand = {
};
const slowmodeCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('slowmode')
.setDescription('Set channel slowmode')
.addIntegerOption((o) => o.setName('seconds').setDescription('0-21600').setRequired(true).setMinValue(0).setMaxValue(21600)),
data: slowmodeCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageChannels, locale))) return;
@@ -467,7 +389,7 @@ const slowmodeCommand: SlashCommand = {
};
const lockCommand: SlashCommand = {
data: new SlashCommandBuilder().setName('lock').setDescription('Lock current text channel'),
data: lockCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageChannels, locale))) return;
@@ -489,7 +411,7 @@ const lockCommand: SlashCommand = {
};
const unlockCommand: SlashCommand = {
data: new SlashCommandBuilder().setName('unlock').setDescription('Unlock current text channel'),
data: unlockCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageChannels, locale))) return;
@@ -511,22 +433,7 @@ const unlockCommand: SlashCommand = {
};
const nickCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('nick')
.setDescription('Manage nicknames')
.addSubcommand((s) =>
s
.setName('set')
.setDescription('Set nickname')
.addUserOption((o) => o.setName('user').setDescription('Target').setRequired(true))
.addStringOption((o) => o.setName('nickname').setDescription('New nickname').setRequired(true))
)
.addSubcommand((s) =>
s
.setName('reset')
.setDescription('Reset nickname')
.addUserOption((o) => o.setName('user').setDescription('Target').setRequired(true))
),
data: nickCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageNicknames, locale))) return;
@@ -540,6 +447,7 @@ const nickCommand: SlashCommand = {
guildId: interaction.guildId!,
targetUserId: user.id,
moderatorId: interaction.user.id,
...auditFrom(interaction),
action: 'NICK_SET',
reason: nickname
});
@@ -549,6 +457,7 @@ const nickCommand: SlashCommand = {
guildId: interaction.guildId!,
targetUserId: user.id,
moderatorId: interaction.user.id,
...auditFrom(interaction),
action: 'NICK_RESET',
reason: 'Nickname reset'
});
@@ -558,22 +467,7 @@ const nickCommand: SlashCommand = {
};
const caseCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('case')
.setDescription('Manage moderation cases')
.addSubcommand((s) =>
s.setName('view').setDescription('View case').addIntegerOption((o) => o.setName('id').setDescription('Case number').setRequired(true))
)
.addSubcommand((s) =>
s
.setName('edit')
.setDescription('Edit case reason')
.addIntegerOption((o) => o.setName('id').setDescription('Case number').setRequired(true))
.addStringOption((o) => o.setName('reason').setDescription('New reason').setRequired(true))
)
.addSubcommand((s) =>
s.setName('delete').setDescription('Soft-delete case').addIntegerOption((o) => o.setName('id').setDescription('Case number').setRequired(true))
),
data: caseCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers, locale))) return;
@@ -606,22 +500,7 @@ const caseCommand: SlashCommand = {
};
const modNoteCommand: SlashCommand = {
data: new SlashCommandBuilder()
.setName('modnote')
.setDescription('Manage mod notes')
.addSubcommand((s) =>
s
.setName('add')
.setDescription('Add note')
.addUserOption((o) => o.setName('user').setDescription('Target').setRequired(true))
.addStringOption((o) => o.setName('note').setDescription('Note').setRequired(true))
)
.addSubcommand((s) =>
s
.setName('list')
.setDescription('List notes')
.addUserOption((o) => o.setName('user').setDescription('Target').setRequired(true))
),
data: modNoteCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers, locale))) return;
@@ -642,6 +521,7 @@ const modNoteCommand: SlashCommand = {
guildId: interaction.guildId!,
targetUserId: user.id,
moderatorId: interaction.user.id,
...auditFrom(interaction),
action: 'MODNOTE_ADD',
reason: note
});