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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user