Implement logging for verification failures and enhance moderation responses
- Introduced a new logging event type `VERIFICATION_FAIL` to capture details of failed verification attempts, including reasons and actions taken. - Updated moderation commands to include reasons in success messages for actions like ban, kick, and timeout. - Enhanced the logging of ban and kick actions to include moderator information and reasons fetched from the audit log. - Improved localization for new logging messages in both English and German. - Documented changes in the phase tracking documentation to reflect the new logging capabilities and verification failure handling.
This commit is contained in:
@@ -10,6 +10,9 @@ import type { LogEventType } from '@nexumi/shared';
|
||||
import { ensureGuild } from '../../guild.js';
|
||||
import { logger } from '../../logger.js';
|
||||
|
||||
/** Audit-log entries can lag a moment behind the gateway event. */
|
||||
const AUDIT_LOG_MAX_AGE_MS = 15_000;
|
||||
|
||||
export async function getLoggingConfig(prisma: BotContext['prisma'], guildId: string) {
|
||||
await ensureGuild(prisma, guildId);
|
||||
let config = await prisma.loggingConfig.findUnique({ where: { guildId } });
|
||||
@@ -19,6 +22,36 @@ export async function getLoggingConfig(prisma: BotContext['prisma'], guildId: st
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discord does not send ban/kick reasons over the gateway.
|
||||
* Reasons must be read from the audit log (requires View Audit Log).
|
||||
*/
|
||||
async function fetchRecentAuditEntry(
|
||||
guild: Guild,
|
||||
type: AuditLogEvent,
|
||||
targetId: string
|
||||
): Promise<{ reason: string | null; executorId: string | null } | null> {
|
||||
try {
|
||||
const auditLogs = await guild.fetchAuditLogs({ type, limit: 5 });
|
||||
const entry = auditLogs.entries.find((item) => {
|
||||
if (item.targetId !== targetId) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() - item.createdTimestamp <= AUDIT_LOG_MAX_AGE_MS;
|
||||
});
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
reason: entry.reason ?? null,
|
||||
executorId: entry.executorId ?? entry.executor?.id ?? null
|
||||
};
|
||||
} catch (error) {
|
||||
logger.warn({ error, guildId: guild.id, type }, 'Failed to fetch audit log for logging');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLogChannelId(
|
||||
prisma: BotContext['prisma'],
|
||||
guildId: string,
|
||||
@@ -124,6 +157,45 @@ export async function logModAction(
|
||||
await sendLogEmbed(context, guild, 'MOD_ACTION', 'Moderation Action', description, 0xef4444);
|
||||
}
|
||||
|
||||
export async function logVerificationFail(
|
||||
context: BotContext,
|
||||
input: {
|
||||
guildId: string;
|
||||
userId: string;
|
||||
userTag?: string;
|
||||
reason: string;
|
||||
detail?: string;
|
||||
failAction?: string;
|
||||
matchedUserId?: string | null;
|
||||
}
|
||||
): Promise<void> {
|
||||
const guild = await context.client.guilds.fetch(input.guildId).catch(() => null);
|
||||
if (!guild) {
|
||||
return;
|
||||
}
|
||||
const lines = [
|
||||
`**User:** ${input.userTag ? `${input.userTag} ` : ''}(<@${input.userId}>) (\`${input.userId}\`)`,
|
||||
`**Reason:** ${input.reason}`
|
||||
];
|
||||
if (input.detail) {
|
||||
lines.push(`**Detail:** ${input.detail}`);
|
||||
}
|
||||
if (input.matchedUserId) {
|
||||
lines.push(`**Matched user:** <@${input.matchedUserId}> (\`${input.matchedUserId}\`)`);
|
||||
}
|
||||
if (input.failAction) {
|
||||
lines.push(`**Fail action:** ${input.failAction}`);
|
||||
}
|
||||
await sendLogEmbed(
|
||||
context,
|
||||
guild,
|
||||
'VERIFICATION_FAIL',
|
||||
'Verification Failed',
|
||||
lines.join('\n'),
|
||||
0xf97316
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleChannelDeleteForAntiNuke(
|
||||
context: BotContext,
|
||||
guild: Guild,
|
||||
@@ -253,6 +325,29 @@ export async function registerLoggingEvents(context: BotContext): Promise<void>
|
||||
if (memberIgnored(config, member as GuildMember, member.user.bot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const kickEntry = await fetchRecentAuditEntry(
|
||||
member.guild,
|
||||
AuditLogEvent.MemberKick,
|
||||
member.id
|
||||
);
|
||||
|
||||
if (kickEntry) {
|
||||
await sendLogEmbed(
|
||||
context,
|
||||
member.guild,
|
||||
'MEMBER_LEAVE',
|
||||
'Member Kicked',
|
||||
[
|
||||
`**User:** ${member.user.tag} (<@${member.id}>)`,
|
||||
`**Moderator:** ${kickEntry.executorId ? `<@${kickEntry.executorId}>` : '—'}`,
|
||||
`**Reason:** ${kickEntry.reason ?? '—'}`
|
||||
].join('\n'),
|
||||
0xf59e0b
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await sendLogEmbed(
|
||||
context,
|
||||
member.guild,
|
||||
@@ -265,12 +360,19 @@ export async function registerLoggingEvents(context: BotContext): Promise<void>
|
||||
client.on('guildBanAdd', async (ban) => {
|
||||
const guild = ban.guild;
|
||||
await handleBanForAntiNuke(context, guild);
|
||||
|
||||
const audit = await fetchRecentAuditEntry(guild, AuditLogEvent.MemberBanAdd, ban.user.id);
|
||||
const reason = audit?.reason ?? ban.reason ?? '—';
|
||||
const executorLine = audit?.executorId
|
||||
? `\n**Moderator:** <@${audit.executorId}>`
|
||||
: '';
|
||||
|
||||
await sendLogEmbed(
|
||||
context,
|
||||
guild,
|
||||
'MEMBER_BAN',
|
||||
'Member Banned',
|
||||
`**User:** ${ban.user.tag} (<@${ban.user.id}>)\n**Reason:** ${ban.reason ?? '—'}`
|
||||
`**User:** ${ban.user.tag} (<@${ban.user.id}>)${executorLine}\n**Reason:** ${reason}`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user