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:
@@ -1,11 +1,17 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import { PermissionFlagsBits } from 'discord.js';
|
||||
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
|
||||
import { basename, join } from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { redis } from './redis.js';
|
||||
import { logger } from './logger.js';
|
||||
import type { BotContext } from './types.js';
|
||||
import { env } from './env.js';
|
||||
|
||||
export const moderationQueueName = 'moderation';
|
||||
export const moderationQueue = new Queue(moderationQueueName, { connection: redis });
|
||||
export const backupQueueName = 'backups';
|
||||
export const backupQueue = new Queue(backupQueueName, { connection: redis });
|
||||
|
||||
type TempBanJob = {
|
||||
guildId: string;
|
||||
@@ -14,7 +20,7 @@ type TempBanJob = {
|
||||
};
|
||||
|
||||
export function startWorkers(context: BotContext): Worker[] {
|
||||
const worker = new Worker<TempBanJob>(
|
||||
const moderationWorker = new Worker<TempBanJob>(
|
||||
moderationQueueName,
|
||||
async (job) => {
|
||||
if (job.name !== 'tempBanExpire') {
|
||||
@@ -31,9 +37,81 @@ export function startWorkers(context: BotContext): Worker[] {
|
||||
{ connection: redis }
|
||||
);
|
||||
|
||||
worker.on('failed', (job, error) => {
|
||||
moderationWorker.on('failed', (job, error) => {
|
||||
logger.error({ jobId: job?.id, error }, 'Moderation job failed');
|
||||
});
|
||||
|
||||
return [worker];
|
||||
const backupWorker = new Worker(
|
||||
backupQueueName,
|
||||
async (job) => {
|
||||
if (job.name !== 'dailyPgDump') {
|
||||
return;
|
||||
}
|
||||
await runPgDumpBackup();
|
||||
},
|
||||
{ connection: redis }
|
||||
);
|
||||
|
||||
backupWorker.on('failed', (job, error) => {
|
||||
logger.error({ jobId: job?.id, error }, 'Backup job failed');
|
||||
});
|
||||
|
||||
return [moderationWorker, backupWorker];
|
||||
}
|
||||
|
||||
export async function ensureRecurringJobs(): Promise<void> {
|
||||
await backupQueue.add(
|
||||
'dailyPgDump',
|
||||
{},
|
||||
{
|
||||
repeat: { pattern: env.BACKUP_CRON },
|
||||
removeOnComplete: 100,
|
||||
removeOnFail: 100
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function runPgDumpBackup(): Promise<void> {
|
||||
await mkdir(env.BACKUP_DIR, { recursive: true });
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `nexumi-${timestamp}.sql`;
|
||||
const outputPath = join(env.BACKUP_DIR, filename);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const dump = spawn('pg_dump', ['--dbname', env.DATABASE_URL, '--file', outputPath], {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
let stderr = '';
|
||||
dump.stderr.on('data', (chunk: Buffer) => {
|
||||
stderr += chunk.toString('utf8');
|
||||
});
|
||||
dump.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
reject(new Error(stderr || `pg_dump exited with code ${code ?? 'unknown'}`));
|
||||
});
|
||||
});
|
||||
|
||||
await cleanupOldBackups();
|
||||
logger.info({ backupFile: basename(outputPath) }, 'Database backup finished');
|
||||
}
|
||||
|
||||
async function cleanupOldBackups(): Promise<void> {
|
||||
const files = await readdir(env.BACKUP_DIR, { withFileTypes: true });
|
||||
const cutoff = Date.now() - env.BACKUP_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||
await Promise.all(
|
||||
files
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.sql'))
|
||||
.map(async (entry) => {
|
||||
const fullPath = join(env.BACKUP_DIR, entry.name);
|
||||
const fileStat = await stat(fullPath);
|
||||
const created = fileStat.mtime.getTime();
|
||||
if (Number.isNaN(created) || created >= cutoff) {
|
||||
return;
|
||||
}
|
||||
await rm(fullPath, { force: true });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user