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

@@ -1,5 +1,6 @@
import { moderationQueue } from '../../jobs.js';
import type { BotContext } from '../../types.js';
import { PermissionFlagsBits, type ChatInputCommandInteraction } from 'discord.js';
type CreateCaseInput = {
guildId: string;
@@ -11,6 +12,12 @@ type CreateCaseInput = {
};
export async function createCase(context: BotContext, input: CreateCaseInput) {
await context.prisma.guild.upsert({
where: { id: input.guildId },
update: {},
create: { id: input.guildId }
});
const lastCase = await context.prisma.case.findFirst({
where: { guildId: input.guildId },
orderBy: { caseNumber: 'desc' }
@@ -45,3 +52,79 @@ export async function scheduleTempBanExpire(
}
);
}
export async function applyWarnEscalation(
interaction: ChatInputCommandInteraction,
context: BotContext,
userId: string,
moderatorId: string
): Promise<string | null> {
const guildId = interaction.guildId;
if (!guildId || !interaction.guild) {
return null;
}
const warningCount = await context.prisma.warning.count({
where: { guildId, userId }
});
const rule = await context.prisma.escalationRule.findUnique({
where: { guildId_warnCount: { guildId, warnCount: warningCount } }
});
if (!rule || !rule.enabled) {
return null;
}
const member = await interaction.guild.members.fetch(userId);
const reason = rule.reason ?? `Auto escalation at ${warningCount} warnings`;
if (rule.action === 'TIMEOUT') {
if (!rule.durationMs) {
return `Escalation rule for ${warningCount} warns skipped (missing duration).`;
}
if (!interaction.guild.members.me?.permissions.has(PermissionFlagsBits.ModerateMembers)) {
return `Escalation rule for ${warningCount} warns skipped (missing bot permissions).`;
}
await member.timeout(rule.durationMs, reason);
await createCase(context, {
guildId,
targetUserId: userId,
moderatorId,
action: 'TIMEOUT',
reason,
metadata: { escalation: true, durationMs: rule.durationMs, warningCount }
});
return `Auto escalation applied: timeout (${rule.durationMs} ms).`;
}
if (rule.action === 'KICK') {
if (!interaction.guild.members.me?.permissions.has(PermissionFlagsBits.KickMembers)) {
return `Escalation rule for ${warningCount} warns skipped (missing bot permissions).`;
}
await member.kick(reason);
await createCase(context, {
guildId,
targetUserId: userId,
moderatorId,
action: 'KICK',
reason,
metadata: { escalation: true, warningCount }
});
return 'Auto escalation applied: kick.';
}
if (!interaction.guild.members.me?.permissions.has(PermissionFlagsBits.BanMembers)) {
return `Escalation rule for ${warningCount} warns skipped (missing bot permissions).`;
}
await interaction.guild.members.ban(userId, { reason });
await createCase(context, {
guildId,
targetUserId: userId,
moderatorId,
action: 'BAN',
reason,
metadata: { escalation: true, warningCount }
});
return 'Auto escalation applied: ban.';
}