Add backup functionality with escalation rules for moderation commands

- Updated .env.example to include backup configuration options.
- Modified docker-compose.yml to mount backup volume.
- Enhanced Dockerfile to install PostgreSQL client.
- Added EscalationRule model to Prisma schema for managing warning escalation.
- Updated environment validation in env.ts to include backup settings.
- Refactored job handling in jobs.ts to implement backup jobs and cleanup.
- Introduced escalation commands in moderation service for managing user warnings.
- Updated moderation commands to apply escalation rules based on warning count.
This commit is contained in:
smueller
2026-07-22 11:16:39 +02:00
parent bc97d1d74c
commit dd3b132f73
11 changed files with 348 additions and 7 deletions

View File

@@ -7,9 +7,9 @@ import {
type GuildMember
} from 'discord.js';
import { t } from '@nexumi/shared';
import type { SlashCommand, BotContext } from '../../types.js';
import type { SlashCommand } from '../../types.js';
import { requirePermission } from '../../permissions.js';
import { createCase, scheduleTempBanExpire } from './service.js';
import { applyWarnEscalation, createCase, scheduleTempBanExpire } from './service.js';
import { parseDuration } from './duration.js';
function reasonFrom(i: ChatInputCommandInteraction): string {
@@ -175,10 +175,102 @@ const warnCommand: SlashCommand = {
.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))
),
async execute(interaction, context) {
if (!(await ensureMod(interaction, PermissionFlagsBits.ModerateMembers))) 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');
const durationMs = durationInput ? parseDuration(durationInput) : null;
if (action === 'TIMEOUT' && !durationMs) {
await interaction.reply({ content: 'Duration is required for TIMEOUT escalation.', 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
}
});
await interaction.reply({
content: `Escalation rule saved: ${warnCount} warnings => ${action}${durationMs ? ` (${durationMs} ms)` : ''}.`,
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
? 'No escalation rules configured.'
: 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: `Escalation rule removed for ${warnCount} warnings.`, ephemeral: true });
return;
}
if (sub === 'add') {
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason', true);
@@ -203,7 +295,18 @@ const warnCommand: SlashCommand = {
caseId: modCase.id
}
});
await interaction.reply({ content: `Warning created: ${warning.id}`, ephemeral: true });
const escalationResult = await applyWarnEscalation(
interaction,
context,
user.id,
interaction.user.id
);
await interaction.reply({
content: escalationResult
? `Warning created: ${warning.id}\n${escalationResult}`
: `Warning created: ${warning.id}`,
ephemeral: true
});
return;
}