import { AuditLogEvent as ALE, type AuditLogEvent, type AuditLogChange, Colors, EmbedBuilder, Events } from "discord.js"; import { ClientEvent } from "../lib/classes/event"; import { db } from "../lib/db"; import { LogLevel } from "../prisma/client"; export default new ClientEvent({ name: Events.GuildAuditLogEntryCreate, once: false, execute: async (entry, guild) => { const dbGuild = await db.servers.findUnique({ where: { id: guild.id }, }); if (!dbGuild) return; // no configuration for this guild // Define base arrays for each log level const minimalEvents: Array = [ ALE.GuildUpdate, ALE.RoleCreate, ALE.RoleDelete, ALE.RoleUpdate, ALE.ChannelCreate, ALE.ChannelDelete, ALE.ChannelUpdate, ALE.ChannelOverwriteCreate, ALE.ChannelOverwriteDelete, ALE.ChannelOverwriteUpdate, ALE.ThreadCreate, ALE.ThreadDelete, ALE.ThreadUpdate, ]; const mediumEvents: Array = [ ...minimalEvents, // Additional medium events ALE.EmojiCreate, ALE.EmojiDelete, ALE.EmojiUpdate, ALE.StickerCreate, ALE.StickerDelete, ALE.StickerUpdate, ALE.WebhookCreate, ALE.WebhookDelete, ALE.WebhookUpdate, ALE.InviteCreate, ALE.InviteDelete, ALE.InviteUpdate, ALE.MemberRoleUpdate, ALE.MemberUpdate, ]; const maximumEvents: Array = [ ...mediumEvents, ALE.BotAdd, ALE.ApplicationCommandPermissionUpdate, ALE.AutoModerationBlockMessage, ALE.AutoModerationFlagToChannel, ALE.AutoModerationRuleCreate, ALE.AutoModerationRuleDelete, ALE.AutoModerationRuleUpdate, ALE.AutoModerationUserCommunicationDisabled, ALE.CreatorMonetizationRequestCreated, ALE.CreatorMonetizationTermsAccepted, ALE.GuildScheduledEventCreate, ALE.GuildScheduledEventDelete, ALE.GuildScheduledEventUpdate, ALE.HomeSettingsCreate, ALE.HomeSettingsUpdate, ALE.IntegrationCreate, ALE.IntegrationDelete, ALE.IntegrationUpdate, ]; // Map log levels to their corresponding arrays const logLevelFilters: Record> = { [LogLevel.NONE]: [], [LogLevel.MINIMAL]: minimalEvents, [LogLevel.MEDIUM]: mediumEvents, [LogLevel.MAXIMUM]: maximumEvents, }; if (!dbGuild) return console.log(guild.id, "is not in the database"); if (dbGuild.logLevel === "NONE") return console.log(guild.id, "does not want anything logged"); if (!dbGuild.logsChannel) return console.log(guild.id, "has not configured the logs channel"); // No logging channel configured const logChannel = await guild.channels.fetch(dbGuild.logsChannel); if (!logChannel) return console.log( guild.id, "has selected a logs channel that does not exist in the guild" ); if ( !logChannel.isTextBased() || !logChannel.isSendable() || logChannel.isDMBased() || logChannel.isVoiceBased() || logChannel.isThread() || logChannel.isThreadOnly() ) return console.log( guild.id, "selected log channel is not a guild channel that is text based or sendable in, or is a thread/thread only channel." ); const logEvent = logLevelFilters[dbGuild.logLevel].includes( entry.action ); if (!logEvent) return console.log( guild.id, "does not want these types of events logged with their log level set to:", dbGuild.logLevel ); // Parsers for different change types type ParsedNameChange = { name?: string }; type ParsedMemberChange = { addedRoles: string[]; removedRoles: string[]; }; type ParsedGeneric = { changes: { key: string; old?: unknown; new?: unknown }[]; }; const parseCreateByName = (chs: AuditLogChange[]): ParsedNameChange => { const c = chs.find((c) => c.key === "name"); return { name: c?.new as string }; }; const parseDeleteByName = (chs: AuditLogChange[]): ParsedNameChange => { const c = chs.find((c) => c.key === "name"); return { name: c?.old as string }; }; const parseMemberRole = (chs: AuditLogChange[]): ParsedMemberChange => { const addChange = chs.find((c) => c.key === "$add"); const removeChange = chs.find((c) => c.key === "$remove"); const added = Array.isArray(addChange?.new) ? (addChange.new as { id: string; name: string }[]).map( (item) => `<@&${item.id}>` ) : []; const removed = Array.isArray(removeChange?.new) ? (removeChange.new as { id: string; name: string }[]).map( (item) => `<@&${item.id}>` ) : []; return { addedRoles: added, removedRoles: removed }; }; const parseGeneric = (chs: AuditLogChange[]): ParsedGeneric => ({ changes: chs.map((c) => ({ key: String(c.key), old: c.old, new: c.new, })), }); // Map AuditLogEvent to its parser (partial) const changeParsers: Partial< Record any> > = { [ALE.GuildUpdate]: parseGeneric, [ALE.RoleCreate]: parseCreateByName, [ALE.RoleDelete]: parseDeleteByName, [ALE.RoleUpdate]: parseGeneric, [ALE.ChannelCreate]: parseGeneric, [ALE.ChannelDelete]: parseGeneric, [ALE.ChannelUpdate]: parseGeneric, [ALE.ChannelOverwriteCreate]: parseGeneric, [ALE.ChannelOverwriteDelete]: parseGeneric, [ALE.ChannelOverwriteUpdate]: parseGeneric, [ALE.ThreadCreate]: parseGeneric, [ALE.ThreadDelete]: parseGeneric, [ALE.ThreadUpdate]: parseGeneric, [ALE.EmojiCreate]: parseCreateByName, [ALE.EmojiDelete]: parseDeleteByName, [ALE.EmojiUpdate]: parseGeneric, [ALE.StickerCreate]: parseCreateByName, [ALE.StickerDelete]: parseDeleteByName, [ALE.StickerUpdate]: parseGeneric, [ALE.WebhookCreate]: parseCreateByName, [ALE.WebhookDelete]: parseDeleteByName, [ALE.WebhookUpdate]: parseGeneric, [ALE.InviteCreate]: parseGeneric, [ALE.InviteDelete]: parseGeneric, [ALE.InviteUpdate]: parseGeneric, [ALE.MemberRoleUpdate]: parseMemberRole, [ALE.MemberUpdate]: parseGeneric, [ALE.BotAdd]: parseGeneric, [ALE.ApplicationCommandPermissionUpdate]: parseGeneric, [ALE.AutoModerationBlockMessage]: parseGeneric, [ALE.AutoModerationFlagToChannel]: parseGeneric, [ALE.AutoModerationRuleCreate]: parseGeneric, [ALE.AutoModerationRuleDelete]: parseGeneric, [ALE.AutoModerationRuleUpdate]: parseGeneric, [ALE.AutoModerationUserCommunicationDisabled]: parseGeneric, [ALE.CreatorMonetizationRequestCreated]: parseGeneric, [ALE.CreatorMonetizationTermsAccepted]: parseGeneric, [ALE.GuildScheduledEventCreate]: parseGeneric, [ALE.GuildScheduledEventDelete]: parseGeneric, [ALE.GuildScheduledEventUpdate]: parseGeneric, [ALE.HomeSettingsCreate]: parseGeneric, [ALE.HomeSettingsUpdate]: parseGeneric, [ALE.IntegrationCreate]: parseGeneric, [ALE.IntegrationDelete]: parseGeneric, [ALE.IntegrationUpdate]: parseGeneric, }; const embed = new EmbedBuilder() .setTitle(`${entry.targetType} ${entry.actionType}`) .setColor( entry.actionType === "Create" ? Colors.Green : entry.actionType === "Update" ? Colors.Orange : entry.actionType === "Delete" ? Colors.Red : Colors.Blurple ) .setTimestamp(); // Include executor information embed.addFields({ name: "Executor", value: entry.executor?.username ?? "Anonymous", inline: true, }); // Determine and add target info let targetDisplay = "Unknown"; try { switch (entry.targetType) { case "User": targetDisplay = `<@${(entry.target as any).id}>`; break; case "Role": targetDisplay = `<@&${(entry.target as any).id}>`; break; case "Channel": case "Thread": targetDisplay = `<#${(entry.target as any).id}>`; break; default: targetDisplay = String( (entry.target as any).id ?? entry.targetType ); } } catch {} embed.addFields({ name: "Target", value: targetDisplay, inline: true }); embed.addFields({ name: "Reason", value: entry.reason ?? "None provided", inline: false, }); // Apply change parser for this event and add to embed const parser = changeParsers[entry.action as AuditLogEvent]; if (parser && entry.changes && entry.changes.length) { const parsed = parser(entry.changes); // Name-based events (Role, Emoji, Sticker, Webhook) if ((parsed as ParsedNameChange).name) { embed.addFields({ name: "Name", value: (parsed as ParsedNameChange).name!, inline: true, }); } // Member role updates if ("addedRoles" in parsed || "removedRoles" in parsed) { const { addedRoles, removedRoles } = parsed as ParsedMemberChange; if (addedRoles.length) embed.addFields({ name: "Roles Added", value: addedRoles.join(", "), inline: true, }); if (removedRoles.length) embed.addFields({ name: "Roles Removed", value: removedRoles.join(", "), inline: true, }); } // Generic changes list if ((parsed as ParsedGeneric).changes) { const lines = (parsed as ParsedGeneric).changes.map( (c) => `**${c.key}**: ${c.old} → ${c.new}` ); embed.addFields({ name: "Changes", value: lines.join("\n"), inline: false, }); } } // Footer with entry ID embed.setFooter({ text: `Entry ID: ${entry.id}` }); // Send the embed logChannel.send({ embeds: [embed] }); }, });