- Added a `leaveAnnounceBan` field to the `WelcomeConfig` model, allowing for optional announcements in the leave channel when a member is banned. - Updated the leave handling logic to check for bans and send a notice in the leave channel if the `leaveAnnounceBan` toggle is enabled. - Enhanced moderation actions (kick and ban) to notify the affected user via DM with the reason for the action. - Introduced a new `notifyModerationTargetDm` function to streamline DM notifications for moderation actions. - Updated the WebUI to include a toggle for the `leaveAnnounceBan` feature, improving user configurability. - Improved localization for new messages related to moderation notifications and leave announcements in both English and German. - Documented changes in phase tracking to reflect the new leave announcement feature and moderation notification enhancements.
685 lines
23 KiB
TypeScript
685 lines
23 KiB
TypeScript
import {
|
|
ChannelType,
|
|
PermissionFlagsBits,
|
|
TextChannel,
|
|
type ChatInputCommandInteraction
|
|
} from 'discord.js';
|
|
import { t, tf } from '@nexumi/shared';
|
|
import type { SlashCommand } from '../../types.js';
|
|
import { requirePermission } from '../../permissions.js';
|
|
import { assertModerableMember } from '../../hierarchy.js';
|
|
import { applyWarnEscalation, createCase, createWarning, notifyModerationTargetDm } from './service.js';
|
|
import { parseDuration, parseTimeoutDuration, tryParseDuration } 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');
|
|
}
|
|
|
|
function auditFrom(interaction: ChatInputCommandInteraction) {
|
|
return {
|
|
channelId: interaction.channelId ?? undefined,
|
|
source: 'slash_command' as const
|
|
};
|
|
}
|
|
|
|
async function ensureMod(
|
|
interaction: ChatInputCommandInteraction,
|
|
permission: bigint,
|
|
locale: 'de' | 'en'
|
|
): Promise<boolean> {
|
|
if (!interaction.inGuild()) {
|
|
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
|
|
return false;
|
|
}
|
|
return requirePermission(interaction, permission, locale, { botPermissions: permission });
|
|
}
|
|
|
|
async function replySuccessCase(
|
|
interaction: ChatInputCommandInteraction,
|
|
locale: 'de' | 'en',
|
|
caseNumber: number,
|
|
reason?: string
|
|
): Promise<void> {
|
|
const content = reason
|
|
? tf(locale, 'moderation.success.caseWithReason', { caseNumber, reason })
|
|
: tf(locale, 'moderation.success.case', { caseNumber });
|
|
if (interaction.deferred || interaction.replied) {
|
|
await interaction.editReply({ content });
|
|
return;
|
|
}
|
|
await interaction.reply({
|
|
content,
|
|
ephemeral: true
|
|
});
|
|
}
|
|
|
|
const banCommand: SlashCommand = {
|
|
data: banCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.BanMembers, locale))) return;
|
|
const user = interaction.options.getUser('user', true);
|
|
const reason = reasonFrom(interaction, locale);
|
|
const duration = interaction.options.getString('duration');
|
|
|
|
if (duration) {
|
|
const durationMs = tryParseDuration(duration);
|
|
if (durationMs === null || durationMs <= 0) {
|
|
await interaction.reply({
|
|
content: t(locale, 'moderation.duration.invalid'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
const hierarchy = await assertModerableMember(interaction, user.id, locale, 'ban');
|
|
if (!hierarchy.ok) return;
|
|
|
|
await promptDestructiveConfirmation(interaction, context, locale, {
|
|
action: 'ban',
|
|
payload: {
|
|
userId: user.id,
|
|
reason,
|
|
duration
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const unbanCommand: SlashCommand = {
|
|
data: unbanCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.BanMembers, locale))) return;
|
|
const userId = interaction.options.getString('user_id', true);
|
|
const reason = reasonFrom(interaction, locale);
|
|
await interaction.guild!.members.unban(userId, reason);
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: userId,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'UNBAN',
|
|
reason
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber, reason);
|
|
}
|
|
};
|
|
|
|
const kickCommand: SlashCommand = {
|
|
data: kickCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.KickMembers, locale))) return;
|
|
const user = interaction.options.getUser('user', true);
|
|
const reason = reasonFrom(interaction, locale);
|
|
const hierarchy = await assertModerableMember(interaction, user.id, locale, 'kick');
|
|
if (!hierarchy.ok || !hierarchy.member) return;
|
|
await notifyModerationTargetDm({
|
|
user,
|
|
guildName: interaction.guild!.name,
|
|
reason,
|
|
locale,
|
|
action: 'kick'
|
|
});
|
|
await hierarchy.member.kick(reason);
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'KICK',
|
|
reason
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber, reason);
|
|
}
|
|
};
|
|
|
|
const timeoutCommand: SlashCommand = {
|
|
data: timeoutCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers, locale))) return;
|
|
const user = interaction.options.getUser('user', true);
|
|
let duration: number;
|
|
try {
|
|
duration = parseTimeoutDuration(interaction.options.getString('duration', true));
|
|
} catch {
|
|
await interaction.reply({
|
|
content: t(locale, 'moderation.duration.invalidTimeout'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
const reason = reasonFrom(interaction, locale);
|
|
const hierarchy = await assertModerableMember(interaction, user.id, locale, 'timeout');
|
|
if (!hierarchy.ok || !hierarchy.member) return;
|
|
await hierarchy.member.timeout(duration, reason);
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'TIMEOUT',
|
|
reason,
|
|
metadata: { durationMs: duration }
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber, reason);
|
|
}
|
|
};
|
|
|
|
const untimeoutCommand: SlashCommand = {
|
|
data: untimeoutCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers, locale))) return;
|
|
const user = interaction.options.getUser('user', true);
|
|
const reason = reasonFrom(interaction, locale);
|
|
const hierarchy = await assertModerableMember(interaction, user.id, locale, 'timeout');
|
|
if (!hierarchy.ok || !hierarchy.member) return;
|
|
await hierarchy.member.timeout(null, reason);
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'UN_TIMEOUT',
|
|
reason
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber, reason);
|
|
}
|
|
};
|
|
|
|
const warnCommand: SlashCommand = {
|
|
data: warnCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers, locale))) return;
|
|
const sub = interaction.options.getSubcommand();
|
|
if (sub === 'escalation-set') {
|
|
const warnCount = interaction.options.getInteger('count', true);
|
|
const action = interaction.options.getString('action', true);
|
|
const durationInput = interaction.options.getString('duration');
|
|
const reason = interaction.options.getString('reason');
|
|
let durationMs: number | null = null;
|
|
if (durationInput) {
|
|
try {
|
|
durationMs =
|
|
action === 'TIMEOUT' ? parseTimeoutDuration(durationInput) : parseDuration(durationInput);
|
|
} catch {
|
|
await interaction.reply({
|
|
content: t(locale, 'moderation.duration.invalid'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
if (action === 'TIMEOUT' && !durationMs) {
|
|
await interaction.reply({
|
|
content: t(locale, 'moderation.escalation.durationRequired'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
await context.prisma.escalationRule.upsert({
|
|
where: {
|
|
guildId_warnCount: {
|
|
guildId: interaction.guildId!,
|
|
warnCount
|
|
}
|
|
},
|
|
update: {
|
|
action,
|
|
durationMs,
|
|
reason,
|
|
enabled: true
|
|
},
|
|
create: {
|
|
guildId: interaction.guildId!,
|
|
warnCount,
|
|
action,
|
|
durationMs,
|
|
reason,
|
|
enabled: true
|
|
}
|
|
});
|
|
const durationText = durationMs ? ` (${durationMs} ms)` : '';
|
|
await interaction.reply({
|
|
content: tf(locale, 'moderation.escalation.saved', {
|
|
warnCount,
|
|
action,
|
|
duration: durationText
|
|
}),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (sub === 'escalation-list') {
|
|
const rules = await context.prisma.escalationRule.findMany({
|
|
where: { guildId: interaction.guildId!, enabled: true },
|
|
orderBy: { warnCount: 'asc' }
|
|
});
|
|
const content =
|
|
rules.length === 0
|
|
? t(locale, 'moderation.escalation.noneConfigured')
|
|
: rules
|
|
.map(
|
|
(rule) =>
|
|
`- ${rule.warnCount} warnings => ${rule.action}${rule.durationMs ? ` (${rule.durationMs} ms)` : ''}${rule.reason ? ` | ${rule.reason}` : ''}`
|
|
)
|
|
.join('\n');
|
|
await interaction.reply({ content, ephemeral: true });
|
|
return;
|
|
}
|
|
|
|
if (sub === 'escalation-remove') {
|
|
const warnCount = interaction.options.getInteger('count', true);
|
|
await context.prisma.escalationRule.delete({
|
|
where: { guildId_warnCount: { guildId: interaction.guildId!, warnCount } }
|
|
});
|
|
await interaction.reply({
|
|
content: tf(locale, 'moderation.escalation.removed', { warnCount }),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (sub === 'add') {
|
|
const user = interaction.options.getUser('user', true);
|
|
const reason = interaction.options.getString('reason', true);
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'WARN_ADD',
|
|
reason
|
|
});
|
|
const { warning } = await createWarning(context, {
|
|
guildId: interaction.guildId!,
|
|
userId: user.id,
|
|
moderatorId: interaction.user.id,
|
|
reason,
|
|
caseId: modCase.id,
|
|
guildName: interaction.guild!.name,
|
|
locale,
|
|
notifyUser: user
|
|
});
|
|
const escalationResult = await applyWarnEscalation(
|
|
interaction,
|
|
context,
|
|
user.id,
|
|
interaction.user.id,
|
|
locale
|
|
);
|
|
await interaction.reply({
|
|
content: escalationResult
|
|
? `${tf(locale, 'moderation.warning.created', { warningNumber: warning.warningNumber })}\n${escalationResult}`
|
|
: tf(locale, 'moderation.warning.created', { warningNumber: warning.warningNumber }),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (sub === 'list') {
|
|
const user = interaction.options.getUser('user', true);
|
|
const warnings = await context.prisma.warning.findMany({
|
|
where: { guildId: interaction.guildId!, userId: user.id },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 20
|
|
});
|
|
const content =
|
|
warnings.length === 0
|
|
? t(locale, 'moderation.warning.none')
|
|
: warnings
|
|
.map(
|
|
(w: { warningNumber: number; reason: string | null }) =>
|
|
`- #${w.warningNumber}: ${w.reason ?? t(locale, 'moderation.warning.defaultReason')}`
|
|
)
|
|
.join('\n');
|
|
await interaction.reply({ content, ephemeral: true });
|
|
return;
|
|
}
|
|
|
|
if (sub === 'remove') {
|
|
const warningNumber = interaction.options.getInteger('id', true);
|
|
const warning = await context.prisma.warning.findFirst({
|
|
where: { warningNumber, guildId: interaction.guildId! }
|
|
});
|
|
if (!warning) {
|
|
await interaction.reply({
|
|
content: t(locale, 'moderation.warning.notFound'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
await context.prisma.warning.delete({ where: { id: warning.id } });
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: warning.userId,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'WARN_REMOVE',
|
|
reason: `Warning #${warningNumber} removed`
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
|
return;
|
|
}
|
|
|
|
const user = interaction.options.getUser('user', true);
|
|
await context.prisma.warning.deleteMany({
|
|
where: { guildId: interaction.guildId!, userId: user.id }
|
|
});
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'WARN_CLEAR',
|
|
reason: 'Warnings cleared'
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
|
}
|
|
};
|
|
|
|
const purgeCommand: SlashCommand = {
|
|
data: purgeCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageMessages, locale))) return;
|
|
const amount = interaction.options.getInteger('amount', true);
|
|
const targetUser = interaction.options.getUser('user');
|
|
const botsOnly = interaction.options.getBoolean('bots') ?? false;
|
|
const linksOnly = interaction.options.getBoolean('links') ?? false;
|
|
const attachmentsOnly = interaction.options.getBoolean('attachments') ?? false;
|
|
const regexInput = interaction.options.getString('regex');
|
|
if (regexInput) {
|
|
try {
|
|
new RegExp(regexInput, 'i');
|
|
} catch {
|
|
await interaction.reply({ content: t(locale, 'generic.invalidRegex'), ephemeral: true });
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!(interaction.channel instanceof TextChannel)) {
|
|
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
|
|
return;
|
|
}
|
|
|
|
await promptDestructiveConfirmation(interaction, context, locale, {
|
|
action: 'purge',
|
|
payload: {
|
|
channelId: interaction.channel.id,
|
|
amount,
|
|
targetUserId: targetUser?.id ?? null,
|
|
botsOnly,
|
|
linksOnly,
|
|
attachmentsOnly,
|
|
regex: regexInput
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const slowmodeCommand: SlashCommand = {
|
|
data: slowmodeCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageChannels, locale))) return;
|
|
const seconds = interaction.options.getInteger('seconds', true);
|
|
if (interaction.channel instanceof TextChannel) {
|
|
await interaction.channel.setRateLimitPerUser(seconds);
|
|
}
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: interaction.user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'SLOWMODE',
|
|
reason: `Set slowmode to ${seconds}s`
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
|
}
|
|
};
|
|
|
|
const lockCommand: SlashCommand = {
|
|
data: lockCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageChannels, locale))) return;
|
|
const serverWide = interaction.options.getBoolean('server') ?? false;
|
|
const guild = interaction.guild!;
|
|
|
|
if (serverWide) {
|
|
await interaction.deferReply({ ephemeral: true });
|
|
for (const channel of guild.channels.cache.values()) {
|
|
if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) {
|
|
continue;
|
|
}
|
|
await channel.permissionOverwrites.edit(guild.roles.everyone, {
|
|
SendMessages: false
|
|
});
|
|
}
|
|
} else if (interaction.channel?.type === ChannelType.GuildText) {
|
|
await interaction.channel.permissionOverwrites.edit(guild.roles.everyone, {
|
|
SendMessages: false
|
|
});
|
|
}
|
|
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: interaction.user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'LOCK',
|
|
reason: serverWide ? 'Server locked' : 'Channel locked'
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
|
}
|
|
};
|
|
|
|
const unlockCommand: SlashCommand = {
|
|
data: unlockCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageChannels, locale))) return;
|
|
const serverWide = interaction.options.getBoolean('server') ?? false;
|
|
const guild = interaction.guild!;
|
|
|
|
if (serverWide) {
|
|
await interaction.deferReply({ ephemeral: true });
|
|
for (const channel of guild.channels.cache.values()) {
|
|
if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) {
|
|
continue;
|
|
}
|
|
await channel.permissionOverwrites.edit(guild.roles.everyone, {
|
|
SendMessages: null
|
|
});
|
|
}
|
|
} else if (interaction.channel?.type === ChannelType.GuildText) {
|
|
await interaction.channel.permissionOverwrites.edit(guild.roles.everyone, {
|
|
SendMessages: null
|
|
});
|
|
}
|
|
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: interaction.user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'UNLOCK',
|
|
reason: serverWide ? 'Server unlocked' : 'Channel unlocked'
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
|
}
|
|
};
|
|
|
|
const nickCommand: SlashCommand = {
|
|
data: nickCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageNicknames, locale))) return;
|
|
const sub = interaction.options.getSubcommand();
|
|
const user = interaction.options.getUser('user', true);
|
|
const hierarchy = await assertModerableMember(interaction, user.id, locale, 'nick');
|
|
if (!hierarchy.ok || !hierarchy.member) return;
|
|
const member = hierarchy.member;
|
|
if (sub === 'set') {
|
|
const nickname = interaction.options.getString('nickname', true);
|
|
await member.setNickname(nickname);
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'NICK_SET',
|
|
reason: nickname
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
|
} else {
|
|
await member.setNickname(null);
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'NICK_RESET',
|
|
reason: 'Nickname reset'
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
|
}
|
|
}
|
|
};
|
|
|
|
const caseCommand: SlashCommand = {
|
|
data: caseCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers, locale))) return;
|
|
const sub = interaction.options.getSubcommand();
|
|
const caseNumber = interaction.options.getInteger('id', true);
|
|
if (sub === 'view') {
|
|
const found = await context.prisma.case.findFirst({
|
|
where: {
|
|
guildId: interaction.guildId!,
|
|
caseNumber,
|
|
deletedAt: null
|
|
}
|
|
});
|
|
await interaction.reply({
|
|
content: found ? JSON.stringify(found, null, 2) : t(locale, 'moderation.case.notFound'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
if (sub === 'edit') {
|
|
const reason = interaction.options.getString('reason', true);
|
|
const existing = await context.prisma.case.findFirst({
|
|
where: {
|
|
guildId: interaction.guildId!,
|
|
caseNumber,
|
|
deletedAt: null
|
|
}
|
|
});
|
|
if (!existing) {
|
|
await interaction.reply({
|
|
content: t(locale, 'moderation.case.notFound'),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
await context.prisma.case.update({
|
|
where: { id: existing.id },
|
|
data: { reason }
|
|
});
|
|
await interaction.reply({
|
|
content: tf(locale, 'moderation.success.case', { caseNumber }),
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
await promptDestructiveConfirmation(interaction, context, locale, {
|
|
action: 'case_delete',
|
|
payload: { caseNumber }
|
|
});
|
|
}
|
|
};
|
|
|
|
const modNoteCommand: SlashCommand = {
|
|
data: modNoteCommandData,
|
|
async execute(interaction, context) {
|
|
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
|
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers, locale))) return;
|
|
const sub = interaction.options.getSubcommand();
|
|
const user = interaction.options.getUser('user', true);
|
|
await context.prisma.user.upsert({ where: { id: user.id }, update: {}, create: { id: user.id } });
|
|
if (sub === 'add') {
|
|
const note = interaction.options.getString('note', true);
|
|
await context.prisma.modNote.create({
|
|
data: {
|
|
guildId: interaction.guildId!,
|
|
userId: user.id,
|
|
moderatorId: interaction.user.id,
|
|
note
|
|
}
|
|
});
|
|
const modCase = await createCase(context, {
|
|
guildId: interaction.guildId!,
|
|
targetUserId: user.id,
|
|
moderatorId: interaction.user.id,
|
|
...auditFrom(interaction),
|
|
action: 'MODNOTE_ADD',
|
|
reason: note
|
|
});
|
|
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
|
return;
|
|
}
|
|
const notes = await context.prisma.modNote.findMany({
|
|
where: { guildId: interaction.guildId!, userId: user.id },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 20
|
|
});
|
|
const content =
|
|
notes.length === 0
|
|
? t(locale, 'moderation.note.none')
|
|
: notes.map((n: { note: string }) => `- ${n.note}`).join('\n');
|
|
await interaction.reply({ content, ephemeral: true });
|
|
}
|
|
};
|
|
|
|
export const moderationCommands: SlashCommand[] = [
|
|
banCommand,
|
|
unbanCommand,
|
|
kickCommand,
|
|
timeoutCommand,
|
|
untimeoutCommand,
|
|
warnCommand,
|
|
purgeCommand,
|
|
slowmodeCommand,
|
|
lockCommand,
|
|
unlockCommand,
|
|
nickCommand,
|
|
caseCommand,
|
|
modNoteCommand
|
|
];
|