From 86b529d92fb070b29d6f44815d720ff3e84fc126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 31 Aug 2023 21:01:06 +0200 Subject: [PATCH 01/25] feat: /purge --- src/commands/purge.ts | 72 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/commands/purge.ts diff --git a/src/commands/purge.ts b/src/commands/purge.ts new file mode 100644 index 0000000..c863936 --- /dev/null +++ b/src/commands/purge.ts @@ -0,0 +1,72 @@ +import { ApplicationCommandOptionType, PermissionFlagsBits } from "discord.js"; +const { ManageMessages } = PermissionFlagsBits; +import type { Command } from "djs-fsrouter"; + +const Purge: Command = { + defaultMemberPermissions: ManageMessages, + dmPermission: false, + description: "Bulk delete messages in the current channel", + options: [ + { + name: "number", + required: true, + type: ApplicationCommandOptionType.Number, + minValue: 2, + maxValue: 100, + description: "How many messages to purge", + }, + { + name: "author", + type: ApplicationCommandOptionType.User, + description: "If set, only messages from this user will be deleted", + }, + ], + + async run(interaction) { + const number = interaction.options.getNumber("number", true); + const author = interaction.options.getMember("author"); + const { channel } = interaction; + + function reply(content: string) { + return interaction.reply({ ephemeral: true, content }); + } + + if (!channel) { + return reply("Error: could not fetch the channel"); + } else if (channel.isDMBased()) { + return reply("Error: can't do that in DMs!"); + } + const { + guild: { members }, + } = channel; + const myself = members.me || (await members.fetchMe()); + if (!channel.permissionsFor(myself).has(ManageMessages)) { + return reply( + "Error: I do not have the permission to delete messages in this channel.", + ); + } + + let messages = Array.from( + ( + await channel.messages.fetch({ + limit: number && !author ? number : 100, + cache: false, + }) + ).values(), + ); + + if (author) { + messages = messages.filter(({ member }) => member === author); + if (messages.length > number) messages.length = number; + } + + channel + .bulkDelete(messages, true) + .then(({ size }) => reply(`Deleted ${size} messages.`)) + .catch((error) => { + console.error(error); + reply(`Error: ${error.message}.\nSee the console for details.`); + }); + }, +}; +export default Purge; From ad13f1b0daa185df495e2449d3cd98ef98c15f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Sat, 2 Sep 2023 05:02:41 +0200 Subject: [PATCH 02/25] feat: /delete-and-warn --- package.json | 2 +- src/commands/delete-and-warn.ts | 61 ++++++++++++++++++++++++++++++++ src/listeners/delete-and-warn.ts | 44 +++++++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/commands/delete-and-warn.ts create mode 100644 src/listeners/delete-and-warn.ts diff --git a/package.json b/package.json index 23cfd36..750e2c4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "bufferutil": "^4.0.7", "cheerio": "^1.0.0-rc.12", "discord.js": "^14.13.0", - "djs-fsrouter": "^0.0.4", + "djs-fsrouter": "^0.0.8", "drizzle-orm": "^0.28.5", "entities-decode": "^2.0.0", "erlpack": "^0.1.4", diff --git a/src/commands/delete-and-warn.ts b/src/commands/delete-and-warn.ts new file mode 100644 index 0000000..662aff8 --- /dev/null +++ b/src/commands/delete-and-warn.ts @@ -0,0 +1,61 @@ +import { + PermissionFlagsBits, + ApplicationCommandType, + ComponentType, + TextInputStyle, +} from "discord.js"; +const { ManageMessages } = PermissionFlagsBits; +const { ActionRow, TextInput } = ComponentType; +import type { MessageCommand } from "djs-fsrouter"; + +export const customId = "delAndWarn"; + +const DeleteAndWarn: MessageCommand = { + type: ApplicationCommandType.Message, + defaultMemberPermissions: ManageMessages, + dmPermission: false, + run: async (interaction) => { + const { channel } = interaction; + if (!channel) + return interaction.reply({ + ephemeral: true, + content: "Error: could not fetch the channel", + }); + if (channel.isDMBased()) + return interaction.reply({ + ephemeral: true, + content: "Cannot use this command in DMs", + }); + + const { + guild: { members }, + } = channel; + const myself = members.me || (await members.fetchMe()); + if (!channel.permissionsFor(myself).has(ManageMessages)) { + return interaction.reply({ + ephemeral: true, + content: + "I do not have the permission to delete messages in this channel.", + }); + } + const { id, author } = interaction.targetMessage; + interaction.showModal({ + title: `Deleting ${author.displayName}'s message`, + customId: `${author.id}_${id}_${customId}`, + components: [ + { + type: ActionRow, + components: [ + { + type: TextInput, + customId: "deletionReason", + label: "Reason", + style: TextInputStyle.Short, + }, + ], + }, + ], + }); + }, +}; +export default DeleteAndWarn; diff --git a/src/listeners/delete-and-warn.ts b/src/listeners/delete-and-warn.ts new file mode 100644 index 0000000..2506170 --- /dev/null +++ b/src/listeners/delete-and-warn.ts @@ -0,0 +1,44 @@ +import type { Listener } from "../types/listener.ts"; +import { customId } from "../commands/delete-and-warn.ts"; + +export default [ + { + event: "interactionCreate", + async handler(interaction) { + if ( + !interaction.isModalSubmit() || + !interaction.customId.endsWith(customId) || + !interaction.guild + ) + return; + + const [targetId, messageId] = interaction.customId.split("_", 2); + const target = await interaction.guild.members.fetch(targetId); + const reason = interaction.fields.getField("deletionReason").value; + const targetMessage = await interaction.channel?.messages.fetch( + messageId, + ); + targetMessage?.delete().catch(console.error); + target + .send( + `Your message in ${interaction.channel} was deleted for the following reason:\n\`\`\`${reason}\`\`\``, + ) + .then(() => { + interaction + .reply({ + ephemeral: true, + content: "Reason sent.", + }) + .catch(console.error); + }) + .catch(() => { + interaction + .reply({ + ephemeral: true, + content: `The reason could not be sent to ${target}; they proabably blocked me or disabled DMs from server members.\n\`\`\`${reason}\`\`\``, + }) + .catch(console.error); + }); + }, + }, +] as Listener[]; From 545ad093efcd9067d0172039e3dd1712952d243a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Wed, 13 Sep 2023 19:11:06 +0200 Subject: [PATCH 03/25] feat: give delete-and-warn modal a placeholder and max length --- src/commands/delete-and-warn.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/delete-and-warn.ts b/src/commands/delete-and-warn.ts index 662aff8..2d645ee 100644 --- a/src/commands/delete-and-warn.ts +++ b/src/commands/delete-and-warn.ts @@ -50,6 +50,8 @@ const DeleteAndWarn: MessageCommand = { type: TextInput, customId: "deletionReason", label: "Reason", + placeholder: "This will be sent in DM to the author.", + maxLength: 512, style: TextInputStyle.Short, }, ], From fd2ccd2d0c5aa064582d652fba84fb81530e598b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Wed, 13 Sep 2023 19:53:16 +0200 Subject: [PATCH 04/25] feat: adds a modalInput helper --- src/components.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components.ts b/src/components.ts index c19fcbf..ec0cd6d 100644 --- a/src/components.ts +++ b/src/components.ts @@ -1,5 +1,11 @@ -import { ComponentType, ButtonStyle } from "discord.js"; -const { ActionRow, StringSelect, Button } = ComponentType; +import { + ComponentType, + ButtonStyle, + type ActionRowData, + type TextInputComponentData, + type ModalActionRowComponentData, +} from "discord.js"; +const { ActionRow, StringSelect, Button, TextInput } = ComponentType; import type { APIActionRowComponent, @@ -44,3 +50,17 @@ export function buttonRow( })), }; } + +export function modalInput( + input: Omit, +): ActionRowData { + return { + type: ActionRow, + components: [ + { + ...input, + type: TextInput, + }, + ], + }; +} From dfc11307fbf4c91aa8c150772d327b53e2fd2351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Wed, 13 Sep 2023 20:15:20 +0200 Subject: [PATCH 05/25] feat: delete-and-warn can timeout the offender --- src/commands/delete-and-warn.ts | 49 ++++++++++++++++---------------- src/listeners/delete-and-warn.ts | 21 ++++++++++++++ 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/commands/delete-and-warn.ts b/src/commands/delete-and-warn.ts index 2d645ee..a137ffd 100644 --- a/src/commands/delete-and-warn.ts +++ b/src/commands/delete-and-warn.ts @@ -1,21 +1,20 @@ import { PermissionFlagsBits, ApplicationCommandType, - ComponentType, TextInputStyle, } from "discord.js"; -const { ManageMessages } = PermissionFlagsBits; -const { ActionRow, TextInput } = ComponentType; +const { ManageMessages, ModerateMembers } = PermissionFlagsBits; +import { modalInput } from "../components.ts"; import type { MessageCommand } from "djs-fsrouter"; export const customId = "delAndWarn"; const DeleteAndWarn: MessageCommand = { type: ApplicationCommandType.Message, - defaultMemberPermissions: ManageMessages, + defaultMemberPermissions: ManageMessages | ModerateMembers, dmPermission: false, run: async (interaction) => { - const { channel } = interaction; + const { channel, targetMessage } = interaction; if (!channel) return interaction.reply({ ephemeral: true, @@ -27,35 +26,37 @@ const DeleteAndWarn: MessageCommand = { content: "Cannot use this command in DMs", }); - const { - guild: { members }, - } = channel; - const myself = members.me || (await members.fetchMe()); - if (!channel.permissionsFor(myself).has(ManageMessages)) { + if (!targetMessage.deletable) { return interaction.reply({ ephemeral: true, content: "I do not have the permission to delete messages in this channel.", }); } - const { id, author } = interaction.targetMessage; + const { id, author } = targetMessage; + // targetMessage.member is always null for some reason + const member = await channel.guild.members.fetch(author); + interaction.showModal({ title: `Deleting ${author.displayName}'s message`, customId: `${author.id}_${id}_${customId}`, components: [ - { - type: ActionRow, - components: [ - { - type: TextInput, - customId: "deletionReason", - label: "Reason", - placeholder: "This will be sent in DM to the author.", - maxLength: 512, - style: TextInputStyle.Short, - }, - ], - }, + modalInput({ + customId: "deletionReason", + label: "Reason", + placeholder: "This will be sent in DM to the author.", + maxLength: 512, + style: TextInputStyle.Short, + }), + modalInput({ + customId: "timeout", + required: false, + label: "Timeout", + placeholder: member.moderatable + ? "e.g 30m, 3h, 1d" + : "This will have no effet; I do not have the permission to time this member out", + style: TextInputStyle.Short, + }), ], }); }, diff --git a/src/listeners/delete-and-warn.ts b/src/listeners/delete-and-warn.ts index 2506170..323d653 100644 --- a/src/listeners/delete-and-warn.ts +++ b/src/listeners/delete-and-warn.ts @@ -19,6 +19,10 @@ export default [ messageId, ); targetMessage?.delete().catch(console.error); + if (target.moderatable) { + const timeout = parseTime(interaction.fields.getField("timeout").value); + if (timeout > 0) target.timeout(timeout, reason).catch(console.error); + } target .send( `Your message in ${interaction.channel} was deleted for the following reason:\n\`\`\`${reason}\`\`\``, @@ -42,3 +46,20 @@ export default [ }, }, ] as Listener[]; + +const units: Record = { + s: 1_000, + m: 60_000, + h: 3600_000, + d: 24 * 3600_000, +}; +function parseTime(time: string) { + const parts = time.matchAll(/([0-9]+)([A-z])/g); + let ms = 0; + for (const [, value, unit] of parts) { + if (+value && unit in units) { + ms += +value * units[unit]; + } + } + return ms; +} From f09f2c897a7e0a4cbb67a3adb3b40099c9855b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Sun, 7 Jan 2024 01:44:17 +0100 Subject: [PATCH 06/25] feat: basic logging --- package.json | 2 +- src/commands/logging/$config.ts | 62 +++++++++++++++++ src/commands/logging/config.ts | 26 +++++++ src/commands/logging/disable.ts | 20 ++++++ src/commands/logging/enable.ts | 53 ++++++++++++++ src/listeners/logging.ts | 119 ++++++++++++++++++++++++++++++++ src/schemas/config.ts | 6 +- 7 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 src/commands/logging/$config.ts create mode 100644 src/commands/logging/config.ts create mode 100644 src/commands/logging/disable.ts create mode 100644 src/commands/logging/enable.ts create mode 100644 src/listeners/logging.ts diff --git a/package.json b/package.json index 750e2c4..4c1cd76 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "bufferutil": "^4.0.7", "cheerio": "^1.0.0-rc.12", "discord.js": "^14.13.0", - "djs-fsrouter": "^0.0.8", + "djs-fsrouter": "^0.0.10", "drizzle-orm": "^0.28.5", "entities-decode": "^2.0.0", "erlpack": "^0.1.4", diff --git a/src/commands/logging/$config.ts b/src/commands/logging/$config.ts new file mode 100644 index 0000000..3f7b1d1 --- /dev/null +++ b/src/commands/logging/$config.ts @@ -0,0 +1,62 @@ +import db from "../../db.ts"; +import { Config } from "../../schemas/config.ts"; +import { LoggingWhitelist } from "../../schemas/loggingWhitelist.ts"; +import type { TextChannel, Guild, Role } from "discord.js"; +import { eq, sql } from "drizzle-orm"; +const { placeholder } = sql; + +export enum LogMode { + NONE = 0, + DELETES = 1 << 0, + EDITS = 1 << 1, + "DELETES & EDITS" = DELETES | EDITS, +} + +export type LogConfig = + | { + mode: LogMode.NONE; + channel: null; + } + | { + mode: LogMode; + channel: TextChannel["id"]; + }; + +const logModes = new Map(); +db.select() + .from(Config) + .then((configs) => { + for (const { id, loggingMode, loggingChannel } of configs) + logModes.set(id, { + mode: loggingMode || LogMode.NONE, + channel: loggingChannel, + }); + }); + +export function setLogging( + { id }: Guild, + config: LogMode.NONE | { mode: LogMode; channel: TextChannel }, +) { + let loggingMode = LogMode.NONE; + let loggingChannel = null; + if (config) { + loggingMode = config.mode; + loggingChannel = config.channel.id; + logModes.set(id, { mode: config.mode, channel: loggingChannel }); + } else { + logModes.set(id, { mode: LogMode.NONE, channel: null }); + } + + return db + .insert(Config) + .values({ id, loggingMode, loggingChannel }) + .onConflictDoUpdate({ + target: Config.id, + set: { loggingMode, loggingChannel }, + }) + .returning(); +} + +export function getConfig({ id }: Guild) { + return logModes.get(id); +} diff --git a/src/commands/logging/config.ts b/src/commands/logging/config.ts new file mode 100644 index 0000000..c24326f --- /dev/null +++ b/src/commands/logging/config.ts @@ -0,0 +1,26 @@ +import { LogMode, getConfig } from "./$config.ts"; + +import { ApplicationCommandType } from "discord.js"; +import type { Command } from "djs-fsrouter"; + +export const type = ApplicationCommandType.ChatInput; +const Disable: Command = { + description: "See the current config", + dmPermission: false, + defaultMemberPermissions: "0", + async run(interaction) { + if (!interaction.guild) return; + + const { mode, channel } = getConfig(interaction.guild) || {}; + const message = mode + ? `Logging ${LogMode[mode].toLowerCase()}\nLogs channel: <#${channel}>` + : "Logging messages is disabled."; + interaction + .reply({ + ephemeral: true, + embeds: [{ description: message }], + }) + .catch(console.error); + }, +}; +export default Disable; diff --git a/src/commands/logging/disable.ts b/src/commands/logging/disable.ts new file mode 100644 index 0000000..f4c8112 --- /dev/null +++ b/src/commands/logging/disable.ts @@ -0,0 +1,20 @@ +import { LogMode, setLogging } from "./$config.ts"; + +import { ApplicationCommandType } from "discord.js"; +import type { Command } from "djs-fsrouter"; + +export const type = ApplicationCommandType.ChatInput; +const Disable: Command = { + description: "Disable message logging", + dmPermission: false, + defaultMemberPermissions: "0", + async run(interaction) { + if (!interaction.guild) return; + + await setLogging(interaction.guild, LogMode.NONE); + interaction + .reply({ ephemeral: true, content: "Logging disabled." }) + .catch(console.error); + }, +}; +export default Disable; diff --git a/src/commands/logging/enable.ts b/src/commands/logging/enable.ts new file mode 100644 index 0000000..db7db8d --- /dev/null +++ b/src/commands/logging/enable.ts @@ -0,0 +1,53 @@ +import { LogMode, setLogging } from "./$config.ts"; + +import { + ApplicationCommandType, + ApplicationCommandOptionType, + ChannelType, +} from "discord.js"; +import type { Command } from "djs-fsrouter"; + +export const type = ApplicationCommandType.ChatInput; +const Disable: Command = { + description: "Enable message logging", + dmPermission: false, + defaultMemberPermissions: "0", + options: [ + { + name: "log", + required: true, + type: ApplicationCommandOptionType.Integer, + description: "What should be logged?", + choices: [ + { name: "Deletions only", value: LogMode.DELETES }, + { + name: "Deletions & edits", + value: LogMode.DELETES | LogMode.EDITS, + }, + ], + }, + { + name: "channel", + required: true, + type: ApplicationCommandOptionType.Channel, + channelTypes: [ChannelType.GuildText], + description: "The channel where the logs will be sent.", + }, + ], + async run(interaction) { + if (!interaction.guild) return; + + const mode = interaction.options.getInteger("log", true); + const channel = interaction.options.getChannel("channel", true, [ + ChannelType.GuildText, + ]); + await setLogging(interaction.guild, { mode, channel }); + interaction + .reply({ + ephemeral: true, + content: `Logging set to ${LogMode[mode]} in ${channel}`, + }) + .catch(console.error); + }, +}; +export default Disable; diff --git a/src/listeners/logging.ts b/src/listeners/logging.ts new file mode 100644 index 0000000..27539b8 --- /dev/null +++ b/src/listeners/logging.ts @@ -0,0 +1,119 @@ +import { type Guild, Message, type APIEmbed } from "discord.js"; +import { LogMode, getConfig } from "../commands/logging/$config.ts"; +import type { Listener } from "../types/listener.ts"; + +export default [ + { + event: "messageDelete", + async handler(message) { + const channel = await getLogChannel(message.guild, LogMode.DELETES); + if ( + !channel?.isTextBased() || + message.partial || + message.system || + message.author.bot + ) + return; + + const embed = { + ...msgDeletionEmbed(message), + description: + `**🗑️ Message from ${message.author} deleted in ${message.channel}**\n\n${message.content}`.substring( + 0, + 2056, + ), + timestamp: new Date().toISOString(), + }; + channel.send({ embeds: [embed] }).catch(console.error); + }, + }, + { + event: "messageDeleteBulk", + async handler(messages) { + const first = messages.first(); + const channel = await getLogChannel( + first?.guild || null, + LogMode.DELETES, + ); + if (!channel?.isTextBased()) return; + + const embeds = ( + await Promise.all(messages.map((msg) => msg.fetch(false))) + ) + .filter(({ system, author }) => !system && !author.bot) + .map(msgDeletionEmbed); + + channel + .send({ + embeds: [ + { + color: 0xdd4444, + description: `**🗑️ ${embeds.length} messages bulk-deleted in ${channel}**`, + timestamp: new Date().toISOString(), + }, + ...embeds.slice(0, 9), + ], + }) + .catch(console.error); + + for (let i = 9; i < embeds.length; i += 10) + channel.send({ embeds: embeds.slice(i, i + 10) }).catch(console.error); + }, + }, + { + event: "messageUpdate", + async handler(oldMessage, newMessage) { + const channel = await getLogChannel(oldMessage.guild, LogMode.EDITS); + if ( + !channel?.isTextBased() || + oldMessage.partial || + oldMessage.author.bot + ) + return; + + const { author, content: oldContent } = oldMessage; + channel + .send({ + embeds: [ + { + color: 0xdd6d0c, + author: { + name: author.tag, + icon_url: author.avatarURL() || undefined, + }, + description: + `**${author} edited a [message](${newMessage.url}) in ${channel}\nPrevious message:**\n\n${oldContent}`.substring( + 0, + 2056, + ), + timestamp: new Date().toISOString(), + }, + ], + }) + .catch(console.error); + }, + }, +] as Listener[]; + +function getLogChannel(guild: Guild | null, requiredMode: LogMode) { + if (!guild) return null; + const config = getConfig(guild); + if (!config?.channel || !(config.mode & requiredMode)) return null; + + return guild.channels.fetch(config.channel); +} + +function msgDeletionEmbed({ content, author, attachments }: Message): APIEmbed { + let attachmentN = 0; + return { + color: 0xdd4444, + author: { name: author.tag, icon_url: author.avatarURL() || undefined }, + description: content, + fields: attachments.size + ? attachments.map(({ url }) => ({ + name: `Attachment ${++attachmentN}`, + value: url, + })) + : undefined, + }; +} diff --git a/src/schemas/config.ts b/src/schemas/config.ts index 5a2d4ed..f4c5ad7 100644 --- a/src/schemas/config.ts +++ b/src/schemas/config.ts @@ -1,5 +1,6 @@ -import { sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; + export const Config = sqliteTable("guildConfig", { id: text("guildId").primaryKey().notNull(), gatewayChannel: text("gatewayChannel"), @@ -9,6 +10,9 @@ export const Config = sqliteTable("guildConfig", { gatewayLeaveTitle: text("gatewayLeaveTitle"), gatewayLeaveContent: text("gatewayLeaveContent"), + + loggingMode: integer("loggingMode", { mode: "number" }).default(0), + loggingChannel: text("loggingChannel").default(""), }); export type ConfigSelect = InferSelectModel; export type ConfigInsert = InferInsertModel; From 2815def666eff10cb469cd1f0115c1d5eeca2a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Sun, 7 Jan 2024 01:47:40 +0100 Subject: [PATCH 07/25] fix[logging]: slight style discrepancy --- src/listeners/logging.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/listeners/logging.ts b/src/listeners/logging.ts index 27539b8..82aa2cc 100644 --- a/src/listeners/logging.ts +++ b/src/listeners/logging.ts @@ -82,7 +82,7 @@ export default [ icon_url: author.avatarURL() || undefined, }, description: - `**${author} edited a [message](${newMessage.url}) in ${channel}\nPrevious message:**\n\n${oldContent}`.substring( + `**✏️ ${author} edited a [message](${newMessage.url}) in ${channel}\nPrevious message:**\n\n${oldContent}`.substring( 0, 2056, ), From 4e9979cca3273c2a04e772a2e7df2324dd6b790b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Mon, 8 Jan 2024 18:18:02 +0100 Subject: [PATCH 08/25] refactor: fix default export names --- src/commands/logging/config.ts | 4 ++-- src/commands/logging/enable.ts | 4 ++-- src/commands/mdn.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/logging/config.ts b/src/commands/logging/config.ts index c24326f..380799b 100644 --- a/src/commands/logging/config.ts +++ b/src/commands/logging/config.ts @@ -4,7 +4,7 @@ import { ApplicationCommandType } from "discord.js"; import type { Command } from "djs-fsrouter"; export const type = ApplicationCommandType.ChatInput; -const Disable: Command = { +const Config: Command = { description: "See the current config", dmPermission: false, defaultMemberPermissions: "0", @@ -23,4 +23,4 @@ const Disable: Command = { .catch(console.error); }, }; -export default Disable; +export default Config; diff --git a/src/commands/logging/enable.ts b/src/commands/logging/enable.ts index db7db8d..981792f 100644 --- a/src/commands/logging/enable.ts +++ b/src/commands/logging/enable.ts @@ -8,7 +8,7 @@ import { import type { Command } from "djs-fsrouter"; export const type = ApplicationCommandType.ChatInput; -const Disable: Command = { +const Enable: Command = { description: "Enable message logging", dmPermission: false, defaultMemberPermissions: "0", @@ -50,4 +50,4 @@ const Disable: Command = { .catch(console.error); }, }; -export default Disable; +export default Enable; diff --git a/src/commands/mdn.ts b/src/commands/mdn.ts index be5bfba..3fab005 100644 --- a/src/commands/mdn.ts +++ b/src/commands/mdn.ts @@ -11,7 +11,7 @@ type SearchResult = { snippet: string; }; -const Info: Command = { +const Mdn: Command = { description: "Search the Modzilla Developer Network", options: [ { @@ -95,7 +95,7 @@ const Info: Command = { }).catch(console.error); }, }; -export default Info; +export default Mdn; const BASE_URL = process.env.CSE_KEY && process.env.CSE_CSX From 7eef1fd871e40545ba7312d99054bb796f3ef6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Mon, 8 Jan 2024 19:20:54 +0100 Subject: [PATCH 09/25] fix[logging]: command permissions --- src/commands/logging/$info.js | 5 +++++ src/commands/logging/config.ts | 2 -- src/commands/logging/disable.ts | 2 -- src/commands/logging/enable.ts | 2 -- 4 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 src/commands/logging/$info.js diff --git a/src/commands/logging/$info.js b/src/commands/logging/$info.js new file mode 100644 index 0000000..27bcff0 --- /dev/null +++ b/src/commands/logging/$info.js @@ -0,0 +1,5 @@ +export default { + description: "Handles logging deleted & edited messages", + dmPermission: false, + defaultMemberPermissions: "0", +}; diff --git a/src/commands/logging/config.ts b/src/commands/logging/config.ts index 380799b..c4f2648 100644 --- a/src/commands/logging/config.ts +++ b/src/commands/logging/config.ts @@ -6,8 +6,6 @@ import type { Command } from "djs-fsrouter"; export const type = ApplicationCommandType.ChatInput; const Config: Command = { description: "See the current config", - dmPermission: false, - defaultMemberPermissions: "0", async run(interaction) { if (!interaction.guild) return; diff --git a/src/commands/logging/disable.ts b/src/commands/logging/disable.ts index f4c8112..b7e3b36 100644 --- a/src/commands/logging/disable.ts +++ b/src/commands/logging/disable.ts @@ -6,8 +6,6 @@ import type { Command } from "djs-fsrouter"; export const type = ApplicationCommandType.ChatInput; const Disable: Command = { description: "Disable message logging", - dmPermission: false, - defaultMemberPermissions: "0", async run(interaction) { if (!interaction.guild) return; diff --git a/src/commands/logging/enable.ts b/src/commands/logging/enable.ts index 981792f..df6a205 100644 --- a/src/commands/logging/enable.ts +++ b/src/commands/logging/enable.ts @@ -10,8 +10,6 @@ import type { Command } from "djs-fsrouter"; export const type = ApplicationCommandType.ChatInput; const Enable: Command = { description: "Enable message logging", - dmPermission: false, - defaultMemberPermissions: "0", options: [ { name: "log", From f24a2c82afcd12624ae24c8adc5d87ce390bbd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Mon, 8 Jan 2024 20:04:23 +0100 Subject: [PATCH 10/25] feat[logging]: whitelisting roles so their messages are not logged --- src/commands/logging/$config.ts | 39 +++++++++++++++- src/commands/logging/config.ts | 15 ++++-- src/commands/logging/whitelist/add.ts | 43 +++++++++++++++++ src/commands/logging/whitelist/remove.ts | 59 ++++++++++++++++++++++++ src/commands/logging/whitelist/see.ts | 33 +++++++++++++ src/listeners/logging.ts | 44 +++++++++++------- src/schemas/loggingWhitelist.ts | 9 ++++ 7 files changed, 221 insertions(+), 21 deletions(-) create mode 100644 src/commands/logging/whitelist/add.ts create mode 100644 src/commands/logging/whitelist/remove.ts create mode 100644 src/commands/logging/whitelist/see.ts create mode 100644 src/schemas/loggingWhitelist.ts diff --git a/src/commands/logging/$config.ts b/src/commands/logging/$config.ts index 3f7b1d1..a78944e 100644 --- a/src/commands/logging/$config.ts +++ b/src/commands/logging/$config.ts @@ -2,7 +2,7 @@ import db from "../../db.ts"; import { Config } from "../../schemas/config.ts"; import { LoggingWhitelist } from "../../schemas/loggingWhitelist.ts"; import type { TextChannel, Guild, Role } from "discord.js"; -import { eq, sql } from "drizzle-orm"; +import { eq, and, sql } from "drizzle-orm"; const { placeholder } = sql; export enum LogMode { @@ -60,3 +60,40 @@ export function setLogging( export function getConfig({ id }: Guild) { return logModes.get(id); } + +export function getWhitelist({ id: guildId }: Guild) { + return whitelistRoles.all({ guildId }).map(({ id }) => id); +} + +export function whitelistRole({ id, guild: { id: guildId } }: Role) { + return whitelistRole_add.execute({ guildId, id }); +} + +// For some reason, simply returning the execute makes the roleDelete listener not work +export async function unwhitelistRole({ id, guild: { id: guildId } }: Role) { + return await whitelistRole_remove.execute({ guildId, id }); +} + +const whitelistRoles = db + .select({ id: LoggingWhitelist.roleId }) + .from(LoggingWhitelist) + .where(eq(LoggingWhitelist.guildId, placeholder("guildId"))) + .prepare(); + +const whitelistRole_add = db + .insert(LoggingWhitelist) + .values({ guildId: placeholder("guildId"), roleId: placeholder("id") }) + .onConflictDoNothing() + .returning() + .prepare(); + +const whitelistRole_remove = db + .delete(LoggingWhitelist) + .where( + and( + eq(LoggingWhitelist.guildId, placeholder("guildId")), + eq(LoggingWhitelist.roleId, placeholder("id")), + ), + ) + .returning() + .prepare(); diff --git a/src/commands/logging/config.ts b/src/commands/logging/config.ts index c4f2648..3c08b74 100644 --- a/src/commands/logging/config.ts +++ b/src/commands/logging/config.ts @@ -1,4 +1,4 @@ -import { LogMode, getConfig } from "./$config.ts"; +import { LogMode, getConfig, getWhitelist } from "./$config.ts"; import { ApplicationCommandType } from "discord.js"; import type { Command } from "djs-fsrouter"; @@ -7,12 +7,19 @@ export const type = ApplicationCommandType.ChatInput; const Config: Command = { description: "See the current config", async run(interaction) { - if (!interaction.guild) return; + const { guild } = interaction; + if (!guild) return; - const { mode, channel } = getConfig(interaction.guild) || {}; - const message = mode + const { mode, channel } = getConfig(guild) || {}; + let message = mode ? `Logging ${LogMode[mode].toLowerCase()}\nLogs channel: <#${channel}>` : "Logging messages is disabled."; + if (mode) { + const whitelist = getWhitelist(guild) + .map((roleId) => `<@&${roleId}>`) + .join(" "); + if (whitelist) message += `\n\n**Whitelisted roles:**\n${whitelist}`; + } interaction .reply({ ephemeral: true, diff --git a/src/commands/logging/whitelist/add.ts b/src/commands/logging/whitelist/add.ts new file mode 100644 index 0000000..8569d01 --- /dev/null +++ b/src/commands/logging/whitelist/add.ts @@ -0,0 +1,43 @@ +import { whitelistRole } from "../$config.ts"; + +import { + ApplicationCommandType, + ApplicationCommandOptionType, +} from "discord.js"; +import type { Command } from "djs-fsrouter"; + +export const type = ApplicationCommandType.ChatInput; +const Disable: Command = { + description: "Add a role to be ignored by the logging system", + dmPermission: false, + defaultMemberPermissions: "0", + options: [ + { + name: "role", + required: true, + type: ApplicationCommandOptionType.Role, + description: "The role to ignore.", + }, + ], + async run(interaction) { + const { guild, options } = interaction; + if (!guild) return; + + const role = await guild.roles.fetch(options.getRole("role", true).id); + if (!role) { + interaction + .reply({ + ephemeral: true, + content: "Error: failed to retrieve the role info.", + }) + .catch(console.error); + return; + } + + await whitelistRole(role); + interaction + .reply({ embeds: [{ description: `${role} has been whitelisted.` }] }) + .catch(console.error); + }, +}; +export default Disable; diff --git a/src/commands/logging/whitelist/remove.ts b/src/commands/logging/whitelist/remove.ts new file mode 100644 index 0000000..9bbbed6 --- /dev/null +++ b/src/commands/logging/whitelist/remove.ts @@ -0,0 +1,59 @@ +import { getWhitelist, unwhitelistRole } from "../$config.ts"; + +import { + ApplicationCommandType, + ApplicationCommandOptionType, + Role, + Collection, +} from "discord.js"; +import type { Command } from "djs-fsrouter"; + +export const type = ApplicationCommandType.ChatInput; +const Remove: Command = { + description: "Add a role to be ignored by the logging system", + options: [ + { + name: "role", + required: true, + type: ApplicationCommandOptionType.String, + description: "The role to stop ignoring.", + autocomplete: true, + }, + ], + async autocomplete(interaction) { + const { guild } = interaction; + if (!guild) return; + + const whitelist = getWhitelist(guild); + const roles = guild.roles.cache.filter(({ id }) => whitelist.includes(id)); + interaction.respond(rolesToChoices(roles)).catch(console.error); + }, + + async run(interaction) { + const { guild, options } = interaction; + if (!guild) return; + + const role = await guild.roles.fetch(options.getString("role", true)); + if (!role) { + interaction + .reply({ + ephemeral: true, + content: "Error: failed to retrieve the role info.", + }) + .catch(console.error); + return; + } + + await unwhitelistRole(role); + interaction + .reply({ embeds: [{ description: `${role} has been un-whitelisted.` }] }) + .catch(console.error); + }, +}; +export default Remove; + +function rolesToChoices(roles: Collection) { + return roles + .map((role) => ({ name: role.name, value: role.id })) + .sort((a, b) => (a.name === b.name ? 0 : a.name > b.name ? 1 : -1)); +} diff --git a/src/commands/logging/whitelist/see.ts b/src/commands/logging/whitelist/see.ts new file mode 100644 index 0000000..320c683 --- /dev/null +++ b/src/commands/logging/whitelist/see.ts @@ -0,0 +1,33 @@ +import { getWhitelist } from "../$config.ts"; + +import { + ApplicationCommandType, + ApplicationCommandOptionType, +} from "discord.js"; +import type { Command } from "djs-fsrouter"; + +export const type = ApplicationCommandType.ChatInput; +const Disable: Command = { + description: "Add a role to be ignored by the logging system", + dmPermission: false, + defaultMemberPermissions: "0", + async run(interaction) { + const { guild } = interaction; + if (!guild) return; + + const whitelist = getWhitelist(guild) + .map((roleId) => `<@&${roleId}>`) + .join("\n"); + interaction + .reply({ + embeds: [ + { + title: "Whitelisted roles", + description: whitelist || "*none*", + }, + ], + }) + .catch(console.error); + }, +}; +export default Disable; diff --git a/src/listeners/logging.ts b/src/listeners/logging.ts index 82aa2cc..f2b9382 100644 --- a/src/listeners/logging.ts +++ b/src/listeners/logging.ts @@ -1,19 +1,25 @@ -import { type Guild, Message, type APIEmbed } from "discord.js"; -import { LogMode, getConfig } from "../commands/logging/$config.ts"; +import { + type Guild, + Message, + type APIEmbed, + GuildMember, + type PartialMessage, +} from "discord.js"; +import { + LogMode, + getConfig, + getWhitelist, + unwhitelistRole, +} from "../commands/logging/$config.ts"; import type { Listener } from "../types/listener.ts"; export default [ { event: "messageDelete", async handler(message) { + if (message.partial || shouldIgnore(message)) return; const channel = await getLogChannel(message.guild, LogMode.DELETES); - if ( - !channel?.isTextBased() || - message.partial || - message.system || - message.author.bot - ) - return; + if (!channel?.isTextBased()) return; const embed = { ...msgDeletionEmbed(message), @@ -40,7 +46,7 @@ export default [ const embeds = ( await Promise.all(messages.map((msg) => msg.fetch(false))) ) - .filter(({ system, author }) => !system && !author.bot) + .filter((message) => !shouldIgnore(message)) .map(msgDeletionEmbed); channel @@ -63,13 +69,9 @@ export default [ { event: "messageUpdate", async handler(oldMessage, newMessage) { + if (oldMessage.partial || shouldIgnore(oldMessage)) return; const channel = await getLogChannel(oldMessage.guild, LogMode.EDITS); - if ( - !channel?.isTextBased() || - oldMessage.partial || - oldMessage.author.bot - ) - return; + if (!channel?.isTextBased()) return; const { author, content: oldContent } = oldMessage; channel @@ -93,8 +95,18 @@ export default [ .catch(console.error); }, }, + { + event: "roleDelete", + handler: unwhitelistRole, + }, ] as Listener[]; +function shouldIgnore({ system, author, member }: Message) { + if (!member || author.bot || system) return true; + const whitelist = getWhitelist(member.guild); + return member.roles.cache.some((_, id) => whitelist.includes(id)); +} + function getLogChannel(guild: Guild | null, requiredMode: LogMode) { if (!guild) return null; const config = getConfig(guild); diff --git a/src/schemas/loggingWhitelist.ts b/src/schemas/loggingWhitelist.ts new file mode 100644 index 0000000..f12885a --- /dev/null +++ b/src/schemas/loggingWhitelist.ts @@ -0,0 +1,9 @@ +import { sqliteTable, text } from "drizzle-orm/sqlite-core"; +import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; + +export const LoggingWhitelist = sqliteTable("LoggingWhitelist", { + guildId: text("guildId").primaryKey(), + roleId: text("roleId").primaryKey(), +}); +export type LoggingWhitelistSelect = InferSelectModel; +export type LoggingWhitelistInsert = InferInsertModel; From c14500cfd97283739b3500bf15d3591b7d960e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Wed, 17 Jan 2024 15:53:30 +0100 Subject: [PATCH 11/25] doc: some doc for the logging system --- src/listeners/logging.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/listeners/logging.ts b/src/listeners/logging.ts index f2b9382..7ec6947 100644 --- a/src/listeners/logging.ts +++ b/src/listeners/logging.ts @@ -101,12 +101,23 @@ export default [ }, ] as Listener[]; +/** + * Tells if the logging system should ignore the given message. + * @param message The message to analyse + * @returns + */ function shouldIgnore({ system, author, member }: Message) { if (!member || author.bot || system) return true; const whitelist = getWhitelist(member.guild); return member.roles.cache.some((_, id) => whitelist.includes(id)); } +/** + * Gets the logging channel conditionally to a logging mode. + * @param guild + * @param requiredMode + * @returns null if the required mode isn't met, otherwise a Promise resolving to the channel + */ function getLogChannel(guild: Guild | null, requiredMode: LogMode) { if (!guild) return null; const config = getConfig(guild); @@ -115,6 +126,11 @@ function getLogChannel(guild: Guild | null, requiredMode: LogMode) { return guild.channels.fetch(config.channel); } +/** + * Generates a deletion log for the provided message + * @param message The message to log + * @returns The embed + */ function msgDeletionEmbed({ content, author, attachments }: Message): APIEmbed { let attachmentN = 0; return { From 41a1cb0d4cdc43822f5ae1d7e1824a478837a623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Wed, 17 Jan 2024 16:01:35 +0100 Subject: [PATCH 12/25] feat[logging]: purge command to remove logs from a given user --- src/commands/logging/clear.ts | 109 ++++++++++++++++++++++++++++++++++ src/listeners/logging.ts | 9 ++- 2 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/commands/logging/clear.ts diff --git a/src/commands/logging/clear.ts b/src/commands/logging/clear.ts new file mode 100644 index 0000000..a46a7b6 --- /dev/null +++ b/src/commands/logging/clear.ts @@ -0,0 +1,109 @@ +import { getConfig } from "./$config.ts"; + +import { + ApplicationCommandOptionType, + ApplicationCommandType, + Message, + User, +} from "discord.js"; +import type { Command } from "djs-fsrouter"; +import { deleteColor, editColor } from "../../listeners/logging.ts"; + +const _14_DAYS = 1209500000; + +export const type = ApplicationCommandType.ChatInput; +const Config: Command = { + description: "Remove all logged messages of a given user up to 14 days", + options: [ + { + name: "user", + required: true, + type: ApplicationCommandOptionType.User, + description: "The user to purge from our records", + }, + ], + async run(interaction) { + const { guild } = interaction; + if (!guild) return; + + await interaction.deferReply().catch(console.error); + const { channel } = getConfig(guild) || {}; + if (!channel) + return interaction + .editReply("Error: No logging channel has been set.") + .catch(console.error); + const logs = await guild.channels.fetch(channel); + if (!logs || !logs.isTextBased()) + return interaction + .editReply("Error: Could not retrieve the logging channel.") + .catch(console.error); + + const target = interaction.options.getUser("user", true); + const targetMention = target.toString(); + const now = Date.now(); + const me = await guild.members.fetchMe(); + let chunk = await logs.messages.fetch({ limit: 100, cache: false }); + let last = chunk.last(); + const promises = []; + const toDelete: Message[] = []; + let bulkPurges = 0; + + while (last) { + chunk = chunk.filter((message) => { + const { member, embeds, createdTimestamp } = message; + if ( + member !== me || + !embeds.length || + now - createdTimestamp > _14_DAYS + ) + return false; + + const [{ description: embed, color }] = embeds; + if (!embed || (color !== deleteColor && color !== editColor)) + return false; + if (embeds.length > 1) { + bulkPurges++; + promises.push(purgeBulk(target, message)); + return false; + } + const mentionPos = embed.indexOf(targetMention); + if (mentionPos !== -1 && mentionPos < embed.indexOf("\n")) return true; + return false; + }); + + if (chunk.size) toDelete.push(...chunk.values()); + + if (now - last.createdTimestamp > _14_DAYS) break; + + chunk = await logs.messages.fetch({ + before: last.id, + limit: 100, + cache: false, + }); + last = chunk.last(); + } + + for (let i = 0; i < toDelete.length; i += 100) + promises.push(logs.bulkDelete(toDelete.slice(i, i + 100))); + + await Promise.allSettled(promises); + interaction + .editReply( + `Erased ${toDelete.length} logs and purged ${bulkPurges} bulk logs.`, + ) + .catch(console.error); + }, +}; +export default Config; + +/** + * Purges logs of a given user from a bulk log + * @param user The user to erase + * @param message The bulk log + * @returns The number of logs removed + */ +function purgeBulk({ tag }: User, message: Message) { + const embeds = message.embeds.filter(({ author }) => author?.name !== tag); + if (embeds.length !== message.embeds.length) + return embeds.length ? message.edit({ embeds }) : message.delete(); +} diff --git a/src/listeners/logging.ts b/src/listeners/logging.ts index 7ec6947..388717f 100644 --- a/src/listeners/logging.ts +++ b/src/listeners/logging.ts @@ -13,6 +13,9 @@ import { } from "../commands/logging/$config.ts"; import type { Listener } from "../types/listener.ts"; +export const deleteColor = 0xdd4444; +export const editColor = 0xdd6d0c; + export default [ { event: "messageDelete", @@ -53,7 +56,7 @@ export default [ .send({ embeds: [ { - color: 0xdd4444, + color: deleteColor, description: `**🗑️ ${embeds.length} messages bulk-deleted in ${channel}**`, timestamp: new Date().toISOString(), }, @@ -78,7 +81,7 @@ export default [ .send({ embeds: [ { - color: 0xdd6d0c, + color: editColor, author: { name: author.tag, icon_url: author.avatarURL() || undefined, @@ -134,7 +137,7 @@ function getLogChannel(guild: Guild | null, requiredMode: LogMode) { function msgDeletionEmbed({ content, author, attachments }: Message): APIEmbed { let attachmentN = 0; return { - color: 0xdd4444, + color: deleteColor, author: { name: author.tag, icon_url: author.avatarURL() || undefined }, description: content, fields: attachments.size From 90e615de08ec8ed859b09ff1647341493930ee1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 18 Jan 2024 18:21:53 +0100 Subject: [PATCH 13/25] fix[logging]: check for partial messages Instead of stupidly trying to fetch deleted messages, tell the compiler the filter ensures they aren't partial (which they will never be as long as we have the MessageContent intent) --- src/listeners/logging.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/listeners/logging.ts b/src/listeners/logging.ts index 388717f..0d9b20a 100644 --- a/src/listeners/logging.ts +++ b/src/listeners/logging.ts @@ -46,10 +46,11 @@ export default [ ); if (!channel?.isTextBased()) return; - const embeds = ( - await Promise.all(messages.map((msg) => msg.fetch(false))) - ) - .filter((message) => !shouldIgnore(message)) + const embeds = messages + .filter( + (message): message is Message => + !message.partial && !shouldIgnore(message), + ) .map(msgDeletionEmbed); channel From e71e6b715ea920de4a229197361b794facaef855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 18 Jan 2024 18:25:53 +0100 Subject: [PATCH 14/25] fix[logging]: "0 messages bulk-deleted" Happenning when a bulk delete only includes bots and ignored users --- src/listeners/logging.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/listeners/logging.ts b/src/listeners/logging.ts index 0d9b20a..48bb643 100644 --- a/src/listeners/logging.ts +++ b/src/listeners/logging.ts @@ -53,6 +53,8 @@ export default [ ) .map(msgDeletionEmbed); + if (!embeds.length) return; + channel .send({ embeds: [ From 4b9682fcb237ec0767ba9a319ed6641b419a0987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 18 Jan 2024 18:41:43 +0100 Subject: [PATCH 15/25] refactor: shorten /clear handler --- src/commands/logging/clear.ts | 49 +++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/commands/logging/clear.ts b/src/commands/logging/clear.ts index a46a7b6..b9c49c0 100644 --- a/src/commands/logging/clear.ts +++ b/src/commands/logging/clear.ts @@ -3,6 +3,7 @@ import { getConfig } from "./$config.ts"; import { ApplicationCommandOptionType, ApplicationCommandType, + GuildMessageManager, Message, User, } from "discord.js"; @@ -40,23 +41,15 @@ const Config: Command = { const target = interaction.options.getUser("user", true); const targetMention = target.toString(); - const now = Date.now(); const me = await guild.members.fetchMe(); - let chunk = await logs.messages.fetch({ limit: 100, cache: false }); - let last = chunk.last(); const promises = []; const toDelete: Message[] = []; let bulkPurges = 0; - while (last) { - chunk = chunk.filter((message) => { - const { member, embeds, createdTimestamp } = message; - if ( - member !== me || - !embeds.length || - now - createdTimestamp > _14_DAYS - ) - return false; + for await (const chunk of fetchTill14days(logs.messages)) { + const targetLogs = chunk.filter((message) => { + const { member, embeds } = message; + if (member !== me || !embeds.length) return false; const [{ description: embed, color }] = embeds; if (!embed || (color !== deleteColor && color !== editColor)) @@ -68,19 +61,9 @@ const Config: Command = { } const mentionPos = embed.indexOf(targetMention); if (mentionPos !== -1 && mentionPos < embed.indexOf("\n")) return true; - return false; }); - if (chunk.size) toDelete.push(...chunk.values()); - - if (now - last.createdTimestamp > _14_DAYS) break; - - chunk = await logs.messages.fetch({ - before: last.id, - limit: 100, - cache: false, - }); - last = chunk.last(); + if (targetLogs.size) toDelete.push(...targetLogs.values()); } for (let i = 0; i < toDelete.length; i += 100) @@ -96,6 +79,26 @@ const Config: Command = { }; export default Config; +async function* fetchTill14days(messageManager: GuildMessageManager) { + const now = Date.now(); + let chunk = await messageManager.fetch({ limit: 100, cache: false }); + let last = chunk.last(); + while (last) { + yield chunk.filter( + ({ createdTimestamp }) => now - createdTimestamp < _14_DAYS, + ); + + if (now - last.createdTimestamp > _14_DAYS) return; + + chunk = await messageManager.fetch({ + before: last.id, + limit: 100, + cache: false, + }); + last = chunk.last(); + } +} + /** * Purges logs of a given user from a bulk log * @param user The user to erase From 5ebf5ad869cc51e1b850903b584c330225067583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Fri, 19 Jan 2024 23:02:15 +0100 Subject: [PATCH 16/25] Update djs-fsrouter This was a actually required change for previous commits but they weren't published in npm yet --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4c1cd76..86cf72f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "bufferutil": "^4.0.7", "cheerio": "^1.0.0-rc.12", "discord.js": "^14.13.0", - "djs-fsrouter": "^0.0.10", + "djs-fsrouter": "0.0.11", "drizzle-orm": "^0.28.5", "entities-decode": "^2.0.0", "erlpack": "^0.1.4", From d5fa314e9cb5099267afa990d6184d660ee67f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Tue, 23 Jan 2024 19:44:21 +0100 Subject: [PATCH 17/25] Update djs-fsrouter --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 86cf72f..e133497 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "bufferutil": "^4.0.7", "cheerio": "^1.0.0-rc.12", "discord.js": "^14.13.0", - "djs-fsrouter": "0.0.11", + "djs-fsrouter": "0.0.12", "drizzle-orm": "^0.28.5", "entities-decode": "^2.0.0", "erlpack": "^0.1.4", From 6c1a108f2dd9afbe6c3d9a32564f3347e209cc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Tue, 6 Feb 2024 00:42:43 +0100 Subject: [PATCH 18/25] [logging] Remove all commands Plan is to make a centralized way to configure the bot --- src/commands/logging/$info.js | 5 - src/commands/logging/clear.ts | 112 ------------------ src/commands/logging/config.ts | 31 ----- src/commands/logging/disable.ts | 18 --- src/commands/logging/enable.ts | 51 -------- src/commands/logging/whitelist/add.ts | 43 ------- src/commands/logging/whitelist/remove.ts | 59 --------- src/commands/logging/whitelist/see.ts | 33 ------ src/listeners/logging.ts | 10 +- .../logging/$config.ts => logging.ts} | 6 +- 10 files changed, 5 insertions(+), 363 deletions(-) delete mode 100644 src/commands/logging/$info.js delete mode 100644 src/commands/logging/clear.ts delete mode 100644 src/commands/logging/config.ts delete mode 100644 src/commands/logging/disable.ts delete mode 100644 src/commands/logging/enable.ts delete mode 100644 src/commands/logging/whitelist/add.ts delete mode 100644 src/commands/logging/whitelist/remove.ts delete mode 100644 src/commands/logging/whitelist/see.ts rename src/{commands/logging/$config.ts => logging.ts} (93%) diff --git a/src/commands/logging/$info.js b/src/commands/logging/$info.js deleted file mode 100644 index 27bcff0..0000000 --- a/src/commands/logging/$info.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - description: "Handles logging deleted & edited messages", - dmPermission: false, - defaultMemberPermissions: "0", -}; diff --git a/src/commands/logging/clear.ts b/src/commands/logging/clear.ts deleted file mode 100644 index b9c49c0..0000000 --- a/src/commands/logging/clear.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { getConfig } from "./$config.ts"; - -import { - ApplicationCommandOptionType, - ApplicationCommandType, - GuildMessageManager, - Message, - User, -} from "discord.js"; -import type { Command } from "djs-fsrouter"; -import { deleteColor, editColor } from "../../listeners/logging.ts"; - -const _14_DAYS = 1209500000; - -export const type = ApplicationCommandType.ChatInput; -const Config: Command = { - description: "Remove all logged messages of a given user up to 14 days", - options: [ - { - name: "user", - required: true, - type: ApplicationCommandOptionType.User, - description: "The user to purge from our records", - }, - ], - async run(interaction) { - const { guild } = interaction; - if (!guild) return; - - await interaction.deferReply().catch(console.error); - const { channel } = getConfig(guild) || {}; - if (!channel) - return interaction - .editReply("Error: No logging channel has been set.") - .catch(console.error); - const logs = await guild.channels.fetch(channel); - if (!logs || !logs.isTextBased()) - return interaction - .editReply("Error: Could not retrieve the logging channel.") - .catch(console.error); - - const target = interaction.options.getUser("user", true); - const targetMention = target.toString(); - const me = await guild.members.fetchMe(); - const promises = []; - const toDelete: Message[] = []; - let bulkPurges = 0; - - for await (const chunk of fetchTill14days(logs.messages)) { - const targetLogs = chunk.filter((message) => { - const { member, embeds } = message; - if (member !== me || !embeds.length) return false; - - const [{ description: embed, color }] = embeds; - if (!embed || (color !== deleteColor && color !== editColor)) - return false; - if (embeds.length > 1) { - bulkPurges++; - promises.push(purgeBulk(target, message)); - return false; - } - const mentionPos = embed.indexOf(targetMention); - if (mentionPos !== -1 && mentionPos < embed.indexOf("\n")) return true; - }); - - if (targetLogs.size) toDelete.push(...targetLogs.values()); - } - - for (let i = 0; i < toDelete.length; i += 100) - promises.push(logs.bulkDelete(toDelete.slice(i, i + 100))); - - await Promise.allSettled(promises); - interaction - .editReply( - `Erased ${toDelete.length} logs and purged ${bulkPurges} bulk logs.`, - ) - .catch(console.error); - }, -}; -export default Config; - -async function* fetchTill14days(messageManager: GuildMessageManager) { - const now = Date.now(); - let chunk = await messageManager.fetch({ limit: 100, cache: false }); - let last = chunk.last(); - while (last) { - yield chunk.filter( - ({ createdTimestamp }) => now - createdTimestamp < _14_DAYS, - ); - - if (now - last.createdTimestamp > _14_DAYS) return; - - chunk = await messageManager.fetch({ - before: last.id, - limit: 100, - cache: false, - }); - last = chunk.last(); - } -} - -/** - * Purges logs of a given user from a bulk log - * @param user The user to erase - * @param message The bulk log - * @returns The number of logs removed - */ -function purgeBulk({ tag }: User, message: Message) { - const embeds = message.embeds.filter(({ author }) => author?.name !== tag); - if (embeds.length !== message.embeds.length) - return embeds.length ? message.edit({ embeds }) : message.delete(); -} diff --git a/src/commands/logging/config.ts b/src/commands/logging/config.ts deleted file mode 100644 index 3c08b74..0000000 --- a/src/commands/logging/config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { LogMode, getConfig, getWhitelist } from "./$config.ts"; - -import { ApplicationCommandType } from "discord.js"; -import type { Command } from "djs-fsrouter"; - -export const type = ApplicationCommandType.ChatInput; -const Config: Command = { - description: "See the current config", - async run(interaction) { - const { guild } = interaction; - if (!guild) return; - - const { mode, channel } = getConfig(guild) || {}; - let message = mode - ? `Logging ${LogMode[mode].toLowerCase()}\nLogs channel: <#${channel}>` - : "Logging messages is disabled."; - if (mode) { - const whitelist = getWhitelist(guild) - .map((roleId) => `<@&${roleId}>`) - .join(" "); - if (whitelist) message += `\n\n**Whitelisted roles:**\n${whitelist}`; - } - interaction - .reply({ - ephemeral: true, - embeds: [{ description: message }], - }) - .catch(console.error); - }, -}; -export default Config; diff --git a/src/commands/logging/disable.ts b/src/commands/logging/disable.ts deleted file mode 100644 index b7e3b36..0000000 --- a/src/commands/logging/disable.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { LogMode, setLogging } from "./$config.ts"; - -import { ApplicationCommandType } from "discord.js"; -import type { Command } from "djs-fsrouter"; - -export const type = ApplicationCommandType.ChatInput; -const Disable: Command = { - description: "Disable message logging", - async run(interaction) { - if (!interaction.guild) return; - - await setLogging(interaction.guild, LogMode.NONE); - interaction - .reply({ ephemeral: true, content: "Logging disabled." }) - .catch(console.error); - }, -}; -export default Disable; diff --git a/src/commands/logging/enable.ts b/src/commands/logging/enable.ts deleted file mode 100644 index df6a205..0000000 --- a/src/commands/logging/enable.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { LogMode, setLogging } from "./$config.ts"; - -import { - ApplicationCommandType, - ApplicationCommandOptionType, - ChannelType, -} from "discord.js"; -import type { Command } from "djs-fsrouter"; - -export const type = ApplicationCommandType.ChatInput; -const Enable: Command = { - description: "Enable message logging", - options: [ - { - name: "log", - required: true, - type: ApplicationCommandOptionType.Integer, - description: "What should be logged?", - choices: [ - { name: "Deletions only", value: LogMode.DELETES }, - { - name: "Deletions & edits", - value: LogMode.DELETES | LogMode.EDITS, - }, - ], - }, - { - name: "channel", - required: true, - type: ApplicationCommandOptionType.Channel, - channelTypes: [ChannelType.GuildText], - description: "The channel where the logs will be sent.", - }, - ], - async run(interaction) { - if (!interaction.guild) return; - - const mode = interaction.options.getInteger("log", true); - const channel = interaction.options.getChannel("channel", true, [ - ChannelType.GuildText, - ]); - await setLogging(interaction.guild, { mode, channel }); - interaction - .reply({ - ephemeral: true, - content: `Logging set to ${LogMode[mode]} in ${channel}`, - }) - .catch(console.error); - }, -}; -export default Enable; diff --git a/src/commands/logging/whitelist/add.ts b/src/commands/logging/whitelist/add.ts deleted file mode 100644 index 8569d01..0000000 --- a/src/commands/logging/whitelist/add.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { whitelistRole } from "../$config.ts"; - -import { - ApplicationCommandType, - ApplicationCommandOptionType, -} from "discord.js"; -import type { Command } from "djs-fsrouter"; - -export const type = ApplicationCommandType.ChatInput; -const Disable: Command = { - description: "Add a role to be ignored by the logging system", - dmPermission: false, - defaultMemberPermissions: "0", - options: [ - { - name: "role", - required: true, - type: ApplicationCommandOptionType.Role, - description: "The role to ignore.", - }, - ], - async run(interaction) { - const { guild, options } = interaction; - if (!guild) return; - - const role = await guild.roles.fetch(options.getRole("role", true).id); - if (!role) { - interaction - .reply({ - ephemeral: true, - content: "Error: failed to retrieve the role info.", - }) - .catch(console.error); - return; - } - - await whitelistRole(role); - interaction - .reply({ embeds: [{ description: `${role} has been whitelisted.` }] }) - .catch(console.error); - }, -}; -export default Disable; diff --git a/src/commands/logging/whitelist/remove.ts b/src/commands/logging/whitelist/remove.ts deleted file mode 100644 index 9bbbed6..0000000 --- a/src/commands/logging/whitelist/remove.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { getWhitelist, unwhitelistRole } from "../$config.ts"; - -import { - ApplicationCommandType, - ApplicationCommandOptionType, - Role, - Collection, -} from "discord.js"; -import type { Command } from "djs-fsrouter"; - -export const type = ApplicationCommandType.ChatInput; -const Remove: Command = { - description: "Add a role to be ignored by the logging system", - options: [ - { - name: "role", - required: true, - type: ApplicationCommandOptionType.String, - description: "The role to stop ignoring.", - autocomplete: true, - }, - ], - async autocomplete(interaction) { - const { guild } = interaction; - if (!guild) return; - - const whitelist = getWhitelist(guild); - const roles = guild.roles.cache.filter(({ id }) => whitelist.includes(id)); - interaction.respond(rolesToChoices(roles)).catch(console.error); - }, - - async run(interaction) { - const { guild, options } = interaction; - if (!guild) return; - - const role = await guild.roles.fetch(options.getString("role", true)); - if (!role) { - interaction - .reply({ - ephemeral: true, - content: "Error: failed to retrieve the role info.", - }) - .catch(console.error); - return; - } - - await unwhitelistRole(role); - interaction - .reply({ embeds: [{ description: `${role} has been un-whitelisted.` }] }) - .catch(console.error); - }, -}; -export default Remove; - -function rolesToChoices(roles: Collection) { - return roles - .map((role) => ({ name: role.name, value: role.id })) - .sort((a, b) => (a.name === b.name ? 0 : a.name > b.name ? 1 : -1)); -} diff --git a/src/commands/logging/whitelist/see.ts b/src/commands/logging/whitelist/see.ts deleted file mode 100644 index 320c683..0000000 --- a/src/commands/logging/whitelist/see.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getWhitelist } from "../$config.ts"; - -import { - ApplicationCommandType, - ApplicationCommandOptionType, -} from "discord.js"; -import type { Command } from "djs-fsrouter"; - -export const type = ApplicationCommandType.ChatInput; -const Disable: Command = { - description: "Add a role to be ignored by the logging system", - dmPermission: false, - defaultMemberPermissions: "0", - async run(interaction) { - const { guild } = interaction; - if (!guild) return; - - const whitelist = getWhitelist(guild) - .map((roleId) => `<@&${roleId}>`) - .join("\n"); - interaction - .reply({ - embeds: [ - { - title: "Whitelisted roles", - description: whitelist || "*none*", - }, - ], - }) - .catch(console.error); - }, -}; -export default Disable; diff --git a/src/listeners/logging.ts b/src/listeners/logging.ts index 48bb643..040ba03 100644 --- a/src/listeners/logging.ts +++ b/src/listeners/logging.ts @@ -1,16 +1,10 @@ -import { - type Guild, - Message, - type APIEmbed, - GuildMember, - type PartialMessage, -} from "discord.js"; +import { type Guild, Message, type APIEmbed } from "discord.js"; import { LogMode, getConfig, getWhitelist, unwhitelistRole, -} from "../commands/logging/$config.ts"; +} from "../logging.ts"; import type { Listener } from "../types/listener.ts"; export const deleteColor = 0xdd4444; diff --git a/src/commands/logging/$config.ts b/src/logging.ts similarity index 93% rename from src/commands/logging/$config.ts rename to src/logging.ts index a78944e..63ec5ce 100644 --- a/src/commands/logging/$config.ts +++ b/src/logging.ts @@ -1,6 +1,6 @@ -import db from "../../db.ts"; -import { Config } from "../../schemas/config.ts"; -import { LoggingWhitelist } from "../../schemas/loggingWhitelist.ts"; +import db from "./db.ts"; +import { Config } from "./schemas/config.ts"; +import { LoggingWhitelist } from "./schemas/loggingWhitelist.ts"; import type { TextChannel, Guild, Role } from "discord.js"; import { eq, and, sql } from "drizzle-orm"; const { placeholder } = sql; From 98d770ea4689038acbb62a540540c8dd4360e750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 8 Feb 2024 04:55:33 +0100 Subject: [PATCH 19/25] fix: LoggingWhitelist primary key --- src/schemas/loggingWhitelist.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/schemas/loggingWhitelist.ts b/src/schemas/loggingWhitelist.ts index f12885a..90de356 100644 --- a/src/schemas/loggingWhitelist.ts +++ b/src/schemas/loggingWhitelist.ts @@ -1,9 +1,15 @@ -import { sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { sqliteTable, text, primaryKey } from "drizzle-orm/sqlite-core"; import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; -export const LoggingWhitelist = sqliteTable("LoggingWhitelist", { - guildId: text("guildId").primaryKey(), - roleId: text("roleId").primaryKey(), -}); +export const LoggingWhitelist = sqliteTable( + "LoggingWhitelist", + { + guildId: text("guildId"), + roleId: text("roleId"), + }, + (table) => ({ + pk: primaryKey(table.guildId, table.roleId), + }), +); export type LoggingWhitelistSelect = InferSelectModel; export type LoggingWhitelistInsert = InferInsertModel; From c55013c1f2999fab7bf863dd13dd5a2ac04be435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 8 Feb 2024 04:59:24 +0100 Subject: [PATCH 20/25] ref: Biome complained --- src/commands/purge.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/purge.ts b/src/commands/purge.ts index c863936..eeae24c 100644 --- a/src/commands/purge.ts +++ b/src/commands/purge.ts @@ -33,7 +33,8 @@ const Purge: Command = { if (!channel) { return reply("Error: could not fetch the channel"); - } else if (channel.isDMBased()) { + } + if (channel.isDMBased()) { return reply("Error: can't do that in DMs!"); } const { From 3fe433eeec759de089c7ff9beca81693aff4ca8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 8 Feb 2024 05:11:50 +0100 Subject: [PATCH 21/25] fix: restore wrongly deleted command --- src/commands/logging/clear.ts | 112 ++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/commands/logging/clear.ts diff --git a/src/commands/logging/clear.ts b/src/commands/logging/clear.ts new file mode 100644 index 0000000..7cf31fc --- /dev/null +++ b/src/commands/logging/clear.ts @@ -0,0 +1,112 @@ +import { getConfig } from "../../logging.ts"; + +import { + ApplicationCommandOptionType, + ApplicationCommandType, + GuildMessageManager, + Message, + User, +} from "discord.js"; +import type { Command } from "djs-fsrouter"; +import { deleteColor, editColor } from "../../listeners/logging.ts"; + +const _14_DAYS = 1209500000; + +export const type = ApplicationCommandType.ChatInput; +const Config: Command = { + description: "Remove all logged messages of a given user up to 14 days", + options: [ + { + name: "user", + required: true, + type: ApplicationCommandOptionType.User, + description: "The user to purge from our records", + }, + ], + async run(interaction) { + const { guild } = interaction; + if (!guild) return; + + await interaction.deferReply().catch(console.error); + const { channel } = getConfig(guild) || {}; + if (!channel) + return interaction + .editReply("Error: No logging channel has been set.") + .catch(console.error); + const logs = await guild.channels.fetch(channel); + if (!logs || !logs.isTextBased()) + return interaction + .editReply("Error: Could not retrieve the logging channel.") + .catch(console.error); + + const target = interaction.options.getUser("user", true); + const targetMention = target.toString(); + const me = await guild.members.fetchMe(); + const promises = []; + const toDelete: Message[] = []; + let bulkPurges = 0; + + for await (const chunk of fetchTill14days(logs.messages)) { + const targetLogs = chunk.filter((message) => { + const { member, embeds } = message; + if (member !== me || !embeds.length) return false; + + const [{ description: embed, color }] = embeds; + if (!embed || (color !== deleteColor && color !== editColor)) + return false; + if (embeds.length > 1) { + bulkPurges++; + promises.push(purgeBulk(target, message)); + return false; + } + const mentionPos = embed.indexOf(targetMention); + if (mentionPos !== -1 && mentionPos < embed.indexOf("\n")) return true; + }); + + if (targetLogs.size) toDelete.push(...targetLogs.values()); + } + + for (let i = 0; i < toDelete.length; i += 100) + promises.push(logs.bulkDelete(toDelete.slice(i, i + 100))); + + await Promise.allSettled(promises); + interaction + .editReply( + `Erased ${toDelete.length} logs and purged ${bulkPurges} bulk logs.`, + ) + .catch(console.error); + }, +}; +export default Config; + +async function* fetchTill14days(messageManager: GuildMessageManager) { + const now = Date.now(); + let chunk = await messageManager.fetch({ limit: 100, cache: false }); + let last = chunk.last(); + while (last) { + yield chunk.filter( + ({ createdTimestamp }) => now - createdTimestamp < _14_DAYS, + ); + + if (now - last.createdTimestamp > _14_DAYS) return; + + chunk = await messageManager.fetch({ + before: last.id, + limit: 100, + cache: false, + }); + last = chunk.last(); + } +} + +/** + * Purges logs of a given user from a bulk log + * @param user The user to erase + * @param message The bulk log + * @returns The number of logs removed + */ +function purgeBulk({ tag }: User, message: Message) { + const embeds = message.embeds.filter(({ author }) => author?.name !== tag); + if (embeds.length !== message.embeds.length) + return embeds.length ? message.edit({ embeds }) : message.delete(); +} From 47e223b4c0607b12165e69b9bf285e50dc16ba35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 8 Feb 2024 05:37:38 +0100 Subject: [PATCH 22/25] refactor[logging]: remove unecessary caching --- src/logging.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 63ec5ce..a1d1797 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -22,17 +22,6 @@ export type LogConfig = channel: TextChannel["id"]; }; -const logModes = new Map(); -db.select() - .from(Config) - .then((configs) => { - for (const { id, loggingMode, loggingChannel } of configs) - logModes.set(id, { - mode: loggingMode || LogMode.NONE, - channel: loggingChannel, - }); - }); - export function setLogging( { id }: Guild, config: LogMode.NONE | { mode: LogMode; channel: TextChannel }, @@ -42,9 +31,6 @@ export function setLogging( if (config) { loggingMode = config.mode; loggingChannel = config.channel.id; - logModes.set(id, { mode: config.mode, channel: loggingChannel }); - } else { - logModes.set(id, { mode: LogMode.NONE, channel: null }); } return db @@ -57,8 +43,9 @@ export function setLogging( .returning(); } -export function getConfig({ id }: Guild) { - return logModes.get(id); +export function getConfig({ id }: { id: string }): LogConfig { + const { mode, channel } = selectConfig.all({ guildId: id })[0]; + return { mode: mode || LogMode.NONE, channel }; } export function getWhitelist({ id: guildId }: Guild) { @@ -74,6 +61,12 @@ export async function unwhitelistRole({ id, guild: { id: guildId } }: Role) { return await whitelistRole_remove.execute({ guildId, id }); } +const selectConfig = db + .select({ mode: Config.loggingMode, channel: Config.loggingChannel }) + .from(Config) + .where(eq(Config.id, placeholder("guildId"))) + .prepare(); + const whitelistRoles = db .select({ id: LoggingWhitelist.roleId }) .from(LoggingWhitelist) From 6c10f39c27326989800313aaae8efcb95185b107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 8 Feb 2024 05:41:03 +0100 Subject: [PATCH 23/25] fix: restore /logging description and permissions --- src/commands/logging/$info.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/commands/logging/$info.js diff --git a/src/commands/logging/$info.js b/src/commands/logging/$info.js new file mode 100644 index 0000000..8a43f03 --- /dev/null +++ b/src/commands/logging/$info.js @@ -0,0 +1,5 @@ +export default { + description: "Handles logging deleted & edited messages", + dmPermission: false, + defaultMemberPermissions: "0", +}; \ No newline at end of file From b3bbca7f3c4d52dabc1c1a3cad6ebd2b1c63c66a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 8 Feb 2024 06:04:23 +0100 Subject: [PATCH 24/25] refactor[logging]: move type defs to the types folder --- src/listeners/logging.ts | 8 ++------ src/logging.ts | 20 ++------------------ src/types/logging.ts | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 24 deletions(-) create mode 100644 src/types/logging.ts diff --git a/src/listeners/logging.ts b/src/listeners/logging.ts index 040ba03..1608d53 100644 --- a/src/listeners/logging.ts +++ b/src/listeners/logging.ts @@ -1,10 +1,6 @@ import { type Guild, Message, type APIEmbed } from "discord.js"; -import { - LogMode, - getConfig, - getWhitelist, - unwhitelistRole, -} from "../logging.ts"; +import { getConfig, getWhitelist, unwhitelistRole } from "../logging.ts"; +import { LogMode } from "../types/logging.ts"; import type { Listener } from "../types/listener.ts"; export const deleteColor = 0xdd4444; diff --git a/src/logging.ts b/src/logging.ts index a1d1797..5980178 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,27 +1,11 @@ import db from "./db.ts"; +import { LogMode, type LogConfig } from "./types/logging.ts"; import { Config } from "./schemas/config.ts"; import { LoggingWhitelist } from "./schemas/loggingWhitelist.ts"; import type { TextChannel, Guild, Role } from "discord.js"; import { eq, and, sql } from "drizzle-orm"; const { placeholder } = sql; -export enum LogMode { - NONE = 0, - DELETES = 1 << 0, - EDITS = 1 << 1, - "DELETES & EDITS" = DELETES | EDITS, -} - -export type LogConfig = - | { - mode: LogMode.NONE; - channel: null; - } - | { - mode: LogMode; - channel: TextChannel["id"]; - }; - export function setLogging( { id }: Guild, config: LogMode.NONE | { mode: LogMode; channel: TextChannel }, @@ -45,7 +29,7 @@ export function setLogging( export function getConfig({ id }: { id: string }): LogConfig { const { mode, channel } = selectConfig.all({ guildId: id })[0]; - return { mode: mode || LogMode.NONE, channel }; + return channel ? { mode, channel } : { mode: LogMode.NONE, channel }; } export function getWhitelist({ id: guildId }: Guild) { diff --git a/src/types/logging.ts b/src/types/logging.ts new file mode 100644 index 0000000..e72fb84 --- /dev/null +++ b/src/types/logging.ts @@ -0,0 +1,18 @@ +import type { TextChannel } from "discord.js"; + +export enum LogMode { + NONE = 0, + DELETES = 1 << 0, + EDITS = 1 << 1, + "DELETES & EDITS" = DELETES | EDITS, +} + +export type LogConfig = + | { + mode: LogMode.NONE; + channel: null; + } + | { + mode: LogMode; + channel: TextChannel["id"]; + }; From ac44fed80d26e9ffa97366907e256daca10049c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morg=C3=A2n=20von=20Bryl=C3=A2n?= Date: Thu, 8 Feb 2024 06:06:05 +0100 Subject: [PATCH 25/25] refactor[logging]: use LogMode type in the schema --- src/schemas/config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/schemas/config.ts b/src/schemas/config.ts index 34faf55..fb6e685 100644 --- a/src/schemas/config.ts +++ b/src/schemas/config.ts @@ -1,5 +1,6 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; +import { LogMode } from "../types/logging.ts"; export const Config = sqliteTable("guildConfig", { id: text("guildId").primaryKey().notNull(), @@ -11,7 +12,10 @@ export const Config = sqliteTable("guildConfig", { gatewayLeaveTitle: text("gatewayLeaveTitle"), gatewayLeaveContent: text("gatewayLeaveContent"), - loggingMode: integer("loggingMode", { mode: "number" }).default(0), + loggingMode: integer("loggingMode", { mode: "number" }) + .$type() + .notNull() + .default(LogMode.NONE), loggingChannel: text("loggingChannel").default(""), suggestionChannel: text("suggestionChannel"),