Enhance bot functionality with new features and improvements
- Updated package dependencies to include `@sentry/node` for error tracking. - Refactored command routing to incorporate user and guild blacklisting checks, improving command execution safety. - Enhanced health server metrics to include queue waiting times, providing better insights into system performance. - Introduced confirmation handling for guild backup commands, streamlining user interactions. - Improved moderation commands with better error handling and case management, ensuring robust moderation capabilities. - Added new duration parsing functions to handle timeout durations effectively, enhancing moderation features. - Updated localization files to reflect new command structures and user guidance improvements.
This commit is contained in:
@@ -2,7 +2,8 @@ import { moderationQueue } from '../../queues.js';
|
||||
import type { BotContext } from '../../types.js';
|
||||
import { PermissionFlagsBits, type ChatInputCommandInteraction } from 'discord.js';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { buildCaseMetadata, type CaseAuditSource } from '@nexumi/shared';
|
||||
import { buildCaseMetadata, type CaseAuditSource, t, type Locale } from '@nexumi/shared';
|
||||
import { MAX_TIMEOUT_MS } from './duration.js';
|
||||
|
||||
type CreateCaseInput = {
|
||||
guildId: string;
|
||||
@@ -22,39 +23,69 @@ export async function createCase(context: BotContext, input: CreateCaseInput) {
|
||||
create: { id: input.guildId }
|
||||
});
|
||||
|
||||
const lastCase = await context.prisma.case.findFirst({
|
||||
where: { guildId: input.guildId },
|
||||
orderBy: { caseNumber: 'desc' }
|
||||
});
|
||||
|
||||
const metadata = buildCaseMetadata({
|
||||
source: input.source ?? 'slash_command',
|
||||
channelId: input.channelId,
|
||||
extras: input.metadata
|
||||
});
|
||||
|
||||
return context.prisma.case.create({
|
||||
data: {
|
||||
guildId: input.guildId,
|
||||
caseNumber: (lastCase?.caseNumber ?? 0) + 1,
|
||||
targetUserId: input.targetUserId,
|
||||
moderatorId: input.moderatorId,
|
||||
action: input.action,
|
||||
reason: input.reason,
|
||||
metadata: metadata as Prisma.InputJsonValue
|
||||
let created = null;
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
created = await context.prisma.$transaction(
|
||||
async (tx) => {
|
||||
const lastCase = await tx.case.findFirst({
|
||||
where: { guildId: input.guildId },
|
||||
orderBy: { caseNumber: 'desc' },
|
||||
select: { caseNumber: true }
|
||||
});
|
||||
return tx.case.create({
|
||||
data: {
|
||||
guildId: input.guildId,
|
||||
caseNumber: (lastCase?.caseNumber ?? 0) + 1,
|
||||
targetUserId: input.targetUserId,
|
||||
moderatorId: input.moderatorId,
|
||||
action: input.action,
|
||||
reason: input.reason,
|
||||
metadata: metadata as Prisma.InputJsonValue
|
||||
}
|
||||
});
|
||||
},
|
||||
{ isolationLevel: 'Serializable' }
|
||||
);
|
||||
break;
|
||||
} catch (error) {
|
||||
const code =
|
||||
typeof error === 'object' && error && 'code' in error
|
||||
? String((error as { code: unknown }).code)
|
||||
: '';
|
||||
if (code !== 'P2002' && attempt === 4) {
|
||||
throw error;
|
||||
}
|
||||
if (attempt === 4) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).then(async (created) => {
|
||||
const { logModAction } = await import('../logging/events.js');
|
||||
await logModAction(context, {
|
||||
guildId: input.guildId,
|
||||
caseNumber: created.caseNumber,
|
||||
action: input.action,
|
||||
targetUserId: input.targetUserId,
|
||||
moderatorId: input.moderatorId,
|
||||
reason: input.reason
|
||||
});
|
||||
return created;
|
||||
}
|
||||
|
||||
if (!created) {
|
||||
throw new Error('Failed to create case');
|
||||
}
|
||||
|
||||
const { logModAction } = await import('../logging/events.js');
|
||||
await logModAction(context, {
|
||||
guildId: input.guildId,
|
||||
caseNumber: created.caseNumber,
|
||||
action: input.action,
|
||||
targetUserId: input.targetUserId,
|
||||
moderatorId: input.moderatorId,
|
||||
reason: input.reason
|
||||
});
|
||||
return created;
|
||||
}
|
||||
|
||||
export function tempBanJobId(guildId: string, userId: string): string {
|
||||
return `tempBan:${guildId}:${userId}`;
|
||||
}
|
||||
|
||||
export async function scheduleTempBanExpire(
|
||||
@@ -63,10 +94,17 @@ export async function scheduleTempBanExpire(
|
||||
durationMs: number,
|
||||
reason?: string
|
||||
) {
|
||||
const jobId = tempBanJobId(guildId, userId);
|
||||
const existing = await moderationQueue.getJob(jobId);
|
||||
if (existing) {
|
||||
await existing.remove();
|
||||
}
|
||||
|
||||
await moderationQueue.add(
|
||||
'tempBanExpire',
|
||||
{ guildId, userId, reason: reason ?? null },
|
||||
{
|
||||
jobId,
|
||||
delay: durationMs,
|
||||
removeOnComplete: 1000,
|
||||
removeOnFail: 500
|
||||
@@ -78,7 +116,8 @@ export async function applyWarnEscalation(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
context: BotContext,
|
||||
userId: string,
|
||||
moderatorId: string
|
||||
moderatorId: string,
|
||||
locale: Locale
|
||||
): Promise<string | null> {
|
||||
const guildId = interaction.guildId;
|
||||
if (!guildId || !interaction.guild) {
|
||||
@@ -98,16 +137,25 @@ export async function applyWarnEscalation(
|
||||
}
|
||||
|
||||
const member = await interaction.guild.members.fetch(userId);
|
||||
const reason = rule.reason ?? `Auto escalation at ${warningCount} warnings`;
|
||||
const reason =
|
||||
rule.reason ??
|
||||
t(locale, 'moderation.escalation.defaultReason').replace('{warnCount}', String(warningCount));
|
||||
|
||||
if (rule.action === 'TIMEOUT') {
|
||||
if (!rule.durationMs) {
|
||||
return `Escalation rule for ${warningCount} warns skipped (missing duration).`;
|
||||
return t(locale, 'moderation.escalation.skippedDuration').replace(
|
||||
'{warnCount}',
|
||||
String(warningCount)
|
||||
);
|
||||
}
|
||||
if (!interaction.guild.members.me?.permissions.has(PermissionFlagsBits.ModerateMembers)) {
|
||||
return `Escalation rule for ${warningCount} warns skipped (missing bot permissions).`;
|
||||
return t(locale, 'moderation.escalation.skippedPermission').replace(
|
||||
'{warnCount}',
|
||||
String(warningCount)
|
||||
);
|
||||
}
|
||||
await member.timeout(rule.durationMs, reason);
|
||||
const durationMs = Math.min(rule.durationMs, MAX_TIMEOUT_MS);
|
||||
await member.timeout(durationMs, reason);
|
||||
await createCase(context, {
|
||||
guildId,
|
||||
targetUserId: userId,
|
||||
@@ -116,14 +164,20 @@ export async function applyWarnEscalation(
|
||||
reason,
|
||||
channelId: interaction.channelId ?? undefined,
|
||||
source: 'escalation',
|
||||
metadata: { escalation: true, durationMs: rule.durationMs, warningCount }
|
||||
metadata: { escalation: true, durationMs, warningCount }
|
||||
});
|
||||
return `Auto escalation applied: timeout (${rule.durationMs} ms).`;
|
||||
return t(locale, 'moderation.escalation.appliedTimeout').replace(
|
||||
'{durationMs}',
|
||||
String(durationMs)
|
||||
);
|
||||
}
|
||||
|
||||
if (rule.action === 'KICK') {
|
||||
if (!interaction.guild.members.me?.permissions.has(PermissionFlagsBits.KickMembers)) {
|
||||
return `Escalation rule for ${warningCount} warns skipped (missing bot permissions).`;
|
||||
return t(locale, 'moderation.escalation.skippedPermission').replace(
|
||||
'{warnCount}',
|
||||
String(warningCount)
|
||||
);
|
||||
}
|
||||
await member.kick(reason);
|
||||
await createCase(context, {
|
||||
@@ -136,11 +190,14 @@ export async function applyWarnEscalation(
|
||||
source: 'escalation',
|
||||
metadata: { escalation: true, warningCount }
|
||||
});
|
||||
return 'Auto escalation applied: kick.';
|
||||
return t(locale, 'moderation.escalation.appliedKick');
|
||||
}
|
||||
|
||||
if (!interaction.guild.members.me?.permissions.has(PermissionFlagsBits.BanMembers)) {
|
||||
return `Escalation rule for ${warningCount} warns skipped (missing bot permissions).`;
|
||||
return t(locale, 'moderation.escalation.skippedPermission').replace(
|
||||
'{warnCount}',
|
||||
String(warningCount)
|
||||
);
|
||||
}
|
||||
await interaction.guild.members.ban(userId, { reason });
|
||||
await createCase(context, {
|
||||
@@ -153,5 +210,5 @@ export async function applyWarnEscalation(
|
||||
source: 'escalation',
|
||||
metadata: { escalation: true, warningCount }
|
||||
});
|
||||
return 'Auto escalation applied: ban.';
|
||||
return t(locale, 'moderation.escalation.appliedBan');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user