From b992669216462d976f46da7f7653276c3ea784d2 Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Sun, 4 Aug 2024 15:53:29 -0400 Subject: [PATCH 1/4] enhancement: separate moderator logs from event logs --- src/commands/ban.ts | 4 ++-- src/commands/kick.ts | 4 ++-- src/commands/settings.ts | 1 + src/commands/slow-mode.ts | 16 ++++++++-------- src/commands/warn.ts | 4 ++-- src/events/guildAuditLogEntryCreate.ts | 8 ++++---- src/leveling.ts | 4 ++-- src/util.ts | 10 ++++++++++ 8 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 8665b29..d03e14b 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -1,7 +1,7 @@ import { EmbedBuilder } from 'discord.js'; import { SlashCommandBuilder } from '@discordjs/builders'; import { Ban } from '@/models/bans'; -import { banMessageDeleteChoices, sendEventLogMessage, ordinal } from '@/util'; +import { banMessageDeleteChoices, sendModLogMessage, ordinal } from '@/util'; import { untrustUser } from '@/leveling'; import { notifyUser } from '@/notifications'; import type { ChatInputCommandInteraction, CommandInteraction, ModalSubmitInteraction } from 'discord.js'; @@ -84,7 +84,7 @@ export async function banHandler(interaction: CommandInteraction | ModalSubmitIn iconURL: guild.iconURL()! }); - await sendEventLogMessage(guild, null, eventLogEmbed); + await sendModLogMessage(guild, eventLogEmbed); const { count, rows } = await Ban.findAndCountAll({ where: { diff --git a/src/commands/kick.ts b/src/commands/kick.ts index 609cb72..145010b 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -2,7 +2,7 @@ import { EmbedBuilder } from 'discord.js'; import { SlashCommandBuilder } from '@discordjs/builders'; import { Kick } from '@/models/kicks'; import { Ban } from '@/models/bans'; -import { banMessageDeleteChoices, ordinal, sendEventLogMessage } from '@/util'; +import { banMessageDeleteChoices, ordinal, sendModLogMessage } from '@/util'; import { untrustUser } from '@/leveling'; import { notifyUser } from '@/notifications'; import type { ChatInputCommandInteraction, CommandInteraction, ModalSubmitInteraction } from 'discord.js'; @@ -154,7 +154,7 @@ export async function kickHandler(interaction: CommandInteraction | ModalSubmitI sendMemberEmbeds.push(kickEmbed); } - await sendEventLogMessage(guild, null, eventLogEmbed); + await sendModLogMessage(guild, eventLogEmbed); if (count > 0) { const pastKicksEmbed = new EmbedBuilder(); diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 3219d8f..8edc49f 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -18,6 +18,7 @@ const editableOptions = [ 'channels.nsfw-logs', 'channels.event-logs', 'channels.event-logs.blacklist', + 'channels.mod-logs', 'channels.matchmaking', 'channels.notifications', 'matchmaking.lock-timeout-seconds', diff --git a/src/commands/slow-mode.ts b/src/commands/slow-mode.ts index 184a3a9..0ec8c4b 100644 --- a/src/commands/slow-mode.ts +++ b/src/commands/slow-mode.ts @@ -1,7 +1,7 @@ import { SlowMode, SlowModeStage } from '@/models/slow-mode'; import handleSlowMode from '@/slow-mode'; import { SlashCommandBuilder } from '@discordjs/builders'; -import { sendEventLogMessage } from '@/util'; +import { sendModLogMessage } from '@/util'; import { ChannelType, EmbedBuilder } from 'discord.js'; import type { GuildTextBasedChannel, ChatInputCommandInteraction } from 'discord.js'; @@ -116,8 +116,8 @@ async function setStageHandler(interaction: ChatInputCommandInteraction): Promis } ]); } - - await sendEventLogMessage(interaction.guild!, channel.id, auditLogEmbed); + + await sendModLogMessage(interaction.guild!, auditLogEmbed); await interaction.followUp({ content: `Set a limit of 1 message every ${limit} seconds above ${threshold} messages per minute on <#${channel.id}>` }); } @@ -190,8 +190,8 @@ async function unsetStageHandler(interaction: ChatInputCommandInteraction): Prom } ]); } - - await sendEventLogMessage(interaction.guild!, channel.id, auditLogEmbed); + + await sendModLogMessage(interaction.guild!, auditLogEmbed); await interaction.followUp({ content: `Unset the limit at ${threshold} messages per minute on <#${channel.id}>` }); } @@ -270,7 +270,7 @@ async function enableAutoSlowModeHandler(interaction: ChatInputCommandInteractio ]); } - await sendEventLogMessage(interaction.guild!, channel.id, auditLogEmbed); + await sendModLogMessage(interaction.guild!, auditLogEmbed); await interaction.followUp({ content: `Auto slow mode enabled for <#${channel.id}>` }); } @@ -324,7 +324,7 @@ async function enableStaticSlowModeHandler(interaction: ChatInputCommandInteract iconURL: interaction.guild!.iconURL()! }); - await sendEventLogMessage(interaction.guild!, channel.id, auditLogEmbed); + await sendModLogMessage(interaction.guild!, auditLogEmbed); await interaction.followUp({ content: `Static slow mode enabled for <#${channel.id}>` }); } @@ -376,7 +376,7 @@ async function disableSlowModeHandler(interaction: ChatInputCommandInteraction): iconURL: interaction.guild!.iconURL()! }); - await sendEventLogMessage(interaction.guild!, channel.id, auditLogEmbed); + await sendModLogMessage(interaction.guild!, auditLogEmbed); await interaction.followUp({ content: `Slow mode disabled for <#${channel.id}>` }); } diff --git a/src/commands/warn.ts b/src/commands/warn.ts index 091ec22..fbf3342 100644 --- a/src/commands/warn.ts +++ b/src/commands/warn.ts @@ -3,7 +3,7 @@ import { SlashCommandBuilder } from '@discordjs/builders'; import { Warning } from '@/models/warnings'; import { Kick } from '@/models/kicks'; import { Ban } from '@/models/bans'; -import { ordinal, sendEventLogMessage } from '@/util'; +import { ordinal, sendModLogMessage } from '@/util'; import { untrustUser } from '@/leveling'; import { notifyUser } from '@/notifications'; import type { ChatInputCommandInteraction, CommandInteraction, ModalSubmitInteraction } from 'discord.js'; @@ -156,7 +156,7 @@ export async function warnHandler(interaction: CommandInteraction | ModalSubmitI isBan = true; } - await sendEventLogMessage(guild, null, eventLogEmbed); + await sendModLogMessage(guild, eventLogEmbed); if (punishmentEmbed) { const pastWarningsEmbed = new EmbedBuilder(); diff --git a/src/events/guildAuditLogEntryCreate.ts b/src/events/guildAuditLogEntryCreate.ts index edb8591..7c935df 100644 --- a/src/events/guildAuditLogEntryCreate.ts +++ b/src/events/guildAuditLogEntryCreate.ts @@ -1,5 +1,5 @@ import { AuditLogEvent, EmbedBuilder } from 'discord.js'; -import { sendEventLogMessage } from '@/util'; +import { sendEventLogMessage, sendModLogMessage } from '@/util'; import type { Guild, User, GuildAuditLogsEntry } from 'discord.js'; export default async function guildAuditLogEntryCreateHandler(auditLogEntry: GuildAuditLogsEntry, guild: Guild): Promise { @@ -76,7 +76,7 @@ async function handleMemberTimedOut(guild: Guild, user: User, executor: User, re } ); - await sendEventLogMessage(guild, null, embed); + await sendModLogMessage(guild, embed); } async function handleMemberNicknameChange(guild: Guild, user: User, oldName?: string, newName?: string): Promise { @@ -160,7 +160,7 @@ async function handleMemberKick(auditLogEntry: GuildAuditLogsEntry, guild: Guild): Promise { @@ -208,7 +208,7 @@ async function handleMemberBanAdd(auditLogEntry: GuildAuditLogsEntry(log: GuildAuditLogsEntry, eventType: EventType): log is GuildAuditLogsEntry { diff --git a/src/leveling.ts b/src/leveling.ts index 31d5cea..9128c5f 100644 --- a/src/leveling.ts +++ b/src/leveling.ts @@ -1,7 +1,7 @@ import { EmbedBuilder } from 'discord.js'; import { getDB, getDBList } from '@/db'; import { User } from '@/models/users'; -import { getRoleFromSettings, sendEventLogMessage } from '@/util'; +import { getRoleFromSettings, sendEventLogMessage, sendModLogMessage } from '@/util'; import { sequelize } from '@/sequelize-instance'; import { notifyUser } from '@/notifications'; import type { GuildMember, Message } from 'discord.js'; @@ -214,5 +214,5 @@ export async function untrustUser(member: GuildMember, newStartDate: Date): Prom text: 'Pretendo Network', iconURL: member.guild.iconURL()! }); - await sendEventLogMessage(member.guild, null, eventLogEmbed); + await sendModLogMessage(member.guild, eventLogEmbed); } diff --git a/src/util.ts b/src/util.ts index f46b8be..334d18d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -45,6 +45,16 @@ export async function sendEventLogMessage(guild: Guild, originId: string | null, return logChannel.send({ content, embeds: [embed] }); } +export async function sendModLogMessage(guild: Guild, embed: EmbedBuilder, content?: string): Promise { + const logChannel = await getChannelFromSettings(guild, 'channels.mod-logs'); + if (!logChannel || logChannel.type !== ChannelType.GuildText) { + console.log('Missing mod log channel!'); + return null; + } + + return logChannel.send({ content, embeds: [embed] }); +} + export async function getChannelFromSettings(guild: Guild, channelName: string): Promise { const channelID = getDB().get(channelName); if (!channelID) { From c1b8ac0b9b0468ff65cc034634d4f271c9491174 Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Sun, 4 Aug 2024 22:39:29 -0400 Subject: [PATCH 2/4] enhancement: log moderator message deletions to mod-logs --- src/events/messageDelete.ts | 80 +++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index ace2236..8d13600 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -1,5 +1,5 @@ -import { BaseGuildTextChannel, EmbedBuilder } from 'discord.js'; -import { sendEventLogMessage } from '@/util'; +import { AuditLogEvent, BaseGuildTextChannel, EmbedBuilder } from 'discord.js'; +import { sendEventLogMessage, sendModLogMessage } from '@/util'; import type { Message, PartialMessage } from 'discord.js'; export default async function messageDeleteHandler(message: Message | PartialMessage): Promise { @@ -12,8 +12,6 @@ export default async function messageDeleteHandler(message: Message | PartialMes } const guild = await message.guild!.fetch(); - const member = await message.member.fetch(); - const user = member.user; let messageContent = message.content.length > 1024 ? message.content.substring(0, 1023) + '…' : message.content; if (messageContent === '') { @@ -25,21 +23,33 @@ export default async function messageDeleteHandler(message: Message | PartialMes channelName = message.channel.name; } + // * Audit logs will not show up at the same time the message delete event + await new Promise(resolve => setTimeout(resolve, 1000)); + + const auditLogs = await guild.fetchAuditLogs({ + type: AuditLogEvent.MessageDelete, + limit: 5 + }); + const auditLogEntry = auditLogs.entries.find(entry => + entry.target.id === message.author.id && + entry.extra.channel.id === message.channelId && + Date.now() - entry.createdTimestamp < 5 * 60 * 1000 + ); + // * This large time range is needed because message delete audit log entries are combined if multiple deletions are + // * performed with the same target user, executor, and channel within 5 minutes. Decreasing this time range would + // * create a risk of false negatives because the combined entry keeps the timestamp of the first deletion. + // ! Edge case: If a user deletes their own message after a moderator deletes one of theirs within 5 minutes and in + // ! the same channel, there will be a false positive mod log entry. + + const isDeletedByModerator = auditLogEntry !== undefined; + const eventLogEmbed = new EmbedBuilder(); eventLogEmbed.setColor(0xC0C0C0); eventLogEmbed.setTitle('Event Type: _Message Delete_'); eventLogEmbed.setDescription('――――――――――――――――――――――――――――――――――'); eventLogEmbed.setTimestamp(Date.now()); - eventLogEmbed.setFields( - { - name: 'User', - value: `<@${user.id}>` - }, - { - name: 'User ID', - value: user.id - }, + eventLogEmbed.addFields( { name: 'Author', value: `<@${message.author.id}>` @@ -47,24 +57,42 @@ export default async function messageDeleteHandler(message: Message | PartialMes { name: 'Author ID', value: message.author.id + }); + + if (isDeletedByModerator) { + const executor = auditLogEntry.executor; + + eventLogEmbed.addFields({ + name: 'Executor', + value: executor ? `<@${executor.id}>` : 'Unknown' }, { - name: 'Channel Tag', - value: `<#${message.channelId}>` - }, - { - name: 'Channel Name', - value: channelName - }, - { - name: 'Message', - value: messageContent - } - ); + name: 'Executor ID', + value: executor?.id ?? 'Unknown' + }); + } + + eventLogEmbed.addFields({ + name: 'Channel Tag', + value: `<#${message.channelId}>` + }, + { + name: 'Channel Name', + value: channelName + }, + { + name: 'Message', + value: messageContent + }); + eventLogEmbed.setFooter({ text: 'Pretendo Network', iconURL: guild.iconURL()! }); - await sendEventLogMessage(guild, message.channelId, eventLogEmbed); + if (isDeletedByModerator) { + await sendModLogMessage(guild, eventLogEmbed); + } else { + await sendEventLogMessage(guild, message.channelId, eventLogEmbed); + } } From 1b0560e0873f8539025ca03311f4e651927468fc Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:09:20 -0400 Subject: [PATCH 3/4] chore: add event logs blacklist to mod logs --- src/commands/ban.ts | 2 +- src/commands/kick.ts | 2 +- src/commands/slow-mode.ts | 10 +++++----- src/commands/warn.ts | 2 +- src/events/guildAuditLogEntryCreate.ts | 6 +++--- src/events/messageDelete.ts | 2 +- src/leveling.ts | 2 +- src/util.ts | 7 ++++++- 8 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/commands/ban.ts b/src/commands/ban.ts index d03e14b..e6700cf 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -84,7 +84,7 @@ export async function banHandler(interaction: CommandInteraction | ModalSubmitIn iconURL: guild.iconURL()! }); - await sendModLogMessage(guild, eventLogEmbed); + await sendModLogMessage(guild, null, eventLogEmbed); const { count, rows } = await Ban.findAndCountAll({ where: { diff --git a/src/commands/kick.ts b/src/commands/kick.ts index 145010b..d94b26c 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -154,7 +154,7 @@ export async function kickHandler(interaction: CommandInteraction | ModalSubmitI sendMemberEmbeds.push(kickEmbed); } - await sendModLogMessage(guild, eventLogEmbed); + await sendModLogMessage(guild, null, eventLogEmbed); if (count > 0) { const pastKicksEmbed = new EmbedBuilder(); diff --git a/src/commands/slow-mode.ts b/src/commands/slow-mode.ts index 0ec8c4b..5b3887c 100644 --- a/src/commands/slow-mode.ts +++ b/src/commands/slow-mode.ts @@ -117,7 +117,7 @@ async function setStageHandler(interaction: ChatInputCommandInteraction): Promis ]); } - await sendModLogMessage(interaction.guild!, auditLogEmbed); + await sendModLogMessage(interaction.guild!, channel.id, auditLogEmbed); await interaction.followUp({ content: `Set a limit of 1 message every ${limit} seconds above ${threshold} messages per minute on <#${channel.id}>` }); } @@ -191,7 +191,7 @@ async function unsetStageHandler(interaction: ChatInputCommandInteraction): Prom ]); } - await sendModLogMessage(interaction.guild!, auditLogEmbed); + await sendModLogMessage(interaction.guild!, channel.id, auditLogEmbed); await interaction.followUp({ content: `Unset the limit at ${threshold} messages per minute on <#${channel.id}>` }); } @@ -270,7 +270,7 @@ async function enableAutoSlowModeHandler(interaction: ChatInputCommandInteractio ]); } - await sendModLogMessage(interaction.guild!, auditLogEmbed); + await sendModLogMessage(interaction.guild!, channel.id, auditLogEmbed); await interaction.followUp({ content: `Auto slow mode enabled for <#${channel.id}>` }); } @@ -324,7 +324,7 @@ async function enableStaticSlowModeHandler(interaction: ChatInputCommandInteract iconURL: interaction.guild!.iconURL()! }); - await sendModLogMessage(interaction.guild!, auditLogEmbed); + await sendModLogMessage(interaction.guild!, channel.id, auditLogEmbed); await interaction.followUp({ content: `Static slow mode enabled for <#${channel.id}>` }); } @@ -376,7 +376,7 @@ async function disableSlowModeHandler(interaction: ChatInputCommandInteraction): iconURL: interaction.guild!.iconURL()! }); - await sendModLogMessage(interaction.guild!, auditLogEmbed); + await sendModLogMessage(interaction.guild!, channel.id, auditLogEmbed); await interaction.followUp({ content: `Slow mode disabled for <#${channel.id}>` }); } diff --git a/src/commands/warn.ts b/src/commands/warn.ts index fbf3342..ceb0ec7 100644 --- a/src/commands/warn.ts +++ b/src/commands/warn.ts @@ -156,7 +156,7 @@ export async function warnHandler(interaction: CommandInteraction | ModalSubmitI isBan = true; } - await sendModLogMessage(guild, eventLogEmbed); + await sendModLogMessage(guild, null, eventLogEmbed); if (punishmentEmbed) { const pastWarningsEmbed = new EmbedBuilder(); diff --git a/src/events/guildAuditLogEntryCreate.ts b/src/events/guildAuditLogEntryCreate.ts index 7c935df..dc0d4da 100644 --- a/src/events/guildAuditLogEntryCreate.ts +++ b/src/events/guildAuditLogEntryCreate.ts @@ -76,7 +76,7 @@ async function handleMemberTimedOut(guild: Guild, user: User, executor: User, re } ); - await sendModLogMessage(guild, embed); + await sendModLogMessage(guild, null, embed); } async function handleMemberNicknameChange(guild: Guild, user: User, oldName?: string, newName?: string): Promise { @@ -160,7 +160,7 @@ async function handleMemberKick(auditLogEntry: GuildAuditLogsEntry, guild: Guild): Promise { @@ -208,7 +208,7 @@ async function handleMemberBanAdd(auditLogEntry: GuildAuditLogsEntry(log: GuildAuditLogsEntry, eventType: EventType): log is GuildAuditLogsEntry { diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index 8d13600..82e3263 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -91,7 +91,7 @@ export default async function messageDeleteHandler(message: Message | PartialMes }); if (isDeletedByModerator) { - await sendModLogMessage(guild, eventLogEmbed); + await sendModLogMessage(guild, message.channelId, eventLogEmbed); } else { await sendEventLogMessage(guild, message.channelId, eventLogEmbed); } diff --git a/src/leveling.ts b/src/leveling.ts index 9128c5f..88051fe 100644 --- a/src/leveling.ts +++ b/src/leveling.ts @@ -214,5 +214,5 @@ export async function untrustUser(member: GuildMember, newStartDate: Date): Prom text: 'Pretendo Network', iconURL: member.guild.iconURL()! }); - await sendModLogMessage(member.guild, eventLogEmbed); + await sendModLogMessage(member.guild, null, eventLogEmbed); } diff --git a/src/util.ts b/src/util.ts index 334d18d..bf97c20 100644 --- a/src/util.ts +++ b/src/util.ts @@ -45,7 +45,12 @@ export async function sendEventLogMessage(guild: Guild, originId: string | null, return logChannel.send({ content, embeds: [embed] }); } -export async function sendModLogMessage(guild: Guild, embed: EmbedBuilder, content?: string): Promise { +export async function sendModLogMessage(guild: Guild, originId: string | null, embed: EmbedBuilder, content?: string): Promise { + const blacklistedIds = getDBList('channels.event-logs.blacklist'); + if (originId && blacklistedIds.includes(originId)) { + return null; + } + const logChannel = await getChannelFromSettings(guild, 'channels.mod-logs'); if (!logChannel || logChannel.type !== ChannelType.GuildText) { console.log('Missing mod log channel!'); From dddc8184f6f248ca0cfdb03371b3633c71f2d19b Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:44:18 -0400 Subject: [PATCH 4/4] chore: correct naming for ID variables --- src/util.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/util.ts b/src/util.ts index bf97c20..47d826f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -30,9 +30,9 @@ export function ordinal(number: number): string { return (number + suffix); } -export async function sendEventLogMessage(guild: Guild, originId: string | null, embed: EmbedBuilder, content?: string): Promise { - const blacklistedIds = getDBList('channels.event-logs.blacklist'); - if (originId && blacklistedIds.includes(originId)) { +export async function sendEventLogMessage(guild: Guild, originID: string | null, embed: EmbedBuilder, content?: string): Promise { + const blacklistedIDs = getDBList('channels.event-logs.blacklist'); + if (originID && blacklistedIDs.includes(originID)) { return null; } @@ -45,9 +45,9 @@ export async function sendEventLogMessage(guild: Guild, originId: string | null, return logChannel.send({ content, embeds: [embed] }); } -export async function sendModLogMessage(guild: Guild, originId: string | null, embed: EmbedBuilder, content?: string): Promise { - const blacklistedIds = getDBList('channels.event-logs.blacklist'); - if (originId && blacklistedIds.includes(originId)) { +export async function sendModLogMessage(guild: Guild, originID: string | null, embed: EmbedBuilder, content?: string): Promise { + const blacklistedIDs = getDBList('channels.event-logs.blacklist'); + if (originID && blacklistedIDs.includes(originID)) { return null; }