From 22f4d9699db35818f576bd4e9572ac593fb7a629 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Tue, 9 Jan 2024 19:45:29 -0800 Subject: [PATCH 01/51] Initial prefix commands management --- src/commands/index.ts | 2 + .../prefixCommands/functions/addCategory.ts | 97 +++ .../functions/addChannelPermission.ts | 130 ++++ .../prefixCommands/functions/addCommand.ts | 124 ++++ .../functions/addRolePermission.ts | 130 ++++ .../prefixCommands/functions/addVersion.ts | 103 +++ .../functions/deleteCategory.ts | 92 +++ .../prefixCommands/functions/deleteCommand.ts | 121 ++++ .../prefixCommands/functions/deleteContent.ts | 117 +++ .../prefixCommands/functions/deleteVersion.ts | 114 +++ .../functions/listCategories.ts | 57 ++ .../functions/listChannelPermissions.ts | 69 ++ .../prefixCommands/functions/listCommands.ts | 57 ++ .../functions/listRolePermissions.ts | 69 ++ .../prefixCommands/functions/listVersions.ts | 57 ++ .../functions/modifyCategory.ts | 96 +++ .../prefixCommands/functions/modifyCommand.ts | 126 ++++ .../prefixCommands/functions/modifyVersion.ts | 102 +++ .../functions/removeChannelPermission.ts | 125 ++++ .../functions/removeRolePermission.ts | 125 ++++ .../prefixCommands/functions/setContent.ts | 141 ++++ .../prefixCommands/functions/showContent.ts | 99 +++ .../prefixCommands/prefixCommands.ts | 673 ++++++++++++++++++ src/lib/index.ts | 1 + src/lib/schemas/prefixCommandSchemas.ts | 99 +++ 25 files changed, 2926 insertions(+) create mode 100644 src/commands/moderation/prefixCommands/functions/addCategory.ts create mode 100644 src/commands/moderation/prefixCommands/functions/addChannelPermission.ts create mode 100644 src/commands/moderation/prefixCommands/functions/addCommand.ts create mode 100644 src/commands/moderation/prefixCommands/functions/addRolePermission.ts create mode 100644 src/commands/moderation/prefixCommands/functions/addVersion.ts create mode 100644 src/commands/moderation/prefixCommands/functions/deleteCategory.ts create mode 100644 src/commands/moderation/prefixCommands/functions/deleteCommand.ts create mode 100644 src/commands/moderation/prefixCommands/functions/deleteContent.ts create mode 100644 src/commands/moderation/prefixCommands/functions/deleteVersion.ts create mode 100644 src/commands/moderation/prefixCommands/functions/listCategories.ts create mode 100644 src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts create mode 100644 src/commands/moderation/prefixCommands/functions/listCommands.ts create mode 100644 src/commands/moderation/prefixCommands/functions/listRolePermissions.ts create mode 100644 src/commands/moderation/prefixCommands/functions/listVersions.ts create mode 100644 src/commands/moderation/prefixCommands/functions/modifyCategory.ts create mode 100644 src/commands/moderation/prefixCommands/functions/modifyCommand.ts create mode 100644 src/commands/moderation/prefixCommands/functions/modifyVersion.ts create mode 100644 src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts create mode 100644 src/commands/moderation/prefixCommands/functions/removeRolePermission.ts create mode 100644 src/commands/moderation/prefixCommands/functions/setContent.ts create mode 100644 src/commands/moderation/prefixCommands/functions/showContent.ts create mode 100644 src/commands/moderation/prefixCommands/prefixCommands.ts create mode 100644 src/lib/schemas/prefixCommandSchemas.ts diff --git a/src/commands/index.ts b/src/commands/index.ts index e16ab038..5dfacd97 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -31,6 +31,7 @@ import commandTable from './moderation/commandTable'; import listRoleUsers from './moderation/listRoleUsers'; import clearMessages from './moderation/clearMessages'; import locate from './utils/locate/locate'; +import prefixCommands from './moderation/prefixCommands/prefixCommands'; const commandArray: SlashCommand[] = [ ping, @@ -65,6 +66,7 @@ const commandArray: SlashCommand[] = [ listRoleUsers, clearMessages, locate, + prefixCommands, ]; export default commandArray; diff --git a/src/commands/moderation/prefixCommands/functions/addCategory.ts b/src/commands/moderation/prefixCommands/functions/addCategory.ts new file mode 100644 index 00000000..823201a8 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/addCategory.ts @@ -0,0 +1,97 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandCategory, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Add Category - No Connection', + description: 'Could not connect to the database. Unable to add the prefix command category.', + color: Colors.Red, +}); + +const failedEmbed = (category: string) => makeEmbed({ + title: 'Prefix Commands - Add Category - Failed', + description: `Failed to add the prefix command category ${category}.`, + color: Colors.Red, +}); + +const alreadyExistsEmbed = (category: string) => makeEmbed({ + title: 'Prefix Commands - Add Category - Already exists', + description: `The prefix command category ${category} already exists. Not adding again.`, + color: Colors.Red, +}); + +const successEmbed = (category: string) => makeEmbed({ + title: `Prefix command category ${category} was added successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, category: string, emoji: string, categoryId: string) => makeEmbed({ + title: 'Prefix command category added', + fields: [ + { + name: 'Category', + value: category, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + { + name: 'Emoji', + value: emoji, + }, + ], + footer: { text: `Category ID: ${categoryId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Add Category - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleAddPrefixCommandCategory(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + } + + const name = interaction.options.getString('name')!; + const emoji = interaction.options.getString('emoji') || ''; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + return; + } + + const existingCategory = await PrefixCommandCategory.findOne({ name }); + + if (!existingCategory) { + const prefixCommandCategory = new PrefixCommandCategory({ + name, + emoji, + }); + try { + await prefixCommandCategory.save(); + await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, emoji, prefixCommandCategory.id)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to add a prefix command category ${name}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(name)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [alreadyExistsEmbed(name)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts new file mode 100644 index 00000000..3c35278f --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts @@ -0,0 +1,130 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandChannelPermission, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Add Channel Permission - No Connection', + description: 'Could not connect to the database. Unable to add the prefix command channel permission.', + color: Colors.Red, +}); + +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Add Channel Permission - No Command', + description: `Failed to add the prefix command channel permission for command ${command} as the command does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const noChannelEmbed = (channel: string) => makeEmbed({ + title: 'Prefix Commands - Add Channel Permission - No Channel', + description: `Failed to add the prefix command channel permission for channel <#${channel}> as the channel does not exist.`, + color: Colors.Red, +}); + +const failedEmbed = (command: string, channel: string, type: string) => makeEmbed({ + title: 'Prefix Commands - Add Channel Permission - Failed', + description: `Failed to add the ${type} prefix command channel permission for command ${command} and channel <#${channel}>.`, + color: Colors.Red, +}); + +const alreadyExistsEmbed = (command: string, channel: string) => makeEmbed({ + title: 'Prefix Commands - Add Channel Permission - Already exists', + description: `A prefix command channel permission for command ${command} and channel <#${channel}> already exists. Not adding again.`, + color: Colors.Red, +}); + +const successEmbed = (command: string, channel: string, type: string, channelPermissionId: string) => makeEmbed({ + title: `Prefix command channel ${type} permission added for command ${command} and channel <#${channel}>. ChannelPermission ID: ${channelPermissionId}`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, command: string, channel: string, type: string, commandId: string, channelPermissionId: string) => makeEmbed({ + title: 'Add prefix command channel permission', + fields: [ + { + name: 'Command', + value: command, + }, + { + name: 'Channel', + value: `<#${channel}>`, + }, + { + name: 'Type', + value: type, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + ], + footer: { text: `Command ID: ${commandId} - Channel Permission ID: ${channelPermissionId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Add Channel Permission - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleAddPrefixCommandChannelPermission(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const command = interaction.options.getString('command')!; + const channel = interaction.options.getString('channel')!; + const type = interaction.options.getString('type')!; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + let foundCommand = await PrefixCommand.find({ name: command }); + if (!foundCommand || foundCommand.length > 1) { + foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + } + if (!foundCommand || foundCommand.length > 1) { + await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + const { id: commandId } = foundCommand[0]; + + const foundChannel = interaction.guild.channels.resolve(channel); + if (!foundChannel) { + await interaction.reply({ embeds: [noChannelEmbed(channel)], ephemeral: true }); + return; + } + const { id: channelId } = foundChannel; + + const existingChannelPermission = await PrefixCommandChannelPermission.findOne({ commandId, channelId }); + if (!existingChannelPermission) { + const newChannelPermission = new PrefixCommandChannelPermission({ + commandId, + channelId, + type, + }); + try { + await newChannelPermission.save(); + await interaction.followUp({ embeds: [successEmbed(command, channel, type, newChannelPermission.id)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channel, type, commandId, newChannelPermission.id)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to add ${type} prefix command channel permission for command ${command} and channel <#${channel}>: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command, channel, type)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [alreadyExistsEmbed(command, channel)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts new file mode 100644 index 00000000..535108a1 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -0,0 +1,124 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandCategory } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Add Command - No Connection', + description: 'Could not connect to the database. Unable to add the prefix command.', + color: Colors.Red, +}); + +const failedEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Add Command - Failed', + description: `Failed to add the prefix command ${command}.`, + color: Colors.Red, +}); + +const categoryNotFoundEmbed = (category: string) => makeEmbed({ + title: 'Prefix Commands - Add Command - Category not found', + description: `The prefix command category ${category} does not exist. Please create it first.`, + color: Colors.Red, +}); + +const alreadyExistsEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Add Command - Already exists', + description: `The prefix command ${command} already exists. Not adding again.`, + color: Colors.Red, +}); + +const successEmbed = (command: string) => makeEmbed({ + title: `Prefix command ${command} was added successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, command: string, aliases: string[], isEmbed: boolean, embedColor: string, commandId: string) => makeEmbed({ + title: 'Prefix command added', + fields: [ + { + name: 'Command', + value: command, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + { + name: 'Aliases', + value: aliases.join(','), + }, + { + name: 'Is Embed', + value: isEmbed ? 'Yes' : 'No', + }, + { + name: 'Embed Color', + value: embedColor || '', + }, + ], + footer: { text: `Command ID: ${commandId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Add Command - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleAddPrefixCommand(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const name = interaction.options.getString('name')!; + const category = interaction.options.getString('category')!; + const aliasesString = interaction.options.getString('aliases') || ''; + const aliases = aliasesString !== '' ? aliasesString.split(',') : []; + const isEmbed = interaction.options.getBoolean('is_embed') || false; + const embedColor = interaction.options.getString('embed_color') || ''; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + const foundCategory = await PrefixCommandCategory.findOne({ name: category }); + if (!foundCategory) { + await interaction.followUp({ embeds: [categoryNotFoundEmbed(category)], ephemeral: true }); + return; + } + const { categoryId } = foundCategory; + const existingCommand = await PrefixCommand.findOne({ name }); + + if (!existingCommand) { + const prefixCommand = new PrefixCommand({ + name, + categoryId, + aliases, + isEmbed, + embedColor, + }); + try { + await prefixCommand.save(); + await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, aliases, isEmbed, embedColor, prefixCommand.id)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to add a prefix command category ${name}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(name)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [alreadyExistsEmbed(name)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts new file mode 100644 index 00000000..f3d55efc --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts @@ -0,0 +1,130 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandRolePermission, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Add Role Permission - No Connection', + description: 'Could not connect to the database. Unable to add the prefix command role permission.', + color: Colors.Red, +}); + +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Add Role Permission - No Command', + description: `Failed to add the prefix command role permission for command ${command} as the command does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const noRoleEmbed = (role: string) => makeEmbed({ + title: 'Prefix Commands - Add Role Permission - No Role', + description: `Failed to add the prefix command role permission for role ${role} as the role does not exist.`, + color: Colors.Red, +}); + +const failedEmbed = (command: string, roleName: string, type: string) => makeEmbed({ + title: 'Prefix Commands - Add Role Permission - Failed', + description: `Failed to add the ${type} prefix command role permission for command ${command} and role ${roleName}.`, + color: Colors.Red, +}); + +const alreadyExistsEmbed = (command: string, roleName: string) => makeEmbed({ + title: 'Prefix Commands - Add Role Permission - Already exists', + description: `A prefix command role permission for command ${command} and role ${roleName} already exists. Not adding again.`, + color: Colors.Red, +}); + +const successEmbed = (command: string, roleName: string, type: string, rolePermissionId: string) => makeEmbed({ + title: `Prefix command role ${type} permission added for command ${command} and role ${roleName}. RolePermission ID: ${rolePermissionId}`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, command: string, roleName: string, type: string, commandId: string, rolePermissionId: string) => makeEmbed({ + title: 'Add prefix command role permission', + fields: [ + { + name: 'Command', + value: command, + }, + { + name: 'Role', + value: roleName, + }, + { + name: 'Type', + value: type, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + ], + footer: { text: `Command ID: ${commandId} - Role Permission ID: ${rolePermissionId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Add Role Permission - No Mod Log', + description: 'I can\'t find the mod logs role. Please check the role still exists.', + color: Colors.Red, +}); + +export async function handleAddPrefixCommandRolePermission(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const command = interaction.options.getString('command')!; + const role = interaction.options.getString('role')!; + const type = interaction.options.getString('type')!; + const moderator = interaction.user; + + //Check if the mod logs role exists + const modLogsRole = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsRole) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + let foundCommand = await PrefixCommand.find({ name: command }); + if (!foundCommand || foundCommand.length > 1) { + foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + } + if (!foundCommand || foundCommand.length > 1) { + await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + const { id: commandId } = foundCommand[0]; + + const foundRole = interaction.guild.roles.resolve(role); + if (!foundRole) { + await interaction.reply({ embeds: [noRoleEmbed(role)], ephemeral: true }); + return; + } + const { id: roleId, name: roleName } = foundRole; + + const existingRolePermission = await PrefixCommandRolePermission.findOne({ commandId, roleId }); + if (!existingRolePermission) { + const newRolePermission = new PrefixCommandRolePermission({ + commandId, + roleId, + type, + }); + try { + await newRolePermission.save(); + await interaction.followUp({ embeds: [successEmbed(command, roleName, type, newRolePermission.id)], ephemeral: true }); + if (modLogsRole) { + try { + await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName, type, commandId, newRolePermission.id)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs role: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to add ${type} prefix command role permission for command ${command} and role ${roleName}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command, roleName, type)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [alreadyExistsEmbed(command, roleName)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/addVersion.ts b/src/commands/moderation/prefixCommands/functions/addVersion.ts new file mode 100644 index 00000000..329d8fb1 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/addVersion.ts @@ -0,0 +1,103 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Add Version - No Connection', + description: 'Could not connect to the database. Unable to add the prefix command version.', + color: Colors.Red, +}); + +const failedEmbed = (version: string) => makeEmbed({ + title: 'Prefix Commands - Add Version - Failed', + description: `Failed to add the prefix command version ${version}.`, + color: Colors.Red, +}); + +const alreadyExistsEmbed = (version: string) => makeEmbed({ + title: 'Prefix Commands - Add Version - Already exists', + description: `The prefix command version ${version} already exists. Not adding again.`, + color: Colors.Red, +}); + +const successEmbed = (version: string) => makeEmbed({ + title: `Prefix command version ${version} was added successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, version: string, emoji: string, enabled: boolean, versionId: string) => makeEmbed({ + title: 'Prefix command version added', + fields: [ + { + name: 'Version', + value: version, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + { + name: 'Emoji', + value: emoji, + }, + { + name: 'Enabled', + value: enabled ? 'Yes' : 'No', + }, + ], + footer: { text: `Version ID: ${versionId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Add Version - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleAddPrefixCommandVersion(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const name = interaction.options.getString('name')!; + const emoji = interaction.options.getString('emoji')!; + const enabled = interaction.options.getBoolean('is_enabled') || false; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + const existingVersion = await PrefixCommandVersion.findOne({ name }); + + if (!existingVersion) { + const prefixCommandVersion = new PrefixCommandVersion({ + name, + emoji, + enabled, + }); + try { + await prefixCommandVersion.save(); + await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, emoji, enabled, prefixCommandVersion.id)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to add a prefix command category ${name}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(name)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [alreadyExistsEmbed(name)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/deleteCategory.ts b/src/commands/moderation/prefixCommands/functions/deleteCategory.ts new file mode 100644 index 00000000..22259bc9 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/deleteCategory.ts @@ -0,0 +1,92 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandCategory, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Delete Category - No Connection', + description: 'Could not connect to the database. Unable to delete the prefix command category.', + color: Colors.Red, +}); + +const failedEmbed = (categoryId: string) => makeEmbed({ + title: 'Prefix Commands - Delete Category - Failed', + description: `Failed to delete the prefix command category with id ${categoryId}.`, + color: Colors.Red, +}); + +const doesNotExistsEmbed = (categoryId: string) => makeEmbed({ + title: 'Prefix Commands - Delete Category - Does not exist', + description: `The prefix command category with id ${categoryId} does not exists. Can not delete it.`, + color: Colors.Red, +}); + +const successEmbed = (category: string, categoryId: string) => makeEmbed({ + title: `Prefix command category ${category} (${categoryId}) was deleted successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, category: string, emoji: string, categoryId: string) => makeEmbed({ + title: 'Prefix command category deleted', + fields: [ + { + name: 'Category', + value: category, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + { + name: 'Emoji', + value: emoji, + }, + ], + footer: { text: `Category ID: ${categoryId}` }, + color: Colors.Red, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Delete Category - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleDeletePrefixCommandCategory(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const categoryId = interaction.options.getString('id')!; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + const existingCategory = await PrefixCommandCategory.findById(categoryId); + + if (existingCategory) { + const { name, emoji } = existingCategory; + try { + await existingCategory.deleteOne(); + await interaction.followUp({ embeds: [successEmbed(name || '', categoryId)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name || '', emoji || '', categoryId)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to delete a prefix command category with id ${categoryId}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(categoryId)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [doesNotExistsEmbed(categoryId)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts new file mode 100644 index 00000000..c697c2ce --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts @@ -0,0 +1,121 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandContent, PrefixCommandChannelPermission, PrefixCommandRolePermission } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Delete Command - No Connection', + description: 'Could not connect to the database. Unable to delete the prefix command.', + color: Colors.Red, +}); + +const failedEmbed = (commandId: string) => makeEmbed({ + title: 'Prefix Commands - Delete Command - Failed', + description: `Failed to delete the prefix command with id ${commandId}.`, + color: Colors.Red, +}); + +const doesNotExistsEmbed = (commandId: string) => makeEmbed({ + title: 'Prefix Commands - Delete Command - Does not exist', + description: `The prefix command with id ${commandId} does not exists. Can not delete it.`, + color: Colors.Red, +}); + +const successEmbed = (command: string, commandId: string) => makeEmbed({ + title: `Prefix command ${command} (${commandId}) was deleted successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, command: string, aliases: string[], isEmbed: boolean, embedColor: string, commandId: string) => makeEmbed({ + title: 'Prefix command deleted', + fields: [ + { + name: 'Command', + value: command, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + { + name: 'Aliases', + value: aliases.join(','), + }, + { + name: 'Is Embed', + value: isEmbed ? 'Yes' : 'No', + }, + { + name: 'Embed Color', + value: embedColor || '', + }, + ], + footer: { text: `Command ID: ${commandId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Delete Command - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleDeletePrefixCommand(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const commandId = interaction.options.getString('id')!; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + const existingCommand = await PrefixCommand.findById(commandId); + + if (existingCommand) { + const { name, aliases, isEmbed, embedColor } = existingCommand; + try { + await existingCommand.deleteOne(); + const foundContents = await PrefixCommandContent.find({ commandId }); + if (foundContents) { + for (const content of foundContents) { + // eslint-disable-next-line no-await-in-loop + await content.deleteOne(); + } + } + const foundChannelPermissions = await PrefixCommandChannelPermission.find({ commandId }); + if (foundChannelPermissions) { + for (const channelPermission of foundChannelPermissions) { + // eslint-disable-next-line no-await-in-loop + await channelPermission.deleteOne(); + } + } + const foundRolePermissions = await PrefixCommandRolePermission.find({ commandId }); + if (foundRolePermissions) { + for (const rolePermission of foundRolePermissions) { + // eslint-disable-next-line no-await-in-loop + await rolePermission.deleteOne(); + } + } + await interaction.followUp({ embeds: [successEmbed(name || '', commandId)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name || '', aliases, isEmbed || false, embedColor || '', commandId)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to delete a prefix command command with id ${commandId}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(commandId)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [doesNotExistsEmbed(commandId)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/deleteContent.ts b/src/commands/moderation/prefixCommands/functions/deleteContent.ts new file mode 100644 index 00000000..146983e4 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/deleteContent.ts @@ -0,0 +1,117 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandContent, Logger, makeEmbed, PrefixCommand, PrefixCommandVersion } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Delete Content - No Connection', + description: 'Could not connect to the database. Unable to delete the prefix command content.', + color: Colors.Red, +}); + +const noContentEmbed = (contentId: string) => makeEmbed({ + title: 'Prefix Commands - Delete Content - No Content', + description: `Failed to delete command content with ID ${contentId} as the content does not exist.`, + color: Colors.Red, +}); + +const failedEmbed = (contentId: string) => makeEmbed({ + title: 'Prefix Commands - Delete Content - Failed', + description: `Failed to delete the prefix command content with id ${contentId}.`, + color: Colors.Red, +}); + +const successEmbed = (command: string, version: string, contentId: string) => makeEmbed({ + title: `Prefix command content for command ${command} and version ${version} (Content ID: ${contentId}) was deleted successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, commandName: string, versionName: string, title: string, content: string, image: string, commandId: string, versionId: string, contentId: string) => makeEmbed({ + title: 'Prefix command content delete', + fields: [ + { + name: 'Command', + value: commandName, + }, + { + name: 'Version', + value: versionName, + }, + { + name: 'Title', + value: title, + }, + { + name: 'Content', + value: content, + }, + { + name: 'Image', + value: image, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + ], + footer: { text: `Command ID: ${commandId} - Version ID: ${versionId} - Content ID: ${contentId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Delete Content - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleDeletePrefixCommandContent(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const contentId = interaction.options.getString('id')!; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + const existingContent = await PrefixCommandContent.findById(contentId); + + if (existingContent) { + const { commandId, versionId, title, content, image } = existingContent; + const foundCommand = await PrefixCommand.findById(commandId); + if (!foundCommand) { + return; + } + const { name: commandName } = foundCommand; + let versionName = ''; + if (versionId !== 'GENERIC') { + const foundVersion = await PrefixCommandVersion.findById(versionId); + if (!foundVersion) { + return; + } + versionName = foundVersion.name || ''; + } + try { + await existingContent.deleteOne(); + await interaction.followUp({ embeds: [successEmbed(`${commandName}`, `${versionName}`, `${contentId}`)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, `${commandName}`, `${versionName}`, `${title}`, `${content}`, `${image}`, `${commandId}`, `${versionId}`, `${contentId}`)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to delete a prefix command content with id ${contentId}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(contentId)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [noContentEmbed(contentId)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/deleteVersion.ts b/src/commands/moderation/prefixCommands/functions/deleteVersion.ts new file mode 100644 index 00000000..db4b6de2 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/deleteVersion.ts @@ -0,0 +1,114 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Delete Version - No Connection', + description: 'Could not connect to the database. Unable to delete the prefix command version.', + color: Colors.Red, +}); + +const contentPresentEmbed = makeEmbed({ + title: 'Prefix Commands - Delete Version - Content Present', + description: 'There is content present for this command version. Please delete the content first, or use the `force` option to delete the command version and all the command contents for the version.', + color: Colors.Red, +}); + +const failedEmbed = (versionId: string) => makeEmbed({ + title: 'Prefix Commands - Delete Version - Failed', + description: `Failed to delete the prefix command version with id ${versionId}.`, + color: Colors.Red, +}); + +const doesNotExistsEmbed = (versionId: string) => makeEmbed({ + title: 'Prefix Commands - Delete Version - Does not exist', + description: `The prefix command version with id ${versionId} does not exists. Can not delete it.`, + color: Colors.Red, +}); + +const successEmbed = (version: string, versionId: string) => makeEmbed({ + title: `Prefix command version ${version} (${versionId}) was deleted successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, version: string, emoji: string, enabled: boolean, versionId: string) => makeEmbed({ + title: 'Prefix command version deleted', + fields: [ + { + name: 'Version', + value: version, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + { + name: 'Emoji', + value: emoji, + }, + { + name: 'Enabled', + value: enabled ? 'Yes' : 'No', + }, + ], + footer: { text: `Version ID: ${versionId}` }, + color: Colors.Red, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Delete Version - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleDeletePrefixCommandVersion(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const versionId = interaction.options.getString('id')!; + const force = interaction.options.getBoolean('force') || false; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + const existingVersion = await PrefixCommandVersion.findById(versionId); + const foundContents = await PrefixCommandVersion.find({ versionId }); + if (foundContents && foundContents.length > 0 && !force) { + await interaction.followUp({ embeds: [contentPresentEmbed], ephemeral: true }); + return; + } + + if (existingVersion) { + const { name, emoji, enabled } = existingVersion; + try { + await existingVersion.deleteOne(); + if (foundContents && force) { + for (const content of foundContents) { + // eslint-disable-next-line no-await-in-loop + await content.deleteOne(); + } + } + await interaction.followUp({ embeds: [successEmbed(name || '', versionId)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name || '', emoji || '', enabled || false, versionId)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to delete a prefix command version with id ${versionId}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(versionId)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [doesNotExistsEmbed(versionId)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/listCategories.ts b/src/commands/moderation/prefixCommands/functions/listCategories.ts new file mode 100644 index 00000000..8731da8e --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/listCategories.ts @@ -0,0 +1,57 @@ +import { APIEmbedField, ChatInputCommandInteraction, Colors } from 'discord.js'; +import { getConn, PrefixCommandCategory, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - List Categories - No Connection', + description: 'Could not connect to the database. Unable to list the prefix command categories.', + color: Colors.Red, +}); + +const failedEmbed = (searchText: string) => makeEmbed({ + title: 'Prefix Commands - List Categories - Failed', + description: `Failed to list the prefix command categories with search text: ${searchText}.`, + color: Colors.Red, +}); + +const noResultsEmbed = (searchText: string) => makeEmbed({ + title: 'Prefix Commands - List Categories - Does not exist', + description: `No prefix command categories found matching the search text: ${searchText}.`, +}); + +const successEmbed = (searchText: string, fields: APIEmbedField[]) => makeEmbed({ + title: 'Prefix Commands - Categories', + description: searchText ? `Matching search: ${searchText}` : undefined, + fields, + color: Colors.Green, +}); + +export async function handleListPrefixCommandCategories(interaction: ChatInputCommandInteraction<'cached'>) { + const conn = getConn(); + if (!conn) { + await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const searchText = interaction.options.getString('search_text') || ''; + const foundCategories = await PrefixCommandCategory.find({ name: { $regex: searchText, $options: 'i' } }); + + if (foundCategories) { + const embedFields: APIEmbedField[] = []; + for (let i = 0; i < foundCategories.length; i++) { + const category = foundCategories[i]; + const { id, name, emoji } = category; + embedFields.push({ + name: `${name} - ${emoji}`, + value: `${id}`, + }); + } + try { + await interaction.reply({ embeds: [successEmbed(searchText, embedFields)], ephemeral: false }); + } catch (error) { + Logger.error(`Failed to list prefix command categories with search ${searchText}: ${error}`); + await interaction.reply({ embeds: [failedEmbed(searchText)], ephemeral: true }); + } + } else { + await interaction.reply({ embeds: [noResultsEmbed(searchText)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts b/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts new file mode 100644 index 00000000..4a26fb47 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts @@ -0,0 +1,69 @@ +import { APIEmbedField, ChatInputCommandInteraction, Colors } from 'discord.js'; +import { getConn, PrefixCommandChannelPermission, Logger, makeEmbed, PrefixCommand } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - List Channel Permissions - No Connection', + description: 'Could not connect to the database. Unable to list the prefix command channel permissions.', + color: Colors.Red, +}); + +const failedEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - List Channel Permissions - Failed', + description: `Failed to list the prefix command channel permissions for command ${command}.`, + color: Colors.Red, +}); + +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - List Channel Permissions - No Command', + description: `Failed to list prefix command channel permissions for command ${command} as the command does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const noResultsEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - List Channel Permissions - Does not exist', + description: `No prefix command channel permissions found for command ${command}.`, +}); + +const successEmbed = (command: string, fields: APIEmbedField[]) => makeEmbed({ + title: `Prefix Commands - Channel Permissions for command ${command}`, + fields, + color: Colors.Green, +}); + +export async function handleListPrefixCommandChannelPermissions(interaction: ChatInputCommandInteraction<'cached'>) { + const conn = getConn(); + if (!conn) { + await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const command = interaction.options.getString('command')!; + const foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length === 0) { + await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + const [foundCommand] = foundCommands; + const { id: commandId } = foundCommand; + const foundChannelPermissions = await PrefixCommandChannelPermission.find({ commandId }); + + if (foundChannelPermissions) { + const embedFields: APIEmbedField[] = []; + for (let i = 0; i < foundChannelPermissions.length; i++) { + const permission = foundChannelPermissions[i]; + const { id, type, channelId } = permission; + embedFields.push({ + name: `<#${channelId}> - ${type}`, + value: `${id}`, + }); + } + try { + await interaction.reply({ embeds: [successEmbed(command, embedFields)], ephemeral: false }); + } catch (error) { + Logger.error(`Failed to list prefix command channel permissions for command ${command}: ${error}`); + await interaction.reply({ embeds: [failedEmbed(command)], ephemeral: true }); + } + } else { + await interaction.reply({ embeds: [noResultsEmbed(command)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/listCommands.ts b/src/commands/moderation/prefixCommands/functions/listCommands.ts new file mode 100644 index 00000000..fd478abf --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/listCommands.ts @@ -0,0 +1,57 @@ +import { APIEmbedField, ChatInputCommandInteraction, Colors } from 'discord.js'; +import { getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - List Commands - No Connection', + description: 'Could not connect to the database. Unable to list the prefix commands.', + color: Colors.Red, +}); + +const failedEmbed = (searchText: string) => makeEmbed({ + title: 'Prefix Commands - List Commands - Failed', + description: `Failed to list the prefix commands with search text: ${searchText}.`, + color: Colors.Red, +}); + +const noResultsEmbed = (searchText: string) => makeEmbed({ + title: 'Prefix Commands - List Commands - Does not exist', + description: `No prefix commands found matching the search text: ${searchText}.`, +}); + +const successEmbed = (searchText: string, fields: APIEmbedField[]) => makeEmbed({ + title: 'Prefix Commands', + description: searchText ? `Matching search: ${searchText}` : undefined, + fields, + color: Colors.Green, +}); + +export async function handleListPrefixCommands(interaction: ChatInputCommandInteraction<'cached'>) { + const conn = getConn(); + if (!conn) { + await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const searchText = interaction.options.getString('search_text') || ''; + const foundCommands = await PrefixCommand.find({ name: { $regex: searchText, $options: 'i' } }); + + if (foundCommands) { + const embedFields: APIEmbedField[] = []; + for (let i = 0; i < foundCommands.length; i++) { + const command = foundCommands[i]; + const { id, name, aliases, isEmbed, embedColor } = command; + embedFields.push({ + name: `${name} - ${aliases.join(',')} - ${isEmbed ? 'Embed' : 'No Embed'} - ${embedColor || 'No Color'}`, + value: `${id}`, + }); + } + try { + await interaction.reply({ embeds: [successEmbed(searchText, embedFields)], ephemeral: false }); + } catch (error) { + Logger.error(`Failed to list prefix command commands with search ${searchText}: ${error}`); + await interaction.reply({ embeds: [failedEmbed(searchText)], ephemeral: true }); + } + } else { + await interaction.reply({ embeds: [noResultsEmbed(searchText)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts b/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts new file mode 100644 index 00000000..b666c2a2 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts @@ -0,0 +1,69 @@ +import { APIEmbedField, ChatInputCommandInteraction, Colors } from 'discord.js'; +import { getConn, PrefixCommandRolePermission, Logger, makeEmbed, PrefixCommand } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - List Role Permissions - No Connection', + description: 'Could not connect to the database. Unable to list the prefix command role permissions.', + color: Colors.Red, +}); + +const failedEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - List Role Permissions - Failed', + description: `Failed to list the prefix command role permissions for command ${command}.`, + color: Colors.Red, +}); + +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - List Role Permissions - No Command', + description: `Failed to list prefix command role permissions for command ${command} as the command does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const noResultsEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - List Role Permissions - Does not exist', + description: `No prefix command role permissions found for command ${command}.`, +}); + +const successEmbed = (command: string, fields: APIEmbedField[]) => makeEmbed({ + title: `Prefix Commands - Role Permissions for command ${command}`, + fields, + color: Colors.Green, +}); + +export async function handleListPrefixCommandRolePermissions(interaction: ChatInputCommandInteraction<'cached'>) { + const conn = getConn(); + if (!conn) { + await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const command = interaction.options.getString('command')!; + const foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length === 0) { + await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + const [foundCommand] = foundCommands; + const { id: commandId } = foundCommand; + const foundRolePermissions = await PrefixCommandRolePermission.find({ commandId }); + + if (foundRolePermissions) { + const embedFields: APIEmbedField[] = []; + for (let i = 0; i < foundRolePermissions.length; i++) { + const permission = foundRolePermissions[i]; + const { id, type, roleId } = permission; + embedFields.push({ + name: `<@&${roleId}> - ${type}`, + value: `${id}`, + }); + } + try { + await interaction.reply({ embeds: [successEmbed(command, embedFields)], ephemeral: false }); + } catch (error) { + Logger.error(`Failed to list prefix command role permissions for command ${command}: ${error}`); + await interaction.reply({ embeds: [failedEmbed(command)], ephemeral: true }); + } + } else { + await interaction.reply({ embeds: [noResultsEmbed(command)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/listVersions.ts b/src/commands/moderation/prefixCommands/functions/listVersions.ts new file mode 100644 index 00000000..11755bcd --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/listVersions.ts @@ -0,0 +1,57 @@ +import { APIEmbedField, ChatInputCommandInteraction, Colors } from 'discord.js'; +import { getConn, PrefixCommandVersion, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - List Versions - No Connection', + description: 'Could not connect to the database. Unable to list the prefix command versions.', + color: Colors.Red, +}); + +const failedEmbed = (searchText: string) => makeEmbed({ + title: 'Prefix Commands - List Versions - Failed', + description: `Failed to list the prefix command versions with search text: ${searchText}.`, + color: Colors.Red, +}); + +const noResultsEmbed = (searchText: string) => makeEmbed({ + title: 'Prefix Commands - List Versions - Does not exist', + description: `No prefix command versions found matching the search text: ${searchText}.`, +}); + +const successEmbed = (searchText: string, fields: APIEmbedField[]) => makeEmbed({ + title: 'Prefix Commands - Versions', + description: searchText ? `Matching search: ${searchText}` : undefined, + fields, + color: Colors.Green, +}); + +export async function handleListPrefixCommandVersions(interaction: ChatInputCommandInteraction<'cached'>) { + const conn = getConn(); + if (!conn) { + await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const searchText = interaction.options.getString('search_text') || ''; + const foundVersions = await PrefixCommandVersion.find({ name: { $regex: searchText, $options: 'i' } }); + + if (foundVersions) { + const embedFields: APIEmbedField[] = []; + for (let i = 0; i < foundVersions.length; i++) { + const version = foundVersions[i]; + const { id, name, emoji, enabled } = version; + embedFields.push({ + name: `${name} - ${emoji} - ${enabled ? 'Enabled' : 'Disabled'}`, + value: `${id}`, + }); + } + try { + await interaction.reply({ embeds: [successEmbed(searchText, embedFields)], ephemeral: false }); + } catch (error) { + Logger.error(`Failed to list prefix command versions with search ${searchText}: ${error}`); + await interaction.reply({ embeds: [failedEmbed(searchText)], ephemeral: true }); + } + } else { + await interaction.reply({ embeds: [noResultsEmbed(searchText)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/modifyCategory.ts b/src/commands/moderation/prefixCommands/functions/modifyCategory.ts new file mode 100644 index 00000000..965581c6 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/modifyCategory.ts @@ -0,0 +1,96 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandCategory, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Modify Category - No Connection', + description: 'Could not connect to the database. Unable to modify the prefix command category.', + color: Colors.Red, +}); + +const failedEmbed = (categoryId: string) => makeEmbed({ + title: 'Prefix Commands - Modify Category - Failed', + description: `Failed to modify the prefix command category with id ${categoryId}.`, + color: Colors.Red, +}); + +const doesNotExistsEmbed = (categoryId: string) => makeEmbed({ + title: 'Prefix Commands - Modify Category - Does not exist', + description: `The prefix command category with id ${categoryId} does not exists. Can not modify it.`, + color: Colors.Red, +}); + +const successEmbed = (category: string, categoryId: string) => makeEmbed({ + title: `Prefix command category ${category} (${categoryId}) was modified successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, category: string, emoji: string, categoryId: string) => makeEmbed({ + title: 'Prefix command category modified', + fields: [ + { + name: 'Category', + value: category, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + { + name: 'Emoji', + value: emoji, + }, + ], + footer: { text: `Category ID: ${categoryId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Modified Category - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleModifyPrefixCommandCategory(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const categoryId = interaction.options.getString('id')!; + const name = interaction.options.getString('name') || ''; + const emoji = interaction.options.getString('emoji') || ''; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + const existingCategory = await PrefixCommandCategory.findById(categoryId); + + if (existingCategory) { + existingCategory.name = name || existingCategory.name; + existingCategory.emoji = emoji || existingCategory.emoji; + try { + await existingCategory.save(); + const { name, emoji } = existingCategory; + await interaction.followUp({ embeds: [successEmbed(name, categoryId)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, emoji || '', categoryId)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to modify a prefix command category with id ${categoryId}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(categoryId)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [doesNotExistsEmbed(categoryId)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts new file mode 100644 index 00000000..a5590452 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -0,0 +1,126 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandCategory } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Modify Command - No Connection', + description: 'Could not connect to the database. Unable to modify the prefix command.', + color: Colors.Red, +}); + +const failedEmbed = (commandId: string) => makeEmbed({ + title: 'Prefix Commands - Modify Command - Failed', + description: `Failed to modify the prefix command with id ${commandId}.`, + color: Colors.Red, +}); + +const categoryNotFoundEmbed = (category: string) => makeEmbed({ + title: 'Prefix Commands - Modify Command - Category not found', + description: `The prefix command category ${category} does not exist. Please create it first.`, + color: Colors.Red, +}); + +const doesNotExistsEmbed = (commandId: string) => makeEmbed({ + title: 'Prefix Commands - Modify Command - Does not exist', + description: `The prefix command with id ${commandId} does not exists. Can not modify it.`, + color: Colors.Red, +}); + +const successEmbed = (command: string, commandId: string) => makeEmbed({ + title: `Prefix command ${command} (${commandId}) was modified successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, command: string, aliases: string[], isEmbed: boolean, embedColor: string, commandId: string) => makeEmbed({ + title: 'Prefix command modified', + fields: [ + { + name: 'Command', + value: command, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + { + name: 'Aliases', + value: aliases.join(','), + }, + { + name: 'Is Embed', + value: isEmbed ? 'Yes' : 'No', + }, + { + name: 'Embed Color', + value: embedColor || '', + }, + ], + footer: { text: `Command ID: ${commandId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Modified Command - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleModifyPrefixCommand(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + } + + const commandId = interaction.options.getString('id')!; + const name = interaction.options.getString('name') || ''; + const category = interaction.options.getString('category') || ''; + const aliasesString = interaction.options.getString('aliases') || ''; + const aliases = aliasesString !== '' ? aliasesString.split(',') : []; + const isEmbed = interaction.options.getBoolean('is_embed') || null; + const embedColor = interaction.options.getString('embed_color') || ''; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + return; + } + + let categoryId = ''; + if (category !== '') { + const [foundCategory] = await PrefixCommandCategory.find({ name: category }); + if (!foundCategory) { + await interaction.followUp({ embeds: [categoryNotFoundEmbed(category)], ephemeral: true }); + return; + } + ({ id: categoryId } = foundCategory); + } + const existingCommand = await PrefixCommand.findById(commandId); + + if (existingCommand) { + existingCommand.name = name || existingCommand.name; + existingCommand.categoryId = categoryId || existingCommand.categoryId; + existingCommand.aliases = aliases.length > 0 ? aliases : existingCommand.aliases; + existingCommand.isEmbed = isEmbed !== null ? isEmbed : existingCommand.isEmbed; + existingCommand.embedColor = embedColor || existingCommand.embedColor; + try { + await existingCommand.save(); + const { name, aliases, isEmbed, embedColor } = existingCommand; + await interaction.followUp({ embeds: [successEmbed(name, commandId)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, aliases, isEmbed || false, embedColor || '', existingCommand.id)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to modify a prefix command command with id ${commandId}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(commandId)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [doesNotExistsEmbed(commandId)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts new file mode 100644 index 00000000..d938bb9c --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts @@ -0,0 +1,102 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Modify Version - No Connection', + description: 'Could not connect to the database. Unable to modify the prefix command version.', + color: Colors.Red, +}); + +const failedEmbed = (versionId: string) => makeEmbed({ + title: 'Prefix Commands - Modify Version - Failed', + description: `Failed to modify the prefix command version with id ${versionId}.`, + color: Colors.Red, +}); + +const doesNotExistsEmbed = (versionId: string) => makeEmbed({ + title: 'Prefix Commands - Modify Version - Does not exist', + description: `The prefix command version with id ${versionId} does not exists. Can not modify it.`, + color: Colors.Red, +}); + +const successEmbed = (version: string, versionId: string) => makeEmbed({ + title: `Prefix command version ${version} (${versionId}) was modified successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, version: string, emoji: string, enabled: boolean, versionId: string) => makeEmbed({ + title: 'Prefix command version modified', + fields: [ + { + name: 'Version', + value: version, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + { + name: 'Emoji', + value: emoji, + }, + { + name: 'Enabled', + value: enabled ? 'Yes' : 'No', + }, + ], + footer: { text: `Version ID: ${versionId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Modified Version - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleModifyPrefixCommandVersion(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const versionId = interaction.options.getString('id')!; + const name = interaction.options.getString('name') || ''; + const emoji = interaction.options.getString('emoji') || ''; + const enabled = interaction.options.getBoolean('is_enabled') || null; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + const existingVersion = await PrefixCommandVersion.findById(versionId); + + if (existingVersion) { + existingVersion.name = name || existingVersion.name; + existingVersion.emoji = emoji || existingVersion.emoji; + existingVersion.enabled = enabled !== null ? enabled : existingVersion.enabled; + try { + await existingVersion.save(); + const { name, emoji, enabled } = existingVersion; + await interaction.followUp({ embeds: [successEmbed(name, versionId)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, emoji, enabled || false, versionId)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to modify a prefix command version with id ${versionId}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(versionId)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [doesNotExistsEmbed(versionId)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts new file mode 100644 index 00000000..3b17ba83 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts @@ -0,0 +1,125 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandChannelPermission, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Remove Channel Permission - No Connection', + description: 'Could not connect to the database. Unable to remove the prefix command channel permission.', + color: Colors.Red, +}); + +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Remove Channel Permission - No Command', + description: `Failed to remove the prefix command channel permission for command ${command} as the command does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const noChannelEmbed = (channel: string) => makeEmbed({ + title: 'Prefix Commands - Remove Channel Permission - No Channel', + description: `Failed to remove the prefix command channel permission for channel <#${channel}> as the channel does not exist.`, + color: Colors.Red, +}); + +const failedEmbed = (command: string, channel: string, type: string) => makeEmbed({ + title: 'Prefix Commands - Remove Channel Permission - Failed', + description: `Failed to remove the ${type} prefix command channel permission for command ${command} and channel <#${channel}>.`, + color: Colors.Red, +}); + +const doesNotExistEmbed = (command: string, channel: string) => makeEmbed({ + title: 'Prefix Commands - Remove Channel Permission - Does not exist', + description: `A prefix command channel permission for command ${command} and channel <#${channel}> does not exist.`, + color: Colors.Red, +}); + +const successEmbed = (command: string, channel: string, type: string, channelPermissionId: string) => makeEmbed({ + title: `Prefix command channel ${type} permission removed for command ${command} and channel <#${channel}>. ChannelPermission ID: ${channelPermissionId}`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, command: string, channel: string, type: string, commandId: string, channelPermissionId: string) => makeEmbed({ + title: 'Remove prefix command channel permission', + fields: [ + { + name: 'Command', + value: command, + }, + { + name: 'Channel', + value: `<#${channel}>`, + }, + { + name: 'Type', + value: type, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + ], + footer: { text: `Command ID: ${commandId} - Channel Permission ID: ${channelPermissionId}` }, + color: Colors.Red, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Remove Channel Permission - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleRemovePrefixCommandChannelPermission(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const command = interaction.options.getString('command')!; + const channel = interaction.options.getString('channel')!; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + let foundCommand = await PrefixCommand.find({ name: command }); + if (!foundCommand || foundCommand.length > 1) { + foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + } + if (!foundCommand || foundCommand.length > 1) { + await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + const { id: commandId } = foundCommand[0]; + + const foundChannel = interaction.guild.channels.resolve(channel); + if (!foundChannel) { + await interaction.reply({ embeds: [noChannelEmbed(channel)], ephemeral: true }); + return; + } + const { id: channelId } = foundChannel; + + const existingChannelPermission = await PrefixCommandChannelPermission.findOne({ commandId, channelId }); + if (existingChannelPermission) { + const { id: channelPermissionId, type } = existingChannelPermission; + try { + await existingChannelPermission.deleteOne(); + await interaction.followUp({ embeds: [successEmbed(command, channel, type, channelPermissionId)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channel, type, commandId, channelPermissionId)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to remove ${type} prefix command channel permission for command ${command} and channel <#${channel}>: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command, channel, type)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [doesNotExistEmbed(command, channel)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts b/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts new file mode 100644 index 00000000..ec519fbc --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts @@ -0,0 +1,125 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandRolePermission, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Remove Role Permission - No Connection', + description: 'Could not connect to the database. Unable to remove the prefix command role permission.', + color: Colors.Red, +}); + +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Remove Role Permission - No Command', + description: `Failed to remove the prefix command role permission for command ${command} as the command does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const noRoleEmbed = (role: string) => makeEmbed({ + title: 'Prefix Commands - Remove Role Permission - No Role', + description: `Failed to remove the prefix command role permission for role ${role} as the role does not exist.`, + color: Colors.Red, +}); + +const failedEmbed = (command: string, roleName: string, type: string) => makeEmbed({ + title: 'Prefix Commands - Remove Role Permission - Failed', + description: `Failed to remove the ${type} prefix command role permission for command ${command} and role ${roleName}.`, + color: Colors.Red, +}); + +const doesNotExistEmbed = (command: string, roleName: string) => makeEmbed({ + title: 'Prefix Commands - Remove Role Permission - Already exists', + description: `A prefix command role permission for command ${command} and role ${roleName} does not exist.`, + color: Colors.Red, +}); + +const successEmbed = (command: string, roleName: string, type: string, rolePermissionId: string) => makeEmbed({ + title: `Prefix command role ${type} permission removed for command ${command} and role ${roleName}. RolePermission ID: ${rolePermissionId}`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, command: string, roleName: string, type: string, commandId: string, rolePermissionId: string) => makeEmbed({ + title: 'Remove prefix command role permission', + fields: [ + { + name: 'Command', + value: command, + }, + { + name: 'Role', + value: roleName, + }, + { + name: 'Type', + value: type, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + ], + footer: { text: `Command ID: ${commandId} - Role Permission ID: ${rolePermissionId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Remove Role Permission - No Mod Log', + description: 'I can\'t find the mod logs role. Please check the role still exists.', + color: Colors.Red, +}); + +export async function handleRemovePrefixCommandRolePermission(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const command = interaction.options.getString('command')!; + const role = interaction.options.getString('role')!; + const moderator = interaction.user; + + //Check if the mod logs role exists + const modLogsRole = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsRole) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + let foundCommand = await PrefixCommand.find({ name: command }); + if (!foundCommand || foundCommand.length > 1) { + foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + } + if (!foundCommand || foundCommand.length > 1) { + await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + const { id: commandId } = foundCommand[0]; + + const foundRole = interaction.guild.roles.resolve(role); + if (!foundRole) { + await interaction.reply({ embeds: [noRoleEmbed(role)], ephemeral: true }); + return; + } + const { id: roleId, name: roleName } = foundRole; + + const existingRolePermission = await PrefixCommandRolePermission.findOne({ commandId, roleId }); + if (existingRolePermission) { + const { id: rolePermissionId, type } = existingRolePermission; + try { + await existingRolePermission.deleteOne(); + await interaction.followUp({ embeds: [successEmbed(command, roleName, type, rolePermissionId)], ephemeral: true }); + if (modLogsRole) { + try { + await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName, type, commandId, rolePermissionId)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs role: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to remove ${type} prefix command role permission for command ${command} and role ${roleName}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command, roleName, type)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [doesNotExistEmbed(command, roleName)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts new file mode 100644 index 00000000..265ba656 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -0,0 +1,141 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandContent, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Set Content - No Connection', + description: 'Could not connect to the database. Unable to set the prefix command content.', + color: Colors.Red, +}); + +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Set Content - No Command', + description: `Failed to set command content for command ${command} as the command does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const noVersionEmbed = (version: string) => makeEmbed({ + title: 'Prefix Commands - Set Content - No Version', + description: `Failed to set command content for version ${version} as the version does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const failedEmbed = (command: string, version: string) => makeEmbed({ + title: 'Prefix Commands - Set Content - Failed', + description: `Failed to set command content for command ${command} and version ${version}.`, + color: Colors.Red, +}); + +const successEmbed = (command: string, version: string, contentId: string) => makeEmbed({ + title: `Prefix command content set for command ${command} and version ${version}. Content ID: ${contentId}`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, command: string, version: string, title: string, content: string, image: string, commandId: string, versionId: string, contentId: string) => makeEmbed({ + title: 'Prefix command content set', + fields: [ + { + name: 'Command', + value: command, + }, + { + name: 'Version', + value: version, + }, + { + name: 'Title', + value: title, + }, + { + name: 'Content', + value: content, + }, + { + name: 'Image', + value: image, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + ], + footer: { text: `Command ID: ${commandId} - Version ID: ${versionId} - Content ID: ${contentId}` }, + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Set Content - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleSetPrefixCommandContent(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const command = interaction.options.getString('command')!; + const version = interaction.options.getString('version')!; + const title = interaction.options.getString('title')!; + const content = interaction.options.getString('content') || ''; + const image = interaction.options.getString('image') || ''; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + let foundCommand = await PrefixCommand.find({ name: command }); + if (!foundCommand || foundCommand.length > 1) { + foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + } + if (!foundCommand || foundCommand.length > 1) { + await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + + const { id: commandId } = foundCommand[0]; + let versionId = ''; + let foundVersion = null; + if (version === 'GENERIC' || version === 'generic') { + versionId = 'GENERIC'; + } else { + foundVersion = await PrefixCommandVersion.find({ name: version }); + if (foundVersion && foundVersion.length === 1) { + versionId = foundVersion[0].id; + } else { + await interaction.reply({ embeds: [noVersionEmbed(version)], ephemeral: true }); + return; + } + } + + let foundContent = await PrefixCommandContent.findOne({ commandId, versionId }); + if (!foundContent) { + foundContent = new PrefixCommandContent(); + foundContent.commandId = commandId; + foundContent.versionId = versionId; + } + foundContent.title = title; + foundContent.content = content; + foundContent.image = image; + + try { + await foundContent.save(); + await interaction.followUp({ embeds: [successEmbed(command, version, foundContent.id)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, version, title, content, image, commandId, versionId, foundContent.id)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to set prefix command content for command ${command} and version ${version}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command, version)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/showContent.ts b/src/commands/moderation/prefixCommands/functions/showContent.ts new file mode 100644 index 00000000..9907dc2c --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/showContent.ts @@ -0,0 +1,99 @@ +import { ChatInputCommandInteraction, Colors } from 'discord.js'; +import { getConn, PrefixCommandContent, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Show Content - No Connection', + description: 'Could not connect to the database. Unable to show the prefix command content.', + color: Colors.Red, +}); + +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Show Content - No Command', + description: `Failed to show command content for command ${command} as the command does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const noVersionEmbed = (version: string) => makeEmbed({ + title: 'Prefix Commands - Show Content - No Version', + description: `Failed to show command content for version ${version} as the version does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const noContentEmbed = (command: string, version: string) => makeEmbed({ + title: 'Prefix Commands - Show Content - No Content', + description: `Failed to show command content for command ${command} and version ${version} as the content does not exist.`, + color: Colors.Red, +}); + +const failedEmbed = (command: string, version: string) => makeEmbed({ + title: 'Prefix Commands - Show Content - Failed', + description: `Failed to show command content for command ${command} and version ${version}.`, + color: Colors.Red, +}); + +const contentEmbed = (command: string, version: string, title: string, content: string, image: string, commandId: string, versionId: string, contentId: string) => makeEmbed({ + title: `Prefix Commands - Show Content - ${command} - ${version}`, + fields: [ + { + name: 'Title', + value: title, + }, + { + name: 'Content', + value: content, + }, + { + name: 'Image', + value: image, + }, + ], + footer: { text: `Command ID: ${commandId} - Version ID: ${versionId} - Content ID: ${contentId}` }, + color: Colors.Green, +}); + +export async function handleShowPrefixCommandContent(interaction: ChatInputCommandInteraction<'cached'>) { + const conn = getConn(); + if (!conn) { + await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const command = interaction.options.getString('command') || ''; + const version = interaction.options.getString('version') || ''; + let foundCommand = await PrefixCommand.find({ name: command }); + if (!foundCommand || foundCommand.length > 1) { + foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + } + if (!foundCommand || foundCommand.length > 1) { + await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + + const { id: commandId } = foundCommand[0]; + let versionId = ''; + if (version === 'GENERIC' || version === 'generic') { + versionId = 'GENERIC'; + } else { + const foundVersions = await PrefixCommandVersion.find({ name: version }); + if (foundVersions && foundVersions.length === 1) { + const [foundVersion] = foundVersions; + ({ id: versionId } = foundVersion); + } else { + await interaction.reply({ embeds: [noVersionEmbed(version)], ephemeral: true }); + return; + } + } + + const foundContent = await PrefixCommandContent.findOne({ commandId, versionId }); + if (!foundContent) { + await interaction.reply({ embeds: [noContentEmbed(command, version)], ephemeral: true }); + return; + } + const { id: contentId, title, content, image } = foundContent; + try { + await interaction.reply({ embeds: [contentEmbed(command, version, `${title}`, `${content}`, `${image}`, `${commandId}`, `${versionId}`, `${contentId}`)], ephemeral: false }); + } catch (error) { + Logger.error(`Failed to show prefix command content for command ${command} and version ${version}: ${error}`); + await interaction.reply({ embeds: [failedEmbed(command, version)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/prefixCommands.ts b/src/commands/moderation/prefixCommands/prefixCommands.ts new file mode 100644 index 00000000..06652700 --- /dev/null +++ b/src/commands/moderation/prefixCommands/prefixCommands.ts @@ -0,0 +1,673 @@ +import { ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'; +import { constantsConfig, slashCommand, slashCommandStructure } from '../../../lib'; +import { handleAddPrefixCommandCategory } from './functions/addCategory'; +import { handleModifyPrefixCommandCategory } from './functions/modifyCategory'; +import { handleDeletePrefixCommandCategory } from './functions/deleteCategory'; +import { handleListPrefixCommandCategories } from './functions/listCategories'; +import { handleAddPrefixCommandVersion } from './functions/addVersion'; +import { handleModifyPrefixCommandVersion } from './functions/modifyVersion'; +import { handleDeletePrefixCommandVersion } from './functions/deleteVersion'; +import { handleListPrefixCommandVersions } from './functions/listVersions'; +import { handleAddPrefixCommand } from './functions/addCommand'; +import { handleModifyPrefixCommand } from './functions/modifyCommand'; +import { handleDeletePrefixCommand } from './functions/deleteCommand'; +import { handleListPrefixCommands } from './functions/listCommands'; +import { handleShowPrefixCommandContent } from './functions/showContent'; +import { handleSetPrefixCommandContent } from './functions/setContent'; +import { handleDeletePrefixCommandContent } from './functions/deleteContent'; +import { handleListPrefixCommandChannelPermissions } from './functions/listChannelPermissions'; +import { handleListPrefixCommandRolePermissions } from './functions/listRolePermissions'; +import { handleAddPrefixCommandChannelPermission } from './functions/addChannelPermission'; +import { handleAddPrefixCommandRolePermission } from './functions/addRolePermission'; +import { handleRemovePrefixCommandChannelPermission } from './functions/removeChannelPermission'; +import { handleRemovePrefixCommandRolePermission } from './functions/removeRolePermission'; + +const colorChoices = []; +for (let i = 0; i < Object.keys(constantsConfig.colors).length; i++) { + const name = Object.keys(constantsConfig.colors)[i]; + const value = constantsConfig.colors[name]; + colorChoices.push({ name, value }); +} + +const data = slashCommandStructure({ + name: 'prefix-commands', + description: 'Command to manage prefix based commands.', + type: ApplicationCommandType.ChatInput, + default_member_permissions: constantsConfig.commandPermission.MANAGE_SERVER, //Overrides need to be added for admin and moderator roles + dm_permission: false, + options: [ + { + name: 'categories', + description: 'Manage prefix command categories.', + type: ApplicationCommandOptionType.SubcommandGroup, + options: [ + { + name: 'add', + description: 'Add a prefix command category.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'name', + description: 'Provide a name for the prefix command category.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'emoji', + description: 'Provide an emoji to identify the prefix command category.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 128, + }, + ], + }, + { + name: 'modify', + description: 'Modify a prefix command category.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'id', + description: 'Provide the id of the prefix command category.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 24, + }, + { + name: 'name', + description: 'Provide a name for the prefix command category.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 32, + }, + { + name: 'emoji', + description: 'Provide an emoji to identify the prefix command category.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 128, + }, + ], + }, + { + name: 'delete', + description: 'Delete a prefix command category.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'id', + description: 'Provide the id of the prefix command category.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 24, + }, + ], + }, + { + name: 'list', + description: 'Get list of prefix command categories.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'search_text', + description: 'Provide an optional search term.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 32, + }, + ], + }, + ], + }, + { + name: 'versions', + description: 'Manage prefix command versions.', + type: ApplicationCommandOptionType.SubcommandGroup, + options: [ + { + name: 'add', + description: 'Add a prefix command version.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'name', + description: 'Provide a name for the prefix command version.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'emoji', + description: 'Provide an emoji to identify the prefix command version.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 128, + }, + { + name: 'is_enabled', + description: 'Indicate wether this version is enabled.', + type: ApplicationCommandOptionType.Boolean, + required: false, + }, + ], + }, + { + name: 'modify', + description: 'Modify a prefix command version.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'id', + description: 'Provide the id of the prefix command version.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 24, + }, + { + name: 'name', + description: 'Provide a name for the prefix command version.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 32, + }, + { + name: 'emoji', + description: 'Provide an emoji to identify the prefix command version.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 128, + }, + { + name: 'is_enabled', + description: 'Indicate wether this version is enabled.', + type: ApplicationCommandOptionType.Boolean, + required: false, + }, + ], + }, + { + name: 'delete', + description: 'Delete a prefix command version.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'id', + description: 'Provide the id of the prefix command version.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 24, + }, + { + name: 'force', + description: 'Force delete the version even if it is used for command content.', + type: ApplicationCommandOptionType.Boolean, + required: false, + }, + ], + }, + { + name: 'list', + description: 'Get list of prefix command versions.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'search_text', + description: 'Provide an optional search term.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 32, + }, + ], + }, + ], + }, + { + name: 'commands', + description: 'Manage prefix commands.', + type: ApplicationCommandOptionType.SubcommandGroup, + options: [ + { + name: 'add', + description: 'Add a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'name', + description: 'Provide a name for the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'category', + description: 'Provide the category for the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'aliases', + description: 'Provide a comma separated list of aliases for the prefix command.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 255, + }, + { + name: 'is_embed', + description: 'Indicate wether this prefix command should print as an embed or regular message.', + type: ApplicationCommandOptionType.Boolean, + required: false, + }, + { + name: 'embed_color', + description: 'If this command results in an embed, specify the color.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 16, + choices: colorChoices, + }, + ], + }, + { + name: 'modify', + description: 'Modify a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'id', + description: 'Provide the id of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 24, + }, + { + name: 'name', + description: 'Provide a name for the prefix command.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 32, + }, + { + name: 'category', + description: 'Provide the category for the prefix command.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 32, + }, + { + name: 'aliases', + description: 'Provide a comma separated list of aliases for the prefix command.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 255, + }, + { + name: 'is_embed', + description: 'Indicate wether this prefix command should print as an embed or regular message.', + type: ApplicationCommandOptionType.Boolean, + required: false, + }, + { + name: 'embed_color', + description: 'If this command results in an embed, specify the color.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 16, + choices: colorChoices, + }, + ], + }, + { + name: 'delete', + description: 'Delete a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'id', + description: 'Provide the id of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 24, + }, + ], + }, + { + name: 'list', + description: 'Get list of prefix commands.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'search_text', + description: 'Provide an optional search term.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 32, + }, + ], + }, + ], + }, + { + name: 'content', + description: 'Manage prefix command content.', + type: ApplicationCommandOptionType.SubcommandGroup, + options: [ + { + name: 'show', + description: 'Show the details of the content of a command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'version', + description: 'Provide the name of the prefix command version. Use GENERIC for the generic content.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + ], + }, + { + name: 'set', + description: 'Set a prefix command\'s content for a specific version.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'version', + description: 'Provide the name of the prefix command version. Use GENERIC for the generic content.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'title', + description: 'Provide a title for the prefix command version content.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 255, + }, + { + name: 'content', + description: 'Provide the content for the prefix command version content.', + type: ApplicationCommandOptionType.String, + required: false, + }, + { + name: 'image', + description: 'Provide a URL for an image for the prefix command version content. Leave empty to set no image.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 255, + }, + ], + }, + { + name: 'delete', + description: 'Delete a prefix command\'s content for a specific version.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'id', + description: 'Provide the ID of the prefix command content.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 24, + }, + ], + }, + ], + }, + { + name: 'permissions', + description: 'Manage prefix command permissions.', + type: ApplicationCommandOptionType.SubcommandGroup, + options: [ + { + name: 'list-channels', + description: 'Get list of prefix command channel permissions.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + ], + }, + { + name: 'list-roles', + description: 'Get list of prefix command role permissions.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + ], + }, + { + name: 'add-channel', + description: 'Add a channel permission for a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'channel', + description: 'Provide the channel to add or remove from the selected list.', + type: ApplicationCommandOptionType.Channel, + required: true, + }, + { + name: 'type', + description: 'Select the type of the permission, permitted or prohibited', + type: ApplicationCommandOptionType.String, + required: true, + choices: [ + { name: 'Permitted', value: 'PERMITTED' }, + { name: 'Prohibited', value: 'PROHIBITED' }, + ], + }, + ], + }, + { + name: 'remove-channel', + description: 'Remove a channel permission for a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'channel', + description: 'Provide the channel to add or remove from the selected list.', + type: ApplicationCommandOptionType.Channel, + required: true, + }, + ], + }, + { + name: 'add-role', + description: 'Add a role permission for a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'role', + description: 'Provide the role to add or remove from the selected list.', + type: ApplicationCommandOptionType.Role, + required: true, + }, + { + name: 'type', + description: 'Select the type of the permission, permitted or prohibited', + type: ApplicationCommandOptionType.String, + required: true, + choices: [ + { name: 'Permitted', value: 'PERMITTED' }, + { name: 'Prohibited', value: 'PROHIBITED' }, + ], + }, + ], + }, + { + name: 'remove-role', + description: 'Remove a role permission for a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'role', + description: 'Provide the role to add or remove from the selected list.', + type: ApplicationCommandOptionType.Role, + required: true, + }, + ], + }, + ], + }, + ], +}); + +export default slashCommand(data, async ({ interaction }) => { + const subcommandGroup = interaction.options.getSubcommandGroup(); + const subcommandName = interaction.options.getSubcommand(); + + switch (subcommandGroup) { + case 'categories': + switch (subcommandName) { + case 'add': + await handleAddPrefixCommandCategory(interaction); + break; + case 'modify': + await handleModifyPrefixCommandCategory(interaction); + break; + case 'delete': + await handleDeletePrefixCommandCategory(interaction); + break; + case 'list': + await handleListPrefixCommandCategories(interaction); + break; + default: + await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + } + break; + case 'versions': + switch (subcommandName) { + case 'add': + await handleAddPrefixCommandVersion(interaction); + break; + case 'modify': + await handleModifyPrefixCommandVersion(interaction); + break; + case 'delete': + await handleDeletePrefixCommandVersion(interaction); + break; + case 'list': + await handleListPrefixCommandVersions(interaction); + break; + default: + await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + } + break; + case 'commands': + switch (subcommandName) { + case 'add': + await handleAddPrefixCommand(interaction); + break; + case 'modify': + await handleModifyPrefixCommand(interaction); + break; + case 'delete': + await handleDeletePrefixCommand(interaction); + break; + case 'list': + await handleListPrefixCommands(interaction); + break; + default: + await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + } + break; + case 'content': + switch (subcommandName) { + case 'show': + await handleShowPrefixCommandContent(interaction); + break; + case 'set': + await handleSetPrefixCommandContent(interaction); + break; + case 'delete': + await handleDeletePrefixCommandContent(interaction); + break; + default: + await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + } + break; + case 'permissions': + switch (subcommandName) { + case 'list-channels': + await handleListPrefixCommandChannelPermissions(interaction); + break; + case 'list-roles': + await handleListPrefixCommandRolePermissions(interaction); + break; + case 'add-channel': + await handleAddPrefixCommandChannelPermission(interaction); + break; + case 'remove-channel': + await handleRemovePrefixCommandChannelPermission(interaction); + break; + case 'add-role': + await handleAddPrefixCommandRolePermission(interaction); + break; + case 'remove-role': + await handleRemovePrefixCommandRolePermission(interaction); + break; + default: + await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + } + break; + default: + await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + } +}); diff --git a/src/lib/index.ts b/src/lib/index.ts index c5983c58..5526ff1d 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -16,6 +16,7 @@ export * from './autocomplete'; export * from './schemas/infractionSchema'; export * from './schemas/faqSchema'; export * from './schemas/birthdaySchema'; +export * from './schemas/prefixCommandSchemas'; //Scheduler Jobs export * from './schedulerJobs/autoDisableSlowMode'; diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts new file mode 100644 index 00000000..917f2b57 --- /dev/null +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -0,0 +1,99 @@ +import mongoose, { Schema } from 'mongoose'; + +const prefixCommandSchema = new Schema({ + commandId: mongoose.Schema.Types.ObjectId, + categoryId: { + type: String, + required: true, + }, + name: { + type: String, + required: true, + unique: true, + }, + aliases: [{ type: String }], + isEmbed: Boolean, + embedColor: String, +}); + +const prefixCommandCategorySchema = new Schema({ + categoryId: mongoose.Schema.Types.ObjectId, + name: { + type: String, + required: true, + unique: true, + }, + emoji: String, +}); + +const prefixCommandVersionSchema = new Schema({ + versionId: mongoose.Schema.Types.ObjectId, + name: { + type: String, + required: true, + unique: true, + }, + emoji: { + type: String, + required: true, + unique: true, + }, + enabled: Boolean, +}); + +const prefixCommandContentSchema = new Schema({ + contentId: mongoose.Schema.Types.ObjectId, + commandId: { + type: String, + required: true, + }, + versionId: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + content: String, + image: String, +}); + +const prefixCommandChannelPermissionSchema = new Schema({ + channelPermissionId: mongoose.Types.ObjectId, + commandId: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + channelId: { + type: String, + required: true, + }, +}); + +const prefixCommandRolePermissionSchema = new Schema({ + rolePermissionId: mongoose.Schema.Types.ObjectId, + commandId: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + roleId: { + type: String, + required: true, + }, +}); + +export const PrefixCommand = mongoose.model('PrefixCommand', prefixCommandSchema); +export const PrefixCommandCategory = mongoose.model('PrefixCommandCategory', prefixCommandCategorySchema); +export const PrefixCommandVersion = mongoose.model('PrefixCommandVersion', prefixCommandVersionSchema); +export const PrefixCommandContent = mongoose.model('PrefixCommandContent', prefixCommandContentSchema); +export const PrefixCommandChannelPermission = mongoose.model('PrefixCommandChannelPermission', prefixCommandChannelPermissionSchema); +export const PrefixCommandRolePermission = mongoose.model('PrefixCommandRolePermission', prefixCommandRolePermissionSchema); From 763c6a022e7110f818abc4db695c4fe1835bbe57 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 10 Jan 2024 22:38:32 -0800 Subject: [PATCH 02/51] Splitting into multiple commands --- src/commands/index.ts | 2 + .../functions/addChannelPermission.ts | 4 +- .../prefixCommands/functions/addCommand.ts | 5 +- .../functions/addRolePermission.ts | 4 +- .../prefixCommands/functions/deleteContent.ts | 2 +- .../functions/removeChannelPermission.ts | 4 +- .../functions/removeRolePermission.ts | 4 +- .../prefixCommands/functions/setContent.ts | 8 +- .../prefixCommandPermissions.ts | 207 ++++++++++++++++++ .../prefixCommands/prefixCommands.ts | 165 -------------- 10 files changed, 225 insertions(+), 180 deletions(-) create mode 100644 src/commands/moderation/prefixCommands/prefixCommandPermissions.ts diff --git a/src/commands/index.ts b/src/commands/index.ts index 5dfacd97..05c36b03 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -32,6 +32,7 @@ import listRoleUsers from './moderation/listRoleUsers'; import clearMessages from './moderation/clearMessages'; import locate from './utils/locate/locate'; import prefixCommands from './moderation/prefixCommands/prefixCommands'; +import prefixCommandPermissions from './moderation/prefixCommands/prefixCommandPermissions'; const commandArray: SlashCommand[] = [ ping, @@ -67,6 +68,7 @@ const commandArray: SlashCommand[] = [ clearMessages, locate, prefixCommands, + prefixCommandPermissions, ]; export default commandArray; diff --git a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts index 3c35278f..dc852f23 100644 --- a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts +++ b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts @@ -91,14 +91,14 @@ export async function handleAddPrefixCommandChannelPermission(interaction: ChatI foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); } if (!foundCommand || foundCommand.length > 1) { - await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } const { id: commandId } = foundCommand[0]; const foundChannel = interaction.guild.channels.resolve(channel); if (!foundChannel) { - await interaction.reply({ embeds: [noChannelEmbed(channel)], ephemeral: true }); + await interaction.followUp({ embeds: [noChannelEmbed(channel)], ephemeral: true }); return; } const { id: channelId } = foundChannel; diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts index 535108a1..8fa4137a 100644 --- a/src/commands/moderation/prefixCommands/functions/addCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -93,7 +93,8 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera await interaction.followUp({ embeds: [categoryNotFoundEmbed(category)], ephemeral: true }); return; } - const { categoryId } = foundCategory; + const { id: categoryId } = foundCategory; + Logger.info(`categoryId: ${categoryId}`); const existingCommand = await PrefixCommand.findOne({ name }); if (!existingCommand) { @@ -115,7 +116,7 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera } } } catch (error) { - Logger.error(`Failed to add a prefix command category ${name}: ${error}`); + Logger.error(`Failed to add a prefix command ${name}: ${error}`); await interaction.followUp({ embeds: [failedEmbed(name)], ephemeral: true }); } } else { diff --git a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts index f3d55efc..7d4781e6 100644 --- a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts +++ b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts @@ -91,14 +91,14 @@ export async function handleAddPrefixCommandRolePermission(interaction: ChatInpu foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); } if (!foundCommand || foundCommand.length > 1) { - await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } const { id: commandId } = foundCommand[0]; const foundRole = interaction.guild.roles.resolve(role); if (!foundRole) { - await interaction.reply({ embeds: [noRoleEmbed(role)], ephemeral: true }); + await interaction.followUp({ embeds: [noRoleEmbed(role)], ephemeral: true }); return; } const { id: roleId, name: roleName } = foundRole; diff --git a/src/commands/moderation/prefixCommands/functions/deleteContent.ts b/src/commands/moderation/prefixCommands/functions/deleteContent.ts index 146983e4..8260e57e 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteContent.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteContent.ts @@ -83,7 +83,7 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom const existingContent = await PrefixCommandContent.findById(contentId); if (existingContent) { - const { commandId, versionId, title, content, image } = existingContent; + const { id: commandId, versionId, title, content, image } = existingContent; const foundCommand = await PrefixCommand.findById(commandId); if (!foundCommand) { return; diff --git a/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts index 3b17ba83..b32d0192 100644 --- a/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts +++ b/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts @@ -90,14 +90,14 @@ export async function handleRemovePrefixCommandChannelPermission(interaction: Ch foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); } if (!foundCommand || foundCommand.length > 1) { - await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } const { id: commandId } = foundCommand[0]; const foundChannel = interaction.guild.channels.resolve(channel); if (!foundChannel) { - await interaction.reply({ embeds: [noChannelEmbed(channel)], ephemeral: true }); + await interaction.followUp({ embeds: [noChannelEmbed(channel)], ephemeral: true }); return; } const { id: channelId } = foundChannel; diff --git a/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts b/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts index ec519fbc..b6c2f20d 100644 --- a/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts +++ b/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts @@ -90,14 +90,14 @@ export async function handleRemovePrefixCommandRolePermission(interaction: ChatI foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); } if (!foundCommand || foundCommand.length > 1) { - await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } const { id: commandId } = foundCommand[0]; const foundRole = interaction.guild.roles.resolve(role); if (!foundRole) { - await interaction.reply({ embeds: [noRoleEmbed(role)], ephemeral: true }); + await interaction.followUp({ embeds: [noRoleEmbed(role)], ephemeral: true }); return; } const { id: roleId, name: roleName } = foundRole; diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index 265ba656..09690d5d 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -91,11 +91,11 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman } let foundCommand = await PrefixCommand.find({ name: command }); - if (!foundCommand || foundCommand.length > 1) { + if (!foundCommand || foundCommand.length !== 1) { foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); } - if (!foundCommand || foundCommand.length > 1) { - await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + if (!foundCommand || foundCommand.length !== 1) { + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } @@ -109,7 +109,7 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman if (foundVersion && foundVersion.length === 1) { versionId = foundVersion[0].id; } else { - await interaction.reply({ embeds: [noVersionEmbed(version)], ephemeral: true }); + await interaction.followUp({ embeds: [noVersionEmbed(version)], ephemeral: true }); return; } } diff --git a/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts b/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts new file mode 100644 index 00000000..e1beb796 --- /dev/null +++ b/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts @@ -0,0 +1,207 @@ +import { ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'; +import { constantsConfig, slashCommand, slashCommandStructure } from '../../../lib'; +import { handleListPrefixCommandChannelPermissions } from './functions/listChannelPermissions'; +import { handleListPrefixCommandRolePermissions } from './functions/listRolePermissions'; +import { handleAddPrefixCommandChannelPermission } from './functions/addChannelPermission'; +import { handleAddPrefixCommandRolePermission } from './functions/addRolePermission'; +import { handleRemovePrefixCommandChannelPermission } from './functions/removeChannelPermission'; +import { handleRemovePrefixCommandRolePermission } from './functions/removeRolePermission'; + +const colorChoices = []; +for (let i = 0; i < Object.keys(constantsConfig.colors).length; i++) { + const name = Object.keys(constantsConfig.colors)[i]; + const value = constantsConfig.colors[name]; + colorChoices.push({ name, value }); +} + +const data = slashCommandStructure({ + name: 'prefix-command-permissions', + description: 'Command to manage the permissions of prefix based commands.', + type: ApplicationCommandType.ChatInput, + default_member_permissions: constantsConfig.commandPermission.MANAGE_SERVER, //Overrides need to be added for admin and moderator roles + dm_permission: false, + options: [ + { + name: 'channels', + description: 'Manage prefix command channel permissions.', + type: ApplicationCommandOptionType.SubcommandGroup, + options: [ + { + name: 'list', + description: 'Get list of prefix command channel permissions.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + ], + }, + { + name: 'add', + description: 'Add a channel permission for a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'channel', + description: 'Provide the channel to add or remove from the selected list.', + type: ApplicationCommandOptionType.Channel, + required: true, + }, + { + name: 'type', + description: 'Select the type of the permission, permitted or prohibited', + type: ApplicationCommandOptionType.String, + required: true, + choices: [ + { name: 'Permitted', value: 'PERMITTED' }, + { name: 'Prohibited', value: 'PROHIBITED' }, + ], + }, + ], + }, + { + name: 'remove', + description: 'Remove a channel permission for a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'channel', + description: 'Provide the channel to add or remove from the selected list.', + type: ApplicationCommandOptionType.Channel, + required: true, + }, + ], + }, + ], + }, + { + name: 'roles', + description: 'Manage prefix command role permissions.', + type: ApplicationCommandOptionType.SubcommandGroup, + options: [ + { + name: 'list', + description: 'Get list of prefix command role permissions.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + ], + }, + { + name: 'add', + description: 'Add a role permission for a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'role', + description: 'Provide the role to add or remove from the selected list.', + type: ApplicationCommandOptionType.Role, + required: true, + }, + { + name: 'type', + description: 'Select the type of the permission, permitted or prohibited', + type: ApplicationCommandOptionType.String, + required: true, + choices: [ + { name: 'Permitted', value: 'PERMITTED' }, + { name: 'Prohibited', value: 'PROHIBITED' }, + ], + }, + ], + }, + { + name: 'remove', + description: 'Remove a role permission for a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, + { + name: 'role', + description: 'Provide the role to add or remove from the selected list.', + type: ApplicationCommandOptionType.Role, + required: true, + }, + ], + }, + ], + }, + ], +}); + +export default slashCommand(data, async ({ interaction }) => { + const subcommandGroup = interaction.options.getSubcommandGroup(); + const subcommandName = interaction.options.getSubcommand(); + + switch (subcommandGroup) { + case 'channels': + switch (subcommandName) { + case 'list': + await handleListPrefixCommandChannelPermissions(interaction); + break; + case 'add': + await handleAddPrefixCommandChannelPermission(interaction); + break; + case 'remove': + await handleRemovePrefixCommandChannelPermission(interaction); + break; + default: + await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + } + break; + case 'roles': + switch (subcommandName) { + case 'list': + await handleListPrefixCommandRolePermissions(interaction); + break; + case 'add': + await handleAddPrefixCommandRolePermission(interaction); + break; + case 'remove': + await handleRemovePrefixCommandRolePermission(interaction); + break; + default: + await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + } + break; + default: + await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + } +}); diff --git a/src/commands/moderation/prefixCommands/prefixCommands.ts b/src/commands/moderation/prefixCommands/prefixCommands.ts index 06652700..306c9889 100644 --- a/src/commands/moderation/prefixCommands/prefixCommands.ts +++ b/src/commands/moderation/prefixCommands/prefixCommands.ts @@ -15,12 +15,6 @@ import { handleListPrefixCommands } from './functions/listCommands'; import { handleShowPrefixCommandContent } from './functions/showContent'; import { handleSetPrefixCommandContent } from './functions/setContent'; import { handleDeletePrefixCommandContent } from './functions/deleteContent'; -import { handleListPrefixCommandChannelPermissions } from './functions/listChannelPermissions'; -import { handleListPrefixCommandRolePermissions } from './functions/listRolePermissions'; -import { handleAddPrefixCommandChannelPermission } from './functions/addChannelPermission'; -import { handleAddPrefixCommandRolePermission } from './functions/addRolePermission'; -import { handleRemovePrefixCommandChannelPermission } from './functions/removeChannelPermission'; -import { handleRemovePrefixCommandRolePermission } from './functions/removeRolePermission'; const colorChoices = []; for (let i = 0; i < Object.keys(constantsConfig.colors).length; i++) { @@ -431,141 +425,6 @@ const data = slashCommandStructure({ }, ], }, - { - name: 'permissions', - description: 'Manage prefix command permissions.', - type: ApplicationCommandOptionType.SubcommandGroup, - options: [ - { - name: 'list-channels', - description: 'Get list of prefix command channel permissions.', - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: 'command', - description: 'Provide the name of the prefix command.', - type: ApplicationCommandOptionType.String, - required: true, - max_length: 32, - }, - ], - }, - { - name: 'list-roles', - description: 'Get list of prefix command role permissions.', - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: 'command', - description: 'Provide the name of the prefix command.', - type: ApplicationCommandOptionType.String, - required: true, - max_length: 32, - }, - ], - }, - { - name: 'add-channel', - description: 'Add a channel permission for a prefix command.', - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: 'command', - description: 'Provide the name of the prefix command.', - type: ApplicationCommandOptionType.String, - required: true, - max_length: 32, - }, - { - name: 'channel', - description: 'Provide the channel to add or remove from the selected list.', - type: ApplicationCommandOptionType.Channel, - required: true, - }, - { - name: 'type', - description: 'Select the type of the permission, permitted or prohibited', - type: ApplicationCommandOptionType.String, - required: true, - choices: [ - { name: 'Permitted', value: 'PERMITTED' }, - { name: 'Prohibited', value: 'PROHIBITED' }, - ], - }, - ], - }, - { - name: 'remove-channel', - description: 'Remove a channel permission for a prefix command.', - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: 'command', - description: 'Provide the name of the prefix command.', - type: ApplicationCommandOptionType.String, - required: true, - max_length: 32, - }, - { - name: 'channel', - description: 'Provide the channel to add or remove from the selected list.', - type: ApplicationCommandOptionType.Channel, - required: true, - }, - ], - }, - { - name: 'add-role', - description: 'Add a role permission for a prefix command.', - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: 'command', - description: 'Provide the name of the prefix command.', - type: ApplicationCommandOptionType.String, - required: true, - max_length: 32, - }, - { - name: 'role', - description: 'Provide the role to add or remove from the selected list.', - type: ApplicationCommandOptionType.Role, - required: true, - }, - { - name: 'type', - description: 'Select the type of the permission, permitted or prohibited', - type: ApplicationCommandOptionType.String, - required: true, - choices: [ - { name: 'Permitted', value: 'PERMITTED' }, - { name: 'Prohibited', value: 'PROHIBITED' }, - ], - }, - ], - }, - { - name: 'remove-role', - description: 'Remove a role permission for a prefix command.', - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: 'command', - description: 'Provide the name of the prefix command.', - type: ApplicationCommandOptionType.String, - required: true, - max_length: 32, - }, - { - name: 'role', - description: 'Provide the role to add or remove from the selected list.', - type: ApplicationCommandOptionType.Role, - required: true, - }, - ], - }, - ], - }, ], }); @@ -643,30 +502,6 @@ export default slashCommand(data, async ({ interaction }) => { await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); } break; - case 'permissions': - switch (subcommandName) { - case 'list-channels': - await handleListPrefixCommandChannelPermissions(interaction); - break; - case 'list-roles': - await handleListPrefixCommandRolePermissions(interaction); - break; - case 'add-channel': - await handleAddPrefixCommandChannelPermission(interaction); - break; - case 'remove-channel': - await handleRemovePrefixCommandChannelPermission(interaction); - break; - case 'add-role': - await handleAddPrefixCommandRolePermission(interaction); - break; - case 'remove-role': - await handleRemovePrefixCommandRolePermission(interaction); - break; - default: - await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); - } - break; default: await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); } From 7a21effa4e7d42591a67873ec8cb53ee12195198 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sat, 2 Mar 2024 19:16:09 -0800 Subject: [PATCH 03/51] Migrating Schema and adapting Content management --- .../prefixCommands/functions/addCommand.ts | 3 ++ .../prefixCommands/functions/deleteContent.ts | 16 +++--- .../prefixCommands/functions/modifyCommand.ts | 7 ++- .../prefixCommands/functions/setContent.ts | 50 ++++++++++-------- .../prefixCommands/functions/showContent.ts | 15 +++--- src/lib/schemas/prefixCommandSchemas.ts | 51 ++++++++----------- 6 files changed, 71 insertions(+), 71 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts index 8fa4137a..8a3113be 100644 --- a/src/commands/moderation/prefixCommands/functions/addCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -104,6 +104,9 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera aliases, isEmbed, embedColor, + contents: [], + channelPermissions: [], + rolePermissions: [], }); try { await prefixCommand.save(); diff --git a/src/commands/moderation/prefixCommands/functions/deleteContent.ts b/src/commands/moderation/prefixCommands/functions/deleteContent.ts index 8260e57e..71fd68bc 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteContent.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteContent.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandContent, Logger, makeEmbed, PrefixCommand, PrefixCommandVersion } from '../../../../lib'; +import { constantsConfig, getConn, Logger, makeEmbed, PrefixCommand, PrefixCommandVersion } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Delete Content - No Connection', @@ -80,14 +80,11 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - const existingContent = await PrefixCommandContent.findById(contentId); + const foundCommand = await PrefixCommand.findOne({ 'contents._id': contentId }); + const existingContent = foundCommand?.contents.id(contentId) || null; - if (existingContent) { - const { id: commandId, versionId, title, content, image } = existingContent; - const foundCommand = await PrefixCommand.findById(commandId); - if (!foundCommand) { - return; - } + if (foundCommand && existingContent) { + const { versionId, title, content, image } = existingContent; const { name: commandName } = foundCommand; let versionName = ''; if (versionId !== 'GENERIC') { @@ -99,10 +96,11 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom } try { await existingContent.deleteOne(); + await foundCommand.save(); await interaction.followUp({ embeds: [successEmbed(`${commandName}`, `${versionName}`, `${contentId}`)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, `${commandName}`, `${versionName}`, `${title}`, `${content}`, `${image}`, `${commandId}`, `${versionId}`, `${contentId}`)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, `${commandName}`, `${versionName}`, `${title}`, `${content}`, `${image}`, `${foundCommand.id}`, `${versionId}`, `${contentId}`)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index a5590452..71892658 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -88,20 +88,19 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt return; } - let categoryId = ''; + let foundCategory; if (category !== '') { - const [foundCategory] = await PrefixCommandCategory.find({ name: category }); + [foundCategory] = await PrefixCommandCategory.find({ name: category }); if (!foundCategory) { await interaction.followUp({ embeds: [categoryNotFoundEmbed(category)], ephemeral: true }); return; } - ({ id: categoryId } = foundCategory); } const existingCommand = await PrefixCommand.findById(commandId); if (existingCommand) { existingCommand.name = name || existingCommand.name; - existingCommand.categoryId = categoryId || existingCommand.categoryId; + existingCommand.categoryId = foundCategory?.id || existingCommand.categoryId; existingCommand.aliases = aliases.length > 0 ? aliases : existingCommand.aliases; existingCommand.isEmbed = isEmbed !== null ? isEmbed : existingCommand.isEmbed; existingCommand.embedColor = embedColor || existingCommand.embedColor; diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index 09690d5d..5770106f 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -90,46 +90,56 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - let foundCommand = await PrefixCommand.find({ name: command }); - if (!foundCommand || foundCommand.length !== 1) { - foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + let foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length !== 1) { + foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); } - if (!foundCommand || foundCommand.length !== 1) { + if (!foundCommands || foundCommands.length !== 1) { await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } - const { id: commandId } = foundCommand[0]; + const foundCommand = foundCommands[0]; + const { id: commandId } = foundCommand; let versionId = ''; - let foundVersion = null; + let foundVersions = null; if (version === 'GENERIC' || version === 'generic') { versionId = 'GENERIC'; } else { - foundVersion = await PrefixCommandVersion.find({ name: version }); - if (foundVersion && foundVersion.length === 1) { - versionId = foundVersion[0].id; + foundVersions = await PrefixCommandVersion.find({ name: version }); + if (foundVersions && foundVersions.length === 1) { + versionId = foundVersions[0].id; } else { await interaction.followUp({ embeds: [noVersionEmbed(version)], ephemeral: true }); return; } } - let foundContent = await PrefixCommandContent.findOne({ commandId, versionId }); - if (!foundContent) { - foundContent = new PrefixCommandContent(); - foundContent.commandId = commandId; - foundContent.versionId = versionId; + const foundContent = foundCommand.contents.find((c) => c.versionId === versionId); + if (foundContent) { + const foundData = foundCommand.contents.id(foundContent.id); + try { + await foundData?.deleteOne(); + } catch (error) { + Logger.error(`Failed to delete existing content for prefix command ${command} and version ${version}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command, version)], ephemeral: true }); + return; + } } - foundContent.title = title; - foundContent.content = content; - foundContent.image = image; + const contentData = new PrefixCommandContent({ + versionId, + title, + content, + image, + }); + foundCommand.contents.push(contentData); try { - await foundContent.save(); - await interaction.followUp({ embeds: [successEmbed(command, version, foundContent.id)], ephemeral: true }); + await foundCommand.save(); + await interaction.followUp({ embeds: [successEmbed(command, version, contentData.id)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, version, title, content, image, commandId, versionId, foundContent.id)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, version, title, content, image, commandId, versionId, contentData.id)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/functions/showContent.ts b/src/commands/moderation/prefixCommands/functions/showContent.ts index 9907dc2c..b0081cd1 100644 --- a/src/commands/moderation/prefixCommands/functions/showContent.ts +++ b/src/commands/moderation/prefixCommands/functions/showContent.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors } from 'discord.js'; -import { getConn, PrefixCommandContent, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { getConn, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Show Content - No Connection', @@ -60,16 +60,17 @@ export async function handleShowPrefixCommandContent(interaction: ChatInputComma const command = interaction.options.getString('command') || ''; const version = interaction.options.getString('version') || ''; - let foundCommand = await PrefixCommand.find({ name: command }); - if (!foundCommand || foundCommand.length > 1) { - foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + let foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length > 1) { + foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); } - if (!foundCommand || foundCommand.length > 1) { + if (!foundCommands || foundCommands.length > 1) { await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } - const { id: commandId } = foundCommand[0]; + const [foundCommand] = foundCommands; + const { id: commandId } = foundCommand; let versionId = ''; if (version === 'GENERIC' || version === 'generic') { versionId = 'GENERIC'; @@ -84,7 +85,7 @@ export async function handleShowPrefixCommandContent(interaction: ChatInputComma } } - const foundContent = await PrefixCommandContent.findOne({ commandId, versionId }); + const foundContent = foundCommand.contents.find((content) => content.versionId === versionId); if (!foundContent) { await interaction.reply({ embeds: [noContentEmbed(command, version)], ephemeral: true }); return; diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts index 917f2b57..5f640233 100644 --- a/src/lib/schemas/prefixCommandSchemas.ts +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -1,21 +1,5 @@ import mongoose, { Schema } from 'mongoose'; -const prefixCommandSchema = new Schema({ - commandId: mongoose.Schema.Types.ObjectId, - categoryId: { - type: String, - required: true, - }, - name: { - type: String, - required: true, - unique: true, - }, - aliases: [{ type: String }], - isEmbed: Boolean, - embedColor: String, -}); - const prefixCommandCategorySchema = new Schema({ categoryId: mongoose.Schema.Types.ObjectId, name: { @@ -42,11 +26,6 @@ const prefixCommandVersionSchema = new Schema({ }); const prefixCommandContentSchema = new Schema({ - contentId: mongoose.Schema.Types.ObjectId, - commandId: { - type: String, - required: true, - }, versionId: { type: String, required: true, @@ -60,11 +39,6 @@ const prefixCommandContentSchema = new Schema({ }); const prefixCommandChannelPermissionSchema = new Schema({ - channelPermissionId: mongoose.Types.ObjectId, - commandId: { - type: String, - required: true, - }, type: { type: String, required: true, @@ -76,24 +50,39 @@ const prefixCommandChannelPermissionSchema = new Schema({ }); const prefixCommandRolePermissionSchema = new Schema({ - rolePermissionId: mongoose.Schema.Types.ObjectId, - commandId: { + type: { type: String, required: true, }, - type: { + roleId: { type: String, required: true, }, - roleId: { +}); + +const prefixCommandSchema = new Schema({ + commandId: mongoose.Schema.Types.ObjectId, + categoryId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'PrefixCommandCategory', + required: true, + }, + name: { type: String, required: true, + unique: true, }, + aliases: [{ type: String }], + isEmbed: Boolean, + embedColor: String, + contents: [prefixCommandContentSchema], + channelPermissions: [prefixCommandChannelPermissionSchema], + rolePermissions: [prefixCommandRolePermissionSchema], }); -export const PrefixCommand = mongoose.model('PrefixCommand', prefixCommandSchema); export const PrefixCommandCategory = mongoose.model('PrefixCommandCategory', prefixCommandCategorySchema); export const PrefixCommandVersion = mongoose.model('PrefixCommandVersion', prefixCommandVersionSchema); export const PrefixCommandContent = mongoose.model('PrefixCommandContent', prefixCommandContentSchema); export const PrefixCommandChannelPermission = mongoose.model('PrefixCommandChannelPermission', prefixCommandChannelPermissionSchema); export const PrefixCommandRolePermission = mongoose.model('PrefixCommandRolePermission', prefixCommandRolePermissionSchema); +export const PrefixCommand = mongoose.model('PrefixCommand', prefixCommandSchema); From d4d401f37dacde9e975a7911de725628b38392a0 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sat, 2 Mar 2024 20:04:25 -0800 Subject: [PATCH 04/51] Updating permissions for new schema and fix issues --- .../functions/addChannelPermission.ts | 47 ++++++++----------- .../functions/addRolePermission.ts | 43 +++++++---------- .../prefixCommands/functions/deleteCommand.ts | 23 +-------- .../prefixCommands/functions/deleteContent.ts | 2 +- .../functions/listChannelPermissions.ts | 5 +- .../functions/listRolePermissions.ts | 5 +- .../functions/removeChannelPermission.ts | 42 +++++++---------- .../functions/removeRolePermission.ts | 34 +++++--------- .../prefixCommands/functions/setContent.ts | 11 +++-- src/lib/schemas/prefixCommandSchemas.ts | 4 +- 10 files changed, 77 insertions(+), 139 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts index dc852f23..25d0fc9d 100644 --- a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts +++ b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandChannelPermission, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Add Channel Permission - No Connection', @@ -13,12 +13,6 @@ const noCommandEmbed = (command: string) => makeEmbed({ color: Colors.Red, }); -const noChannelEmbed = (channel: string) => makeEmbed({ - title: 'Prefix Commands - Add Channel Permission - No Channel', - description: `Failed to add the prefix command channel permission for channel <#${channel}> as the channel does not exist.`, - color: Colors.Red, -}); - const failedEmbed = (command: string, channel: string, type: string) => makeEmbed({ title: 'Prefix Commands - Add Channel Permission - Failed', description: `Failed to add the ${type} prefix command channel permission for command ${command} and channel <#${channel}>.`, @@ -76,7 +70,7 @@ export async function handleAddPrefixCommandChannelPermission(interaction: ChatI } const command = interaction.options.getString('command')!; - const channel = interaction.options.getString('channel')!; + const channel = interaction.options.getChannel('channel')!; const type = interaction.options.getString('type')!; const moderator = interaction.user; @@ -86,45 +80,42 @@ export async function handleAddPrefixCommandChannelPermission(interaction: ChatI await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - let foundCommand = await PrefixCommand.find({ name: command }); - if (!foundCommand || foundCommand.length > 1) { - foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + let foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length > 1) { + foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); } - if (!foundCommand || foundCommand.length > 1) { + if (!foundCommands || foundCommands.length > 1) { await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } - const { id: commandId } = foundCommand[0]; - - const foundChannel = interaction.guild.channels.resolve(channel); - if (!foundChannel) { - await interaction.followUp({ embeds: [noChannelEmbed(channel)], ephemeral: true }); - return; - } - const { id: channelId } = foundChannel; + const [foundCommand] = foundCommands; + const { id: commandId } = foundCommand; + const { id: channelId, name: channelName } = channel; - const existingChannelPermission = await PrefixCommandChannelPermission.findOne({ commandId, channelId }); + const existingChannelPermission = foundCommand.channelPermissions.find((channelPermission) => channelPermission.channelId === channelId); if (!existingChannelPermission) { - const newChannelPermission = new PrefixCommandChannelPermission({ + const newChannelPermission = { commandId, channelId, type, - }); + }; try { - await newChannelPermission.save(); - await interaction.followUp({ embeds: [successEmbed(command, channel, type, newChannelPermission.id)], ephemeral: true }); + foundCommand.channelPermissions.push(newChannelPermission); + await foundCommand.save(); + const { id: channelPermissionId } = foundCommand.channelPermissions.find((channelPermission) => channelPermission.channelId === channelId)!; + await interaction.followUp({ embeds: [successEmbed(command, channelName, type, channelPermissionId)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channel, type, commandId, newChannelPermission.id)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channelName, type, commandId, channelPermissionId)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } } } catch (error) { Logger.error(`Failed to add ${type} prefix command channel permission for command ${command} and channel <#${channel}>: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(command, channel, type)], ephemeral: true }); + await interaction.followUp({ embeds: [failedEmbed(command, channelName, type)], ephemeral: true }); } } else { - await interaction.followUp({ embeds: [alreadyExistsEmbed(command, channel)], ephemeral: true }); + await interaction.followUp({ embeds: [alreadyExistsEmbed(command, channelName)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts index 7d4781e6..01f5507b 100644 --- a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts +++ b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandRolePermission, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Add Role Permission - No Connection', @@ -13,12 +13,6 @@ const noCommandEmbed = (command: string) => makeEmbed({ color: Colors.Red, }); -const noRoleEmbed = (role: string) => makeEmbed({ - title: 'Prefix Commands - Add Role Permission - No Role', - description: `Failed to add the prefix command role permission for role ${role} as the role does not exist.`, - color: Colors.Red, -}); - const failedEmbed = (command: string, roleName: string, type: string) => makeEmbed({ title: 'Prefix Commands - Add Role Permission - Failed', description: `Failed to add the ${type} prefix command role permission for command ${command} and role ${roleName}.`, @@ -76,7 +70,7 @@ export async function handleAddPrefixCommandRolePermission(interaction: ChatInpu } const command = interaction.options.getString('command')!; - const role = interaction.options.getString('role')!; + const role = interaction.options.getRole('role')!; const type = interaction.options.getString('type')!; const moderator = interaction.user; @@ -86,36 +80,33 @@ export async function handleAddPrefixCommandRolePermission(interaction: ChatInpu await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - let foundCommand = await PrefixCommand.find({ name: command }); - if (!foundCommand || foundCommand.length > 1) { - foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + let foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length > 1) { + foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); } - if (!foundCommand || foundCommand.length > 1) { + if (!foundCommands || foundCommands.length > 1) { await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } - const { id: commandId } = foundCommand[0]; - - const foundRole = interaction.guild.roles.resolve(role); - if (!foundRole) { - await interaction.followUp({ embeds: [noRoleEmbed(role)], ephemeral: true }); - return; - } - const { id: roleId, name: roleName } = foundRole; + const [foundCommand] = foundCommands; + const { id: commandId } = foundCommand; + const { id: roleId, name: roleName } = role; - const existingRolePermission = await PrefixCommandRolePermission.findOne({ commandId, roleId }); + const existingRolePermission = foundCommand.rolePermissions.find((rolePermission) => rolePermission.roleId === roleId); if (!existingRolePermission) { - const newRolePermission = new PrefixCommandRolePermission({ + const newRolePermission = { commandId, roleId, type, - }); + }; try { - await newRolePermission.save(); - await interaction.followUp({ embeds: [successEmbed(command, roleName, type, newRolePermission.id)], ephemeral: true }); + foundCommand.rolePermissions.push(newRolePermission); + await foundCommand.save(); + const { id: rolePermissionId } = foundCommand.rolePermissions.find((rolePermission) => rolePermission.roleId === roleId)!; + await interaction.followUp({ embeds: [successEmbed(command, roleName, type, rolePermissionId)], ephemeral: true }); if (modLogsRole) { try { - await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName, type, commandId, newRolePermission.id)] }); + await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName, type, commandId, rolePermissionId)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs role: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts index c697c2ce..503968f9 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandContent, PrefixCommandChannelPermission, PrefixCommandRolePermission } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Delete Command - No Connection', @@ -82,27 +82,6 @@ export async function handleDeletePrefixCommand(interaction: ChatInputCommandInt const { name, aliases, isEmbed, embedColor } = existingCommand; try { await existingCommand.deleteOne(); - const foundContents = await PrefixCommandContent.find({ commandId }); - if (foundContents) { - for (const content of foundContents) { - // eslint-disable-next-line no-await-in-loop - await content.deleteOne(); - } - } - const foundChannelPermissions = await PrefixCommandChannelPermission.find({ commandId }); - if (foundChannelPermissions) { - for (const channelPermission of foundChannelPermissions) { - // eslint-disable-next-line no-await-in-loop - await channelPermission.deleteOne(); - } - } - const foundRolePermissions = await PrefixCommandRolePermission.find({ commandId }); - if (foundRolePermissions) { - for (const rolePermission of foundRolePermissions) { - // eslint-disable-next-line no-await-in-loop - await rolePermission.deleteOne(); - } - } await interaction.followUp({ embeds: [successEmbed(name || '', commandId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/deleteContent.ts b/src/commands/moderation/prefixCommands/functions/deleteContent.ts index 71fd68bc..bade158c 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteContent.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteContent.ts @@ -95,7 +95,7 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom versionName = foundVersion.name || ''; } try { - await existingContent.deleteOne(); + foundCommand.contents.id(contentId)?.deleteOne(); await foundCommand.save(); await interaction.followUp({ embeds: [successEmbed(`${commandName}`, `${versionName}`, `${contentId}`)], ephemeral: true }); if (modLogsChannel) { diff --git a/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts b/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts index 4a26fb47..64732157 100644 --- a/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts +++ b/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts @@ -1,5 +1,5 @@ import { APIEmbedField, ChatInputCommandInteraction, Colors } from 'discord.js'; -import { getConn, PrefixCommandChannelPermission, Logger, makeEmbed, PrefixCommand } from '../../../../lib'; +import { getConn, Logger, makeEmbed, PrefixCommand } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - List Channel Permissions - No Connection', @@ -44,8 +44,7 @@ export async function handleListPrefixCommandChannelPermissions(interaction: Cha return; } const [foundCommand] = foundCommands; - const { id: commandId } = foundCommand; - const foundChannelPermissions = await PrefixCommandChannelPermission.find({ commandId }); + const foundChannelPermissions = foundCommand.channelPermissions; if (foundChannelPermissions) { const embedFields: APIEmbedField[] = []; diff --git a/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts b/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts index b666c2a2..b535bdcb 100644 --- a/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts +++ b/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts @@ -1,5 +1,5 @@ import { APIEmbedField, ChatInputCommandInteraction, Colors } from 'discord.js'; -import { getConn, PrefixCommandRolePermission, Logger, makeEmbed, PrefixCommand } from '../../../../lib'; +import { getConn, Logger, makeEmbed, PrefixCommand } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - List Role Permissions - No Connection', @@ -44,8 +44,7 @@ export async function handleListPrefixCommandRolePermissions(interaction: ChatIn return; } const [foundCommand] = foundCommands; - const { id: commandId } = foundCommand; - const foundRolePermissions = await PrefixCommandRolePermission.find({ commandId }); + const foundRolePermissions = foundCommand.rolePermissions; if (foundRolePermissions) { const embedFields: APIEmbedField[] = []; diff --git a/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts index b32d0192..bf92fc8f 100644 --- a/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts +++ b/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandChannelPermission, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Remove Channel Permission - No Connection', @@ -13,12 +13,6 @@ const noCommandEmbed = (command: string) => makeEmbed({ color: Colors.Red, }); -const noChannelEmbed = (channel: string) => makeEmbed({ - title: 'Prefix Commands - Remove Channel Permission - No Channel', - description: `Failed to remove the prefix command channel permission for channel <#${channel}> as the channel does not exist.`, - color: Colors.Red, -}); - const failedEmbed = (command: string, channel: string, type: string) => makeEmbed({ title: 'Prefix Commands - Remove Channel Permission - Failed', description: `Failed to remove the ${type} prefix command channel permission for command ${command} and channel <#${channel}>.`, @@ -76,7 +70,7 @@ export async function handleRemovePrefixCommandChannelPermission(interaction: Ch } const command = interaction.options.getString('command')!; - const channel = interaction.options.getString('channel')!; + const channel = interaction.options.getChannel('channel')!; const moderator = interaction.user; //Check if the mod logs channel exists @@ -85,41 +79,37 @@ export async function handleRemovePrefixCommandChannelPermission(interaction: Ch await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - let foundCommand = await PrefixCommand.find({ name: command }); - if (!foundCommand || foundCommand.length > 1) { - foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + let foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length > 1) { + foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); } - if (!foundCommand || foundCommand.length > 1) { + if (!foundCommands || foundCommands.length > 1) { await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } - const { id: commandId } = foundCommand[0]; - - const foundChannel = interaction.guild.channels.resolve(channel); - if (!foundChannel) { - await interaction.followUp({ embeds: [noChannelEmbed(channel)], ephemeral: true }); - return; - } - const { id: channelId } = foundChannel; + const [foundCommand] = foundCommands; + const { id: commandId } = foundCommand; + const { id: channelId, name: channelName } = channel; - const existingChannelPermission = await PrefixCommandChannelPermission.findOne({ commandId, channelId }); + const existingChannelPermission = foundCommand.channelPermissions.find((channelPermission) => channelPermission.channelId === channelId); if (existingChannelPermission) { const { id: channelPermissionId, type } = existingChannelPermission; try { - await existingChannelPermission.deleteOne(); - await interaction.followUp({ embeds: [successEmbed(command, channel, type, channelPermissionId)], ephemeral: true }); + foundCommand.channelPermissions.id(channelPermissionId)?.deleteOne(); + await foundCommand.save(); + await interaction.followUp({ embeds: [successEmbed(command, channelName, type, channelPermissionId)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channel, type, commandId, channelPermissionId)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channelName, type, commandId, channelPermissionId)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } } } catch (error) { Logger.error(`Failed to remove ${type} prefix command channel permission for command ${command} and channel <#${channel}>: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(command, channel, type)], ephemeral: true }); + await interaction.followUp({ embeds: [failedEmbed(command, channelName, type)], ephemeral: true }); } } else { - await interaction.followUp({ embeds: [doesNotExistEmbed(command, channel)], ephemeral: true }); + await interaction.followUp({ embeds: [doesNotExistEmbed(command, channelName)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts b/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts index b6c2f20d..6b61bfc4 100644 --- a/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts +++ b/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandRolePermission, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Remove Role Permission - No Connection', @@ -13,12 +13,6 @@ const noCommandEmbed = (command: string) => makeEmbed({ color: Colors.Red, }); -const noRoleEmbed = (role: string) => makeEmbed({ - title: 'Prefix Commands - Remove Role Permission - No Role', - description: `Failed to remove the prefix command role permission for role ${role} as the role does not exist.`, - color: Colors.Red, -}); - const failedEmbed = (command: string, roleName: string, type: string) => makeEmbed({ title: 'Prefix Commands - Remove Role Permission - Failed', description: `Failed to remove the ${type} prefix command role permission for command ${command} and role ${roleName}.`, @@ -76,7 +70,7 @@ export async function handleRemovePrefixCommandRolePermission(interaction: ChatI } const command = interaction.options.getString('command')!; - const role = interaction.options.getString('role')!; + const role = interaction.options.getRole('role')!; const moderator = interaction.user; //Check if the mod logs role exists @@ -85,28 +79,24 @@ export async function handleRemovePrefixCommandRolePermission(interaction: ChatI await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - let foundCommand = await PrefixCommand.find({ name: command }); - if (!foundCommand || foundCommand.length > 1) { - foundCommand = await PrefixCommand.find({ aliases: { $in: [command] } }); + let foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length > 1) { + foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); } - if (!foundCommand || foundCommand.length > 1) { + if (!foundCommands || foundCommands.length > 1) { await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } - const { id: commandId } = foundCommand[0]; - - const foundRole = interaction.guild.roles.resolve(role); - if (!foundRole) { - await interaction.followUp({ embeds: [noRoleEmbed(role)], ephemeral: true }); - return; - } - const { id: roleId, name: roleName } = foundRole; + const [foundCommand] = foundCommands; + const { id: commandId } = foundCommand; + const { id: roleId, name: roleName } = role; - const existingRolePermission = await PrefixCommandRolePermission.findOne({ commandId, roleId }); + const existingRolePermission = foundCommand.rolePermissions.find((rolePermission) => rolePermission.roleId === roleId); if (existingRolePermission) { const { id: rolePermissionId, type } = existingRolePermission; try { - await existingRolePermission.deleteOne(); + foundCommand.rolePermissions.id(rolePermissionId)?.deleteOne(); + await foundCommand.save(); await interaction.followUp({ embeds: [successEmbed(command, roleName, type, rolePermissionId)], ephemeral: true }); if (modLogsRole) { try { diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index 5770106f..381f3736 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandContent, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Set Content - No Connection', @@ -126,20 +126,21 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman return; } } - const contentData = new PrefixCommandContent({ + const contentData = { versionId, title, content, image, - }); + }; foundCommand.contents.push(contentData); try { await foundCommand.save(); - await interaction.followUp({ embeds: [successEmbed(command, version, contentData.id)], ephemeral: true }); + const { id: contentId } = foundCommand.contents.find((c) => c.versionId === versionId)!; + await interaction.followUp({ embeds: [successEmbed(command, version, contentId)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, version, title, content, image, commandId, versionId, contentData.id)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, version, title, content, image, commandId, versionId, contentId)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts index 5f640233..4ac89f32 100644 --- a/src/lib/schemas/prefixCommandSchemas.ts +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -36,6 +36,7 @@ const prefixCommandContentSchema = new Schema({ }, content: String, image: String, + autoChannels: [String], }); const prefixCommandChannelPermissionSchema = new Schema({ @@ -82,7 +83,4 @@ const prefixCommandSchema = new Schema({ export const PrefixCommandCategory = mongoose.model('PrefixCommandCategory', prefixCommandCategorySchema); export const PrefixCommandVersion = mongoose.model('PrefixCommandVersion', prefixCommandVersionSchema); -export const PrefixCommandContent = mongoose.model('PrefixCommandContent', prefixCommandContentSchema); -export const PrefixCommandChannelPermission = mongoose.model('PrefixCommandChannelPermission', prefixCommandChannelPermissionSchema); -export const PrefixCommandRolePermission = mongoose.model('PrefixCommandRolePermission', prefixCommandRolePermissionSchema); export const PrefixCommand = mongoose.model('PrefixCommand', prefixCommandSchema); From b9905bc6b2e616a34f07db7e040052b9c4e8b13b Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Mon, 27 May 2024 02:43:05 -0700 Subject: [PATCH 05/51] Adding AutoComplete and Content Modal * AutoComplete added for categories, versions and command names where appropriate * Added Modal for Content --- .../functions/listCategories.ts | 10 ++- .../functions/listChannelPermissions.ts | 12 +-- .../prefixCommands/functions/listCommands.ts | 10 ++- .../functions/listRolePermissions.ts | 12 +-- .../prefixCommands/functions/listVersions.ts | 10 ++- .../prefixCommands/functions/setContent.ts | 84 +++++++++++++++++-- .../prefixCommands/functions/showContent.ts | 14 ++-- .../prefixCommandPermissions.ts | 38 ++++++++- .../prefixCommands/prefixCommands.ts | 83 +++++++++++++----- 9 files changed, 212 insertions(+), 61 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/listCategories.ts b/src/commands/moderation/prefixCommands/functions/listCategories.ts index 8731da8e..071817ce 100644 --- a/src/commands/moderation/prefixCommands/functions/listCategories.ts +++ b/src/commands/moderation/prefixCommands/functions/listCategories.ts @@ -26,9 +26,11 @@ const successEmbed = (searchText: string, fields: APIEmbedField[]) => makeEmbed( }); export async function handleListPrefixCommandCategories(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + const conn = getConn(); if (!conn) { - await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); return; } @@ -46,12 +48,12 @@ export async function handleListPrefixCommandCategories(interaction: ChatInputCo }); } try { - await interaction.reply({ embeds: [successEmbed(searchText, embedFields)], ephemeral: false }); + await interaction.followUp({ embeds: [successEmbed(searchText, embedFields)], ephemeral: false }); } catch (error) { Logger.error(`Failed to list prefix command categories with search ${searchText}: ${error}`); - await interaction.reply({ embeds: [failedEmbed(searchText)], ephemeral: true }); + await interaction.followUp({ embeds: [failedEmbed(searchText)], ephemeral: true }); } } else { - await interaction.reply({ embeds: [noResultsEmbed(searchText)], ephemeral: true }); + await interaction.followUp({ embeds: [noResultsEmbed(searchText)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts b/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts index 64732157..01feddc3 100644 --- a/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts +++ b/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts @@ -31,16 +31,18 @@ const successEmbed = (command: string, fields: APIEmbedField[]) => makeEmbed({ }); export async function handleListPrefixCommandChannelPermissions(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + const conn = getConn(); if (!conn) { - await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); return; } const command = interaction.options.getString('command')!; const foundCommands = await PrefixCommand.find({ name: command }); if (!foundCommands || foundCommands.length === 0) { - await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } const [foundCommand] = foundCommands; @@ -57,12 +59,12 @@ export async function handleListPrefixCommandChannelPermissions(interaction: Cha }); } try { - await interaction.reply({ embeds: [successEmbed(command, embedFields)], ephemeral: false }); + await interaction.followUp({ embeds: [successEmbed(command, embedFields)], ephemeral: false }); } catch (error) { Logger.error(`Failed to list prefix command channel permissions for command ${command}: ${error}`); - await interaction.reply({ embeds: [failedEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [failedEmbed(command)], ephemeral: true }); } } else { - await interaction.reply({ embeds: [noResultsEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [noResultsEmbed(command)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/listCommands.ts b/src/commands/moderation/prefixCommands/functions/listCommands.ts index fd478abf..1f7160fe 100644 --- a/src/commands/moderation/prefixCommands/functions/listCommands.ts +++ b/src/commands/moderation/prefixCommands/functions/listCommands.ts @@ -26,9 +26,11 @@ const successEmbed = (searchText: string, fields: APIEmbedField[]) => makeEmbed( }); export async function handleListPrefixCommands(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + const conn = getConn(); if (!conn) { - await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); return; } @@ -46,12 +48,12 @@ export async function handleListPrefixCommands(interaction: ChatInputCommandInte }); } try { - await interaction.reply({ embeds: [successEmbed(searchText, embedFields)], ephemeral: false }); + await interaction.followUp({ embeds: [successEmbed(searchText, embedFields)], ephemeral: false }); } catch (error) { Logger.error(`Failed to list prefix command commands with search ${searchText}: ${error}`); - await interaction.reply({ embeds: [failedEmbed(searchText)], ephemeral: true }); + await interaction.followUp({ embeds: [failedEmbed(searchText)], ephemeral: true }); } } else { - await interaction.reply({ embeds: [noResultsEmbed(searchText)], ephemeral: true }); + await interaction.followUp({ embeds: [noResultsEmbed(searchText)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts b/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts index b535bdcb..1aefa54b 100644 --- a/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts +++ b/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts @@ -31,16 +31,18 @@ const successEmbed = (command: string, fields: APIEmbedField[]) => makeEmbed({ }); export async function handleListPrefixCommandRolePermissions(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + const conn = getConn(); if (!conn) { - await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); return; } const command = interaction.options.getString('command')!; const foundCommands = await PrefixCommand.find({ name: command }); if (!foundCommands || foundCommands.length === 0) { - await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } const [foundCommand] = foundCommands; @@ -57,12 +59,12 @@ export async function handleListPrefixCommandRolePermissions(interaction: ChatIn }); } try { - await interaction.reply({ embeds: [successEmbed(command, embedFields)], ephemeral: false }); + await interaction.followUp({ embeds: [successEmbed(command, embedFields)], ephemeral: false }); } catch (error) { Logger.error(`Failed to list prefix command role permissions for command ${command}: ${error}`); - await interaction.reply({ embeds: [failedEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [failedEmbed(command)], ephemeral: true }); } } else { - await interaction.reply({ embeds: [noResultsEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [noResultsEmbed(command)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/listVersions.ts b/src/commands/moderation/prefixCommands/functions/listVersions.ts index 11755bcd..a84ab8e6 100644 --- a/src/commands/moderation/prefixCommands/functions/listVersions.ts +++ b/src/commands/moderation/prefixCommands/functions/listVersions.ts @@ -26,9 +26,11 @@ const successEmbed = (searchText: string, fields: APIEmbedField[]) => makeEmbed( }); export async function handleListPrefixCommandVersions(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + const conn = getConn(); if (!conn) { - await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); return; } @@ -46,12 +48,12 @@ export async function handleListPrefixCommandVersions(interaction: ChatInputComm }); } try { - await interaction.reply({ embeds: [successEmbed(searchText, embedFields)], ephemeral: false }); + await interaction.followUp({ embeds: [successEmbed(searchText, embedFields)], ephemeral: false }); } catch (error) { Logger.error(`Failed to list prefix command versions with search ${searchText}: ${error}`); - await interaction.reply({ embeds: [failedEmbed(searchText)], ephemeral: true }); + await interaction.followUp({ embeds: [failedEmbed(searchText)], ephemeral: true }); } } else { - await interaction.reply({ embeds: [noResultsEmbed(searchText)], ephemeral: true }); + await interaction.followUp({ embeds: [noResultsEmbed(searchText)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index 381f3736..6763a2cf 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -1,4 +1,4 @@ -import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { ActionRowBuilder, ChatInputCommandInteraction, Colors, ModalBuilder, TextChannel, TextInputBuilder, TextInputStyle, User } from 'discord.js'; import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; const noConnEmbed = makeEmbed({ @@ -69,21 +69,91 @@ const noModLogs = makeEmbed({ }); export async function handleSetPrefixCommandContent(interaction: ChatInputCommandInteraction<'cached'>) { - await interaction.deferReply({ ephemeral: true }); - const conn = getConn(); if (!conn) { - await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); return; } const command = interaction.options.getString('command')!; const version = interaction.options.getString('version')!; - const title = interaction.options.getString('title')!; - const content = interaction.options.getString('content') || ''; - const image = interaction.options.getString('image') || ''; const moderator = interaction.user; + const contentModal = new ModalBuilder({ + customId: 'commandContentModal', + title: `Content for ${command} - ${version}`, + }); + + const commandContentTitle = new TextInputBuilder() + .setCustomId('commandContentTitle') + .setLabel('Title') + .setPlaceholder('Provide a title for the command.') + .setStyle(TextInputStyle.Short) + .setMaxLength(255) + .setMinLength(0) + .setRequired(true); + + const commandContentContent = new TextInputBuilder() + .setCustomId('commandContentContent') + .setLabel('Content') + .setPlaceholder('Provide the content for the command.') + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(2047) + .setMinLength(0) + .setRequired(true); + + const commandContentImageUrl = new TextInputBuilder() + .setCustomId('commandContentImageUrl') + .setLabel('Image URL') + .setPlaceholder('Provide an optional Image URL for the command.') + .setStyle(TextInputStyle.Short) + .setMaxLength(255) + .setMinLength(0) + .setRequired(false); + + const titleActionRow = new ActionRowBuilder().addComponents(commandContentTitle); + const contentActionRow = new ActionRowBuilder().addComponents(commandContentContent); + const imageUrlActionRow = new ActionRowBuilder().addComponents(commandContentImageUrl); + + contentModal.addComponents(titleActionRow); + contentModal.addComponents(contentActionRow); + contentModal.addComponents(imageUrlActionRow); + + await interaction.showModal(contentModal); + + const filter = (interaction: { + customId: string; + user: { id: any; }; + }) => interaction.customId === 'commandContentModal' && interaction.user.id; + + let title = ''; + let content = ''; + let image = ''; + + try { + //Await a modal response + const modalSubmitInteraction = await interaction.awaitModalSubmit({ + filter, + time: 120000, + }); + + await modalSubmitInteraction.reply({ + content: 'Processing command content data.', + ephemeral: true, + }); + + title = modalSubmitInteraction.fields.getTextInputValue('commandContentTitle').trim(); + content = modalSubmitInteraction.fields.getTextInputValue('commandContentContent').trim(); + image = modalSubmitInteraction.fields.getTextInputValue('commandContentImageUrl').trim(); + } catch (error) { + //Handle the error if the user does not respond in time + Logger.error(error); + await interaction.followUp({ + content: 'You did not provide the necessary content information and the change was not made.', + ephemeral: true, + }); + return; + } //Check if the mod logs channel exists const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (!modLogsChannel) { diff --git a/src/commands/moderation/prefixCommands/functions/showContent.ts b/src/commands/moderation/prefixCommands/functions/showContent.ts index b0081cd1..5de6c2f5 100644 --- a/src/commands/moderation/prefixCommands/functions/showContent.ts +++ b/src/commands/moderation/prefixCommands/functions/showContent.ts @@ -52,9 +52,11 @@ const contentEmbed = (command: string, version: string, title: string, content: }); export async function handleShowPrefixCommandContent(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + const conn = getConn(); if (!conn) { - await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); return; } @@ -65,7 +67,7 @@ export async function handleShowPrefixCommandContent(interaction: ChatInputComma foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); } if (!foundCommands || foundCommands.length > 1) { - await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } @@ -80,21 +82,21 @@ export async function handleShowPrefixCommandContent(interaction: ChatInputComma const [foundVersion] = foundVersions; ({ id: versionId } = foundVersion); } else { - await interaction.reply({ embeds: [noVersionEmbed(version)], ephemeral: true }); + await interaction.followUp({ embeds: [noVersionEmbed(version)], ephemeral: true }); return; } } const foundContent = foundCommand.contents.find((content) => content.versionId === versionId); if (!foundContent) { - await interaction.reply({ embeds: [noContentEmbed(command, version)], ephemeral: true }); + await interaction.followUp({ embeds: [noContentEmbed(command, version)], ephemeral: true }); return; } const { id: contentId, title, content, image } = foundContent; try { - await interaction.reply({ embeds: [contentEmbed(command, version, `${title}`, `${content}`, `${image}`, `${commandId}`, `${versionId}`, `${contentId}`)], ephemeral: false }); + await interaction.followUp({ embeds: [contentEmbed(command, version, `${title}`, `${content}`, `${image}`, `${commandId}`, `${versionId}`, `${contentId}`)], ephemeral: false }); } catch (error) { Logger.error(`Failed to show prefix command content for command ${command} and version ${version}: ${error}`); - await interaction.reply({ embeds: [failedEmbed(command, version)], ephemeral: true }); + await interaction.followUp({ embeds: [failedEmbed(command, version)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts b/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts index e1beb796..afc2f29a 100644 --- a/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts +++ b/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts @@ -1,5 +1,5 @@ -import { ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'; -import { constantsConfig, slashCommand, slashCommandStructure } from '../../../lib'; +import { ApplicationCommandOptionChoiceData, ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'; +import { AutocompleteCallback, constantsConfig, getConn, PrefixCommand, slashCommand, slashCommandStructure } from '../../../lib'; import { handleListPrefixCommandChannelPermissions } from './functions/listChannelPermissions'; import { handleListPrefixCommandRolePermissions } from './functions/listRolePermissions'; import { handleAddPrefixCommandChannelPermission } from './functions/addChannelPermission'; @@ -36,6 +36,7 @@ const data = slashCommandStructure({ description: 'Provide the name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, ], @@ -50,6 +51,7 @@ const data = slashCommandStructure({ description: 'Provide the name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, { @@ -80,6 +82,7 @@ const data = slashCommandStructure({ description: 'Provide the name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, { @@ -107,6 +110,7 @@ const data = slashCommandStructure({ description: 'Provide the name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, ], @@ -121,6 +125,7 @@ const data = slashCommandStructure({ description: 'Provide the name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, { @@ -151,6 +156,7 @@ const data = slashCommandStructure({ description: 'Provide the name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, { @@ -166,6 +172,32 @@ const data = slashCommandStructure({ ], }); +const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { + const autoCompleteOption = interaction.options.getFocused(true); + const { name: optionName, value: searchText } = autoCompleteOption; + let choices: ApplicationCommandOptionChoiceData[] = []; + + const conn = getConn(); + + switch (optionName) { + case 'command': + if (!conn) { + return interaction.respond(choices); + } + const foundCommands = await PrefixCommand.find({ name: { $regex: searchText, $options: 'i' } }); + for (let i = 0; i < foundCommands.length; i++) { + const command = foundCommands[i]; + const { name } = command; + choices.push({ name, value: name }); + } + break; + default: + choices = []; + } + + return interaction.respond(choices); +}; + export default slashCommand(data, async ({ interaction }) => { const subcommandGroup = interaction.options.getSubcommandGroup(); const subcommandName = interaction.options.getSubcommand(); @@ -204,4 +236,4 @@ export default slashCommand(data, async ({ interaction }) => { default: await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); } -}); +}, autocompleteCallback); diff --git a/src/commands/moderation/prefixCommands/prefixCommands.ts b/src/commands/moderation/prefixCommands/prefixCommands.ts index 306c9889..74eb2bdb 100644 --- a/src/commands/moderation/prefixCommands/prefixCommands.ts +++ b/src/commands/moderation/prefixCommands/prefixCommands.ts @@ -1,5 +1,5 @@ -import { ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'; -import { constantsConfig, slashCommand, slashCommandStructure } from '../../../lib'; +import { ApplicationCommandOptionChoiceData, ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'; +import { AutocompleteCallback, constantsConfig, getConn, PrefixCommand, PrefixCommandCategory, PrefixCommandVersion, slashCommand, slashCommandStructure } from '../../../lib'; import { handleAddPrefixCommandCategory } from './functions/addCategory'; import { handleModifyPrefixCommandCategory } from './functions/modifyCategory'; import { handleDeletePrefixCommandCategory } from './functions/deleteCategory'; @@ -238,6 +238,7 @@ const data = slashCommandStructure({ description: 'Provide the category for the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, { @@ -287,6 +288,7 @@ const data = slashCommandStructure({ description: 'Provide the category for the prefix command.', type: ApplicationCommandOptionType.String, required: false, + autocomplete: true, max_length: 32, }, { @@ -357,6 +359,7 @@ const data = slashCommandStructure({ description: 'Provide the name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, { @@ -364,6 +367,7 @@ const data = slashCommandStructure({ description: 'Provide the name of the prefix command version. Use GENERIC for the generic content.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, ], @@ -378,6 +382,7 @@ const data = slashCommandStructure({ description: 'Provide the name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, { @@ -385,28 +390,9 @@ const data = slashCommandStructure({ description: 'Provide the name of the prefix command version. Use GENERIC for the generic content.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, - { - name: 'title', - description: 'Provide a title for the prefix command version content.', - type: ApplicationCommandOptionType.String, - required: true, - max_length: 255, - }, - { - name: 'content', - description: 'Provide the content for the prefix command version content.', - type: ApplicationCommandOptionType.String, - required: false, - }, - { - name: 'image', - description: 'Provide a URL for an image for the prefix command version content. Leave empty to set no image.', - type: ApplicationCommandOptionType.String, - required: false, - max_length: 255, - }, ], }, { @@ -428,6 +414,57 @@ const data = slashCommandStructure({ ], }); +const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { + const autoCompleteOption = interaction.options.getFocused(true); + const { name: optionName, value: searchText } = autoCompleteOption; + let choices: ApplicationCommandOptionChoiceData[] = []; + + const conn = getConn(); + + switch (optionName) { + case 'category': + if (!conn) { + return interaction.respond(choices); + } + const foundCategories = await PrefixCommandCategory.find({ name: { $regex: searchText, $options: 'i' } }); + for (let i = 0; i < foundCategories.length; i++) { + const category = foundCategories[i]; + const { name } = category; + choices.push({ name, value: name }); + } + break; + case 'command': + if (!conn) { + return interaction.respond(choices); + } + const foundCommands = await PrefixCommand.find({ name: { $regex: searchText, $options: 'i' } }); + for (let i = 0; i < foundCommands.length; i++) { + const command = foundCommands[i]; + const { name } = command; + choices.push({ name, value: name }); + } + break; + case 'version': + choices = [ + { name: 'GENERIC', value: 'GENERIC' }, + ]; + if (!conn) { + return interaction.respond(choices); + } + const foundVersions = await PrefixCommandVersion.find({ name: { $regex: searchText, $options: 'i' } }); + for (let i = 0; i < foundVersions.length; i++) { + const version = foundVersions[i]; + const { name } = version; + choices.push({ name, value: name }); + } + break; + default: + choices = []; + } + + return interaction.respond(choices); +}; + export default slashCommand(data, async ({ interaction }) => { const subcommandGroup = interaction.options.getSubcommandGroup(); const subcommandName = interaction.options.getSubcommand(); @@ -505,4 +542,4 @@ export default slashCommand(data, async ({ interaction }) => { default: await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); } -}); +}, autocompleteCallback); From 96c1c3afb43e976d1de26a1ba6dbc29ffbf7b57f Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Mon, 2 Sep 2024 16:20:07 -0700 Subject: [PATCH 06/51] Adding Default Channel Versions and add autocomplete to everything --- .../prefixCommands/functions/addVersion.ts | 10 +- .../functions/deleteCategory.ts | 12 +- .../prefixCommands/functions/deleteCommand.ts | 12 +- .../prefixCommands/functions/deleteContent.ts | 20 ++- .../prefixCommands/functions/deleteVersion.ts | 42 +++++- .../prefixCommands/functions/listVersions.ts | 4 +- .../functions/modifyCategory.ts | 11 +- .../prefixCommands/functions/modifyCommand.ts | 11 +- .../prefixCommands/functions/modifyVersion.ts | 23 ++- .../functions/setChannelDefaultVersion.ts | 108 ++++++++++++++ .../prefixCommands/functions/setContent.ts | 62 ++++---- .../functions/showChannelDefaultVersion.ts | 76 ++++++++++ .../functions/unsetChannelDefaultVersion.ts | 87 +++++++++++ .../prefixCommands/prefixCommands.ts | 140 +++++++++++++++--- src/lib/schemas/prefixCommandSchemas.ts | 18 ++- 15 files changed, 538 insertions(+), 98 deletions(-) create mode 100644 src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts create mode 100644 src/commands/moderation/prefixCommands/functions/showChannelDefaultVersion.ts create mode 100644 src/commands/moderation/prefixCommands/functions/unsetChannelDefaultVersion.ts diff --git a/src/commands/moderation/prefixCommands/functions/addVersion.ts b/src/commands/moderation/prefixCommands/functions/addVersion.ts index 329d8fb1..16f1b871 100644 --- a/src/commands/moderation/prefixCommands/functions/addVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/addVersion.ts @@ -24,7 +24,7 @@ const successEmbed = (version: string) => makeEmbed({ color: Colors.Green, }); -const modLogEmbed = (moderator: User, version: string, emoji: string, enabled: boolean, versionId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, version: string, emoji: string, alias: string, enabled: boolean, versionId: string) => makeEmbed({ title: 'Prefix command version added', fields: [ { @@ -39,6 +39,10 @@ const modLogEmbed = (moderator: User, version: string, emoji: string, enabled: b name: 'Emoji', value: emoji, }, + { + name: 'Alias', + value: alias, + }, { name: 'Enabled', value: enabled ? 'Yes' : 'No', @@ -66,6 +70,7 @@ export async function handleAddPrefixCommandVersion(interaction: ChatInputComman const name = interaction.options.getString('name')!; const emoji = interaction.options.getString('emoji')!; + const alias = interaction.options.getString('alias')!; const enabled = interaction.options.getBoolean('is_enabled') || false; const moderator = interaction.user; @@ -82,13 +87,14 @@ export async function handleAddPrefixCommandVersion(interaction: ChatInputComman name, emoji, enabled, + alias, }); try { await prefixCommandVersion.save(); await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, emoji, enabled, prefixCommandVersion.id)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, emoji, alias, enabled, prefixCommandVersion.id)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/functions/deleteCategory.ts b/src/commands/moderation/prefixCommands/functions/deleteCategory.ts index 22259bc9..3dabe9c7 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteCategory.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteCategory.ts @@ -13,9 +13,9 @@ const failedEmbed = (categoryId: string) => makeEmbed({ color: Colors.Red, }); -const doesNotExistsEmbed = (categoryId: string) => makeEmbed({ +const doesNotExistsEmbed = (category: string) => makeEmbed({ title: 'Prefix Commands - Delete Category - Does not exist', - description: `The prefix command category with id ${categoryId} does not exists. Can not delete it.`, + description: `The prefix command category ${category} does not exists. Can not delete it.`, color: Colors.Red, }); @@ -59,7 +59,7 @@ export async function handleDeletePrefixCommandCategory(interaction: ChatInputCo return; } - const categoryId = interaction.options.getString('id')!; + const category = interaction.options.getString('category')!; const moderator = interaction.user; //Check if the mod logs channel exists @@ -68,10 +68,10 @@ export async function handleDeletePrefixCommandCategory(interaction: ChatInputCo await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - const existingCategory = await PrefixCommandCategory.findById(categoryId); + const existingCategory = await PrefixCommandCategory.findOne({ name: category }); if (existingCategory) { - const { name, emoji } = existingCategory; + const { id: categoryId, name, emoji } = existingCategory; try { await existingCategory.deleteOne(); await interaction.followUp({ embeds: [successEmbed(name || '', categoryId)], ephemeral: true }); @@ -87,6 +87,6 @@ export async function handleDeletePrefixCommandCategory(interaction: ChatInputCo await interaction.followUp({ embeds: [failedEmbed(categoryId)], ephemeral: true }); } } else { - await interaction.followUp({ embeds: [doesNotExistsEmbed(categoryId)], ephemeral: true }); + await interaction.followUp({ embeds: [doesNotExistsEmbed(category)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts index 503968f9..4d3a7a47 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts @@ -13,9 +13,9 @@ const failedEmbed = (commandId: string) => makeEmbed({ color: Colors.Red, }); -const doesNotExistsEmbed = (commandId: string) => makeEmbed({ +const doesNotExistsEmbed = (command: string) => makeEmbed({ title: 'Prefix Commands - Delete Command - Does not exist', - description: `The prefix command with id ${commandId} does not exists. Can not delete it.`, + description: `The prefix command ${command} does not exists. Can not delete it.`, color: Colors.Red, }); @@ -67,7 +67,7 @@ export async function handleDeletePrefixCommand(interaction: ChatInputCommandInt return; } - const commandId = interaction.options.getString('id')!; + const command = interaction.options.getString('command')!; const moderator = interaction.user; //Check if the mod logs channel exists @@ -76,10 +76,10 @@ export async function handleDeletePrefixCommand(interaction: ChatInputCommandInt await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - const existingCommand = await PrefixCommand.findById(commandId); + const existingCommand = await PrefixCommand.findOne({ name: command }); if (existingCommand) { - const { name, aliases, isEmbed, embedColor } = existingCommand; + const { id: commandId, name, aliases, isEmbed, embedColor } = existingCommand; try { await existingCommand.deleteOne(); await interaction.followUp({ embeds: [successEmbed(name || '', commandId)], ephemeral: true }); @@ -95,6 +95,6 @@ export async function handleDeletePrefixCommand(interaction: ChatInputCommandInt await interaction.followUp({ embeds: [failedEmbed(commandId)], ephemeral: true }); } } else { - await interaction.followUp({ embeds: [doesNotExistsEmbed(commandId)], ephemeral: true }); + await interaction.followUp({ embeds: [doesNotExistsEmbed(command)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/deleteContent.ts b/src/commands/moderation/prefixCommands/functions/deleteContent.ts index bade158c..cb6e79f2 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteContent.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteContent.ts @@ -7,9 +7,9 @@ const noConnEmbed = makeEmbed({ color: Colors.Red, }); -const noContentEmbed = (contentId: string) => makeEmbed({ +const noContentEmbed = (command: string, version: string) => makeEmbed({ title: 'Prefix Commands - Delete Content - No Content', - description: `Failed to delete command content with ID ${contentId} as the content does not exist.`, + description: `Failed to delete command content for command ${command} and version ${version} as the content does not exist.`, color: Colors.Red, }); @@ -71,7 +71,8 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom return; } - const contentId = interaction.options.getString('id')!; + const command = interaction.options.getString('command')!; + const version = interaction.options.getString('version')!; const moderator = interaction.user; //Check if the mod logs channel exists @@ -80,11 +81,16 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - const foundCommand = await PrefixCommand.findOne({ 'contents._id': contentId }); - const existingContent = foundCommand?.contents.id(contentId) || null; + let versionId = 'GENERIC'; + if (version !== 'GENERIC') { + const foundVersion = await PrefixCommandVersion.findOne({ name: version }); + versionId = foundVersion?.id; + } + const foundCommand = await PrefixCommand.findOne({ 'name': command, 'contents.versionId': versionId }); + const existingContent = foundCommand?.contents.filter((content) => content.versionId === versionId)[0] || null; if (foundCommand && existingContent) { - const { versionId, title, content, image } = existingContent; + const { id: contentId, title, content, image } = existingContent; const { name: commandName } = foundCommand; let versionName = ''; if (versionId !== 'GENERIC') { @@ -110,6 +116,6 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom await interaction.followUp({ embeds: [failedEmbed(contentId)], ephemeral: true }); } } else { - await interaction.followUp({ embeds: [noContentEmbed(contentId)], ephemeral: true }); + await interaction.followUp({ embeds: [noContentEmbed(command, version)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/deleteVersion.ts b/src/commands/moderation/prefixCommands/functions/deleteVersion.ts index db4b6de2..996acd8d 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, PrefixCommandChannelDefaultVersion } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Delete Version - No Connection', @@ -13,15 +13,21 @@ const contentPresentEmbed = makeEmbed({ color: Colors.Red, }); +const channelDefaultVersionPresentEmbed = makeEmbed({ + title: 'Prefix Commands - Delete Version - Default Channel Versions Present', + description: 'There is one or more channel with this version selected as its default version. Please change or unset the default version for those channels first, or use the `force` option to delete the command version and all the default channel versions referencing it (making them default back to the GENERIC version).', + color: Colors.Red, +}); + const failedEmbed = (versionId: string) => makeEmbed({ title: 'Prefix Commands - Delete Version - Failed', description: `Failed to delete the prefix command version with id ${versionId}.`, color: Colors.Red, }); -const doesNotExistsEmbed = (versionId: string) => makeEmbed({ +const doesNotExistsEmbed = (version: string) => makeEmbed({ title: 'Prefix Commands - Delete Version - Does not exist', - description: `The prefix command version with id ${versionId} does not exists. Can not delete it.`, + description: `The prefix command version ${version} does not exists. Can not delete it.`, color: Colors.Red, }); @@ -30,7 +36,7 @@ const successEmbed = (version: string, versionId: string) => makeEmbed({ color: Colors.Green, }); -const modLogEmbed = (moderator: User, version: string, emoji: string, enabled: boolean, versionId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, version: string, emoji: string, alias: string, enabled: boolean, versionId: string) => makeEmbed({ title: 'Prefix command version deleted', fields: [ { @@ -45,6 +51,10 @@ const modLogEmbed = (moderator: User, version: string, emoji: string, enabled: b name: 'Emoji', value: emoji, }, + { + name: 'Alias', + value: alias, + }, { name: 'Enabled', value: enabled ? 'Yes' : 'No', @@ -69,7 +79,7 @@ export async function handleDeletePrefixCommandVersion(interaction: ChatInputCom return; } - const versionId = interaction.options.getString('id')!; + const version = interaction.options.getString('version')!; const force = interaction.options.getBoolean('force') || false; const moderator = interaction.user; @@ -79,15 +89,25 @@ export async function handleDeletePrefixCommandVersion(interaction: ChatInputCom await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - const existingVersion = await PrefixCommandVersion.findById(versionId); + const existingVersion = await PrefixCommandVersion.findOne({ name: version }); + if (!existingVersion) { + await interaction.followUp({ embeds: [doesNotExistsEmbed(version)], ephemeral: true }); + return; + } + const { id: versionId } = existingVersion; const foundContents = await PrefixCommandVersion.find({ versionId }); if (foundContents && foundContents.length > 0 && !force) { await interaction.followUp({ embeds: [contentPresentEmbed], ephemeral: true }); return; } + const foundChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find({ versionId }); + if (foundChannelDefaultVersions && foundChannelDefaultVersions.length > 0 && !force) { + await interaction.followUp({ embeds: [channelDefaultVersionPresentEmbed], ephemeral: true }); + return; + } if (existingVersion) { - const { name, emoji, enabled } = existingVersion; + const { name, emoji, enabled, alias } = existingVersion; try { await existingVersion.deleteOne(); if (foundContents && force) { @@ -96,10 +116,16 @@ export async function handleDeletePrefixCommandVersion(interaction: ChatInputCom await content.deleteOne(); } } + if (foundChannelDefaultVersions && force) { + for (const channelDefaultVersion of foundChannelDefaultVersions) { + // eslint-disable-next-line no-await-in-loop + await channelDefaultVersion.deleteOne(); + } + } await interaction.followUp({ embeds: [successEmbed(name || '', versionId)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name || '', emoji || '', enabled || false, versionId)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name || '', emoji || '', alias || '', enabled || false, versionId)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/functions/listVersions.ts b/src/commands/moderation/prefixCommands/functions/listVersions.ts index a84ab8e6..50cdd0e1 100644 --- a/src/commands/moderation/prefixCommands/functions/listVersions.ts +++ b/src/commands/moderation/prefixCommands/functions/listVersions.ts @@ -41,9 +41,9 @@ export async function handleListPrefixCommandVersions(interaction: ChatInputComm const embedFields: APIEmbedField[] = []; for (let i = 0; i < foundVersions.length; i++) { const version = foundVersions[i]; - const { id, name, emoji, enabled } = version; + const { id, name, emoji, enabled, alias } = version; embedFields.push({ - name: `${name} - ${emoji} - ${enabled ? 'Enabled' : 'Disabled'}`, + name: `${name} - ${emoji} - ${enabled ? 'Enabled' : 'Disabled'} - ${alias}`, value: `${id}`, }); } diff --git a/src/commands/moderation/prefixCommands/functions/modifyCategory.ts b/src/commands/moderation/prefixCommands/functions/modifyCategory.ts index 965581c6..ffd3bbb3 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCategory.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCategory.ts @@ -13,9 +13,9 @@ const failedEmbed = (categoryId: string) => makeEmbed({ color: Colors.Red, }); -const doesNotExistsEmbed = (categoryId: string) => makeEmbed({ +const doesNotExistsEmbed = (category: string) => makeEmbed({ title: 'Prefix Commands - Modify Category - Does not exist', - description: `The prefix command category with id ${categoryId} does not exists. Can not modify it.`, + description: `The prefix command category ${category} does not exists. Can not modify it.`, color: Colors.Red, }); @@ -59,7 +59,7 @@ export async function handleModifyPrefixCommandCategory(interaction: ChatInputCo return; } - const categoryId = interaction.options.getString('id')!; + const category = interaction.options.getString('category')!; const name = interaction.options.getString('name') || ''; const emoji = interaction.options.getString('emoji') || ''; const moderator = interaction.user; @@ -70,9 +70,10 @@ export async function handleModifyPrefixCommandCategory(interaction: ChatInputCo await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - const existingCategory = await PrefixCommandCategory.findById(categoryId); + const existingCategory = await PrefixCommandCategory.findOne({ name: category }); if (existingCategory) { + const { id: categoryId } = existingCategory; existingCategory.name = name || existingCategory.name; existingCategory.emoji = emoji || existingCategory.emoji; try { @@ -91,6 +92,6 @@ export async function handleModifyPrefixCommandCategory(interaction: ChatInputCo await interaction.followUp({ embeds: [failedEmbed(categoryId)], ephemeral: true }); } } else { - await interaction.followUp({ embeds: [doesNotExistsEmbed(categoryId)], ephemeral: true }); + await interaction.followUp({ embeds: [doesNotExistsEmbed(category)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index 71892658..6f42ca4c 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -19,9 +19,9 @@ const categoryNotFoundEmbed = (category: string) => makeEmbed({ color: Colors.Red, }); -const doesNotExistsEmbed = (commandId: string) => makeEmbed({ +const doesNotExistsEmbed = (command: string) => makeEmbed({ title: 'Prefix Commands - Modify Command - Does not exist', - description: `The prefix command with id ${commandId} does not exists. Can not modify it.`, + description: `The prefix command ${command} does not exists. Can not modify it.`, color: Colors.Red, }); @@ -72,7 +72,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); } - const commandId = interaction.options.getString('id')!; + const command = interaction.options.getString('command')!; const name = interaction.options.getString('name') || ''; const category = interaction.options.getString('category') || ''; const aliasesString = interaction.options.getString('aliases') || ''; @@ -96,9 +96,10 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt return; } } - const existingCommand = await PrefixCommand.findById(commandId); + const existingCommand = await PrefixCommand.findOne({ name: command }); if (existingCommand) { + const { id: commandId } = existingCommand; existingCommand.name = name || existingCommand.name; existingCommand.categoryId = foundCategory?.id || existingCommand.categoryId; existingCommand.aliases = aliases.length > 0 ? aliases : existingCommand.aliases; @@ -120,6 +121,6 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt await interaction.followUp({ embeds: [failedEmbed(commandId)], ephemeral: true }); } } else { - await interaction.followUp({ embeds: [doesNotExistsEmbed(commandId)], ephemeral: true }); + await interaction.followUp({ embeds: [doesNotExistsEmbed(command)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts index d938bb9c..a47e33c4 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts @@ -13,9 +13,9 @@ const failedEmbed = (versionId: string) => makeEmbed({ color: Colors.Red, }); -const doesNotExistsEmbed = (versionId: string) => makeEmbed({ +const doesNotExistsEmbed = (version: string) => makeEmbed({ title: 'Prefix Commands - Modify Version - Does not exist', - description: `The prefix command version with id ${versionId} does not exists. Can not modify it.`, + description: `The prefix command version ${version} does not exists. Can not modify it.`, color: Colors.Red, }); @@ -24,7 +24,7 @@ const successEmbed = (version: string, versionId: string) => makeEmbed({ color: Colors.Green, }); -const modLogEmbed = (moderator: User, version: string, emoji: string, enabled: boolean, versionId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, version: string, emoji: string, alias: string, enabled: boolean, versionId: string) => makeEmbed({ title: 'Prefix command version modified', fields: [ { @@ -39,6 +39,10 @@ const modLogEmbed = (moderator: User, version: string, emoji: string, enabled: b name: 'Emoji', value: emoji, }, + { + name: 'Alias', + value: alias, + }, { name: 'Enabled', value: enabled ? 'Yes' : 'No', @@ -63,9 +67,10 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom return; } - const versionId = interaction.options.getString('id')!; + const version = interaction.options.getString('version')!; const name = interaction.options.getString('name') || ''; const emoji = interaction.options.getString('emoji') || ''; + const alias = interaction.options.getString('alias') || ''; const enabled = interaction.options.getBoolean('is_enabled') || null; const moderator = interaction.user; @@ -75,19 +80,21 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - const existingVersion = await PrefixCommandVersion.findById(versionId); + const existingVersion = await PrefixCommandVersion.findOne({ version }); if (existingVersion) { + const { id: versionId } = existingVersion; existingVersion.name = name || existingVersion.name; existingVersion.emoji = emoji || existingVersion.emoji; + existingVersion.alias = alias || existingVersion.alias; existingVersion.enabled = enabled !== null ? enabled : existingVersion.enabled; try { await existingVersion.save(); - const { name, emoji, enabled } = existingVersion; + const { name, emoji, alias, enabled } = existingVersion; await interaction.followUp({ embeds: [successEmbed(name, versionId)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, emoji, enabled || false, versionId)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, emoji, alias, enabled || false, versionId)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } @@ -97,6 +104,6 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom await interaction.followUp({ embeds: [failedEmbed(versionId)], ephemeral: true }); } } else { - await interaction.followUp({ embeds: [doesNotExistsEmbed(versionId)], ephemeral: true }); + await interaction.followUp({ embeds: [doesNotExistsEmbed(version)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts new file mode 100644 index 00000000..33c841ea --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts @@ -0,0 +1,108 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommandChannelDefaultVersion, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Set Default Channel Version - No Connection', + description: 'Could not connect to the database. Unable to set the channel default version.', + color: Colors.Red, +}); + +const noVersionEmbed = (channel: string) => makeEmbed({ + title: 'Prefix Commands - Show Default Channel Version - No Version', + description: `Failed to show default channel version for channel ${channel} as the configured version does not exist.`, + color: Colors.Red, +}); + +const failedEmbed = (channel: string) => makeEmbed({ + title: 'Prefix Commands - Set Default Channel Version - Failed', + description: `Failed to set the channel default version for channel ${channel}.`, + color: Colors.Red, +}); + +const successEmbed = (channel: string, version: string, emoji: string) => makeEmbed({ + title: `Prefix Command Channel Default version set for channel ${channel} to version ${version} ${emoji}.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, channel: string, version: string, emoji: string) => makeEmbed({ + title: 'Prefix command version added', + fields: [ + { + name: 'Channel', + value: channel, + }, + { + name: 'Version', + value: `${version} - ${emoji}`, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + ], + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Set Default Channel Version - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleSetPrefixCommandChannelDefaultVersion(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const channel = interaction.options.getChannel('channel')!; + const version = interaction.options.getString('version')!; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + let foundVersion; + if (version !== 'GENERIC') { + foundVersion = await PrefixCommandVersion.findOne({ name: version }); + } + + if (foundVersion || version === 'GENERIC') { + const { id: channelId, name: channelName } = channel; + let versionId = ''; + let emoji = ''; + if (version === 'GENERIC') { + versionId = 'GENERIC'; + emoji = ''; + } else if (foundVersion) { + versionId = foundVersion.id; + emoji = foundVersion.emoji; + } + const foundChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find({ channelId }); + const channelDefaultVersion = foundChannelDefaultVersions.length === 1 ? foundChannelDefaultVersions[0] : new PrefixCommandChannelDefaultVersion({ channelId, versionId }); + channelDefaultVersion.versionId = versionId; + try { + await channelDefaultVersion.save(); + await interaction.followUp({ embeds: [successEmbed(channelName, version, emoji)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, channelName, version, emoji)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to set the default channel version for channel ${channelName} to version ${version}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(channelName)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [noVersionEmbed(version)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index 6763a2cf..c6ff8e12 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -79,6 +79,33 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman const version = interaction.options.getString('version')!; const moderator = interaction.user; + let foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length !== 1) { + foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); + } + if (!foundCommands || foundCommands.length !== 1) { + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + + const foundCommand = foundCommands[0]; + const { id: commandId } = foundCommand; + let versionId = ''; + let foundVersions = null; + if (version === 'GENERIC' || version === 'generic') { + versionId = 'GENERIC'; + } else { + foundVersions = await PrefixCommandVersion.find({ name: version }); + if (foundVersions && foundVersions.length === 1) { + versionId = foundVersions[0].id; + } else { + await interaction.followUp({ embeds: [noVersionEmbed(version)], ephemeral: true }); + return; + } + } + + const foundContent = foundCommand.contents.find((c) => c.versionId === versionId); + const contentModal = new ModalBuilder({ customId: 'commandContentModal', title: `Content for ${command} - ${version}`, @@ -91,7 +118,8 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman .setStyle(TextInputStyle.Short) .setMaxLength(255) .setMinLength(0) - .setRequired(true); + .setRequired(true) + .setValue(foundContent ? foundContent.title : ''); const commandContentContent = new TextInputBuilder() .setCustomId('commandContentContent') @@ -100,7 +128,8 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman .setStyle(TextInputStyle.Paragraph) .setMaxLength(2047) .setMinLength(0) - .setRequired(true); + .setRequired(true) + .setValue(foundContent && foundContent.content ? foundContent.content : ''); const commandContentImageUrl = new TextInputBuilder() .setCustomId('commandContentImageUrl') @@ -109,7 +138,8 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman .setStyle(TextInputStyle.Short) .setMaxLength(255) .setMinLength(0) - .setRequired(false); + .setRequired(false) + .setValue(foundContent && foundContent.image ? foundContent.image : ''); const titleActionRow = new ActionRowBuilder().addComponents(commandContentTitle); const contentActionRow = new ActionRowBuilder().addComponents(commandContentContent); @@ -160,32 +190,6 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - let foundCommands = await PrefixCommand.find({ name: command }); - if (!foundCommands || foundCommands.length !== 1) { - foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); - } - if (!foundCommands || foundCommands.length !== 1) { - await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); - return; - } - - const foundCommand = foundCommands[0]; - const { id: commandId } = foundCommand; - let versionId = ''; - let foundVersions = null; - if (version === 'GENERIC' || version === 'generic') { - versionId = 'GENERIC'; - } else { - foundVersions = await PrefixCommandVersion.find({ name: version }); - if (foundVersions && foundVersions.length === 1) { - versionId = foundVersions[0].id; - } else { - await interaction.followUp({ embeds: [noVersionEmbed(version)], ephemeral: true }); - return; - } - } - - const foundContent = foundCommand.contents.find((c) => c.versionId === versionId); if (foundContent) { const foundData = foundCommand.contents.id(foundContent.id); try { diff --git a/src/commands/moderation/prefixCommands/functions/showChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/showChannelDefaultVersion.ts new file mode 100644 index 00000000..4f8ca271 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/showChannelDefaultVersion.ts @@ -0,0 +1,76 @@ +import { ChatInputCommandInteraction, Colors } from 'discord.js'; +import { getConn, PrefixCommandChannelDefaultVersion, PrefixCommandVersion, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Show Default Channel Version - No Connection', + description: 'Could not connect to the database. Unable to show the channel default version.', + color: Colors.Red, +}); + +const noVersionEmbed = (channel: string) => makeEmbed({ + title: 'Prefix Commands - Show Default Channel Version - No Version', + description: `Failed to show default channel version for channel ${channel} as the configured version does not exist.`, + color: Colors.Red, +}); + +const noChannelDefaultVersionEmbed = (channel: string) => makeEmbed({ + title: 'Prefix Commands - Show Default Channel Version - No Default Channel Version', + description: `Failed to show the channel default version for channel ${channel} as there is no default version set.`, + color: Colors.Red, +}); + +const failedEmbed = (channel: string) => makeEmbed({ + title: 'Prefix Commands - Show Default Channel Version - Failed', + description: `Failed to show the channel default version for channel ${channel}.`, + color: Colors.Red, +}); + +const contentEmbed = (channel: string, version: string, emoji: string, versionId: string) => makeEmbed({ + title: `Prefix Commands - Show Default Channel Version - ${channel} - ${version}`, + fields: [ + { + name: 'Channel', + value: channel, + }, + { + name: 'Version', + value: `${version} - ${emoji}`, + }, + ], + footer: { text: `Version ID: ${versionId}` }, + color: Colors.Green, +}); + +export async function handleShowPrefixCommandChannelDefaultVersion(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const channel = interaction.options.getChannel('channel')!; + const { id: channelId, name: channelName } = channel; + const foundChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find({ channelId }); + if (!foundChannelDefaultVersions || foundChannelDefaultVersions.length > 1) { + await interaction.followUp({ embeds: [noChannelDefaultVersionEmbed(channelName)], ephemeral: true }); + return; + } + + const [foundChannelDefaultVersion] = foundChannelDefaultVersions; + const { versionId } = foundChannelDefaultVersion; + const foundVersion = await PrefixCommandVersion.findById(versionId); + if (!foundVersion) { + await interaction.followUp({ embeds: [noVersionEmbed(channelName)], ephemeral: true }); + return; + } + + const { name: version, emoji } = foundVersion; + try { + await interaction.followUp({ embeds: [contentEmbed(channelName, `${version}`, `${emoji}`, `${versionId}`)], ephemeral: false }); + } catch (error) { + Logger.error(`Failed to show the channel default version for channel ${channel} and version ${version}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(channelName)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/unsetChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/unsetChannelDefaultVersion.ts new file mode 100644 index 00000000..dd19f2f7 --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/unsetChannelDefaultVersion.ts @@ -0,0 +1,87 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, PrefixCommandChannelDefaultVersion, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Unset Default Channel Version - No Connection', + description: 'Could not connect to the database. Unable to unset the default channel version.', + color: Colors.Red, +}); + +const failedEmbed = (channel: string) => makeEmbed({ + title: 'Prefix Commands - Unset Default Channel Version - Failed', + description: `Failed to unset the default channel version with for ${channel}.`, + color: Colors.Red, +}); + +const doesNotExistsEmbed = (channel: string) => makeEmbed({ + title: 'Prefix Commands - Unset Default Channel Version - Does not exist', + description: `The default channel version with for ${channel} does not exists. Can not unset it.`, + color: Colors.Red, +}); + +const successEmbed = (channel: string) => makeEmbed({ + title: `Default channel version for channel ${channel} was unset successfully.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, channel: string) => makeEmbed({ + title: 'Prefix Commands - Default Channel Version unset', + fields: [ + { + name: 'Channel', + value: channel, + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + ], + color: Colors.Red, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Unset Default Channel Version - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleUnsetPrefixCommandChannelDefaultVersion(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const channel = interaction.options.getChannel('channel')!; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + const { id: channelId, name: channelName } = channel; + const foundChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find({ channelId }); + + if (foundChannelDefaultVersions && foundChannelDefaultVersions.length > 0) { + try { + await PrefixCommandChannelDefaultVersion.deleteOne({ channelId }); + await interaction.followUp({ embeds: [successEmbed(channelName)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, channelName)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to unset a default channel version for channel ${channelName}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(channelName)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [doesNotExistsEmbed(channelName)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/prefixCommands.ts b/src/commands/moderation/prefixCommands/prefixCommands.ts index 74eb2bdb..d19d8b66 100644 --- a/src/commands/moderation/prefixCommands/prefixCommands.ts +++ b/src/commands/moderation/prefixCommands/prefixCommands.ts @@ -15,6 +15,9 @@ import { handleListPrefixCommands } from './functions/listCommands'; import { handleShowPrefixCommandContent } from './functions/showContent'; import { handleSetPrefixCommandContent } from './functions/setContent'; import { handleDeletePrefixCommandContent } from './functions/deleteContent'; +import { handleShowPrefixCommandChannelDefaultVersion } from './functions/showChannelDefaultVersion'; +import { handleSetPrefixCommandChannelDefaultVersion } from './functions/setChannelDefaultVersion'; +import { handleUnsetPrefixCommandChannelDefaultVersion } from './functions/unsetChannelDefaultVersion'; const colorChoices = []; for (let i = 0; i < Object.keys(constantsConfig.colors).length; i++) { @@ -45,6 +48,7 @@ const data = slashCommandStructure({ description: 'Provide a name for the prefix command category.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 32, }, { @@ -62,11 +66,12 @@ const data = slashCommandStructure({ type: ApplicationCommandOptionType.Subcommand, options: [ { - name: 'id', - description: 'Provide the id of the prefix command category.', + name: 'category', + description: 'Provide the category name of the prefix command category.', type: ApplicationCommandOptionType.String, required: true, - max_length: 24, + autocomplete: true, + max_length: 32, }, { name: 'name', @@ -90,11 +95,12 @@ const data = slashCommandStructure({ type: ApplicationCommandOptionType.Subcommand, options: [ { - name: 'id', - description: 'Provide the id of the prefix command category.', + name: 'category', + description: 'Provide the category name of the prefix command category.', type: ApplicationCommandOptionType.String, required: true, - max_length: 24, + autocomplete: true, + max_length: 32, }, ], }, @@ -138,6 +144,13 @@ const data = slashCommandStructure({ required: true, max_length: 128, }, + { + name: 'alias', + description: 'Provide an alias for the prefix command version.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 32, + }, { name: 'is_enabled', description: 'Indicate wether this version is enabled.', @@ -152,11 +165,12 @@ const data = slashCommandStructure({ type: ApplicationCommandOptionType.Subcommand, options: [ { - name: 'id', - description: 'Provide the id of the prefix command version.', + name: 'version', + description: 'Provide the name of the prefix command version.', type: ApplicationCommandOptionType.String, required: true, - max_length: 24, + autocomplete: true, + max_length: 32, }, { name: 'name', @@ -172,6 +186,13 @@ const data = slashCommandStructure({ required: false, max_length: 128, }, + { + name: 'alias', + description: 'Provide an alias for the prefix command version.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 32, + }, { name: 'is_enabled', description: 'Indicate wether this version is enabled.', @@ -186,11 +207,12 @@ const data = slashCommandStructure({ type: ApplicationCommandOptionType.Subcommand, options: [ { - name: 'id', - description: 'Provide the id of the prefix command version.', + name: 'version', + description: 'Provide the name of the prefix command version.', type: ApplicationCommandOptionType.String, required: true, - max_length: 24, + autocomplete: true, + max_length: 32, }, { name: 'force', @@ -270,10 +292,11 @@ const data = slashCommandStructure({ type: ApplicationCommandOptionType.Subcommand, options: [ { - name: 'id', - description: 'Provide the id of the prefix command.', + name: 'command', + description: 'Provide the command name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 24, }, { @@ -320,10 +343,11 @@ const data = slashCommandStructure({ type: ApplicationCommandOptionType.Subcommand, options: [ { - name: 'id', - description: 'Provide the id of the prefix command.', + name: 'command', + description: 'Provide the command name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, + autocomplete: true, max_length: 24, }, ], @@ -401,11 +425,74 @@ const data = slashCommandStructure({ type: ApplicationCommandOptionType.Subcommand, options: [ { - name: 'id', - description: 'Provide the ID of the prefix command content.', + name: 'command', + description: 'Provide the name of the prefix command.', type: ApplicationCommandOptionType.String, required: true, - max_length: 24, + autocomplete: true, + max_length: 32, + }, + { + name: 'version', + description: 'Provide the name of the prefix command version. Use GENERIC for the generic content.', + type: ApplicationCommandOptionType.String, + required: true, + autocomplete: true, + max_length: 32, + }, + ], + }, + ], + }, + { + name: 'channel-default-version', + description: 'Manage prefix command default versions for channels.', + type: ApplicationCommandOptionType.SubcommandGroup, + options: [ + { + name: 'show', + description: 'Show the default version for a channel.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'channel', + description: 'Provide the channel to show the default version for.', + type: ApplicationCommandOptionType.Channel, + required: true, + }, + ], + }, + { + name: 'set', + description: 'Set the default version for a channel.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'channel', + description: 'Provide the channel to set the default version for.', + type: ApplicationCommandOptionType.Channel, + required: true, + }, + { + name: 'version', + description: 'Provide the version to set as default.', + type: ApplicationCommandOptionType.String, + required: true, + autocomplete: true, + max_length: 32, + }, + ], + }, + { + name: 'unset', + description: 'Unset the default version for a channel.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'channel', + description: 'Provide the channel to unset the default version for.', + type: ApplicationCommandOptionType.Channel, + required: true, }, ], }, @@ -539,6 +626,21 @@ export default slashCommand(data, async ({ interaction }) => { await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); } break; + case 'channel-default': + switch (subcommandName) { + case 'show': + await handleShowPrefixCommandChannelDefaultVersion(interaction); + break; + case 'set': + await handleSetPrefixCommandChannelDefaultVersion(interaction); + break; + case 'unset': + await handleUnsetPrefixCommandChannelDefaultVersion(interaction); + break; + default: + await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + } + break; default: await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); } diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts index 4ac89f32..fe5e670c 100644 --- a/src/lib/schemas/prefixCommandSchemas.ts +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -22,9 +22,25 @@ const prefixCommandVersionSchema = new Schema({ required: true, unique: true, }, + alias: { + type: String, + required: true, + unique: true, + }, enabled: Boolean, }); +const prefixCommandChannelDefaultVersionSchema = new Schema({ + channelId: { + type: String, + required: true, + }, + versionId: { + type: String, + required: true, + }, +}); + const prefixCommandContentSchema = new Schema({ versionId: { type: String, @@ -36,7 +52,6 @@ const prefixCommandContentSchema = new Schema({ }, content: String, image: String, - autoChannels: [String], }); const prefixCommandChannelPermissionSchema = new Schema({ @@ -83,4 +98,5 @@ const prefixCommandSchema = new Schema({ export const PrefixCommandCategory = mongoose.model('PrefixCommandCategory', prefixCommandCategorySchema); export const PrefixCommandVersion = mongoose.model('PrefixCommandVersion', prefixCommandVersionSchema); +export const PrefixCommandChannelDefaultVersion = mongoose.model('PrefixCommandChannelDefaultVersion', prefixCommandChannelDefaultVersionSchema); export const PrefixCommand = mongoose.model('PrefixCommand', prefixCommandSchema); From 4a76dfa5784ebe2f76941c73998c8c96900b7ca5 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 4 Sep 2024 22:08:33 -0700 Subject: [PATCH 07/51] Introducing cache mechanism to keep local cache, manage it and initial message create handler --- config/production.json | 1 + config/staging.json | 3 +- package-lock.json | 38 +++++ package.json | 1 + src/commands/index.ts | 2 + .../prefixCommands/functions/addCommand.ts | 22 ++- .../prefixCommands/functions/addVersion.ts | 20 ++- .../prefixCommands/functions/deleteCommand.ts | 3 +- .../prefixCommands/functions/deleteContent.ts | 5 +- .../prefixCommands/functions/deleteVersion.ts | 3 +- .../prefixCommands/functions/modifyCommand.ts | 24 ++- .../prefixCommands/functions/modifyVersion.ts | 24 ++- .../prefixCommands/functions/setContent.ts | 5 +- .../prefixCommandCacheUpdate.ts | 72 +++++++++ src/events/index.ts | 2 + src/events/messageCreateHandler.ts | 92 +++++++++++ src/events/ready.ts | 42 +++++ src/lib/cache/cacheManager.ts | 149 ++++++++++++++++++ src/lib/config.ts | 1 + src/lib/index.ts | 3 + 20 files changed, 498 insertions(+), 14 deletions(-) create mode 100644 src/commands/moderation/prefixCommands/prefixCommandCacheUpdate.ts create mode 100644 src/events/messageCreateHandler.ts create mode 100644 src/lib/cache/cacheManager.ts diff --git a/config/production.json b/config/production.json index 97e71b7c..750329f2 100644 --- a/config/production.json +++ b/config/production.json @@ -30,6 +30,7 @@ "1163130801131634868", "1021464464928809022" ], + "prefixCommandPrefix": ".", "roleAssignmentIds": [ { "group": "interestedIn", diff --git a/config/staging.json b/config/staging.json index 76389adb..4415fad9 100644 --- a/config/staging.json +++ b/config/staging.json @@ -31,6 +31,7 @@ "1163130801131634868", "1021464464928809022" ], + "prefixCommandPrefix": ".", "roleAssignmentIds": [ { "group": "interestedIn", @@ -87,7 +88,7 @@ "MEDIA_TEAM", "FBW_EMERITUS" ] - }, + , "roles": { "ADMIN_TEAM": "1162821800225419272", "BOT_DEVELOPER": "1162822853306101901", diff --git a/package-lock.json b/package-lock.json index 1da2a388..28533493 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@hokify/agenda": "^6.0.0", "@octokit/request": "^8.1.1", "bad-words": "^3.0.4", + "cache-manager": "^5.7.6", "config": "^3.3.9", "discord.js": "^14.11.0", "jsdom": "^23.2.0", @@ -3426,6 +3427,25 @@ "ieee754": "^1.1.13" } }, + "node_modules/cache-manager": { + "version": "5.7.6", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.6.tgz", + "integrity": "sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.2.2", + "promise-coalesce": "^1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -4572,6 +4592,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -5734,6 +5759,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6403,6 +6433,14 @@ "node": ">=0.4.0" } }, + "node_modules/promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==", + "engines": { + "node": ">=16" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 20845ba2..8c34b285 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@hokify/agenda": "^6.0.0", "@octokit/request": "^8.1.1", "bad-words": "^3.0.4", + "cache-manager": "^5.7.6", "config": "^3.3.9", "discord.js": "^14.11.0", "jsdom": "^23.2.0", diff --git a/src/commands/index.ts b/src/commands/index.ts index 05c36b03..7e0b060d 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -33,6 +33,7 @@ import clearMessages from './moderation/clearMessages'; import locate from './utils/locate/locate'; import prefixCommands from './moderation/prefixCommands/prefixCommands'; import prefixCommandPermissions from './moderation/prefixCommands/prefixCommandPermissions'; +import prefixCommandCacheUpdate from './moderation/prefixCommands/prefixCommandCacheUpdate'; const commandArray: SlashCommand[] = [ ping, @@ -69,6 +70,7 @@ const commandArray: SlashCommand[] = [ locate, prefixCommands, prefixCommandPermissions, + prefixCommandCacheUpdate, ]; export default commandArray; diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts index 8a3113be..6edfede0 100644 --- a/src/commands/moderation/prefixCommands/functions/addCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandCategory } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandCategory, loadSinglePrefixCommandToCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Add Command - No Connection', @@ -13,6 +13,12 @@ const failedEmbed = (command: string) => makeEmbed({ color: Colors.Red, }); +const wrongFormatEmbed = (invalidString: string) => makeEmbed({ + title: 'Prefix Commands - Add Command - Wrong format', + description: `The name and aliases of a command can only contain alphanumerical characters, underscores and dashes. ${invalidString} is invalid.`, + color: Colors.Red, +}); + const categoryNotFoundEmbed = (category: string) => makeEmbed({ title: 'Prefix Commands - Add Command - Category not found', description: `The prefix command category ${category} does not exist. Please create it first.`, @@ -82,6 +88,19 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera const embedColor = interaction.options.getString('embed_color') || ''; const moderator = interaction.user; + const nameRegex = /^[\w\d-_]+$/; + if (!nameRegex.test(name)) { + await interaction.followUp({ embeds: [wrongFormatEmbed(name)], ephemeral: true }); + return; + } + for (const alias of aliases) { + if (!nameRegex.test(alias)) { + // eslint-disable-next-line no-await-in-loop + await interaction.followUp({ embeds: [wrongFormatEmbed(alias)], ephemeral: true }); + return; + } + } + //Check if the mod logs channel exists const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (!modLogsChannel) { @@ -110,6 +129,7 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera }); try { await prefixCommand.save(); + await loadSinglePrefixCommandToCache(prefixCommand.toObject(), name, aliases); await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/addVersion.ts b/src/commands/moderation/prefixCommands/functions/addVersion.ts index 16f1b871..50bd70c5 100644 --- a/src/commands/moderation/prefixCommands/functions/addVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/addVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, loadSinglePrefixCommandVersionToCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Add Version - No Connection', @@ -13,6 +13,12 @@ const failedEmbed = (version: string) => makeEmbed({ color: Colors.Red, }); +const wrongFormatEmbed = (invalidString: string) => makeEmbed({ + title: 'Prefix Commands - Add Version - Wrong format', + description: `The name and alias of a version can only contain alphanumerical characters, underscores and dashes. "${invalidString}" is invalid.`, + color: Colors.Red, +}); + const alreadyExistsEmbed = (version: string) => makeEmbed({ title: 'Prefix Commands - Add Version - Already exists', description: `The prefix command version ${version} already exists. Not adding again.`, @@ -74,6 +80,17 @@ export async function handleAddPrefixCommandVersion(interaction: ChatInputComman const enabled = interaction.options.getBoolean('is_enabled') || false; const moderator = interaction.user; + const nameRegex = /^[\w\d-_]+$/; + if (!nameRegex.test(name)) { + await interaction.followUp({ embeds: [wrongFormatEmbed(name)], ephemeral: true }); + return; + } + if (!nameRegex.test(alias)) { + // eslint-disable-next-line no-await-in-loop + await interaction.followUp({ embeds: [wrongFormatEmbed(alias)], ephemeral: true }); + return; + } + //Check if the mod logs channel exists const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (!modLogsChannel) { @@ -91,6 +108,7 @@ export async function handleAddPrefixCommandVersion(interaction: ChatInputComman }); try { await prefixCommandVersion.save(); + await loadSinglePrefixCommandVersionToCache(prefixCommandVersion.toObject(), alias); await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts index 4d3a7a47..1013b5ee 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, clearSinglePrefixCommandCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Delete Command - No Connection', @@ -82,6 +82,7 @@ export async function handleDeletePrefixCommand(interaction: ChatInputCommandInt const { id: commandId, name, aliases, isEmbed, embedColor } = existingCommand; try { await existingCommand.deleteOne(); + await clearSinglePrefixCommandCache(name); await interaction.followUp({ embeds: [successEmbed(name || '', commandId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/deleteContent.ts b/src/commands/moderation/prefixCommands/functions/deleteContent.ts index cb6e79f2..4ef35ad2 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteContent.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteContent.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, Logger, makeEmbed, PrefixCommand, PrefixCommandVersion } from '../../../../lib'; +import { constantsConfig, getConn, Logger, makeEmbed, PrefixCommand, PrefixCommandVersion, refreshSinglePrefixCommandCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Delete Content - No Connection', @@ -91,7 +91,7 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom if (foundCommand && existingContent) { const { id: contentId, title, content, image } = existingContent; - const { name: commandName } = foundCommand; + const { name: commandName, aliases: commandAliases } = foundCommand; let versionName = ''; if (versionId !== 'GENERIC') { const foundVersion = await PrefixCommandVersion.findById(versionId); @@ -103,6 +103,7 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom try { foundCommand.contents.id(contentId)?.deleteOne(); await foundCommand.save(); + await refreshSinglePrefixCommandCache(commandName, foundCommand.toObject(), commandName, commandAliases); await interaction.followUp({ embeds: [successEmbed(`${commandName}`, `${versionName}`, `${contentId}`)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/deleteVersion.ts b/src/commands/moderation/prefixCommands/functions/deleteVersion.ts index 996acd8d..b6af5f56 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, PrefixCommandChannelDefaultVersion } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, PrefixCommandChannelDefaultVersion, clearSinglePrefixCommandVersionCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Delete Version - No Connection', @@ -110,6 +110,7 @@ export async function handleDeletePrefixCommandVersion(interaction: ChatInputCom const { name, emoji, enabled, alias } = existingVersion; try { await existingVersion.deleteOne(); + await clearSinglePrefixCommandVersionCache(alias); if (foundContents && force) { for (const content of foundContents) { // eslint-disable-next-line no-await-in-loop diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index 6f42ca4c..5152e932 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandCategory } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandCategory, refreshSinglePrefixCommandCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Modify Command - No Connection', @@ -13,6 +13,12 @@ const failedEmbed = (commandId: string) => makeEmbed({ color: Colors.Red, }); +const wrongFormatEmbed = (invalidString: string) => makeEmbed({ + title: 'Prefix Commands - Modify Command - Wrong format', + description: `The name and aliases of a command can only contain alphanumerical characters, underscores and dashes. "${invalidString}" is invalid.`, + color: Colors.Red, +}); + const categoryNotFoundEmbed = (category: string) => makeEmbed({ title: 'Prefix Commands - Modify Command - Category not found', description: `The prefix command category ${category} does not exist. Please create it first.`, @@ -81,6 +87,19 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt const embedColor = interaction.options.getString('embed_color') || ''; const moderator = interaction.user; + const nameRegex = /^[\w\d-_]+$/; + if (name && !nameRegex.test(name)) { + await interaction.followUp({ embeds: [wrongFormatEmbed(name)], ephemeral: true }); + return; + } + for (const alias of aliases) { + if (!nameRegex.test(alias)) { + // eslint-disable-next-line no-await-in-loop + await interaction.followUp({ embeds: [wrongFormatEmbed(alias)], ephemeral: true }); + return; + } + } + //Check if the mod logs channel exists const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (!modLogsChannel) { @@ -99,7 +118,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt const existingCommand = await PrefixCommand.findOne({ name: command }); if (existingCommand) { - const { id: commandId } = existingCommand; + const { id: commandId, name: oldName } = existingCommand; existingCommand.name = name || existingCommand.name; existingCommand.categoryId = foundCategory?.id || existingCommand.categoryId; existingCommand.aliases = aliases.length > 0 ? aliases : existingCommand.aliases; @@ -108,6 +127,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt try { await existingCommand.save(); const { name, aliases, isEmbed, embedColor } = existingCommand; + await refreshSinglePrefixCommandCache(oldName, existingCommand.toObject(), name, aliases); await interaction.followUp({ embeds: [successEmbed(name, commandId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts index a47e33c4..762d433b 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, refreshAllPrefixCommandVersionsCache, refreshSinglePrefixCommandVersionCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Modify Version - No Connection', @@ -13,6 +13,12 @@ const failedEmbed = (versionId: string) => makeEmbed({ color: Colors.Red, }); +const wrongFormatEmbed = (invalidString: string) => makeEmbed({ + title: 'Prefix Commands - Modify Version - Wrong format', + description: `The name and alias of a version can only contain alphanumerical characters, underscores and dashes. "${invalidString}" is invalid.`, + color: Colors.Red, +}); + const doesNotExistsEmbed = (version: string) => makeEmbed({ title: 'Prefix Commands - Modify Version - Does not exist', description: `The prefix command version ${version} does not exists. Can not modify it.`, @@ -74,16 +80,27 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom const enabled = interaction.options.getBoolean('is_enabled') || null; const moderator = interaction.user; + const nameRegex = /^[\w\d-_]+$/; + if (name && !nameRegex.test(name)) { + await interaction.followUp({ embeds: [wrongFormatEmbed(name)], ephemeral: true }); + return; + } + if (alias && !nameRegex.test(alias)) { + // eslint-disable-next-line no-await-in-loop + await interaction.followUp({ embeds: [wrongFormatEmbed(alias)], ephemeral: true }); + return; + } + //Check if the mod logs channel exists const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (!modLogsChannel) { await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - const existingVersion = await PrefixCommandVersion.findOne({ version }); + const existingVersion = await PrefixCommandVersion.findOne({ name: version }); if (existingVersion) { - const { id: versionId } = existingVersion; + const { id: versionId, alias: oldAlias } = existingVersion; existingVersion.name = name || existingVersion.name; existingVersion.emoji = emoji || existingVersion.emoji; existingVersion.alias = alias || existingVersion.alias; @@ -91,6 +108,7 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom try { await existingVersion.save(); const { name, emoji, alias, enabled } = existingVersion; + await refreshSinglePrefixCommandVersionCache(oldAlias, existingVersion.toObject(), alias); await interaction.followUp({ embeds: [successEmbed(name, versionId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index c6ff8e12..17d54bbd 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -1,5 +1,5 @@ import { ActionRowBuilder, ChatInputCommandInteraction, Colors, ModalBuilder, TextChannel, TextInputBuilder, TextInputStyle, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed, refreshSinglePrefixCommandCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Set Content - No Connection', @@ -89,7 +89,7 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman } const foundCommand = foundCommands[0]; - const { id: commandId } = foundCommand; + const { id: commandId, name: commandName, aliases: commandAliases } = foundCommand; let versionId = ''; let foundVersions = null; if (version === 'GENERIC' || version === 'generic') { @@ -210,6 +210,7 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman try { await foundCommand.save(); + await refreshSinglePrefixCommandCache(commandName, foundCommand.toObject(), commandName, commandAliases); const { id: contentId } = foundCommand.contents.find((c) => c.versionId === versionId)!; await interaction.followUp({ embeds: [successEmbed(command, version, contentId)], ephemeral: true }); if (modLogsChannel) { diff --git a/src/commands/moderation/prefixCommands/prefixCommandCacheUpdate.ts b/src/commands/moderation/prefixCommands/prefixCommandCacheUpdate.ts new file mode 100644 index 00000000..a90e3910 --- /dev/null +++ b/src/commands/moderation/prefixCommands/prefixCommandCacheUpdate.ts @@ -0,0 +1,72 @@ +import { ApplicationCommandType, Colors, EmbedField, TextChannel } from 'discord.js'; +import { constantsConfig, slashCommand, slashCommandStructure, makeEmbed, refreshAllPrefixCommandsCache, refreshAllPrefixCommandVersionsCache } from '../../../lib'; + +const data = slashCommandStructure({ + name: 'prefix-commands-cache-update', + description: 'Updates the in-memory prefix command cache of the bot.', + type: ApplicationCommandType.ChatInput, + default_member_permissions: constantsConfig.commandPermission.MANAGE_SERVER, //Overrides need to be added for admin, moderator and bot developer roles + dm_permission: false, + options: [], +}); + +const cacheUpdateEmbed = (fields: any, color: number) => makeEmbed({ + title: 'Prefix Command Cache Update', + fields, + color, +}); + +const noChannelEmbed = (channelName: string) => makeEmbed({ + title: `Prefix Command Cache Update - No ${channelName} channel`, + description: `The command was successful, but no message to ${channelName} was sent. Please check the channel still exists.`, + color: Colors.Yellow, +}); + +const cacheUpdateEmbedField = (moderator: string, duration: string): EmbedField[] => [ + { + name: 'Moderator', + value: moderator, + inline: true, + }, + { + name: 'Duration', + value: `${duration}s`, + inline: true, + }, +]; + +export default slashCommand(data, async ({ interaction }) => { + await interaction.deferReply({ ephemeral: true }); + + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + const start = new Date().getTime(); + + await refreshAllPrefixCommandsCache(); + await refreshAllPrefixCommandVersionsCache(); + + const duration = ((new Date().getTime() - start) / 1000).toFixed(2); + + await interaction.editReply({ + embeds: [cacheUpdateEmbed( + cacheUpdateEmbedField( + interaction.user.tag, + duration, + ), + Colors.Green, + )], + }); + + try { + await modLogsChannel.send({ + embeds: [cacheUpdateEmbed( + cacheUpdateEmbedField( + interaction.user.tag, + duration, + ), + Colors.Green, + )], + }); + } catch (error) { + await interaction.followUp({ embeds: [noChannelEmbed('mod-log')] }); + } +}); diff --git a/src/events/index.ts b/src/events/index.ts index 8de11afd..684f5870 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -7,6 +7,7 @@ import slashCommandHandler from './slashCommandHandler'; import contextInteractionHandler from './contextInteractionHandler'; import messageDelete from './logging/messageDelete'; import messageUpdate from './logging/messageUpdate'; +import messageCreateHandler from './messageCreateHandler'; import autocompleteHandler from './autocompleteHandler'; import buttonHandler from './buttonHandlers/buttonHandler'; @@ -18,6 +19,7 @@ export default [ contextInteractionHandler, messageDelete, messageUpdate, + messageCreateHandler, autocompleteHandler, buttonHandler, ] as Event[]; diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts new file mode 100644 index 00000000..b2703aa7 --- /dev/null +++ b/src/events/messageCreateHandler.ts @@ -0,0 +1,92 @@ +import { EmbedBuilder, Message } from 'discord.js'; +import { event, getInMemoryCache, Logger, Events, constantsConfig, makeEmbed, makeLines } from '../lib'; +import { PrefixCommand, PrefixCommandVersion } from '../lib/schemas/prefixCommandSchemas'; + +const commandEmbed = (title: string, description: string, color: string, imageUrl: string = '') => makeEmbed({ + title, + description, + color: Number(color), + ...(imageUrl && { image: { url: imageUrl } }), +}); + +async function replyWithEmbed(msg: Message, embed: EmbedBuilder) : Promise> { + return msg.fetchReference() + .then((res) => { + let existingFooterText = ''; + const existingFooter = embed.data.footer; + if (existingFooter) { + existingFooterText = `${existingFooter.text}\n\n`; + } + embed = EmbedBuilder.from(embed.data); + embed.setFooter({ text: `${existingFooterText}Executed by ${msg.author.tag} - ${msg.author.id}` }); + return res.reply({ embeds: [embed] }); + }) + .catch(() => msg.reply({ embeds: [embed] })); +} + +async function replyWithMsg(msg: Message, text: string) : Promise> { + return msg.fetchReference() + .then((res) => res.reply(`${text}\n\n\`Executed by ${msg.author.tag} - ${msg.author.id}\``)) + .catch(() => msg.reply(text)); +} + +export default event(Events.MessageCreate, async (_, message) => { + const { id: messageId, author, channel, content } = message; + const { id: authorId, bot } = author; + + if (bot || channel.isDMBased()) return; + const { id: channelId, guild } = channel; + const { id: guildId } = guild; + Logger.debug(`Processing message ${messageId} from user ${authorId} in channel ${channelId} of server ${guildId}.`); + + const inMemoryCache = getInMemoryCache(); + if (inMemoryCache && content.startsWith(constantsConfig.prefixCommandPrefix)) { + const commandTextMatch = content.match(`^\\${constantsConfig.prefixCommandPrefix}([\\w\\d-_]+)[^\\w\\d-_]*([\\w\\d-_]+)?`); + if (commandTextMatch) { + let [commandText] = commandTextMatch.slice(1); + const commandCachedVersion = await inMemoryCache.get(`PF_VERSION:${commandText}`); + let commandVersionId; + let commandVersionName; + let commandVersionEnabled; + if (commandCachedVersion) { + const commandVersion = PrefixCommandVersion.hydrate(commandCachedVersion); + ({ id: commandVersionId, name: commandVersionName, enabled: commandVersionEnabled } = commandVersion); + } else { + commandVersionId = 'GENERIC'; + commandVersionName = 'GENERIC'; + commandVersionEnabled = true; + } + if (commandCachedVersion && commandTextMatch[2]) { + [commandText] = commandTextMatch.slice(2); + } + if (!commandVersionEnabled) { + Logger.debug(`Prefix Command - Version "${commandVersionName}" is disabled - Not executing command "${commandText}"`); + return; + } + const cachedCommandDetails = await inMemoryCache.get(`PF_COMMAND:${commandText}`); + if (cachedCommandDetails) { + const commandDetails = PrefixCommand.hydrate(cachedCommandDetails); + const { name, contents, isEmbed, embedColor, channelPermissions, rolePermissions } = commandDetails; + const commandContentData = contents.find(({ versionId }) => versionId === commandVersionId); + if (!commandContentData) { + Logger.debug(`Prefix Command - Version "${commandVersionName}" not found for command "${name}" based on user command "${commandText}"`); + return; + } + Logger.debug(`Prefix Command - Executing version "${commandVersionName}" for command "${name}" based on user command "${commandText}"`); + const { title: commandTitle, content: commandContent, image: commandImage } = commandContentData; + try { + if (isEmbed) { + replyWithEmbed(message, commandEmbed(commandTitle, commandContent || '', embedColor || constantsConfig.colors.FBW_CYAN, commandImage || '')); + } else { + replyWithMsg(message, makeLines([ + `**${commandTitle}**`, + ...(commandContent ? [commandContent] : []), + ])); + } + } catch (error) { + Logger.error(error); + } + } + } + } +}); diff --git a/src/events/ready.ts b/src/events/ready.ts index d907af85..74e2ff14 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -9,6 +9,9 @@ import { Logger, imageBaseUrl, getScheduler, + setupInMemoryCache, + loadAllPrefixCommandsToCache, + loadAllPrefixCommandVersionsToCache, } from '../lib'; import { deployCommands } from '../scripts/deployCommands'; import commandArray from '../commands'; @@ -50,6 +53,18 @@ export default event(Events.ClientReady, async ({ log }, client) => { } } + // Setup cache manager + let inMemoryCacheSetup = false; + let inMemoryCacheError: Error | undefined; + await setupInMemoryCache() + .then(() => { + inMemoryCacheSetup = true; + }) + .catch((error) => { + inMemoryCacheError = error; + Logger.error(error); + }); + // Connect to MongoDB and set up scheduler let dbConnected = false; let dbError: Error | undefined; @@ -119,6 +134,28 @@ export default event(Events.ClientReady, async ({ log }, client) => { } } + // Loading in-memory cache with prefix commands + if (inMemoryCacheSetup && dbConnected) { + await loadAllPrefixCommandsToCache() + .then(() => { + Logger.info('Loaded prefix commands to cache.'); + }) + .catch((error) => { + Logger.error(`Failed to load prefix commands to cache: ${error}`); + }); + } + + // Loading in-memory cache with prefix command versions + if (inMemoryCacheSetup && dbConnected) { + await loadAllPrefixCommandVersionsToCache() + .then(() => { + Logger.info('Loaded prefix command versions to cache.'); + }) + .catch((error) => { + Logger.error(`Failed to load prefix command versions to cache: ${error}`); + }); + } + // Send bot status message to bot-dev channel const botDevChannel = client.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (botDevChannel) { @@ -138,6 +175,11 @@ export default event(Events.ClientReady, async ({ log }, client) => { logMessage += ` - Scheduler Error: ${schedulerError.message}`; } + logMessage += ` - Cache State: ${inMemoryCacheSetup ? 'Setup' : 'Not Setup'}`; + if (!inMemoryCacheSetup && inMemoryCacheError) { + logMessage += ` - Cache Error: ${inMemoryCacheError.message}`; + } + await botDevChannel.send({ content: logMessage }); } else { log('Unable to find bot-dev channel. Cannot send bot status message.'); diff --git a/src/lib/cache/cacheManager.ts b/src/lib/cache/cacheManager.ts new file mode 100644 index 00000000..bcb4e296 --- /dev/null +++ b/src/lib/cache/cacheManager.ts @@ -0,0 +1,149 @@ +import { Cache, caching } from 'cache-manager'; +import { getConn, Logger, PrefixCommand, PrefixCommandVersion } from '../index'; + +let inMemoryCache: Cache; +const commandCachePrefix = 'PF_COMMAND'; +const commandVersionCachePrefix = 'PF_VERSION'; + +export async function setupInMemoryCache(callback = Logger.error) { + try { + inMemoryCache = await caching( + 'memory', + { + ttl: 3600 * 1000, // 1 hour caching + max: 10000, // 10000 items max + }, + ); + Logger.info('In memory cache set up'); + } catch (err) { + callback(err); + } +} + +export function getInMemoryCache(callback = Logger.error) { + if (!inMemoryCache) { + callback(new Error('No in memory cache available.')); + return null; + } + return inMemoryCache; +} + +export async function clearSinglePrefixCommandCache(commandName: string) { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + Logger.debug(`Clearing cache for command or alias "${commandName}"`); + const commandCache = await inMemoryCache.get(`${commandCachePrefix}:${commandName}`); + const command = PrefixCommand.hydrate(commandCache); + if (!command) return; + const { aliases } = command; + for (const alias of aliases) { + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.del(`${commandCachePrefix}:${alias}`); + } + await inMemoryCache.del(`${commandCachePrefix}:${commandName}`); +} + +export async function clearSinglePrefixCommandVersionCache(alias: string) { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + Logger.debug(`Clearing cache for command version alias "${alias}"`); + await inMemoryCache.del(`${commandVersionCachePrefix}:${alias}`); +} + +export async function clearAllPrefixCommandsCache() { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + const keys = await inMemoryCache.store.keys(); + for (const key of keys) { + if (key.startsWith(`${commandCachePrefix}:`)) { + Logger.debug(`Clearing cache for command or alias "${key}"`); + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.del(key); + } + } +} + +export async function clearAllPrefixCommandVersionsCache() { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + const keys = await inMemoryCache.store.keys(); + for (const key of keys) { + if (key.startsWith(`${commandVersionCachePrefix}:`)) { + Logger.debug(`Clearing cache for command version alias "${key}"`); + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.del(key); + } + } +} + +export async function loadSinglePrefixCommandToCache(command: Object, name: string, aliases: string[]) { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + Logger.debug(`Loading command ${name} to cache`); + await inMemoryCache.set(`${commandCachePrefix}:${name}`, command); + for (const alias of aliases) { + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.set(`${commandCachePrefix}:${alias}`, command); + } +} + +export async function loadSinglePrefixCommandVersionToCache(version: Object, alias: string) { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + Logger.debug(`Loading version with alias ${alias} to cache`); + await inMemoryCache.set(`${commandVersionCachePrefix}:${alias}`, version); +} + +export async function loadAllPrefixCommandsToCache() { + const conn = getConn(); + const inMemoryCache = getInMemoryCache(); + if (!conn || !inMemoryCache) return; + + const PrefixCommands = await PrefixCommand.find(); + + for (const command of PrefixCommands) { + const { name, aliases } = command; + // eslint-disable-next-line no-await-in-loop + await loadSinglePrefixCommandToCache(command, name, aliases); + } +} + +export async function loadAllPrefixCommandVersionsToCache() { + const conn = getConn(); + const inMemoryCache = getInMemoryCache(); + if (!conn || !inMemoryCache) return; + + const PrefixCommandVersions = await PrefixCommandVersion.find(); + + for (const version of PrefixCommandVersions) { + const { alias } = version; + // eslint-disable-next-line no-await-in-loop + await loadSinglePrefixCommandVersionToCache(version, alias); + } +} + +export async function refreshSinglePrefixCommandCache(oldName: string, command: Object, newName: string, newAliases: string[]) { + await clearSinglePrefixCommandCache(oldName); + await loadSinglePrefixCommandToCache(command, newName, newAliases); +} + +export async function refreshSinglePrefixCommandVersionCache(oldAlias: string, version: Object, newAlias: string) { + await clearSinglePrefixCommandVersionCache(oldAlias); + await loadSinglePrefixCommandVersionToCache(version, newAlias); +} + +export async function refreshAllPrefixCommandsCache() { + await clearAllPrefixCommandsCache(); + await loadAllPrefixCommandsToCache(); +} + +export async function refreshAllPrefixCommandVersionsCache() { + await clearAllPrefixCommandVersionsCache(); + await loadAllPrefixCommandVersionsToCache(); +} diff --git a/src/lib/config.ts b/src/lib/config.ts index 7a9e9760..1ccf0bd2 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -49,6 +49,7 @@ interface Config { roleGroups: { [x: string]: string[], }, + prefixCommandPrefix: string, roles: { ADMIN_TEAM: string, BOT_DEVELOPER: string, diff --git a/src/lib/index.ts b/src/lib/index.ts index 5526ff1d..fd90474c 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -22,3 +22,6 @@ export * from './schemas/prefixCommandSchemas'; export * from './schedulerJobs/autoDisableSlowMode'; export * from './schedulerJobs/sendHeartbeat'; export * from './schedulerJobs/postBirthdays'; + +//Cache Management +export * from './cache/cacheManager'; From 03163ee93e8e1d0b4d4339f1533fcb726a48ef3a Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 4 Sep 2024 22:53:56 -0700 Subject: [PATCH 08/51] Making cache and command checks case-insensitive --- .../prefixCommands/functions/modifyVersion.ts | 2 +- src/events/messageCreateHandler.ts | 8 +++++-- src/lib/cache/cacheManager.ts | 24 ++++++++++--------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts index 762d433b..7a049193 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, refreshAllPrefixCommandVersionsCache, refreshSinglePrefixCommandVersionCache } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, refreshSinglePrefixCommandVersionCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Modify Version - No Connection', diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index b2703aa7..d7f87d83 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -39,12 +39,16 @@ export default event(Events.MessageCreate, async (_, message) => { const { id: guildId } = guild; Logger.debug(`Processing message ${messageId} from user ${authorId} in channel ${channelId} of server ${guildId}.`); + // TODO: Permission verification + // TODO: If generic, check available versions and show selections + // TODO: case-insensitive command matching + const inMemoryCache = getInMemoryCache(); if (inMemoryCache && content.startsWith(constantsConfig.prefixCommandPrefix)) { const commandTextMatch = content.match(`^\\${constantsConfig.prefixCommandPrefix}([\\w\\d-_]+)[^\\w\\d-_]*([\\w\\d-_]+)?`); if (commandTextMatch) { let [commandText] = commandTextMatch.slice(1); - const commandCachedVersion = await inMemoryCache.get(`PF_VERSION:${commandText}`); + const commandCachedVersion = await inMemoryCache.get(`PF_VERSION:${commandText.toLowerCase()}`); let commandVersionId; let commandVersionName; let commandVersionEnabled; @@ -63,7 +67,7 @@ export default event(Events.MessageCreate, async (_, message) => { Logger.debug(`Prefix Command - Version "${commandVersionName}" is disabled - Not executing command "${commandText}"`); return; } - const cachedCommandDetails = await inMemoryCache.get(`PF_COMMAND:${commandText}`); + const cachedCommandDetails = await inMemoryCache.get(`PF_COMMAND:${commandText.toLowerCase()}`); if (cachedCommandDetails) { const commandDetails = PrefixCommand.hydrate(cachedCommandDetails); const { name, contents, isEmbed, embedColor, channelPermissions, rolePermissions } = commandDetails; diff --git a/src/lib/cache/cacheManager.ts b/src/lib/cache/cacheManager.ts index bcb4e296..c014203b 100644 --- a/src/lib/cache/cacheManager.ts +++ b/src/lib/cache/cacheManager.ts @@ -4,14 +4,16 @@ import { getConn, Logger, PrefixCommand, PrefixCommandVersion } from '../index'; let inMemoryCache: Cache; const commandCachePrefix = 'PF_COMMAND'; const commandVersionCachePrefix = 'PF_VERSION'; +const cacheSize = 10000; +const cacheTTL = 3600 * 1000; // 1 hour export async function setupInMemoryCache(callback = Logger.error) { try { inMemoryCache = await caching( 'memory', { - ttl: 3600 * 1000, // 1 hour caching - max: 10000, // 10000 items max + ttl: cacheTTL, + max: cacheSize, }, ); Logger.info('In memory cache set up'); @@ -33,15 +35,15 @@ export async function clearSinglePrefixCommandCache(commandName: string) { if (!inMemoryCache) return; Logger.debug(`Clearing cache for command or alias "${commandName}"`); - const commandCache = await inMemoryCache.get(`${commandCachePrefix}:${commandName}`); + const commandCache = await inMemoryCache.get(`${commandCachePrefix}:${commandName.toLowerCase()}`); const command = PrefixCommand.hydrate(commandCache); if (!command) return; const { aliases } = command; for (const alias of aliases) { // eslint-disable-next-line no-await-in-loop - await inMemoryCache.del(`${commandCachePrefix}:${alias}`); + await inMemoryCache.del(`${commandCachePrefix}:${alias.toLowerCase()}`); } - await inMemoryCache.del(`${commandCachePrefix}:${commandName}`); + await inMemoryCache.del(`${commandCachePrefix}:${commandName.toLowerCase()}`); } export async function clearSinglePrefixCommandVersionCache(alias: string) { @@ -49,7 +51,7 @@ export async function clearSinglePrefixCommandVersionCache(alias: string) { if (!inMemoryCache) return; Logger.debug(`Clearing cache for command version alias "${alias}"`); - await inMemoryCache.del(`${commandVersionCachePrefix}:${alias}`); + await inMemoryCache.del(`${commandVersionCachePrefix}:${alias.toLowerCase()}`); } export async function clearAllPrefixCommandsCache() { @@ -85,10 +87,10 @@ export async function loadSinglePrefixCommandToCache(command: Object, name: stri if (!inMemoryCache) return; Logger.debug(`Loading command ${name} to cache`); - await inMemoryCache.set(`${commandCachePrefix}:${name}`, command); + await inMemoryCache.set(`${commandCachePrefix}:${name.toLowerCase()}`, command); for (const alias of aliases) { // eslint-disable-next-line no-await-in-loop - await inMemoryCache.set(`${commandCachePrefix}:${alias}`, command); + await inMemoryCache.set(`${commandCachePrefix}:${alias.toLowerCase()}`, command); } } @@ -97,7 +99,7 @@ export async function loadSinglePrefixCommandVersionToCache(version: Object, ali if (!inMemoryCache) return; Logger.debug(`Loading version with alias ${alias} to cache`); - await inMemoryCache.set(`${commandVersionCachePrefix}:${alias}`, version); + await inMemoryCache.set(`${commandVersionCachePrefix}:${alias.toLowerCase()}`, version); } export async function loadAllPrefixCommandsToCache() { @@ -110,7 +112,7 @@ export async function loadAllPrefixCommandsToCache() { for (const command of PrefixCommands) { const { name, aliases } = command; // eslint-disable-next-line no-await-in-loop - await loadSinglePrefixCommandToCache(command, name, aliases); + await loadSinglePrefixCommandToCache(command.toObject(), name, aliases); } } @@ -124,7 +126,7 @@ export async function loadAllPrefixCommandVersionsToCache() { for (const version of PrefixCommandVersions) { const { alias } = version; // eslint-disable-next-line no-await-in-loop - await loadSinglePrefixCommandVersionToCache(version, alias); + await loadSinglePrefixCommandVersionToCache(version.toObject(), alias); } } From fc35d7c8adbef764cefc8e21b24c0efaa06535af Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 4 Sep 2024 23:27:47 -0700 Subject: [PATCH 09/51] Adding a scheduled task to refresh the cache automatically --- .env.example | 3 ++ src/events/ready.ts | 23 +++++++++++++ src/lib/cache/cacheManager.ts | 3 +- src/lib/index.ts | 1 + src/lib/scheduler.ts | 3 +- src/lib/schedulerJobs/refreshInMemoryCache.ts | 34 +++++++++++++++++++ 6 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/lib/schedulerJobs/refreshInMemoryCache.ts diff --git a/.env.example b/.env.example index 7baa8750..b4ece774 100644 --- a/.env.example +++ b/.env.example @@ -36,3 +36,6 @@ HEARTBEAT_INTERVAL=300 # Set the interval in seconds for the birthday handler to be run, 0 to disable BIRTHDAY_INTERVAL=1800 + +# Set the interval in seconds for the in memory cache to be refreshed, 1800 is the default +CACHE_REFRESH_INTERVAL=1800 diff --git a/src/events/ready.ts b/src/events/ready.ts index 74e2ff14..632dbf66 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -134,6 +134,29 @@ export default event(Events.ClientReady, async ({ log }, client) => { } } + const cacheRefreshInterval = process.env.CACHE_REFRESH_INTERVAL ? Number(process.env.CACHE_REFRESH_INTERVAL) : 1800; + // Set in memory cache refresh handler + if (schedulerConnected && cacheRefreshInterval) { + const scheduler = getScheduler(); + if (scheduler) { + const cacheJobList = await scheduler.jobs({ name: 'refreshInMemoryCache' }); + if (cacheJobList.length === 0) { + scheduler.every(`${cacheRefreshInterval} seconds`, 'refreshInMemoryCache', { interval: cacheRefreshInterval }); + Logger.info(`Cache refresh job scheduled with interval ${cacheRefreshInterval}`); + } else { + const cacheJob = cacheJobList[0]; + const { interval } = cacheJob.attrs.data as { interval: number }; + if (interval !== cacheRefreshInterval) { + await scheduler.cancel({ name: 'refreshInMemoryCache' }); + scheduler.every(`${cacheRefreshInterval} seconds`, 'refreshInMemoryCache', { interval: cacheRefreshInterval }); + Logger.info(`Cache refresh job rescheduled with new interval ${cacheRefreshInterval}`); + } else { + Logger.info('Cache refresh job already scheduled'); + } + } + } + } + // Loading in-memory cache with prefix commands if (inMemoryCacheSetup && dbConnected) { await loadAllPrefixCommandsToCache() diff --git a/src/lib/cache/cacheManager.ts b/src/lib/cache/cacheManager.ts index c014203b..87cc6644 100644 --- a/src/lib/cache/cacheManager.ts +++ b/src/lib/cache/cacheManager.ts @@ -5,7 +5,8 @@ let inMemoryCache: Cache; const commandCachePrefix = 'PF_COMMAND'; const commandVersionCachePrefix = 'PF_VERSION'; const cacheSize = 10000; -const cacheTTL = 3600 * 1000; // 1 hour +const cacheRefreshInterval = process.env.CACHE_REFRESH_INTERVAL ? Number(process.env.CACHE_REFRESH_INTERVAL) : 1800; +const cacheTTL = cacheRefreshInterval * 2 * 1000; export async function setupInMemoryCache(callback = Logger.error) { try { diff --git a/src/lib/index.ts b/src/lib/index.ts index fd90474c..2f98d658 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -22,6 +22,7 @@ export * from './schemas/prefixCommandSchemas'; export * from './schedulerJobs/autoDisableSlowMode'; export * from './schedulerJobs/sendHeartbeat'; export * from './schedulerJobs/postBirthdays'; +export * from './schedulerJobs/refreshInMemoryCache'; //Cache Management export * from './cache/cacheManager'; diff --git a/src/lib/scheduler.ts b/src/lib/scheduler.ts index 95c674fc..ddc1f6a8 100644 --- a/src/lib/scheduler.ts +++ b/src/lib/scheduler.ts @@ -1,5 +1,5 @@ import { Agenda } from '@hokify/agenda'; -import { Logger, autoDisableSlowMode, sendHeartbeat, postBirthdays } from './index'; +import { Logger, autoDisableSlowMode, sendHeartbeat, postBirthdays, refreshInMemoryCache } from './index'; let scheduler: Agenda; @@ -18,6 +18,7 @@ export async function setupScheduler(name: string, url: string, callback = Logge scheduler.define('autoDisableSlowMode', autoDisableSlowMode); scheduler.define('sendHeartbeat', sendHeartbeat); scheduler.define('postBirthdays', postBirthdays); + scheduler.define('refreshInMemoryCache', refreshInMemoryCache); Logger.info('Scheduler set up'); } catch (err) { callback(err); diff --git a/src/lib/schedulerJobs/refreshInMemoryCache.ts b/src/lib/schedulerJobs/refreshInMemoryCache.ts new file mode 100644 index 00000000..9ea84abf --- /dev/null +++ b/src/lib/schedulerJobs/refreshInMemoryCache.ts @@ -0,0 +1,34 @@ +import { Job } from '@hokify/agenda'; +import { Logger, getInMemoryCache, getScheduler, refreshAllPrefixCommandVersionsCache, refreshAllPrefixCommandsCache } from '../index'; + +export async function refreshInMemoryCache(job: Job) { + const scheduler = getScheduler(); + if (!scheduler) { + Logger.error('Failed to get scheduler instance'); + return; + } + + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) { + Logger.error('Failed to get in-memory cache instance'); + return; + } + + // Needed because of https://github.com/agenda/agenda/issues/401 + // eslint-disable-next-line no-underscore-dangle + const matchingJobs = await scheduler.jobs({ _id: job.attrs._id }); + if (matchingJobs.length !== 1) { + Logger.debug('Job has been deleted already, skipping execution.'); + return; + } + + const start = new Date().getTime(); + try { + await refreshAllPrefixCommandsCache(); + await refreshAllPrefixCommandVersionsCache(); + } catch (error) { + Logger.error('Failed to refresh the in memory cache:', error); + } + const duration = ((new Date().getTime() - start) / 1000).toFixed(2); + Logger.info(`In memory cache refreshed successfully, duration: ${duration}s`); +} From 4546bfe9a5a1f766fb788d1659c770a9ad659c05 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sat, 7 Sep 2024 12:26:10 -0700 Subject: [PATCH 10/51] Adding Categories and Default Channel Versions to cache --- src/events/messageCreateHandler.ts | 3 +- src/lib/cache/cacheManager.ts | 210 +++++++++++++++++++++++------ 2 files changed, 171 insertions(+), 42 deletions(-) diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index d7f87d83..ad0bdbb0 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -41,7 +41,8 @@ export default event(Events.MessageCreate, async (_, message) => { // TODO: Permission verification // TODO: If generic, check available versions and show selections - // TODO: case-insensitive command matching + // TODO: Categories Cache + // TODO: Default Version Cache const inMemoryCache = getInMemoryCache(); if (inMemoryCache && content.startsWith(constantsConfig.prefixCommandPrefix)) { diff --git a/src/lib/cache/cacheManager.ts b/src/lib/cache/cacheManager.ts index 87cc6644..659dcd84 100644 --- a/src/lib/cache/cacheManager.ts +++ b/src/lib/cache/cacheManager.ts @@ -1,5 +1,5 @@ import { Cache, caching } from 'cache-manager'; -import { getConn, Logger, PrefixCommand, PrefixCommandVersion } from '../index'; +import { getConn, Logger, PrefixCommand, PrefixCommandCategory, PrefixCommandChannelDefaultVersion, PrefixCommandVersion } from '../index'; let inMemoryCache: Cache; const commandCachePrefix = 'PF_COMMAND'; @@ -8,6 +8,10 @@ const cacheSize = 10000; const cacheRefreshInterval = process.env.CACHE_REFRESH_INTERVAL ? Number(process.env.CACHE_REFRESH_INTERVAL) : 1800; const cacheTTL = cacheRefreshInterval * 2 * 1000; +/** + * Cache Management Functions + */ + export async function setupInMemoryCache(callback = Logger.error) { try { inMemoryCache = await caching( @@ -31,6 +35,10 @@ export function getInMemoryCache(callback = Logger.error) { return inMemoryCache; } +/** + * Prefix Command Cache Management Functions + */ + export async function clearSinglePrefixCommandCache(commandName: string) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; @@ -47,14 +55,6 @@ export async function clearSinglePrefixCommandCache(commandName: string) { await inMemoryCache.del(`${commandCachePrefix}:${commandName.toLowerCase()}`); } -export async function clearSinglePrefixCommandVersionCache(alias: string) { - const inMemoryCache = getInMemoryCache(); - if (!inMemoryCache) return; - - Logger.debug(`Clearing cache for command version alias "${alias}"`); - await inMemoryCache.del(`${commandVersionCachePrefix}:${alias.toLowerCase()}`); -} - export async function clearAllPrefixCommandsCache() { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; @@ -69,20 +69,6 @@ export async function clearAllPrefixCommandsCache() { } } -export async function clearAllPrefixCommandVersionsCache() { - const inMemoryCache = getInMemoryCache(); - if (!inMemoryCache) return; - - const keys = await inMemoryCache.store.keys(); - for (const key of keys) { - if (key.startsWith(`${commandVersionCachePrefix}:`)) { - Logger.debug(`Clearing cache for command version alias "${key}"`); - // eslint-disable-next-line no-await-in-loop - await inMemoryCache.del(key); - } - } -} - export async function loadSinglePrefixCommandToCache(command: Object, name: string, aliases: string[]) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; @@ -95,14 +81,6 @@ export async function loadSinglePrefixCommandToCache(command: Object, name: stri } } -export async function loadSinglePrefixCommandVersionToCache(version: Object, alias: string) { - const inMemoryCache = getInMemoryCache(); - if (!inMemoryCache) return; - - Logger.debug(`Loading version with alias ${alias} to cache`); - await inMemoryCache.set(`${commandVersionCachePrefix}:${alias.toLowerCase()}`, version); -} - export async function loadAllPrefixCommandsToCache() { const conn = getConn(); const inMemoryCache = getInMemoryCache(); @@ -117,6 +95,50 @@ export async function loadAllPrefixCommandsToCache() { } } +export async function refreshSinglePrefixCommandCache(oldName: string, command: Object, newName: string, newAliases: string[]) { + await clearSinglePrefixCommandCache(oldName); + await loadSinglePrefixCommandToCache(command, newName, newAliases); +} + +export async function refreshAllPrefixCommandsCache() { + await clearAllPrefixCommandsCache(); + await loadAllPrefixCommandsToCache(); +} + +/** + * Prefix Command Version Cache Management Functions + */ + +export async function clearSinglePrefixCommandVersionCache(alias: string) { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + Logger.debug(`Clearing cache for command version alias "${alias}"`); + await inMemoryCache.del(`${commandVersionCachePrefix}:${alias.toLowerCase()}`); +} + +export async function clearAllPrefixCommandVersionsCache() { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + const keys = await inMemoryCache.store.keys(); + for (const key of keys) { + if (key.startsWith(`${commandVersionCachePrefix}:`)) { + Logger.debug(`Clearing cache for command version alias "${key}"`); + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.del(key); + } + } +} + +export async function loadSinglePrefixCommandVersionToCache(version: Object, alias: string) { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + Logger.debug(`Loading version with alias ${alias} to cache`); + await inMemoryCache.set(`${commandVersionCachePrefix}:${alias.toLowerCase()}`, version); +} + export async function loadAllPrefixCommandVersionsToCache() { const conn = getConn(); const inMemoryCache = getInMemoryCache(); @@ -131,22 +153,128 @@ export async function loadAllPrefixCommandVersionsToCache() { } } -export async function refreshSinglePrefixCommandCache(oldName: string, command: Object, newName: string, newAliases: string[]) { - await clearSinglePrefixCommandCache(oldName); - await loadSinglePrefixCommandToCache(command, newName, newAliases); -} - export async function refreshSinglePrefixCommandVersionCache(oldAlias: string, version: Object, newAlias: string) { await clearSinglePrefixCommandVersionCache(oldAlias); await loadSinglePrefixCommandVersionToCache(version, newAlias); } -export async function refreshAllPrefixCommandsCache() { - await clearAllPrefixCommandsCache(); - await loadAllPrefixCommandsToCache(); -} - export async function refreshAllPrefixCommandVersionsCache() { await clearAllPrefixCommandVersionsCache(); await loadAllPrefixCommandVersionsToCache(); } + +/** + * Prefix Command Category Cache Management Functions + */ + +export async function clearSinglePrefixCommandCategoryCache(name: string) { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + Logger.debug(`Clearing cache for command category "${name}"`); + await inMemoryCache.del(`PF_CATEGORY:${name.toLowerCase()}`); +} + +export async function clearAllPrefixCommandCategoriesCache() { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + const keys = await inMemoryCache.store.keys(); + for (const key of keys) { + if (key.startsWith('PF_CATEGORY:')) { + Logger.debug(`Clearing cache for command category "${key}"`); + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.del(key); + } + } +} + +export async function loadSinglePrefixCommandCategoryToCache(category: Object, name: string) { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + Logger.debug(`Loading category ${name} to cache`); + await inMemoryCache.set(`PF_CATEGORY:${name.toLowerCase()}`, category); +} + +export async function loadAllPrefixCommandCategoriesToCache() { + const conn = getConn(); + const inMemoryCache = getInMemoryCache(); + if (!conn || !inMemoryCache) return; + + const PrefixCommandCategories = await PrefixCommandCategory.find(); + + for (const category of PrefixCommandCategories) { + const { name } = category; + // eslint-disable-next-line no-await-in-loop + await loadSinglePrefixCommandCategoryToCache(category.toObject(), name); + } +} + +export async function refreshSinglePrefixCommandCategoryCache(oldName: string, category: Object, newName: string) { + await clearSinglePrefixCommandCategoryCache(oldName); + await loadSinglePrefixCommandCategoryToCache(category, newName); +} + +export async function refreshAllPrefixCommandCategoriesCache() { + await clearAllPrefixCommandCategoriesCache(); + await loadAllPrefixCommandCategoriesToCache(); +} + +/** + * Prefix Command Channel Default Version Cache Management Functions + */ + +export async function clearSinglePrefixCommandChannelDefaultVersionCache(channelId: string) { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + Logger.debug(`Clearing cache for channel default version for channel "${channelId}"`); + await inMemoryCache.del(`PF_CHANNEL_VERSION:${channelId}`); +} + +export async function clearAllPrefixCommandChannelDefaultVersionsCache() { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + const keys = await inMemoryCache.store.keys(); + for (const key of keys) { + if (key.startsWith('PF_CHANNEL_VERSION:')) { + Logger.debug(`Clearing cache for channel default version for channel "${key}"`); + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.del(key); + } + } +} + +export async function loadSinglePrefixCommandChannelDefaultVersionToCache(version: Object, channelId: string) { + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) return; + + Logger.debug(`Loading default version for channel ${channelId} to cache`); + await inMemoryCache.set(`PF_CHANNEL_VERSION:${channelId}`, version); +} + +export async function loadAllPrefixCommandChannelDefaultVersionsToCache() { + const conn = getConn(); + const inMemoryCache = getInMemoryCache(); + if (!conn || !inMemoryCache) return; + + const PrefixCommandChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find(); + + for (const version of PrefixCommandChannelDefaultVersions) { + const { channelId } = version; + // eslint-disable-next-line no-await-in-loop + await loadSinglePrefixCommandChannelDefaultVersionToCache(version.toObject(), channelId); + } +} + +export async function refreshSinglePrefixCommandChannelDefaultVersionCache(channelId: string, version: Object) { + await clearSinglePrefixCommandChannelDefaultVersionCache(channelId); + await loadSinglePrefixCommandChannelDefaultVersionToCache(version, channelId); +} + +export async function refreshAllPrefixCommandChannelDefaultVersionsCache() { + await clearAllPrefixCommandChannelDefaultVersionsCache(); + await loadAllPrefixCommandChannelDefaultVersionsToCache(); +} From 73ca86e17794ed3eb308c72660b3412594f99823 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sat, 7 Sep 2024 12:37:36 -0700 Subject: [PATCH 11/51] Managing Category cache and Default Version Cache --- .../prefixCommands/functions/addCategory.ts | 3 ++- .../functions/deleteCategory.ts | 3 ++- ...sion.ts => deleteChannelDefaultVersion.ts} | 5 ++-- .../functions/modifyCategory.ts | 5 ++-- .../functions/setChannelDefaultVersion.ts | 3 ++- .../prefixCommands/prefixCommands.ts | 4 ++-- src/events/ready.ts | 24 +++++++++++++++++++ src/lib/schedulerJobs/refreshInMemoryCache.ts | 4 +++- src/lib/schemas/prefixCommandSchemas.ts | 1 + 9 files changed, 42 insertions(+), 10 deletions(-) rename src/commands/moderation/prefixCommands/functions/{unsetChannelDefaultVersion.ts => deleteChannelDefaultVersion.ts} (91%) diff --git a/src/commands/moderation/prefixCommands/functions/addCategory.ts b/src/commands/moderation/prefixCommands/functions/addCategory.ts index 823201a8..35e2ba02 100644 --- a/src/commands/moderation/prefixCommands/functions/addCategory.ts +++ b/src/commands/moderation/prefixCommands/functions/addCategory.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandCategory, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandCategory, Logger, makeEmbed, loadSinglePrefixCommandCategoryToCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Add Category - No Connection', @@ -79,6 +79,7 @@ export async function handleAddPrefixCommandCategory(interaction: ChatInputComma }); try { await prefixCommandCategory.save(); + await loadSinglePrefixCommandCategoryToCache(prefixCommandCategory.toObject(), name); await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/deleteCategory.ts b/src/commands/moderation/prefixCommands/functions/deleteCategory.ts index 3dabe9c7..d3fe0827 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteCategory.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteCategory.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandCategory, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandCategory, Logger, makeEmbed, clearSinglePrefixCommandCategoryCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Delete Category - No Connection', @@ -74,6 +74,7 @@ export async function handleDeletePrefixCommandCategory(interaction: ChatInputCo const { id: categoryId, name, emoji } = existingCategory; try { await existingCategory.deleteOne(); + await clearSinglePrefixCommandCategoryCache(name); await interaction.followUp({ embeds: [successEmbed(name || '', categoryId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/unsetChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/deleteChannelDefaultVersion.ts similarity index 91% rename from src/commands/moderation/prefixCommands/functions/unsetChannelDefaultVersion.ts rename to src/commands/moderation/prefixCommands/functions/deleteChannelDefaultVersion.ts index dd19f2f7..0a4bfdf1 100644 --- a/src/commands/moderation/prefixCommands/functions/unsetChannelDefaultVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteChannelDefaultVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandChannelDefaultVersion, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandChannelDefaultVersion, Logger, makeEmbed, clearSinglePrefixCommandChannelDefaultVersionCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Unset Default Channel Version - No Connection', @@ -45,7 +45,7 @@ const noModLogs = makeEmbed({ color: Colors.Red, }); -export async function handleUnsetPrefixCommandChannelDefaultVersion(interaction: ChatInputCommandInteraction<'cached'>) { +export async function handleDeletePrefixCommandChannelDefaultVersion(interaction: ChatInputCommandInteraction<'cached'>) { await interaction.deferReply({ ephemeral: true }); const conn = getConn(); @@ -69,6 +69,7 @@ export async function handleUnsetPrefixCommandChannelDefaultVersion(interaction: if (foundChannelDefaultVersions && foundChannelDefaultVersions.length > 0) { try { await PrefixCommandChannelDefaultVersion.deleteOne({ channelId }); + await clearSinglePrefixCommandChannelDefaultVersionCache(channelId); await interaction.followUp({ embeds: [successEmbed(channelName)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/modifyCategory.ts b/src/commands/moderation/prefixCommands/functions/modifyCategory.ts index ffd3bbb3..bf5950b1 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCategory.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCategory.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandCategory, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandCategory, Logger, makeEmbed, refreshSinglePrefixCommandCategoryCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Modify Category - No Connection', @@ -73,12 +73,13 @@ export async function handleModifyPrefixCommandCategory(interaction: ChatInputCo const existingCategory = await PrefixCommandCategory.findOne({ name: category }); if (existingCategory) { - const { id: categoryId } = existingCategory; + const { id: categoryId, name: oldName } = existingCategory; existingCategory.name = name || existingCategory.name; existingCategory.emoji = emoji || existingCategory.emoji; try { await existingCategory.save(); const { name, emoji } = existingCategory; + await refreshSinglePrefixCommandCategoryCache(oldName, existingCategory.toObject(), name); await interaction.followUp({ embeds: [successEmbed(name, categoryId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts index 33c841ea..c9e63427 100644 --- a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommandChannelDefaultVersion, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommandChannelDefaultVersion, Logger, makeEmbed, refreshSinglePrefixCommandChannelDefaultVersionCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Set Default Channel Version - No Connection', @@ -90,6 +90,7 @@ export async function handleSetPrefixCommandChannelDefaultVersion(interaction: C channelDefaultVersion.versionId = versionId; try { await channelDefaultVersion.save(); + await refreshSinglePrefixCommandChannelDefaultVersionCache(channelId, channelDefaultVersion.toObject()); await interaction.followUp({ embeds: [successEmbed(channelName, version, emoji)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/prefixCommands.ts b/src/commands/moderation/prefixCommands/prefixCommands.ts index d19d8b66..0a712547 100644 --- a/src/commands/moderation/prefixCommands/prefixCommands.ts +++ b/src/commands/moderation/prefixCommands/prefixCommands.ts @@ -17,7 +17,7 @@ import { handleSetPrefixCommandContent } from './functions/setContent'; import { handleDeletePrefixCommandContent } from './functions/deleteContent'; import { handleShowPrefixCommandChannelDefaultVersion } from './functions/showChannelDefaultVersion'; import { handleSetPrefixCommandChannelDefaultVersion } from './functions/setChannelDefaultVersion'; -import { handleUnsetPrefixCommandChannelDefaultVersion } from './functions/unsetChannelDefaultVersion'; +import { handleDeletePrefixCommandChannelDefaultVersion } from './functions/deleteChannelDefaultVersion'; const colorChoices = []; for (let i = 0; i < Object.keys(constantsConfig.colors).length; i++) { @@ -635,7 +635,7 @@ export default slashCommand(data, async ({ interaction }) => { await handleSetPrefixCommandChannelDefaultVersion(interaction); break; case 'unset': - await handleUnsetPrefixCommandChannelDefaultVersion(interaction); + await handleDeletePrefixCommandChannelDefaultVersion(interaction); break; default: await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); diff --git a/src/events/ready.ts b/src/events/ready.ts index 632dbf66..2572190f 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -12,6 +12,8 @@ import { setupInMemoryCache, loadAllPrefixCommandsToCache, loadAllPrefixCommandVersionsToCache, + loadAllPrefixCommandCategoriesToCache, + loadAllPrefixCommandChannelDefaultVersionsToCache, } from '../lib'; import { deployCommands } from '../scripts/deployCommands'; import commandArray from '../commands'; @@ -179,6 +181,28 @@ export default event(Events.ClientReady, async ({ log }, client) => { }); } + // Loading in-memory cache with prefix command categories + if (inMemoryCacheSetup && dbConnected) { + await loadAllPrefixCommandCategoriesToCache() + .then(() => { + Logger.info('Loaded prefix command categories to cache.'); + }) + .catch((error) => { + Logger.error(`Failed to load prefix command categories to cache: ${error}`); + }); + } + + // Loading in-memory cache with prefix command channel default versions + if (inMemoryCacheSetup && dbConnected) { + await loadAllPrefixCommandChannelDefaultVersionsToCache() + .then(() => { + Logger.info('Loaded prefix command channel default versions to cache.'); + }) + .catch((error) => { + Logger.error(`Failed to load prefix command channel default versions to cache: ${error}`); + }); + } + // Send bot status message to bot-dev channel const botDevChannel = client.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (botDevChannel) { diff --git a/src/lib/schedulerJobs/refreshInMemoryCache.ts b/src/lib/schedulerJobs/refreshInMemoryCache.ts index 9ea84abf..de020150 100644 --- a/src/lib/schedulerJobs/refreshInMemoryCache.ts +++ b/src/lib/schedulerJobs/refreshInMemoryCache.ts @@ -1,5 +1,5 @@ import { Job } from '@hokify/agenda'; -import { Logger, getInMemoryCache, getScheduler, refreshAllPrefixCommandVersionsCache, refreshAllPrefixCommandsCache } from '../index'; +import { Logger, getInMemoryCache, getScheduler, refreshAllPrefixCommandCategoriesCache, refreshAllPrefixCommandChannelDefaultVersionsCache, refreshAllPrefixCommandVersionsCache, refreshAllPrefixCommandsCache } from '../index'; export async function refreshInMemoryCache(job: Job) { const scheduler = getScheduler(); @@ -26,6 +26,8 @@ export async function refreshInMemoryCache(job: Job) { try { await refreshAllPrefixCommandsCache(); await refreshAllPrefixCommandVersionsCache(); + await refreshAllPrefixCommandCategoriesCache(); + await refreshAllPrefixCommandChannelDefaultVersionsCache(); } catch (error) { Logger.error('Failed to refresh the in memory cache:', error); } diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts index fe5e670c..224117f2 100644 --- a/src/lib/schemas/prefixCommandSchemas.ts +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -34,6 +34,7 @@ const prefixCommandChannelDefaultVersionSchema = new Schema({ channelId: { type: String, required: true, + unique: true, }, versionId: { type: String, From 3dd97ce7ade3816cc55b56e71e2142474e06472c Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sat, 7 Sep 2024 16:24:48 -0700 Subject: [PATCH 12/51] Adding default channel version parsing --- .../functions/setChannelDefaultVersion.ts | 4 +++- .../prefixCommands/prefixCommands.ts | 8 +++---- src/events/messageCreateHandler.ts | 24 +++++++++++++++---- src/lib/cache/cacheManager.ts | 10 +++++--- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts index c9e63427..723b18be 100644 --- a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts @@ -90,7 +90,9 @@ export async function handleSetPrefixCommandChannelDefaultVersion(interaction: C channelDefaultVersion.versionId = versionId; try { await channelDefaultVersion.save(); - await refreshSinglePrefixCommandChannelDefaultVersionCache(channelId, channelDefaultVersion.toObject()); + if (foundVersion) { + await refreshSinglePrefixCommandChannelDefaultVersionCache(channelId, foundVersion.toObject()); + } await interaction.followUp({ embeds: [successEmbed(channelName, version, emoji)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/prefixCommands.ts b/src/commands/moderation/prefixCommands/prefixCommands.ts index 0a712547..269777ca 100644 --- a/src/commands/moderation/prefixCommands/prefixCommands.ts +++ b/src/commands/moderation/prefixCommands/prefixCommands.ts @@ -484,8 +484,8 @@ const data = slashCommandStructure({ ], }, { - name: 'unset', - description: 'Unset the default version for a channel.', + name: 'delete', + description: 'Delete the default version for a channel.', type: ApplicationCommandOptionType.Subcommand, options: [ { @@ -626,7 +626,7 @@ export default slashCommand(data, async ({ interaction }) => { await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); } break; - case 'channel-default': + case 'channel-default-version': switch (subcommandName) { case 'show': await handleShowPrefixCommandChannelDefaultVersion(interaction); @@ -634,7 +634,7 @@ export default slashCommand(data, async ({ interaction }) => { case 'set': await handleSetPrefixCommandChannelDefaultVersion(interaction); break; - case 'unset': + case 'delete': await handleDeletePrefixCommandChannelDefaultVersion(interaction); break; default: diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index ad0bdbb0..40dbb86c 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -41,14 +41,15 @@ export default event(Events.MessageCreate, async (_, message) => { // TODO: Permission verification // TODO: If generic, check available versions and show selections - // TODO: Categories Cache - // TODO: Default Version Cache const inMemoryCache = getInMemoryCache(); if (inMemoryCache && content.startsWith(constantsConfig.prefixCommandPrefix)) { const commandTextMatch = content.match(`^\\${constantsConfig.prefixCommandPrefix}([\\w\\d-_]+)[^\\w\\d-_]*([\\w\\d-_]+)?`); if (commandTextMatch) { let [commandText] = commandTextMatch.slice(1); + const commandVersionExplicitGeneric = (commandText.toLowerCase() === 'generic'); + + // Step 1: Check if the command is actually a version alias const commandCachedVersion = await inMemoryCache.get(`PF_VERSION:${commandText.toLowerCase()}`); let commandVersionId; let commandVersionName; @@ -61,13 +62,28 @@ export default event(Events.MessageCreate, async (_, message) => { commandVersionName = 'GENERIC'; commandVersionEnabled = true; } - if (commandCachedVersion && commandTextMatch[2]) { - [commandText] = commandTextMatch.slice(2); + + // Step 2: Check if there's a default version for the channel if commandVersionName is GENERIC + if (commandVersionName === 'GENERIC' && !commandVersionExplicitGeneric) { + const channelDefaultVersionCached = await inMemoryCache.get(`PF_CHANNEL_VERSION:${channelId}`); + if (channelDefaultVersionCached) { + const channelDefaultVersion = PrefixCommandVersion.hydrate(channelDefaultVersionCached); + ({ id: commandVersionId, name: commandVersionName, enabled: commandVersionEnabled } = channelDefaultVersion); + } } + + // Drop execution if the version is disabled if (!commandVersionEnabled) { Logger.debug(`Prefix Command - Version "${commandVersionName}" is disabled - Not executing command "${commandText}"`); return; } + + // Step 2.5: If the first command was actually a version alias, take the actual command as CommandText + if ((commandCachedVersion || commandVersionExplicitGeneric) && commandTextMatch[2]) { + [commandText] = commandTextMatch.slice(2); + } + + // Step 3: Check if the command exists itself and process it const cachedCommandDetails = await inMemoryCache.get(`PF_COMMAND:${commandText.toLowerCase()}`); if (cachedCommandDetails) { const commandDetails = PrefixCommand.hydrate(cachedCommandDetails); diff --git a/src/lib/cache/cacheManager.ts b/src/lib/cache/cacheManager.ts index 659dcd84..5a5bd035 100644 --- a/src/lib/cache/cacheManager.ts +++ b/src/lib/cache/cacheManager.ts @@ -262,10 +262,14 @@ export async function loadAllPrefixCommandChannelDefaultVersionsToCache() { const PrefixCommandChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find(); - for (const version of PrefixCommandChannelDefaultVersions) { - const { channelId } = version; + for (const defaultVersion of PrefixCommandChannelDefaultVersions) { + const { channelId, versionId } = defaultVersion; // eslint-disable-next-line no-await-in-loop - await loadSinglePrefixCommandChannelDefaultVersionToCache(version.toObject(), channelId); + const version = await PrefixCommandVersion.findById(versionId); + if (version) { + // eslint-disable-next-line no-await-in-loop + await loadSinglePrefixCommandChannelDefaultVersionToCache(version.toObject(), channelId); + } } } From 52a7351714b16261813c6758f589ba9112016bf6 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 8 Sep 2024 00:23:54 -0700 Subject: [PATCH 13/51] Refactor cache management to be more intuitive and introduce version selection for GENERIC versions --- .../prefixCommands/functions/addCategory.ts | 2 +- .../functions/addChannelPermission.ts | 27 +++--- .../prefixCommands/functions/addCommand.ts | 2 +- .../functions/addRolePermission.ts | 21 ++-- .../prefixCommands/functions/addVersion.ts | 2 +- .../functions/deleteCategory.ts | 2 +- .../functions/deleteChannelDefaultVersion.ts | 8 +- .../prefixCommands/functions/deleteCommand.ts | 2 +- .../prefixCommands/functions/deleteContent.ts | 38 ++++---- .../prefixCommands/functions/deleteVersion.ts | 24 +++-- .../functions/modifyCategory.ts | 5 +- .../prefixCommands/functions/modifyCommand.ts | 5 +- .../prefixCommands/functions/modifyVersion.ts | 5 +- .../functions/removeChannelPermission.ts | 22 ++--- .../functions/removeRolePermission.ts | 16 ++-- .../functions/setChannelDefaultVersion.ts | 6 +- .../prefixCommands/functions/setContent.ts | 27 +++--- src/events/messageCreateHandler.ts | 95 ++++++++++++++---- src/lib/cache/cacheManager.ts | 96 +++++++++---------- src/lib/schemas/prefixCommandSchemas.ts | 67 +++++++++++-- 20 files changed, 285 insertions(+), 187 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/addCategory.ts b/src/commands/moderation/prefixCommands/functions/addCategory.ts index 35e2ba02..58709c1f 100644 --- a/src/commands/moderation/prefixCommands/functions/addCategory.ts +++ b/src/commands/moderation/prefixCommands/functions/addCategory.ts @@ -79,7 +79,7 @@ export async function handleAddPrefixCommandCategory(interaction: ChatInputComma }); try { await prefixCommandCategory.save(); - await loadSinglePrefixCommandCategoryToCache(prefixCommandCategory.toObject(), name); + await loadSinglePrefixCommandCategoryToCache(prefixCommandCategory); await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts index 25d0fc9d..c7e9e43b 100644 --- a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts +++ b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandChannelPermission, refreshSinglePrefixCommandCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Add Channel Permission - No Connection', @@ -25,12 +25,12 @@ const alreadyExistsEmbed = (command: string, channel: string) => makeEmbed({ color: Colors.Red, }); -const successEmbed = (command: string, channel: string, type: string, channelPermissionId: string) => makeEmbed({ - title: `Prefix command channel ${type} permission added for command ${command} and channel <#${channel}>. ChannelPermission ID: ${channelPermissionId}`, +const successEmbed = (command: string, channel: string, type: string) => makeEmbed({ + title: `Prefix command channel ${type} permission added for command ${command} and channel <#${channel}>.}`, color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, channel: string, type: string, commandId: string, channelPermissionId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, channel: string, type: string) => makeEmbed({ title: 'Add prefix command channel permission', fields: [ { @@ -50,7 +50,6 @@ const modLogEmbed = (moderator: User, command: string, channel: string, type: st value: `${moderator}`, }, ], - footer: { text: `Command ID: ${commandId} - Channel Permission ID: ${channelPermissionId}` }, color: Colors.Green, }); @@ -89,33 +88,31 @@ export async function handleAddPrefixCommandChannelPermission(interaction: ChatI return; } const [foundCommand] = foundCommands; - const { id: commandId } = foundCommand; - const { id: channelId, name: channelName } = channel; + const { id: channelId } = channel; const existingChannelPermission = foundCommand.channelPermissions.find((channelPermission) => channelPermission.channelId === channelId); if (!existingChannelPermission) { - const newChannelPermission = { - commandId, + const newChannelPermission = new PrefixCommandChannelPermission({ channelId, type, - }; + }); try { foundCommand.channelPermissions.push(newChannelPermission); await foundCommand.save(); - const { id: channelPermissionId } = foundCommand.channelPermissions.find((channelPermission) => channelPermission.channelId === channelId)!; - await interaction.followUp({ embeds: [successEmbed(command, channelName, type, channelPermissionId)], ephemeral: true }); + await refreshSinglePrefixCommandCache(foundCommand, foundCommand); + await interaction.followUp({ embeds: [successEmbed(command, channelId, type)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channelName, type, commandId, channelPermissionId)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channelId, type)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } } } catch (error) { Logger.error(`Failed to add ${type} prefix command channel permission for command ${command} and channel <#${channel}>: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(command, channelName, type)], ephemeral: true }); + await interaction.followUp({ embeds: [failedEmbed(command, channelId, type)], ephemeral: true }); } } else { - await interaction.followUp({ embeds: [alreadyExistsEmbed(command, channelName)], ephemeral: true }); + await interaction.followUp({ embeds: [alreadyExistsEmbed(command, channelId)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts index 6edfede0..4625d6d4 100644 --- a/src/commands/moderation/prefixCommands/functions/addCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -129,7 +129,7 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera }); try { await prefixCommand.save(); - await loadSinglePrefixCommandToCache(prefixCommand.toObject(), name, aliases); + await loadSinglePrefixCommandToCache(prefixCommand); await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts index 01f5507b..329867e8 100644 --- a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts +++ b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandRolePermission, refreshSinglePrefixCommandCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Add Role Permission - No Connection', @@ -25,12 +25,12 @@ const alreadyExistsEmbed = (command: string, roleName: string) => makeEmbed({ color: Colors.Red, }); -const successEmbed = (command: string, roleName: string, type: string, rolePermissionId: string) => makeEmbed({ - title: `Prefix command role ${type} permission added for command ${command} and role ${roleName}. RolePermission ID: ${rolePermissionId}`, +const successEmbed = (command: string, roleName: string, type: string) => makeEmbed({ + title: `Prefix command role ${type} permission added for command ${command} and role ${roleName}.}`, color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, roleName: string, type: string, commandId: string, rolePermissionId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, roleName: string, type: string) => makeEmbed({ title: 'Add prefix command role permission', fields: [ { @@ -50,7 +50,6 @@ const modLogEmbed = (moderator: User, command: string, roleName: string, type: s value: `${moderator}`, }, ], - footer: { text: `Command ID: ${commandId} - Role Permission ID: ${rolePermissionId}` }, color: Colors.Green, }); @@ -89,24 +88,22 @@ export async function handleAddPrefixCommandRolePermission(interaction: ChatInpu return; } const [foundCommand] = foundCommands; - const { id: commandId } = foundCommand; const { id: roleId, name: roleName } = role; const existingRolePermission = foundCommand.rolePermissions.find((rolePermission) => rolePermission.roleId === roleId); if (!existingRolePermission) { - const newRolePermission = { - commandId, + const newRolePermission = new PrefixCommandRolePermission({ roleId, type, - }; + }); try { foundCommand.rolePermissions.push(newRolePermission); await foundCommand.save(); - const { id: rolePermissionId } = foundCommand.rolePermissions.find((rolePermission) => rolePermission.roleId === roleId)!; - await interaction.followUp({ embeds: [successEmbed(command, roleName, type, rolePermissionId)], ephemeral: true }); + await refreshSinglePrefixCommandCache(foundCommand, foundCommand); + await interaction.followUp({ embeds: [successEmbed(command, roleName, type)], ephemeral: true }); if (modLogsRole) { try { - await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName, type, commandId, rolePermissionId)] }); + await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName, type)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs role: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/functions/addVersion.ts b/src/commands/moderation/prefixCommands/functions/addVersion.ts index 50bd70c5..d2c7dabe 100644 --- a/src/commands/moderation/prefixCommands/functions/addVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/addVersion.ts @@ -108,7 +108,7 @@ export async function handleAddPrefixCommandVersion(interaction: ChatInputComman }); try { await prefixCommandVersion.save(); - await loadSinglePrefixCommandVersionToCache(prefixCommandVersion.toObject(), alias); + await loadSinglePrefixCommandVersionToCache(prefixCommandVersion); await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/deleteCategory.ts b/src/commands/moderation/prefixCommands/functions/deleteCategory.ts index d3fe0827..25756683 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteCategory.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteCategory.ts @@ -73,8 +73,8 @@ export async function handleDeletePrefixCommandCategory(interaction: ChatInputCo if (existingCategory) { const { id: categoryId, name, emoji } = existingCategory; try { + await clearSinglePrefixCommandCategoryCache(existingCategory); await existingCategory.deleteOne(); - await clearSinglePrefixCommandCategoryCache(name); await interaction.followUp({ embeds: [successEmbed(name || '', categoryId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/deleteChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/deleteChannelDefaultVersion.ts index 0a4bfdf1..bd0962d4 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteChannelDefaultVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteChannelDefaultVersion.ts @@ -64,12 +64,12 @@ export async function handleDeletePrefixCommandChannelDefaultVersion(interaction } const { id: channelId, name: channelName } = channel; - const foundChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find({ channelId }); + const existingChannelDefaultVersion = await PrefixCommandChannelDefaultVersion.findOne({ channelId }); - if (foundChannelDefaultVersions && foundChannelDefaultVersions.length > 0) { + if (existingChannelDefaultVersion) { try { - await PrefixCommandChannelDefaultVersion.deleteOne({ channelId }); - await clearSinglePrefixCommandChannelDefaultVersionCache(channelId); + await clearSinglePrefixCommandChannelDefaultVersionCache(existingChannelDefaultVersion); + await existingChannelDefaultVersion.deleteOne(); await interaction.followUp({ embeds: [successEmbed(channelName)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts index 1013b5ee..4e3d7c7f 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts @@ -81,8 +81,8 @@ export async function handleDeletePrefixCommand(interaction: ChatInputCommandInt if (existingCommand) { const { id: commandId, name, aliases, isEmbed, embedColor } = existingCommand; try { + await clearSinglePrefixCommandCache(existingCommand); await existingCommand.deleteOne(); - await clearSinglePrefixCommandCache(name); await interaction.followUp({ embeds: [successEmbed(name || '', commandId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/deleteContent.ts b/src/commands/moderation/prefixCommands/functions/deleteContent.ts index 4ef35ad2..a54704bc 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteContent.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteContent.ts @@ -13,18 +13,18 @@ const noContentEmbed = (command: string, version: string) => makeEmbed({ color: Colors.Red, }); -const failedEmbed = (contentId: string) => makeEmbed({ +const failedEmbed = (version: string) => makeEmbed({ title: 'Prefix Commands - Delete Content - Failed', - description: `Failed to delete the prefix command content with id ${contentId}.`, + description: `Failed to delete the prefix command content with version ${version}.`, color: Colors.Red, }); -const successEmbed = (command: string, version: string, contentId: string) => makeEmbed({ - title: `Prefix command content for command ${command} and version ${version} (Content ID: ${contentId}) was deleted successfully.`, +const successEmbed = (command: string, version: string) => makeEmbed({ + title: `Prefix command content for command ${command} and version ${version} was deleted successfully.`, color: Colors.Green, }); -const modLogEmbed = (moderator: User, commandName: string, versionName: string, title: string, content: string, image: string, commandId: string, versionId: string, contentId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, commandName: string, versionName: string, title: string, content: string, image: string) => makeEmbed({ title: 'Prefix command content delete', fields: [ { @@ -52,7 +52,6 @@ const modLogEmbed = (moderator: User, commandName: string, versionName: string, value: `${moderator}`, }, ], - footer: { text: `Command ID: ${commandId} - Version ID: ${versionId} - Content ID: ${contentId}` }, color: Colors.Green, }); @@ -81,17 +80,14 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - let versionId = 'GENERIC'; - if (version !== 'GENERIC') { - const foundVersion = await PrefixCommandVersion.findOne({ name: version }); - versionId = foundVersion?.id; - } - const foundCommand = await PrefixCommand.findOne({ 'name': command, 'contents.versionId': versionId }); - const existingContent = foundCommand?.contents.filter((content) => content.versionId === versionId)[0] || null; + const foundVersion = await PrefixCommandVersion.findOne({ name: version }); + const { versionId } = foundVersion ?? { versionId: 'GENERIC' }; + const foundCommand = await PrefixCommand.findOne({ name: command }); + const [existingContent] = foundCommand?.contents.filter((content) => content.versionId === versionId) ?? []; if (foundCommand && existingContent) { - const { id: contentId, title, content, image } = existingContent; - const { name: commandName, aliases: commandAliases } = foundCommand; + const { title, content, image } = existingContent; + const { name: commandName } = foundCommand; let versionName = ''; if (versionId !== 'GENERIC') { const foundVersion = await PrefixCommandVersion.findById(versionId); @@ -101,20 +97,20 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom versionName = foundVersion.name || ''; } try { - foundCommand.contents.id(contentId)?.deleteOne(); + foundCommand.contents.find((con) => con.versionId === versionId)?.deleteOne(); await foundCommand.save(); - await refreshSinglePrefixCommandCache(commandName, foundCommand.toObject(), commandName, commandAliases); - await interaction.followUp({ embeds: [successEmbed(`${commandName}`, `${versionName}`, `${contentId}`)], ephemeral: true }); + await refreshSinglePrefixCommandCache(foundCommand, foundCommand); + await interaction.followUp({ embeds: [successEmbed(`${commandName}`, `${versionName}`)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, `${commandName}`, `${versionName}`, `${title}`, `${content}`, `${image}`, `${foundCommand.id}`, `${versionId}`, `${contentId}`)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, `${commandName}`, `${versionName}`, `${title}`, `${content}`, `${image}`)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } } } catch (error) { - Logger.error(`Failed to delete a prefix command content with id ${contentId}: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(contentId)], ephemeral: true }); + Logger.error(`Failed to delete a prefix command content with version ${version}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(version)], ephemeral: true }); } } else { await interaction.followUp({ embeds: [noContentEmbed(command, version)], ephemeral: true }); diff --git a/src/commands/moderation/prefixCommands/functions/deleteVersion.ts b/src/commands/moderation/prefixCommands/functions/deleteVersion.ts index b6af5f56..1ff68030 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, PrefixCommandChannelDefaultVersion, clearSinglePrefixCommandVersionCache } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, PrefixCommandChannelDefaultVersion, clearSinglePrefixCommandVersionCache, PrefixCommand, refreshSinglePrefixCommandCache, clearSinglePrefixCommandChannelDefaultVersionCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Delete Version - No Connection', @@ -95,8 +95,9 @@ export async function handleDeletePrefixCommandVersion(interaction: ChatInputCom return; } const { id: versionId } = existingVersion; - const foundContents = await PrefixCommandVersion.find({ versionId }); - if (foundContents && foundContents.length > 0 && !force) { + // Find all PrefixCommands with content where version ID == versionId + const foundCommandsWithContent = await PrefixCommand.find({ 'contents.versionId': versionId }); + if (foundCommandsWithContent && foundCommandsWithContent.length > 0 && !force) { await interaction.followUp({ embeds: [contentPresentEmbed], ephemeral: true }); return; } @@ -109,20 +110,27 @@ export async function handleDeletePrefixCommandVersion(interaction: ChatInputCom if (existingVersion) { const { name, emoji, enabled, alias } = existingVersion; try { - await existingVersion.deleteOne(); - await clearSinglePrefixCommandVersionCache(alias); - if (foundContents && force) { - for (const content of foundContents) { + if (foundCommandsWithContent && force) { + for (const command of foundCommandsWithContent) { + const { _id: commandId } = command; // eslint-disable-next-line no-await-in-loop - await content.deleteOne(); + const updatedCommand = await PrefixCommand.findOneAndUpdate({ _id: commandId }, { $pull: { contents: { versionId } } }, { new: true }); + if (updatedCommand) { + // eslint-disable-next-line no-await-in-loop + await refreshSinglePrefixCommandCache(command, updatedCommand); + } } } if (foundChannelDefaultVersions && force) { for (const channelDefaultVersion of foundChannelDefaultVersions) { + // eslint-disable-next-line no-await-in-loop + await clearSinglePrefixCommandChannelDefaultVersionCache(channelDefaultVersion); // eslint-disable-next-line no-await-in-loop await channelDefaultVersion.deleteOne(); } } + await clearSinglePrefixCommandVersionCache(existingVersion); + await existingVersion.deleteOne(); await interaction.followUp({ embeds: [successEmbed(name || '', versionId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/modifyCategory.ts b/src/commands/moderation/prefixCommands/functions/modifyCategory.ts index bf5950b1..53755406 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCategory.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCategory.ts @@ -73,13 +73,14 @@ export async function handleModifyPrefixCommandCategory(interaction: ChatInputCo const existingCategory = await PrefixCommandCategory.findOne({ name: category }); if (existingCategory) { - const { id: categoryId, name: oldName } = existingCategory; + const { id: categoryId } = existingCategory; + const oldCategory = existingCategory.$clone(); existingCategory.name = name || existingCategory.name; existingCategory.emoji = emoji || existingCategory.emoji; try { await existingCategory.save(); const { name, emoji } = existingCategory; - await refreshSinglePrefixCommandCategoryCache(oldName, existingCategory.toObject(), name); + await refreshSinglePrefixCommandCategoryCache(oldCategory, existingCategory); await interaction.followUp({ embeds: [successEmbed(name, categoryId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index 5152e932..e29be2dd 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -118,7 +118,8 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt const existingCommand = await PrefixCommand.findOne({ name: command }); if (existingCommand) { - const { id: commandId, name: oldName } = existingCommand; + const { id: commandId } = existingCommand; + const oldCommand = existingCommand.$clone(); existingCommand.name = name || existingCommand.name; existingCommand.categoryId = foundCategory?.id || existingCommand.categoryId; existingCommand.aliases = aliases.length > 0 ? aliases : existingCommand.aliases; @@ -127,7 +128,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt try { await existingCommand.save(); const { name, aliases, isEmbed, embedColor } = existingCommand; - await refreshSinglePrefixCommandCache(oldName, existingCommand.toObject(), name, aliases); + await refreshSinglePrefixCommandCache(oldCommand, existingCommand); await interaction.followUp({ embeds: [successEmbed(name, commandId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts index 7a049193..c27e029e 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts @@ -100,7 +100,8 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom const existingVersion = await PrefixCommandVersion.findOne({ name: version }); if (existingVersion) { - const { id: versionId, alias: oldAlias } = existingVersion; + const { id: versionId } = existingVersion; + const oldVersion = existingVersion.$clone(); existingVersion.name = name || existingVersion.name; existingVersion.emoji = emoji || existingVersion.emoji; existingVersion.alias = alias || existingVersion.alias; @@ -108,7 +109,7 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom try { await existingVersion.save(); const { name, emoji, alias, enabled } = existingVersion; - await refreshSinglePrefixCommandVersionCache(oldAlias, existingVersion.toObject(), alias); + await refreshSinglePrefixCommandVersionCache(oldVersion, existingVersion); await interaction.followUp({ embeds: [successEmbed(name, versionId)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts index bf92fc8f..c82113c5 100644 --- a/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts +++ b/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts @@ -25,12 +25,12 @@ const doesNotExistEmbed = (command: string, channel: string) => makeEmbed({ color: Colors.Red, }); -const successEmbed = (command: string, channel: string, type: string, channelPermissionId: string) => makeEmbed({ - title: `Prefix command channel ${type} permission removed for command ${command} and channel <#${channel}>. ChannelPermission ID: ${channelPermissionId}`, +const successEmbed = (command: string, channel: string, type: string) => makeEmbed({ + title: `Prefix command channel ${type} permission removed for command ${command} and channel <#${channel}>.`, color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, channel: string, type: string, commandId: string, channelPermissionId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, channel: string, type: string) => makeEmbed({ title: 'Remove prefix command channel permission', fields: [ { @@ -50,7 +50,6 @@ const modLogEmbed = (moderator: User, command: string, channel: string, type: st value: `${moderator}`, }, ], - footer: { text: `Command ID: ${commandId} - Channel Permission ID: ${channelPermissionId}` }, color: Colors.Red, }); @@ -88,28 +87,27 @@ export async function handleRemovePrefixCommandChannelPermission(interaction: Ch return; } const [foundCommand] = foundCommands; - const { id: commandId } = foundCommand; - const { id: channelId, name: channelName } = channel; + const { id: channelId } = channel; const existingChannelPermission = foundCommand.channelPermissions.find((channelPermission) => channelPermission.channelId === channelId); if (existingChannelPermission) { - const { id: channelPermissionId, type } = existingChannelPermission; + const { type } = existingChannelPermission; try { - foundCommand.channelPermissions.id(channelPermissionId)?.deleteOne(); + foundCommand.channelPermissions.find((channelPermission) => channelPermission.channelId === channelId)?.deleteOne(); await foundCommand.save(); - await interaction.followUp({ embeds: [successEmbed(command, channelName, type, channelPermissionId)], ephemeral: true }); + await interaction.followUp({ embeds: [successEmbed(command, channelId, type)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channelName, type, commandId, channelPermissionId)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channelId, type)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } } } catch (error) { Logger.error(`Failed to remove ${type} prefix command channel permission for command ${command} and channel <#${channel}>: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(command, channelName, type)], ephemeral: true }); + await interaction.followUp({ embeds: [failedEmbed(command, channelId, type)], ephemeral: true }); } } else { - await interaction.followUp({ embeds: [doesNotExistEmbed(command, channelName)], ephemeral: true }); + await interaction.followUp({ embeds: [doesNotExistEmbed(command, channelId)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts b/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts index 6b61bfc4..707e0642 100644 --- a/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts +++ b/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts @@ -25,12 +25,12 @@ const doesNotExistEmbed = (command: string, roleName: string) => makeEmbed({ color: Colors.Red, }); -const successEmbed = (command: string, roleName: string, type: string, rolePermissionId: string) => makeEmbed({ - title: `Prefix command role ${type} permission removed for command ${command} and role ${roleName}. RolePermission ID: ${rolePermissionId}`, +const successEmbed = (command: string, roleName: string, type: string) => makeEmbed({ + title: `Prefix command role ${type} permission removed for command ${command} and role ${roleName}.`, color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, roleName: string, type: string, commandId: string, rolePermissionId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, roleName: string, type: string) => makeEmbed({ title: 'Remove prefix command role permission', fields: [ { @@ -50,7 +50,6 @@ const modLogEmbed = (moderator: User, command: string, roleName: string, type: s value: `${moderator}`, }, ], - footer: { text: `Command ID: ${commandId} - Role Permission ID: ${rolePermissionId}` }, color: Colors.Green, }); @@ -88,19 +87,18 @@ export async function handleRemovePrefixCommandRolePermission(interaction: ChatI return; } const [foundCommand] = foundCommands; - const { id: commandId } = foundCommand; const { id: roleId, name: roleName } = role; const existingRolePermission = foundCommand.rolePermissions.find((rolePermission) => rolePermission.roleId === roleId); if (existingRolePermission) { - const { id: rolePermissionId, type } = existingRolePermission; + const { type } = existingRolePermission; try { - foundCommand.rolePermissions.id(rolePermissionId)?.deleteOne(); + foundCommand.rolePermissions.find((rolePermission) => rolePermission.roleId === roleId)?.deleteOne(); await foundCommand.save(); - await interaction.followUp({ embeds: [successEmbed(command, roleName, type, rolePermissionId)], ephemeral: true }); + await interaction.followUp({ embeds: [successEmbed(command, roleName, type)], ephemeral: true }); if (modLogsRole) { try { - await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName, type, commandId, rolePermissionId)] }); + await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName, type)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs role: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts index 723b18be..fcb7ec6a 100644 --- a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts @@ -85,13 +85,13 @@ export async function handleSetPrefixCommandChannelDefaultVersion(interaction: C versionId = foundVersion.id; emoji = foundVersion.emoji; } - const foundChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find({ channelId }); - const channelDefaultVersion = foundChannelDefaultVersions.length === 1 ? foundChannelDefaultVersions[0] : new PrefixCommandChannelDefaultVersion({ channelId, versionId }); + const foundChannelDefaultVersion = await PrefixCommandChannelDefaultVersion.findOne({ channelId }); + const channelDefaultVersion = foundChannelDefaultVersion || new PrefixCommandChannelDefaultVersion({ channelId, versionId }); channelDefaultVersion.versionId = versionId; try { await channelDefaultVersion.save(); if (foundVersion) { - await refreshSinglePrefixCommandChannelDefaultVersionCache(channelId, foundVersion.toObject()); + await refreshSinglePrefixCommandChannelDefaultVersionCache(channelDefaultVersion, channelDefaultVersion); } await interaction.followUp({ embeds: [successEmbed(channelName, version, emoji)], ephemeral: true }); if (modLogsChannel) { diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index 17d54bbd..3d366a14 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -1,5 +1,5 @@ import { ActionRowBuilder, ChatInputCommandInteraction, Colors, ModalBuilder, TextChannel, TextInputBuilder, TextInputStyle, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed, refreshSinglePrefixCommandCache } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommand, Logger, makeEmbed, refreshSinglePrefixCommandCache, PrefixCommandContent } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Set Content - No Connection', @@ -25,12 +25,12 @@ const failedEmbed = (command: string, version: string) => makeEmbed({ color: Colors.Red, }); -const successEmbed = (command: string, version: string, contentId: string) => makeEmbed({ - title: `Prefix command content set for command ${command} and version ${version}. Content ID: ${contentId}`, +const successEmbed = (command: string, version: string) => makeEmbed({ + title: `Prefix command content set for command ${command} and version ${version}.`, color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, version: string, title: string, content: string, image: string, commandId: string, versionId: string, contentId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, version: string, title: string, content: string, image: string, commandId: string, versionId: string) => makeEmbed({ title: 'Prefix command content set', fields: [ { @@ -58,7 +58,7 @@ const modLogEmbed = (moderator: User, command: string, version: string, title: s value: `${moderator}`, }, ], - footer: { text: `Command ID: ${commandId} - Version ID: ${versionId} - Content ID: ${contentId}` }, + footer: { text: `Command ID: ${commandId} - Version ID: ${versionId}` }, color: Colors.Green, }); @@ -89,7 +89,7 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman } const foundCommand = foundCommands[0]; - const { id: commandId, name: commandName, aliases: commandAliases } = foundCommand; + const { _id: commandId } = foundCommand; let versionId = ''; let foundVersions = null; if (version === 'GENERIC' || version === 'generic') { @@ -97,7 +97,7 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman } else { foundVersions = await PrefixCommandVersion.find({ name: version }); if (foundVersions && foundVersions.length === 1) { - versionId = foundVersions[0].id; + [{ _id: versionId }] = foundVersions; } else { await interaction.followUp({ embeds: [noVersionEmbed(version)], ephemeral: true }); return; @@ -191,7 +191,7 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman } if (foundContent) { - const foundData = foundCommand.contents.id(foundContent.id); + const foundData = foundCommand.contents.find((c) => c.versionId === foundContent.versionId); try { await foundData?.deleteOne(); } catch (error) { @@ -200,22 +200,21 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman return; } } - const contentData = { + const contentData = new PrefixCommandContent({ versionId, title, content, image, - }; + }); foundCommand.contents.push(contentData); try { await foundCommand.save(); - await refreshSinglePrefixCommandCache(commandName, foundCommand.toObject(), commandName, commandAliases); - const { id: contentId } = foundCommand.contents.find((c) => c.versionId === versionId)!; - await interaction.followUp({ embeds: [successEmbed(command, version, contentId)], ephemeral: true }); + await refreshSinglePrefixCommandCache(foundCommand, foundCommand); + await interaction.followUp({ embeds: [successEmbed(command, version)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, version, title, content, image, commandId, versionId, contentId)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, version, title, content, image, commandId, versionId)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index 40dbb86c..03e2d862 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -1,4 +1,4 @@ -import { EmbedBuilder, Message } from 'discord.js'; +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder, Interaction, Message } from 'discord.js'; import { event, getInMemoryCache, Logger, Events, constantsConfig, makeEmbed, makeLines } from '../lib'; import { PrefixCommand, PrefixCommandVersion } from '../lib/schemas/prefixCommandSchemas'; @@ -9,7 +9,7 @@ const commandEmbed = (title: string, description: string, color: string, imageUr ...(imageUrl && { image: { url: imageUrl } }), }); -async function replyWithEmbed(msg: Message, embed: EmbedBuilder) : Promise> { +async function replyWithEmbed(msg: Message, embed: EmbedBuilder, buttonRow?: ActionRowBuilder) : Promise> { return msg.fetchReference() .then((res) => { let existingFooterText = ''; @@ -19,15 +19,42 @@ async function replyWithEmbed(msg: Message, embed: EmbedBuilder) : Promise msg.reply({ embeds: [embed] })); + .catch(() => msg.reply({ + embeds: [embed], + components: buttonRow ? [buttonRow] : [], + })); } -async function replyWithMsg(msg: Message, text: string) : Promise> { +async function replyWithMsg(msg: Message, text: string, buttonRow?:ActionRowBuilder) : Promise> { return msg.fetchReference() - .then((res) => res.reply(`${text}\n\n\`Executed by ${msg.author.tag} - ${msg.author.id}\``)) - .catch(() => msg.reply(text)); + .then((res) => res.reply({ + content: `${text}\n\n\`Executed by ${msg.author.tag} - ${msg.author.id}\``, + components: buttonRow ? [buttonRow] : [], + })) + .catch(() => msg.reply({ + content: text, + components: buttonRow ? [buttonRow] : [], + })); +} + +async function sendReply(message: Message, commandTitle: string, commandContent: string, isEmbed: boolean, embedColor: string, commandImage: string, versionButtonRow?: ActionRowBuilder) : Promise> { + try { + if (isEmbed) { + return replyWithEmbed(message, commandEmbed(commandTitle, commandContent, embedColor, commandImage), versionButtonRow); + } + return replyWithMsg(message, makeLines([ + `**${commandTitle}**`, + ...(commandContent ? [commandContent] : []), + ]), versionButtonRow); + } catch (error) { + Logger.error(error); + return message.reply('An error occurred while processing the command.'); + } } export default event(Events.MessageCreate, async (_, message) => { @@ -39,9 +66,6 @@ export default event(Events.MessageCreate, async (_, message) => { const { id: guildId } = guild; Logger.debug(`Processing message ${messageId} from user ${authorId} in channel ${channelId} of server ${guildId}.`); - // TODO: Permission verification - // TODO: If generic, check available versions and show selections - const inMemoryCache = getInMemoryCache(); if (inMemoryCache && content.startsWith(constantsConfig.prefixCommandPrefix)) { const commandTextMatch = content.match(`^\\${constantsConfig.prefixCommandPrefix}([\\w\\d-_]+)[^\\w\\d-_]*([\\w\\d-_]+)?`); @@ -88,24 +112,53 @@ export default event(Events.MessageCreate, async (_, message) => { if (cachedCommandDetails) { const commandDetails = PrefixCommand.hydrate(cachedCommandDetails); const { name, contents, isEmbed, embedColor, channelPermissions, rolePermissions } = commandDetails; + // TODO: Check permissions + const commandContentData = contents.find(({ versionId }) => versionId === commandVersionId); if (!commandContentData) { Logger.debug(`Prefix Command - Version "${commandVersionName}" not found for command "${name}" based on user command "${commandText}"`); return; } - Logger.debug(`Prefix Command - Executing version "${commandVersionName}" for command "${name}" based on user command "${commandText}"`); const { title: commandTitle, content: commandContent, image: commandImage } = commandContentData; - try { - if (isEmbed) { - replyWithEmbed(message, commandEmbed(commandTitle, commandContent || '', embedColor || constantsConfig.colors.FBW_CYAN, commandImage || '')); - } else { - replyWithMsg(message, makeLines([ - `**${commandTitle}**`, - ...(commandContent ? [commandContent] : []), - ])); + // If generic and multiple versions, show the selection + if (commandVersionName === 'GENERIC' && contents.length > 1) { + Logger.debug(`Prefix Command - Multiple versions found for command "${name}" based on user command "${commandText}", showing version selection`); + const versionSelectionButtons: ButtonBuilder[] = []; + for (const { versionId: versionIdForButton } of contents) { + // eslint-disable-next-line no-await-in-loop + const versionCached = await inMemoryCache.get(`PF_VERSION:${versionIdForButton}`); + if (versionCached) { + const version = PrefixCommandVersion.hydrate(versionCached); + const { emoji } = version; + versionSelectionButtons.push( + new ButtonBuilder() + .setCustomId(`${versionIdForButton}`) + .setEmoji(emoji) + .setStyle(ButtonStyle.Primary), + ); + } } - } catch (error) { - Logger.error(error); + const versionSelectButtonRow = new ActionRowBuilder().addComponents(versionSelectionButtons); + const buttonMessage = await sendReply(message, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || '', versionSelectButtonRow); + + const filter = (interaction: Interaction) => interaction.user.id === authorId; + const collector = buttonMessage.createMessageComponentCollector({ filter, time: 60_000 }); + collector.on('collect', async (collectedInteraction: ButtonInteraction) => { + Logger.debug(`Prefix Command - User selected version "${collectedInteraction.customId}" for command "${name}" based on user command "${commandText}"`); + await collectedInteraction.deferUpdate(); + buttonMessage.delete(); + const { customId: selectedVersionId } = collectedInteraction; + const commandContentData = contents.find(({ versionId }) => versionId === selectedVersionId); + if (!commandContentData) { + Logger.debug(`Prefix Command - Version ID "${selectedVersionId}" not found for command "${name}" based on user command "${commandText}"`); + return; + } + const { title: commandTitle, content: commandContent, image: commandImage } = commandContentData; + await sendReply(message, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || ''); + }); + } else { + Logger.debug(`Prefix Command - Executing version "${commandVersionName}" for command "${name}" based on user command "${commandText}"`); + await sendReply(message, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || ''); } } } diff --git a/src/lib/cache/cacheManager.ts b/src/lib/cache/cacheManager.ts index 5a5bd035..db423331 100644 --- a/src/lib/cache/cacheManager.ts +++ b/src/lib/cache/cacheManager.ts @@ -1,5 +1,5 @@ import { Cache, caching } from 'cache-manager'; -import { getConn, Logger, PrefixCommand, PrefixCommandCategory, PrefixCommandChannelDefaultVersion, PrefixCommandVersion } from '../index'; +import { getConn, IPrefixCommand, IPrefixCommandCategory, IPrefixCommandChannelDefaultVersion, IPrefixCommandVersion, Logger, PrefixCommand, PrefixCommandCategory, PrefixCommandChannelDefaultVersion, PrefixCommandVersion } from '../index'; let inMemoryCache: Cache; const commandCachePrefix = 'PF_COMMAND'; @@ -39,20 +39,17 @@ export function getInMemoryCache(callback = Logger.error) { * Prefix Command Cache Management Functions */ -export async function clearSinglePrefixCommandCache(commandName: string) { +export async function clearSinglePrefixCommandCache(command: IPrefixCommand) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; - Logger.debug(`Clearing cache for command or alias "${commandName}"`); - const commandCache = await inMemoryCache.get(`${commandCachePrefix}:${commandName.toLowerCase()}`); - const command = PrefixCommand.hydrate(commandCache); - if (!command) return; - const { aliases } = command; + const { name, aliases } = command; + Logger.debug(`Clearing cache for command or alias "${name}"`); for (const alias of aliases) { // eslint-disable-next-line no-await-in-loop await inMemoryCache.del(`${commandCachePrefix}:${alias.toLowerCase()}`); } - await inMemoryCache.del(`${commandCachePrefix}:${commandName.toLowerCase()}`); + await inMemoryCache.del(`${commandCachePrefix}:${name.toLowerCase()}`); } export async function clearAllPrefixCommandsCache() { @@ -69,15 +66,16 @@ export async function clearAllPrefixCommandsCache() { } } -export async function loadSinglePrefixCommandToCache(command: Object, name: string, aliases: string[]) { +export async function loadSinglePrefixCommandToCache(command: IPrefixCommand) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; + const { name, aliases } = command; Logger.debug(`Loading command ${name} to cache`); - await inMemoryCache.set(`${commandCachePrefix}:${name.toLowerCase()}`, command); + await inMemoryCache.set(`${commandCachePrefix}:${name.toLowerCase()}`, command.toObject()); for (const alias of aliases) { // eslint-disable-next-line no-await-in-loop - await inMemoryCache.set(`${commandCachePrefix}:${alias.toLowerCase()}`, command); + await inMemoryCache.set(`${commandCachePrefix}:${alias.toLowerCase()}`, command.toObject()); } } @@ -87,17 +85,15 @@ export async function loadAllPrefixCommandsToCache() { if (!conn || !inMemoryCache) return; const PrefixCommands = await PrefixCommand.find(); - for (const command of PrefixCommands) { - const { name, aliases } = command; // eslint-disable-next-line no-await-in-loop - await loadSinglePrefixCommandToCache(command.toObject(), name, aliases); + await loadSinglePrefixCommandToCache(command); } } -export async function refreshSinglePrefixCommandCache(oldName: string, command: Object, newName: string, newAliases: string[]) { - await clearSinglePrefixCommandCache(oldName); - await loadSinglePrefixCommandToCache(command, newName, newAliases); +export async function refreshSinglePrefixCommandCache(oldCommand: IPrefixCommand, newCommand: IPrefixCommand) { + await clearSinglePrefixCommandCache(oldCommand); + await loadSinglePrefixCommandToCache(newCommand); } export async function refreshAllPrefixCommandsCache() { @@ -109,12 +105,14 @@ export async function refreshAllPrefixCommandsCache() { * Prefix Command Version Cache Management Functions */ -export async function clearSinglePrefixCommandVersionCache(alias: string) { +export async function clearSinglePrefixCommandVersionCache(version: IPrefixCommandVersion) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; + const { alias, _id: versionId } = version; Logger.debug(`Clearing cache for command version alias "${alias}"`); await inMemoryCache.del(`${commandVersionCachePrefix}:${alias.toLowerCase()}`); + await inMemoryCache.del(`${commandVersionCachePrefix}:${versionId}`); } export async function clearAllPrefixCommandVersionsCache() { @@ -124,19 +122,21 @@ export async function clearAllPrefixCommandVersionsCache() { const keys = await inMemoryCache.store.keys(); for (const key of keys) { if (key.startsWith(`${commandVersionCachePrefix}:`)) { - Logger.debug(`Clearing cache for command version alias "${key}"`); + Logger.debug(`Clearing cache for command version alias/id "${key}"`); // eslint-disable-next-line no-await-in-loop await inMemoryCache.del(key); } } } -export async function loadSinglePrefixCommandVersionToCache(version: Object, alias: string) { +export async function loadSinglePrefixCommandVersionToCache(version: IPrefixCommandVersion) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; + const { alias, _id: versionId } = version; Logger.debug(`Loading version with alias ${alias} to cache`); - await inMemoryCache.set(`${commandVersionCachePrefix}:${alias.toLowerCase()}`, version); + await inMemoryCache.set(`${commandVersionCachePrefix}:${alias.toLowerCase()}`, version.toObject()); + await inMemoryCache.set(`${commandVersionCachePrefix}:${versionId}`, version.toObject()); } export async function loadAllPrefixCommandVersionsToCache() { @@ -145,17 +145,15 @@ export async function loadAllPrefixCommandVersionsToCache() { if (!conn || !inMemoryCache) return; const PrefixCommandVersions = await PrefixCommandVersion.find(); - for (const version of PrefixCommandVersions) { - const { alias } = version; // eslint-disable-next-line no-await-in-loop - await loadSinglePrefixCommandVersionToCache(version.toObject(), alias); + await loadSinglePrefixCommandVersionToCache(version); } } -export async function refreshSinglePrefixCommandVersionCache(oldAlias: string, version: Object, newAlias: string) { - await clearSinglePrefixCommandVersionCache(oldAlias); - await loadSinglePrefixCommandVersionToCache(version, newAlias); +export async function refreshSinglePrefixCommandVersionCache(oldVersion: IPrefixCommandVersion, newVersion: IPrefixCommandVersion) { + await clearSinglePrefixCommandVersionCache(oldVersion); + await loadSinglePrefixCommandVersionToCache(newVersion); } export async function refreshAllPrefixCommandVersionsCache() { @@ -167,10 +165,11 @@ export async function refreshAllPrefixCommandVersionsCache() { * Prefix Command Category Cache Management Functions */ -export async function clearSinglePrefixCommandCategoryCache(name: string) { +export async function clearSinglePrefixCommandCategoryCache(category: IPrefixCommandCategory) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; + const { name } = category; Logger.debug(`Clearing cache for command category "${name}"`); await inMemoryCache.del(`PF_CATEGORY:${name.toLowerCase()}`); } @@ -189,12 +188,13 @@ export async function clearAllPrefixCommandCategoriesCache() { } } -export async function loadSinglePrefixCommandCategoryToCache(category: Object, name: string) { +export async function loadSinglePrefixCommandCategoryToCache(category: IPrefixCommandCategory) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; + const { name } = category; Logger.debug(`Loading category ${name} to cache`); - await inMemoryCache.set(`PF_CATEGORY:${name.toLowerCase()}`, category); + await inMemoryCache.set(`PF_CATEGORY:${name.toLowerCase()}`, category.toObject()); } export async function loadAllPrefixCommandCategoriesToCache() { @@ -203,17 +203,15 @@ export async function loadAllPrefixCommandCategoriesToCache() { if (!conn || !inMemoryCache) return; const PrefixCommandCategories = await PrefixCommandCategory.find(); - for (const category of PrefixCommandCategories) { - const { name } = category; // eslint-disable-next-line no-await-in-loop - await loadSinglePrefixCommandCategoryToCache(category.toObject(), name); + await loadSinglePrefixCommandCategoryToCache(category); } } -export async function refreshSinglePrefixCommandCategoryCache(oldName: string, category: Object, newName: string) { - await clearSinglePrefixCommandCategoryCache(oldName); - await loadSinglePrefixCommandCategoryToCache(category, newName); +export async function refreshSinglePrefixCommandCategoryCache(oldCategory: IPrefixCommandCategory, newCategory: IPrefixCommandCategory) { + await clearSinglePrefixCommandCategoryCache(oldCategory); + await loadSinglePrefixCommandCategoryToCache(newCategory); } export async function refreshAllPrefixCommandCategoriesCache() { @@ -225,10 +223,11 @@ export async function refreshAllPrefixCommandCategoriesCache() { * Prefix Command Channel Default Version Cache Management Functions */ -export async function clearSinglePrefixCommandChannelDefaultVersionCache(channelId: string) { +export async function clearSinglePrefixCommandChannelDefaultVersionCache(channelDefaultVersion: IPrefixCommandChannelDefaultVersion) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; + const { channelId } = channelDefaultVersion; Logger.debug(`Clearing cache for channel default version for channel "${channelId}"`); await inMemoryCache.del(`PF_CHANNEL_VERSION:${channelId}`); } @@ -247,12 +246,16 @@ export async function clearAllPrefixCommandChannelDefaultVersionsCache() { } } -export async function loadSinglePrefixCommandChannelDefaultVersionToCache(version: Object, channelId: string) { +export async function loadSinglePrefixCommandChannelDefaultVersionToCache(channelDefaultVersion: IPrefixCommandChannelDefaultVersion) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; - Logger.debug(`Loading default version for channel ${channelId} to cache`); - await inMemoryCache.set(`PF_CHANNEL_VERSION:${channelId}`, version); + const { channelId, versionId } = channelDefaultVersion; + const version = await PrefixCommandVersion.findById(versionId); + if (version) { + Logger.debug(`Loading default version for channel ${channelId} to cache`); + await inMemoryCache.set(`PF_CHANNEL_VERSION:${channelId}`, version.toObject()); + } } export async function loadAllPrefixCommandChannelDefaultVersionsToCache() { @@ -263,19 +266,14 @@ export async function loadAllPrefixCommandChannelDefaultVersionsToCache() { const PrefixCommandChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find(); for (const defaultVersion of PrefixCommandChannelDefaultVersions) { - const { channelId, versionId } = defaultVersion; // eslint-disable-next-line no-await-in-loop - const version = await PrefixCommandVersion.findById(versionId); - if (version) { - // eslint-disable-next-line no-await-in-loop - await loadSinglePrefixCommandChannelDefaultVersionToCache(version.toObject(), channelId); - } + await loadSinglePrefixCommandChannelDefaultVersionToCache(defaultVersion); } } -export async function refreshSinglePrefixCommandChannelDefaultVersionCache(channelId: string, version: Object) { - await clearSinglePrefixCommandChannelDefaultVersionCache(channelId); - await loadSinglePrefixCommandChannelDefaultVersionToCache(version, channelId); +export async function refreshSinglePrefixCommandChannelDefaultVersionCache(oldChannelDefaultVersion: IPrefixCommandChannelDefaultVersion, newChannelDefaultVersion: IPrefixCommandChannelDefaultVersion) { + await clearSinglePrefixCommandChannelDefaultVersionCache(oldChannelDefaultVersion); + await loadSinglePrefixCommandChannelDefaultVersionToCache(newChannelDefaultVersion); } export async function refreshAllPrefixCommandChannelDefaultVersionsCache() { diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts index 224117f2..d40fd6de 100644 --- a/src/lib/schemas/prefixCommandSchemas.ts +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -1,6 +1,12 @@ -import mongoose, { Schema } from 'mongoose'; +import mongoose, { Schema, Document } from 'mongoose'; -const prefixCommandCategorySchema = new Schema({ +export interface IPrefixCommandCategory extends Document { + categoryId: mongoose.Schema.Types.ObjectId; + name: string; + emoji: string; +} + +const prefixCommandCategorySchema = new Schema({ categoryId: mongoose.Schema.Types.ObjectId, name: { type: String, @@ -10,7 +16,15 @@ const prefixCommandCategorySchema = new Schema({ emoji: String, }); -const prefixCommandVersionSchema = new Schema({ +export interface IPrefixCommandVersion extends Document { + versionId: mongoose.Schema.Types.ObjectId; + name: string; + emoji: string; + alias: string; + enabled: boolean; +} + +const prefixCommandVersionSchema = new Schema({ versionId: mongoose.Schema.Types.ObjectId, name: { type: String, @@ -30,7 +44,12 @@ const prefixCommandVersionSchema = new Schema({ enabled: Boolean, }); -const prefixCommandChannelDefaultVersionSchema = new Schema({ +export interface IPrefixCommandChannelDefaultVersion extends Document { + channelId: string; + versionId: string; +} + +const prefixCommandChannelDefaultVersionSchema = new Schema({ channelId: { type: String, required: true, @@ -42,7 +61,14 @@ const prefixCommandChannelDefaultVersionSchema = new Schema({ }, }); -const prefixCommandContentSchema = new Schema({ +export interface IPrefixCommandContent extends Document{ + versionId: string; + title: string; + content?: string; + image?: string; +} + +const prefixCommandContentSchema = new Schema({ versionId: { type: String, required: true, @@ -55,7 +81,12 @@ const prefixCommandContentSchema = new Schema({ image: String, }); -const prefixCommandChannelPermissionSchema = new Schema({ +export interface IPrefixCommandChannelPermission extends Document { + type: string; + channelId: string; +} + +const prefixCommandChannelPermissionSchema = new Schema({ type: { type: String, required: true, @@ -66,7 +97,12 @@ const prefixCommandChannelPermissionSchema = new Schema({ }, }); -const prefixCommandRolePermissionSchema = new Schema({ +export interface IPrefixCommandRolePermission extends Document { + type: string; + roleId: string; +} + +const prefixCommandRolePermissionSchema = new Schema({ type: { type: String, required: true, @@ -77,7 +113,19 @@ const prefixCommandRolePermissionSchema = new Schema({ }, }); -const prefixCommandSchema = new Schema({ +export interface IPrefixCommand extends Document { + commandId: mongoose.Schema.Types.ObjectId; + categoryId: mongoose.Schema.Types.ObjectId; + name: string; + aliases: string[]; + isEmbed: boolean; + embedColor?: string; + contents: IPrefixCommandContent[]; + channelPermissions: IPrefixCommandChannelPermission[]; + rolePermissions: IPrefixCommandRolePermission[]; +} + +const prefixCommandSchema = new Schema({ commandId: mongoose.Schema.Types.ObjectId, categoryId: { type: mongoose.Schema.Types.ObjectId, @@ -99,5 +147,8 @@ const prefixCommandSchema = new Schema({ export const PrefixCommandCategory = mongoose.model('PrefixCommandCategory', prefixCommandCategorySchema); export const PrefixCommandVersion = mongoose.model('PrefixCommandVersion', prefixCommandVersionSchema); +export const PrefixCommandContent = mongoose.model('PrefixCommandContent', prefixCommandContentSchema); +export const PrefixCommandChannelPermission = mongoose.model('PrefixCommandChannelPermission', prefixCommandChannelPermissionSchema); +export const PrefixCommandRolePermission = mongoose.model('PrefixCommandRolePermission', prefixCommandRolePermissionSchema); export const PrefixCommandChannelDefaultVersion = mongoose.model('PrefixCommandChannelDefaultVersion', prefixCommandChannelDefaultVersionSchema); export const PrefixCommand = mongoose.model('PrefixCommand', prefixCommandSchema); From 114ed1f2b9486085791077dfc0268f9cf0d2a695 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 8 Sep 2024 00:27:11 -0700 Subject: [PATCH 14/51] Fixing staging config json --- config/staging.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/staging.json b/config/staging.json index 4415fad9..382c726d 100644 --- a/config/staging.json +++ b/config/staging.json @@ -88,7 +88,7 @@ "MEDIA_TEAM", "FBW_EMERITUS" ] - , + }, "roles": { "ADMIN_TEAM": "1162821800225419272", "BOT_DEVELOPER": "1162822853306101901", From ce783167a98d31554ae9c71081f5c9758db2cf08 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 8 Sep 2024 15:25:01 -0700 Subject: [PATCH 15/51] Refactor Permission mechanism --- .../functions/addChannelPermission.ts | 50 ++++--- .../functions/addRolePermission.ts | 52 ++++---- .../functions/listChannelPermissions.ts | 70 ---------- .../functions/listRolePermissions.ts | 70 ---------- .../functions/removeChannelPermission.ts | 46 +++---- .../functions/removeRolePermission.ts | 46 +++---- .../functions/setCommandPermissionSettings.ts | 123 +++++++++++++++++ .../functions/showCommandPermissions.ts | 104 +++++++++++++++ .../prefixCommandPermissions.ts | 124 ++++++++++-------- src/events/messageCreateHandler.ts | 2 +- src/lib/schemas/prefixCommandSchemas.ts | 51 +++---- 11 files changed, 398 insertions(+), 340 deletions(-) delete mode 100644 src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts delete mode 100644 src/commands/moderation/prefixCommands/functions/listRolePermissions.ts create mode 100644 src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts create mode 100644 src/commands/moderation/prefixCommands/functions/showCommandPermissions.ts diff --git a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts index c7e9e43b..c96bc667 100644 --- a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts +++ b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts @@ -1,36 +1,36 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandChannelPermission, refreshSinglePrefixCommandCache } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, refreshSinglePrefixCommandCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ - title: 'Prefix Commands - Add Channel Permission - No Connection', - description: 'Could not connect to the database. Unable to add the prefix command channel permission.', + title: 'Prefix Commands - Add Channel - No Connection', + description: 'Could not connect to the database. Unable to add the prefix command channel.', color: Colors.Red, }); const noCommandEmbed = (command: string) => makeEmbed({ - title: 'Prefix Commands - Add Channel Permission - No Command', - description: `Failed to add the prefix command channel permission for command ${command} as the command does not exist or there are more than one matching.`, + title: 'Prefix Commands - Add Channel - No Command', + description: `Failed to add the prefix command channel for command ${command} as the command does not exist or there are more than one matching.`, color: Colors.Red, }); -const failedEmbed = (command: string, channel: string, type: string) => makeEmbed({ +const failedEmbed = (command: string, channel: string) => makeEmbed({ title: 'Prefix Commands - Add Channel Permission - Failed', - description: `Failed to add the ${type} prefix command channel permission for command ${command} and channel <#${channel}>.`, + description: `Failed to add the prefix command channel <#${channel}> for command ${command}.`, color: Colors.Red, }); const alreadyExistsEmbed = (command: string, channel: string) => makeEmbed({ - title: 'Prefix Commands - Add Channel Permission - Already exists', - description: `A prefix command channel permission for command ${command} and channel <#${channel}> already exists. Not adding again.`, + title: 'Prefix Commands - Add Channel - Already exists', + description: `A prefix command channel <#${channel}> for command ${command} already exists. Not adding again.`, color: Colors.Red, }); -const successEmbed = (command: string, channel: string, type: string) => makeEmbed({ - title: `Prefix command channel ${type} permission added for command ${command} and channel <#${channel}>.}`, +const successEmbed = (command: string, channel: string) => makeEmbed({ + title: `Prefix command channel <#${channel}> added for command ${command}.}`, color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, channel: string, type: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, channel: string) => makeEmbed({ title: 'Add prefix command channel permission', fields: [ { @@ -41,10 +41,6 @@ const modLogEmbed = (moderator: User, command: string, channel: string, type: st name: 'Channel', value: `<#${channel}>`, }, - { - name: 'Type', - value: type, - }, { name: 'Moderator', value: `${moderator}`, @@ -54,7 +50,7 @@ const modLogEmbed = (moderator: User, command: string, channel: string, type: st }); const noModLogs = makeEmbed({ - title: 'Prefix Commands - Add Channel Permission - No Mod Log', + title: 'Prefix Commands - Add Channel - No Mod Log', description: 'I can\'t find the mod logs channel. Please check the channel still exists.', color: Colors.Red, }); @@ -70,7 +66,6 @@ export async function handleAddPrefixCommandChannelPermission(interaction: ChatI const command = interaction.options.getString('command')!; const channel = interaction.options.getChannel('channel')!; - const type = interaction.options.getString('type')!; const moderator = interaction.user; //Check if the mod logs channel exists @@ -90,27 +85,26 @@ export async function handleAddPrefixCommandChannelPermission(interaction: ChatI const [foundCommand] = foundCommands; const { id: channelId } = channel; - const existingChannelPermission = foundCommand.channelPermissions.find((channelPermission) => channelPermission.channelId === channelId); + const existingChannelPermission = foundCommand.permissions.channels?.includes(channelId); if (!existingChannelPermission) { - const newChannelPermission = new PrefixCommandChannelPermission({ - channelId, - type, - }); + if (!foundCommand.permissions.channels) { + foundCommand.permissions.channels = []; + } + foundCommand.permissions.channels.push(channelId); try { - foundCommand.channelPermissions.push(newChannelPermission); await foundCommand.save(); await refreshSinglePrefixCommandCache(foundCommand, foundCommand); - await interaction.followUp({ embeds: [successEmbed(command, channelId, type)], ephemeral: true }); + await interaction.followUp({ embeds: [successEmbed(command, channelId)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channelId, type)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channelId)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } } } catch (error) { - Logger.error(`Failed to add ${type} prefix command channel permission for command ${command} and channel <#${channel}>: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(command, channelId, type)], ephemeral: true }); + Logger.error(`Failed to add prefix command channel <#${channel}> for command ${command}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command, channelId)], ephemeral: true }); } } else { await interaction.followUp({ embeds: [alreadyExistsEmbed(command, channelId)], ephemeral: true }); diff --git a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts index 329867e8..74756669 100644 --- a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts +++ b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts @@ -1,36 +1,36 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandRolePermission, refreshSinglePrefixCommandCache } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, refreshSinglePrefixCommandCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ - title: 'Prefix Commands - Add Role Permission - No Connection', - description: 'Could not connect to the database. Unable to add the prefix command role permission.', + title: 'Prefix Commands - Add Role - No Connection', + description: 'Could not connect to the database. Unable to add the prefix command role.', color: Colors.Red, }); const noCommandEmbed = (command: string) => makeEmbed({ - title: 'Prefix Commands - Add Role Permission - No Command', - description: `Failed to add the prefix command role permission for command ${command} as the command does not exist or there are more than one matching.`, + title: 'Prefix Commands - Add Role - No Command', + description: `Failed to add the prefix command role for command ${command} as the command does not exist or there are more than one matching.`, color: Colors.Red, }); -const failedEmbed = (command: string, roleName: string, type: string) => makeEmbed({ - title: 'Prefix Commands - Add Role Permission - Failed', - description: `Failed to add the ${type} prefix command role permission for command ${command} and role ${roleName}.`, +const failedEmbed = (command: string, roleName: string) => makeEmbed({ + title: 'Prefix Commands - Add Role - Failed', + description: `Failed to add the prefix command role ${roleName} for command ${command}.`, color: Colors.Red, }); const alreadyExistsEmbed = (command: string, roleName: string) => makeEmbed({ - title: 'Prefix Commands - Add Role Permission - Already exists', - description: `A prefix command role permission for command ${command} and role ${roleName} already exists. Not adding again.`, + title: 'Prefix Commands - Add Role - Already exists', + description: `A prefix command role ${roleName} for command ${command} already exists. Not adding again.`, color: Colors.Red, }); -const successEmbed = (command: string, roleName: string, type: string) => makeEmbed({ - title: `Prefix command role ${type} permission added for command ${command} and role ${roleName}.}`, +const successEmbed = (command: string, roleName: string) => makeEmbed({ + title: `Prefix command role ${roleName} added for command ${command}.}`, color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, roleName: string, type: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, roleName: string) => makeEmbed({ title: 'Add prefix command role permission', fields: [ { @@ -41,10 +41,6 @@ const modLogEmbed = (moderator: User, command: string, roleName: string, type: s name: 'Role', value: roleName, }, - { - name: 'Type', - value: type, - }, { name: 'Moderator', value: `${moderator}`, @@ -54,7 +50,7 @@ const modLogEmbed = (moderator: User, command: string, roleName: string, type: s }); const noModLogs = makeEmbed({ - title: 'Prefix Commands - Add Role Permission - No Mod Log', + title: 'Prefix Commands - Add Role - No Mod Log', description: 'I can\'t find the mod logs role. Please check the role still exists.', color: Colors.Red, }); @@ -70,7 +66,6 @@ export async function handleAddPrefixCommandRolePermission(interaction: ChatInpu const command = interaction.options.getString('command')!; const role = interaction.options.getRole('role')!; - const type = interaction.options.getString('type')!; const moderator = interaction.user; //Check if the mod logs role exists @@ -90,27 +85,26 @@ export async function handleAddPrefixCommandRolePermission(interaction: ChatInpu const [foundCommand] = foundCommands; const { id: roleId, name: roleName } = role; - const existingRolePermission = foundCommand.rolePermissions.find((rolePermission) => rolePermission.roleId === roleId); + const existingRolePermission = foundCommand.permissions.roles?.includes(roleId); if (!existingRolePermission) { - const newRolePermission = new PrefixCommandRolePermission({ - roleId, - type, - }); + if (!foundCommand.permissions.roles) { + foundCommand.permissions.roles = []; + } + foundCommand.permissions.roles.push(roleId); try { - foundCommand.rolePermissions.push(newRolePermission); await foundCommand.save(); await refreshSinglePrefixCommandCache(foundCommand, foundCommand); - await interaction.followUp({ embeds: [successEmbed(command, roleName, type)], ephemeral: true }); + await interaction.followUp({ embeds: [successEmbed(command, roleName)], ephemeral: true }); if (modLogsRole) { try { - await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName, type)] }); + await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs role: ${error}`); } } } catch (error) { - Logger.error(`Failed to add ${type} prefix command role permission for command ${command} and role ${roleName}: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(command, roleName, type)], ephemeral: true }); + Logger.error(`Failed to add prefix command role ${roleName} for command ${command}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command, roleName)], ephemeral: true }); } } else { await interaction.followUp({ embeds: [alreadyExistsEmbed(command, roleName)], ephemeral: true }); diff --git a/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts b/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts deleted file mode 100644 index 01feddc3..00000000 --- a/src/commands/moderation/prefixCommands/functions/listChannelPermissions.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { APIEmbedField, ChatInputCommandInteraction, Colors } from 'discord.js'; -import { getConn, Logger, makeEmbed, PrefixCommand } from '../../../../lib'; - -const noConnEmbed = makeEmbed({ - title: 'Prefix Commands - List Channel Permissions - No Connection', - description: 'Could not connect to the database. Unable to list the prefix command channel permissions.', - color: Colors.Red, -}); - -const failedEmbed = (command: string) => makeEmbed({ - title: 'Prefix Commands - List Channel Permissions - Failed', - description: `Failed to list the prefix command channel permissions for command ${command}.`, - color: Colors.Red, -}); - -const noCommandEmbed = (command: string) => makeEmbed({ - title: 'Prefix Commands - List Channel Permissions - No Command', - description: `Failed to list prefix command channel permissions for command ${command} as the command does not exist or there are more than one matching.`, - color: Colors.Red, -}); - -const noResultsEmbed = (command: string) => makeEmbed({ - title: 'Prefix Commands - List Channel Permissions - Does not exist', - description: `No prefix command channel permissions found for command ${command}.`, -}); - -const successEmbed = (command: string, fields: APIEmbedField[]) => makeEmbed({ - title: `Prefix Commands - Channel Permissions for command ${command}`, - fields, - color: Colors.Green, -}); - -export async function handleListPrefixCommandChannelPermissions(interaction: ChatInputCommandInteraction<'cached'>) { - await interaction.deferReply({ ephemeral: true }); - - const conn = getConn(); - if (!conn) { - await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); - return; - } - - const command = interaction.options.getString('command')!; - const foundCommands = await PrefixCommand.find({ name: command }); - if (!foundCommands || foundCommands.length === 0) { - await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); - return; - } - const [foundCommand] = foundCommands; - const foundChannelPermissions = foundCommand.channelPermissions; - - if (foundChannelPermissions) { - const embedFields: APIEmbedField[] = []; - for (let i = 0; i < foundChannelPermissions.length; i++) { - const permission = foundChannelPermissions[i]; - const { id, type, channelId } = permission; - embedFields.push({ - name: `<#${channelId}> - ${type}`, - value: `${id}`, - }); - } - try { - await interaction.followUp({ embeds: [successEmbed(command, embedFields)], ephemeral: false }); - } catch (error) { - Logger.error(`Failed to list prefix command channel permissions for command ${command}: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(command)], ephemeral: true }); - } - } else { - await interaction.followUp({ embeds: [noResultsEmbed(command)], ephemeral: true }); - } -} diff --git a/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts b/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts deleted file mode 100644 index 1aefa54b..00000000 --- a/src/commands/moderation/prefixCommands/functions/listRolePermissions.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { APIEmbedField, ChatInputCommandInteraction, Colors } from 'discord.js'; -import { getConn, Logger, makeEmbed, PrefixCommand } from '../../../../lib'; - -const noConnEmbed = makeEmbed({ - title: 'Prefix Commands - List Role Permissions - No Connection', - description: 'Could not connect to the database. Unable to list the prefix command role permissions.', - color: Colors.Red, -}); - -const failedEmbed = (command: string) => makeEmbed({ - title: 'Prefix Commands - List Role Permissions - Failed', - description: `Failed to list the prefix command role permissions for command ${command}.`, - color: Colors.Red, -}); - -const noCommandEmbed = (command: string) => makeEmbed({ - title: 'Prefix Commands - List Role Permissions - No Command', - description: `Failed to list prefix command role permissions for command ${command} as the command does not exist or there are more than one matching.`, - color: Colors.Red, -}); - -const noResultsEmbed = (command: string) => makeEmbed({ - title: 'Prefix Commands - List Role Permissions - Does not exist', - description: `No prefix command role permissions found for command ${command}.`, -}); - -const successEmbed = (command: string, fields: APIEmbedField[]) => makeEmbed({ - title: `Prefix Commands - Role Permissions for command ${command}`, - fields, - color: Colors.Green, -}); - -export async function handleListPrefixCommandRolePermissions(interaction: ChatInputCommandInteraction<'cached'>) { - await interaction.deferReply({ ephemeral: true }); - - const conn = getConn(); - if (!conn) { - await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); - return; - } - - const command = interaction.options.getString('command')!; - const foundCommands = await PrefixCommand.find({ name: command }); - if (!foundCommands || foundCommands.length === 0) { - await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); - return; - } - const [foundCommand] = foundCommands; - const foundRolePermissions = foundCommand.rolePermissions; - - if (foundRolePermissions) { - const embedFields: APIEmbedField[] = []; - for (let i = 0; i < foundRolePermissions.length; i++) { - const permission = foundRolePermissions[i]; - const { id, type, roleId } = permission; - embedFields.push({ - name: `<@&${roleId}> - ${type}`, - value: `${id}`, - }); - } - try { - await interaction.followUp({ embeds: [successEmbed(command, embedFields)], ephemeral: false }); - } catch (error) { - Logger.error(`Failed to list prefix command role permissions for command ${command}: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(command)], ephemeral: true }); - } - } else { - await interaction.followUp({ embeds: [noResultsEmbed(command)], ephemeral: true }); - } -} diff --git a/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts index c82113c5..0d2f5571 100644 --- a/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts +++ b/src/commands/moderation/prefixCommands/functions/removeChannelPermission.ts @@ -1,36 +1,36 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, refreshSinglePrefixCommandCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ - title: 'Prefix Commands - Remove Channel Permission - No Connection', - description: 'Could not connect to the database. Unable to remove the prefix command channel permission.', + title: 'Prefix Commands - Remove Channel - No Connection', + description: 'Could not connect to the database. Unable to remove the prefix command channel.', color: Colors.Red, }); const noCommandEmbed = (command: string) => makeEmbed({ - title: 'Prefix Commands - Remove Channel Permission - No Command', - description: `Failed to remove the prefix command channel permission for command ${command} as the command does not exist or there are more than one matching.`, + title: 'Prefix Commands - Remove Channel - No Command', + description: `Failed to remove the prefix command channel for command ${command} as the command does not exist or there are more than one matching.`, color: Colors.Red, }); -const failedEmbed = (command: string, channel: string, type: string) => makeEmbed({ - title: 'Prefix Commands - Remove Channel Permission - Failed', - description: `Failed to remove the ${type} prefix command channel permission for command ${command} and channel <#${channel}>.`, +const failedEmbed = (command: string, channel: string) => makeEmbed({ + title: 'Prefix Commands - Remove Channel- Failed', + description: `Failed to remove the prefix command channel <#${channel}> for command ${command}.`, color: Colors.Red, }); const doesNotExistEmbed = (command: string, channel: string) => makeEmbed({ - title: 'Prefix Commands - Remove Channel Permission - Does not exist', - description: `A prefix command channel permission for command ${command} and channel <#${channel}> does not exist.`, + title: 'Prefix Commands - Remove Channel - Does not exist', + description: `A prefix command channel <#${channel}> for command ${command} does not exist.`, color: Colors.Red, }); -const successEmbed = (command: string, channel: string, type: string) => makeEmbed({ - title: `Prefix command channel ${type} permission removed for command ${command} and channel <#${channel}>.`, +const successEmbed = (command: string, channel: string) => makeEmbed({ + title: `Prefix command channel <#${channel}> removed for command ${command}.`, color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, channel: string, type: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, channel: string) => makeEmbed({ title: 'Remove prefix command channel permission', fields: [ { @@ -41,10 +41,6 @@ const modLogEmbed = (moderator: User, command: string, channel: string, type: st name: 'Channel', value: `<#${channel}>`, }, - { - name: 'Type', - value: type, - }, { name: 'Moderator', value: `${moderator}`, @@ -54,7 +50,7 @@ const modLogEmbed = (moderator: User, command: string, channel: string, type: st }); const noModLogs = makeEmbed({ - title: 'Prefix Commands - Remove Channel Permission - No Mod Log', + title: 'Prefix Commands - Remove Channel - No Mod Log', description: 'I can\'t find the mod logs channel. Please check the channel still exists.', color: Colors.Red, }); @@ -89,23 +85,23 @@ export async function handleRemovePrefixCommandChannelPermission(interaction: Ch const [foundCommand] = foundCommands; const { id: channelId } = channel; - const existingChannelPermission = foundCommand.channelPermissions.find((channelPermission) => channelPermission.channelId === channelId); + const existingChannelPermission = foundCommand.permissions.channels?.includes(channelId); if (existingChannelPermission) { - const { type } = existingChannelPermission; + foundCommand.permissions.channels = foundCommand.permissions.channels?.filter((id) => id !== channelId); try { - foundCommand.channelPermissions.find((channelPermission) => channelPermission.channelId === channelId)?.deleteOne(); await foundCommand.save(); - await interaction.followUp({ embeds: [successEmbed(command, channelId, type)], ephemeral: true }); + await refreshSinglePrefixCommandCache(foundCommand, foundCommand); + await interaction.followUp({ embeds: [successEmbed(command, channelId)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channelId, type)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, channelId)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } } } catch (error) { - Logger.error(`Failed to remove ${type} prefix command channel permission for command ${command} and channel <#${channel}>: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(command, channelId, type)], ephemeral: true }); + Logger.error(`Failed to remove prefix command channel <#${channel}> for command ${command}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command, channelId)], ephemeral: true }); } } else { await interaction.followUp({ embeds: [doesNotExistEmbed(command, channelId)], ephemeral: true }); diff --git a/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts b/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts index 707e0642..d72f465d 100644 --- a/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts +++ b/src/commands/moderation/prefixCommands/functions/removeRolePermission.ts @@ -1,36 +1,36 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, refreshSinglePrefixCommandCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ - title: 'Prefix Commands - Remove Role Permission - No Connection', - description: 'Could not connect to the database. Unable to remove the prefix command role permission.', + title: 'Prefix Commands - Remove Role - No Connection', + description: 'Could not connect to the database. Unable to remove the prefix command role.', color: Colors.Red, }); const noCommandEmbed = (command: string) => makeEmbed({ - title: 'Prefix Commands - Remove Role Permission - No Command', - description: `Failed to remove the prefix command role permission for command ${command} as the command does not exist or there are more than one matching.`, + title: 'Prefix Commands - Remove Role - No Command', + description: `Failed to remove the prefix command role for command ${command} as the command does not exist or there are more than one matching.`, color: Colors.Red, }); -const failedEmbed = (command: string, roleName: string, type: string) => makeEmbed({ - title: 'Prefix Commands - Remove Role Permission - Failed', - description: `Failed to remove the ${type} prefix command role permission for command ${command} and role ${roleName}.`, +const failedEmbed = (command: string, roleName: string) => makeEmbed({ + title: 'Prefix Commands - Remove Role - Failed', + description: `Failed to remove the prefix command role ${roleName} for command ${command}.`, color: Colors.Red, }); const doesNotExistEmbed = (command: string, roleName: string) => makeEmbed({ - title: 'Prefix Commands - Remove Role Permission - Already exists', - description: `A prefix command role permission for command ${command} and role ${roleName} does not exist.`, + title: 'Prefix Commands - Remove Role - Already exists', + description: `A prefix command role ${roleName} for command ${command} and role does not exist.`, color: Colors.Red, }); -const successEmbed = (command: string, roleName: string, type: string) => makeEmbed({ - title: `Prefix command role ${type} permission removed for command ${command} and role ${roleName}.`, +const successEmbed = (command: string, roleName: string) => makeEmbed({ + title: `Prefix command role ${roleName} removed for command ${command}.`, color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, roleName: string, type: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, roleName: string) => makeEmbed({ title: 'Remove prefix command role permission', fields: [ { @@ -41,10 +41,6 @@ const modLogEmbed = (moderator: User, command: string, roleName: string, type: s name: 'Role', value: roleName, }, - { - name: 'Type', - value: type, - }, { name: 'Moderator', value: `${moderator}`, @@ -54,7 +50,7 @@ const modLogEmbed = (moderator: User, command: string, roleName: string, type: s }); const noModLogs = makeEmbed({ - title: 'Prefix Commands - Remove Role Permission - No Mod Log', + title: 'Prefix Commands - Remove Role - No Mod Log', description: 'I can\'t find the mod logs role. Please check the role still exists.', color: Colors.Red, }); @@ -89,23 +85,23 @@ export async function handleRemovePrefixCommandRolePermission(interaction: ChatI const [foundCommand] = foundCommands; const { id: roleId, name: roleName } = role; - const existingRolePermission = foundCommand.rolePermissions.find((rolePermission) => rolePermission.roleId === roleId); + const existingRolePermission = foundCommand.permissions.roles?.includes(roleId); if (existingRolePermission) { - const { type } = existingRolePermission; + foundCommand.permissions.roles = foundCommand.permissions.roles?.filter((id) => id !== roleId); try { - foundCommand.rolePermissions.find((rolePermission) => rolePermission.roleId === roleId)?.deleteOne(); await foundCommand.save(); - await interaction.followUp({ embeds: [successEmbed(command, roleName, type)], ephemeral: true }); + await refreshSinglePrefixCommandCache(foundCommand, foundCommand); + await interaction.followUp({ embeds: [successEmbed(command, roleName)], ephemeral: true }); if (modLogsRole) { try { - await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName, type)] }); + await modLogsRole.send({ embeds: [modLogEmbed(moderator, command, roleName)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs role: ${error}`); } } } catch (error) { - Logger.error(`Failed to remove ${type} prefix command role permission for command ${command} and role ${roleName}: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(command, roleName, type)], ephemeral: true }); + Logger.error(`Failed to remove prefix command role ${roleName} for command ${command}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command, roleName)], ephemeral: true }); } } else { await interaction.followUp({ embeds: [doesNotExistEmbed(command, roleName)], ephemeral: true }); diff --git a/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts b/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts new file mode 100644 index 00000000..032152ca --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts @@ -0,0 +1,123 @@ +import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; +import { constantsConfig, getConn, Logger, makeEmbed, PrefixCommand, PrefixCommandPermissions, refreshSinglePrefixCommandCache } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Set Permission Settings - No Connection', + description: 'Could not connect to the database. Unable to set the permission settings.', + color: Colors.Red, +}); + +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Set Permission Settings - No Command', + description: `Failed to set default channel version for command ${command} as the command does not exist.`, + color: Colors.Red, +}); + +const failedEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Set Permission Settings - Failed', + description: `Failed to set the permission settings for command ${command}.`, + color: Colors.Red, +}); + +const successEmbed = (command: string) => makeEmbed({ + title: `Prefix Command permission settings set for command ${command}.`, + color: Colors.Green, +}); + +const modLogEmbed = (moderator: User, command: string, rolesBlacklist: boolean, channelsBlacklist: boolean, quietErrors: boolean, verboseErrors: boolean) => makeEmbed({ + title: 'Prefix command version added', + fields: [ + { + name: 'Command', + value: command, + }, + { + name: 'Roles Blacklist', + value: rolesBlacklist ? 'Enabled' : 'Disabled', + }, + { + name: 'Channels Blacklist', + value: channelsBlacklist ? 'Enabled' : 'Disabled', + }, + { + name: 'Quiet Errors', + value: quietErrors ? 'Enabled' : 'Disabled', + }, + { + name: 'Verbose Errors', + value: verboseErrors ? 'Enabled' : 'Disabled', + }, + { + name: 'Moderator', + value: `${moderator}`, + }, + ], + color: Colors.Green, +}); + +const noModLogs = makeEmbed({ + title: 'Prefix Commands - Set Permission Settings - No Mod Log', + description: 'I can\'t find the mod logs channel. Please check the channel still exists.', + color: Colors.Red, +}); + +export async function handleSetPrefixCommandPermissionSettings(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const command = interaction.options.getString('command')!; + const rolesBlacklist = interaction.options.getBoolean('role-blacklist') || false; + const channelsBlacklist = interaction.options.getBoolean('channel-blacklist') || false; + const quietErrors = interaction.options.getBoolean('quiet-errors') || false; + const verboseErrors = interaction.options.getBoolean('verbose-errors') || false; + const moderator = interaction.user; + + //Check if the mod logs channel exists + const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; + if (!modLogsChannel) { + await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); + } + + let foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length > 1) { + foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); + } + if (!foundCommands || foundCommands.length > 1) { + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + + const [foundCommand] = foundCommands; + if (foundCommand) { + if (!foundCommand.permissions) { + foundCommand.permissions = new PrefixCommandPermissions(); + } + foundCommand.permissions.rolesBlacklist = rolesBlacklist; + foundCommand.permissions.channelsBlacklist = channelsBlacklist; + foundCommand.permissions.quietErrors = quietErrors; + foundCommand.permissions.verboseErrors = verboseErrors; + try { + await foundCommand.save(); + await refreshSinglePrefixCommandCache(foundCommand, foundCommand); + await interaction.followUp({ embeds: [successEmbed(command)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, rolesBlacklist, channelsBlacklist, quietErrors, verboseErrors)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); + } + } + } catch (error) { + Logger.error(`Failed to set the permission settings for command ${command}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command)], ephemeral: true }); + } + } else { + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/functions/showCommandPermissions.ts b/src/commands/moderation/prefixCommands/functions/showCommandPermissions.ts new file mode 100644 index 00000000..3bd8489e --- /dev/null +++ b/src/commands/moderation/prefixCommands/functions/showCommandPermissions.ts @@ -0,0 +1,104 @@ +import { ChatInputCommandInteraction, Colors } from 'discord.js'; +import { getConn, PrefixCommand, Logger, makeEmbed } from '../../../../lib'; + +const noConnEmbed = makeEmbed({ + title: 'Prefix Commands - Show Command Permissions - No Connection', + description: 'Could not connect to the database. Unable to show the prefix command permissions.', + color: Colors.Red, +}); + +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Show Command Permissions - No Command', + description: `Failed to show command permissions for command ${command} as the command does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const failedEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Show Command Permissions - Failed', + description: `Failed to show command permissions for command ${command}.`, + color: Colors.Red, +}); + +const permissionEmbed = (command: string, roles: string[], rolesBlacklist: boolean, channels: string[], channelsBlacklist: boolean, quietErrors: boolean, verboseErrors: boolean) => makeEmbed({ + title: `Prefix Commands - Show Command Permissions - ${command}`, + fields: [ + { + name: 'Roles', + value: roles.length > 0 ? roles.join(', ') : 'None', + }, + { + name: 'Roles Blacklist', + value: rolesBlacklist ? 'Enabled' : 'Disabled', + }, + { + name: 'Channels', + value: channels.length > 0 ? channels.join(', ') : 'None', + }, + { + name: 'Channels Blacklist', + value: channelsBlacklist ? 'Enabled' : 'Disabled', + }, + { + name: 'Quiet Errors', + value: quietErrors ? 'Enabled' : 'Disabled', + }, + { + name: 'Verbose Errors', + value: verboseErrors ? 'Enabled' : 'Disabled', + }, + ], + color: Colors.Green, +}); + +export async function handleShowPrefixCommandPermissions(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + + const conn = getConn(); + if (!conn) { + await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; + } + + const command = interaction.options.getString('command') || ''; + let foundCommands = await PrefixCommand.find({ name: command }); + if (!foundCommands || foundCommands.length > 1) { + foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); + } + if (!foundCommands || foundCommands.length > 1) { + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + + const [foundCommand] = foundCommands; + const { permissions } = foundCommand; + const { roles, rolesBlacklist, channels, channelsBlacklist, quietErrors, verboseErrors } = permissions; + const roleNames = []; + const channelNames = []; + if (roles) { + for (const role of roles) { + // eslint-disable-next-line no-await-in-loop + const discordRole = await interaction.guild.roles.fetch(role); + if (discordRole) { + const { name } = discordRole; + roleNames.push(name); + } + } + } + if (channels) { + for (const channel of channels) { + // eslint-disable-next-line no-await-in-loop + const discordChannel = await interaction.guild.channels.fetch(channel); + if (discordChannel) { + const { id: channelId } = discordChannel; + channelNames.push(`<#${channelId}>`); + } + } + } + + try { + await interaction.followUp({ embeds: [permissionEmbed(command, roleNames, rolesBlacklist || false, channelNames, channelsBlacklist || false, quietErrors || false, verboseErrors || false)], ephemeral: false }); + } catch (error) { + Logger.error(`Failed to show prefix command content for command ${command}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(command)], ephemeral: true }); + } +} diff --git a/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts b/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts index afc2f29a..03f9410a 100644 --- a/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts +++ b/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts @@ -1,11 +1,11 @@ import { ApplicationCommandOptionChoiceData, ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'; import { AutocompleteCallback, constantsConfig, getConn, PrefixCommand, slashCommand, slashCommandStructure } from '../../../lib'; -import { handleListPrefixCommandChannelPermissions } from './functions/listChannelPermissions'; -import { handleListPrefixCommandRolePermissions } from './functions/listRolePermissions'; import { handleAddPrefixCommandChannelPermission } from './functions/addChannelPermission'; import { handleAddPrefixCommandRolePermission } from './functions/addRolePermission'; import { handleRemovePrefixCommandChannelPermission } from './functions/removeChannelPermission'; import { handleRemovePrefixCommandRolePermission } from './functions/removeRolePermission'; +import { handleSetPrefixCommandPermissionSettings } from './functions/setCommandPermissionSettings'; +import { handleShowPrefixCommandPermissions } from './functions/showCommandPermissions'; const colorChoices = []; for (let i = 0; i < Object.keys(constantsConfig.colors).length; i++) { @@ -21,26 +21,65 @@ const data = slashCommandStructure({ default_member_permissions: constantsConfig.commandPermission.MANAGE_SERVER, //Overrides need to be added for admin and moderator roles dm_permission: false, options: [ + { + name: 'show', + description: 'Show the permissions of a prefix command.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + autocomplete: true, + max_length: 32, + }, + ], + }, + { + name: 'settings', + description: 'Manage prefix command permission settings.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: 'command', + description: 'Provide the name of the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + autocomplete: true, + max_length: 32, + }, + { + name: 'roles-blacklist', + description: 'Enable or disable the role blacklist.', + type: ApplicationCommandOptionType.Boolean, + required: false, + }, + { + name: 'channels-blacklist', + description: 'Enable or disable the channel blacklist.', + type: ApplicationCommandOptionType.Boolean, + required: false, + }, + { + name: 'quiet-errors', + description: 'Enable or disable quiet errors.', + type: ApplicationCommandOptionType.Boolean, + required: false, + }, + { + name: 'verbose-errors', + description: 'Enable or disable verbose errors.', + type: ApplicationCommandOptionType.Boolean, + required: false, + }, + ], + }, { name: 'channels', description: 'Manage prefix command channel permissions.', type: ApplicationCommandOptionType.SubcommandGroup, options: [ - { - name: 'list', - description: 'Get list of prefix command channel permissions.', - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: 'command', - description: 'Provide the name of the prefix command.', - type: ApplicationCommandOptionType.String, - required: true, - autocomplete: true, - max_length: 32, - }, - ], - }, { name: 'add', description: 'Add a channel permission for a prefix command.', @@ -60,16 +99,6 @@ const data = slashCommandStructure({ type: ApplicationCommandOptionType.Channel, required: true, }, - { - name: 'type', - description: 'Select the type of the permission, permitted or prohibited', - type: ApplicationCommandOptionType.String, - required: true, - choices: [ - { name: 'Permitted', value: 'PERMITTED' }, - { name: 'Prohibited', value: 'PROHIBITED' }, - ], - }, ], }, { @@ -100,21 +129,6 @@ const data = slashCommandStructure({ description: 'Manage prefix command role permissions.', type: ApplicationCommandOptionType.SubcommandGroup, options: [ - { - name: 'list', - description: 'Get list of prefix command role permissions.', - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: 'command', - description: 'Provide the name of the prefix command.', - type: ApplicationCommandOptionType.String, - required: true, - autocomplete: true, - max_length: 32, - }, - ], - }, { name: 'add', description: 'Add a role permission for a prefix command.', @@ -134,16 +148,6 @@ const data = slashCommandStructure({ type: ApplicationCommandOptionType.Role, required: true, }, - { - name: 'type', - description: 'Select the type of the permission, permitted or prohibited', - type: ApplicationCommandOptionType.String, - required: true, - choices: [ - { name: 'Permitted', value: 'PERMITTED' }, - { name: 'Prohibited', value: 'PROHIBITED' }, - ], - }, ], }, { @@ -202,12 +206,19 @@ export default slashCommand(data, async ({ interaction }) => { const subcommandGroup = interaction.options.getSubcommandGroup(); const subcommandName = interaction.options.getSubcommand(); + switch (subcommandName) { + case 'show': + await handleShowPrefixCommandPermissions(interaction); + return; + case 'settings': + await handleSetPrefixCommandPermissionSettings(interaction); + return; + default: + } + switch (subcommandGroup) { case 'channels': switch (subcommandName) { - case 'list': - await handleListPrefixCommandChannelPermissions(interaction); - break; case 'add': await handleAddPrefixCommandChannelPermission(interaction); break; @@ -220,9 +231,6 @@ export default slashCommand(data, async ({ interaction }) => { break; case 'roles': switch (subcommandName) { - case 'list': - await handleListPrefixCommandRolePermissions(interaction); - break; case 'add': await handleAddPrefixCommandRolePermission(interaction); break; diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index 03e2d862..1ba51678 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -111,7 +111,7 @@ export default event(Events.MessageCreate, async (_, message) => { const cachedCommandDetails = await inMemoryCache.get(`PF_COMMAND:${commandText.toLowerCase()}`); if (cachedCommandDetails) { const commandDetails = PrefixCommand.hydrate(cachedCommandDetails); - const { name, contents, isEmbed, embedColor, channelPermissions, rolePermissions } = commandDetails; + const { name, contents, isEmbed, embedColor, permissions } = commandDetails; // TODO: Check permissions const commandContentData = contents.find(({ versionId }) => versionId === commandVersionId); diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts index d40fd6de..b3644089 100644 --- a/src/lib/schemas/prefixCommandSchemas.ts +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -81,36 +81,22 @@ const prefixCommandContentSchema = new Schema({ image: String, }); -export interface IPrefixCommandChannelPermission extends Document { - type: string; - channelId: string; -} - -const prefixCommandChannelPermissionSchema = new Schema({ - type: { - type: String, - required: true, - }, - channelId: { - type: String, - required: true, - }, -}); - -export interface IPrefixCommandRolePermission extends Document { - type: string; - roleId: string; +export interface IPrefixCommandPermissions extends Document { + roles?: string[], + rolesBlacklist?: boolean, + channels?: string[], + channelsBlacklist?: boolean, + quietErrors?: boolean, + verboseErrors?: boolean, } -const prefixCommandRolePermissionSchema = new Schema({ - type: { - type: String, - required: true, - }, - roleId: { - type: String, - required: true, - }, +const prefixCommandPermissionsSchema = new Schema({ + roles: [String], + rolesBlacklist: Boolean, + channels: [String], + channelsBlacklist: Boolean, + quietErrors: Boolean, + verboseErrors: Boolean, }); export interface IPrefixCommand extends Document { @@ -121,8 +107,7 @@ export interface IPrefixCommand extends Document { isEmbed: boolean; embedColor?: string; contents: IPrefixCommandContent[]; - channelPermissions: IPrefixCommandChannelPermission[]; - rolePermissions: IPrefixCommandRolePermission[]; + permissions: IPrefixCommandPermissions; } const prefixCommandSchema = new Schema({ @@ -141,14 +126,12 @@ const prefixCommandSchema = new Schema({ isEmbed: Boolean, embedColor: String, contents: [prefixCommandContentSchema], - channelPermissions: [prefixCommandChannelPermissionSchema], - rolePermissions: [prefixCommandRolePermissionSchema], + permissions: prefixCommandPermissionsSchema, }); export const PrefixCommandCategory = mongoose.model('PrefixCommandCategory', prefixCommandCategorySchema); export const PrefixCommandVersion = mongoose.model('PrefixCommandVersion', prefixCommandVersionSchema); export const PrefixCommandContent = mongoose.model('PrefixCommandContent', prefixCommandContentSchema); -export const PrefixCommandChannelPermission = mongoose.model('PrefixCommandChannelPermission', prefixCommandChannelPermissionSchema); -export const PrefixCommandRolePermission = mongoose.model('PrefixCommandRolePermission', prefixCommandRolePermissionSchema); +export const PrefixCommandPermissions = mongoose.model('PrefixCommandPermissions', prefixCommandPermissionsSchema); export const PrefixCommandChannelDefaultVersion = mongoose.model('PrefixCommandChannelDefaultVersion', prefixCommandChannelDefaultVersionSchema); export const PrefixCommand = mongoose.model('PrefixCommand', prefixCommandSchema); From 7dd62077289b9ae5e11439d6459b81151fe3b45e Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 8 Sep 2024 16:21:58 -0700 Subject: [PATCH 16/51] Finalizing permission work --- config/production.json | 1 + config/staging.json | 1 + .../functions/setCommandPermissionSettings.ts | 4 +- src/events/messageCreateHandler.ts | 64 ++++++++++++++++++- src/lib/config.ts | 1 + 5 files changed, 68 insertions(+), 3 deletions(-) diff --git a/config/production.json b/config/production.json index 750329f2..d7288b0a 100644 --- a/config/production.json +++ b/config/production.json @@ -31,6 +31,7 @@ "1021464464928809022" ], "prefixCommandPrefix": ".", + "prefixCommandPermissionDelay": 10000, "roleAssignmentIds": [ { "group": "interestedIn", diff --git a/config/staging.json b/config/staging.json index 382c726d..69be73e6 100644 --- a/config/staging.json +++ b/config/staging.json @@ -32,6 +32,7 @@ "1021464464928809022" ], "prefixCommandPrefix": ".", + "prefixCommandPermissionDelay": 10000, "roleAssignmentIds": [ { "group": "interestedIn", diff --git a/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts b/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts index 032152ca..98bd9175 100644 --- a/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts +++ b/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts @@ -72,8 +72,8 @@ export async function handleSetPrefixCommandPermissionSettings(interaction: Chat } const command = interaction.options.getString('command')!; - const rolesBlacklist = interaction.options.getBoolean('role-blacklist') || false; - const channelsBlacklist = interaction.options.getBoolean('channel-blacklist') || false; + const rolesBlacklist = interaction.options.getBoolean('roles-blacklist') || false; + const channelsBlacklist = interaction.options.getBoolean('channels-blacklist') || false; const quietErrors = interaction.options.getBoolean('quiet-errors') || false; const verboseErrors = interaction.options.getBoolean('verbose-errors') || false; const moderator = interaction.user; diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index 1ba51678..de9124a8 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -57,6 +57,23 @@ async function sendReply(message: Message, commandTitle: string, commandContent: } } +async function sendPermError(message: Message, errorText: string) { + if (constantsConfig.prefixCommandPermissionDelay > 0) { + errorText += `\n\nThis message & the original command message will be deleted in ${constantsConfig.prefixCommandPermissionDelay / 1000} seconds.`; + } + const permReply = await sendReply(message, 'Permission Error', errorText, true, constantsConfig.colors.FBW_RED, ''); + if (constantsConfig.prefixCommandPermissionDelay > 0) { + setTimeout(() => { + try { + permReply.delete(); + message.delete(); + } catch (error) { + Logger.error(`Error while deleting permission error message for command: ${error}`); + } + }, constantsConfig.prefixCommandPermissionDelay); + } +} + export default event(Events.MessageCreate, async (_, message) => { const { id: messageId, author, channel, content } = message; const { id: authorId, bot } = author; @@ -112,7 +129,52 @@ export default event(Events.MessageCreate, async (_, message) => { if (cachedCommandDetails) { const commandDetails = PrefixCommand.hydrate(cachedCommandDetails); const { name, contents, isEmbed, embedColor, permissions } = commandDetails; - // TODO: Check permissions + const { roles: permRoles, rolesBlacklist, channels: permChannels, channelsBlacklist, quietErrors, verboseErrors } = permissions; + const authorMember = await guild.members.fetch(authorId); + + // Check permissions + const hasAnyRole = permRoles && permRoles.some((role) => authorMember.roles.cache.has(role)); + const isInChannel = permChannels && permChannels.includes(channelId); + const meetsRoleRequirements = !permRoles || permRoles.length === 0 + || (hasAnyRole && !rolesBlacklist) + || (!hasAnyRole && rolesBlacklist); + const meetsChannelRequirements = !permChannels || permChannels.length === 0 + || (isInChannel && !channelsBlacklist) + || (!isInChannel && channelsBlacklist); + + if (!meetsRoleRequirements) { + Logger.debug(`Prefix Command - User does not meet role requirements for command "${name}" based on user command "${commandText}"`); + if (quietErrors) return; + let errorText = ''; + if (verboseErrors && !rolesBlacklist) { + errorText = `You do not have the required role to execute this command. Required roles: ${permRoles.map((role) => guild.roles.cache.get(role)?.name).join(', ')}.`; + } else if (verboseErrors && rolesBlacklist) { + errorText = `You have a blacklisted role for this command. Blacklisted roles: ${permRoles.map((role) => guild.roles.cache.get(role)?.name).join(', ')}.`; + } else if (!verboseErrors && !rolesBlacklist) { + errorText = 'You do not have the required role to execute this command.'; + } else { + errorText = 'You have a blacklisted role for this command.'; + } + await sendPermError(message, errorText); + return; + } + + if (!meetsChannelRequirements) { + Logger.debug(`Prefix Command - Message does not meet channel requirements for command "${name}" based on user command "${commandText}"`); + if (quietErrors) return; + let errorText = ''; + if (verboseErrors && !channelsBlacklist) { + errorText = `This command is not available in this channel. Required channels: ${permChannels.map((channel) => guild.channels.cache.get(channel)?.toString()).join(', ')}.`; + } else if (verboseErrors && channelsBlacklist) { + errorText = `This command is blacklisted in this channel. Blacklisted channels: ${permChannels.map((channel) => guild.channels.cache.get(channel)?.toString()).join(', ')}.`; + } else if (!verboseErrors && !channelsBlacklist) { + errorText = 'This command is not available in this channel.'; + } else { + errorText = 'This command is blacklisted in this channel.'; + } + await sendPermError(message, errorText); + return; + } const commandContentData = contents.find(({ versionId }) => versionId === commandVersionId); if (!commandContentData) { diff --git a/src/lib/config.ts b/src/lib/config.ts index 1ccf0bd2..cf36dae1 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -50,6 +50,7 @@ interface Config { [x: string]: string[], }, prefixCommandPrefix: string, + prefixCommandPermissionDelay: number, roles: { ADMIN_TEAM: string, BOT_DEVELOPER: string, From 3caa8fa72bf53c9d83c4ff070d40f36aedabc670 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 13 Oct 2024 19:11:09 -0700 Subject: [PATCH 17/51] Adding description to command and fixing autoComplete for Prefix commands --- .../prefixCommands/functions/addCommand.ts | 10 +++++++-- .../prefixCommands/functions/deleteCommand.ts | 12 ++++++---- .../prefixCommands/functions/listCommands.ts | 4 ++-- .../prefixCommands/functions/modifyCommand.ts | 10 +++++++-- .../prefixCommands/prefixCommands.ts | 22 ++++++++++++++----- src/lib/schemas/prefixCommandSchemas.ts | 2 ++ 6 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts index 4625d6d4..c92f2df4 100644 --- a/src/commands/moderation/prefixCommands/functions/addCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -36,7 +36,7 @@ const successEmbed = (command: string) => makeEmbed({ color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, aliases: string[], isEmbed: boolean, embedColor: string, commandId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, aliases: string[], description: string, isEmbed: boolean, embedColor: string, commandId: string) => makeEmbed({ title: 'Prefix command added', fields: [ { @@ -51,6 +51,10 @@ const modLogEmbed = (moderator: User, command: string, aliases: string[], isEmbe name: 'Aliases', value: aliases.join(','), }, + { + name: 'Description', + value: description, + }, { name: 'Is Embed', value: isEmbed ? 'Yes' : 'No', @@ -82,6 +86,7 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera const name = interaction.options.getString('name')!; const category = interaction.options.getString('category')!; + const description = interaction.options.getString('description')!; const aliasesString = interaction.options.getString('aliases') || ''; const aliases = aliasesString !== '' ? aliasesString.split(',') : []; const isEmbed = interaction.options.getBoolean('is_embed') || false; @@ -121,6 +126,7 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera name, categoryId, aliases, + description, isEmbed, embedColor, contents: [], @@ -133,7 +139,7 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, aliases, isEmbed, embedColor, prefixCommand.id)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, aliases, description, isEmbed, embedColor, prefixCommand.id)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts index 4e3d7c7f..b412a9eb 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteCommand.ts @@ -24,7 +24,7 @@ const successEmbed = (command: string, commandId: string) => makeEmbed({ color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, aliases: string[], isEmbed: boolean, embedColor: string, commandId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, aliases: string[], description: string, isEmbed: boolean, embedColor: string, commandId: string) => makeEmbed({ title: 'Prefix command deleted', fields: [ { @@ -39,6 +39,10 @@ const modLogEmbed = (moderator: User, command: string, aliases: string[], isEmbe name: 'Aliases', value: aliases.join(','), }, + { + name: 'Description', + value: description, + }, { name: 'Is Embed', value: isEmbed ? 'Yes' : 'No', @@ -49,7 +53,7 @@ const modLogEmbed = (moderator: User, command: string, aliases: string[], isEmbe }, ], footer: { text: `Command ID: ${commandId}` }, - color: Colors.Green, + color: Colors.Red, }); const noModLogs = makeEmbed({ @@ -79,14 +83,14 @@ export async function handleDeletePrefixCommand(interaction: ChatInputCommandInt const existingCommand = await PrefixCommand.findOne({ name: command }); if (existingCommand) { - const { id: commandId, name, aliases, isEmbed, embedColor } = existingCommand; + const { id: commandId, name, description, aliases, isEmbed, embedColor } = existingCommand; try { await clearSinglePrefixCommandCache(existingCommand); await existingCommand.deleteOne(); await interaction.followUp({ embeds: [successEmbed(name || '', commandId)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name || '', aliases, isEmbed || false, embedColor || '', commandId)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name || '', aliases, description, isEmbed || false, embedColor || '', commandId)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/functions/listCommands.ts b/src/commands/moderation/prefixCommands/functions/listCommands.ts index 1f7160fe..53d8727c 100644 --- a/src/commands/moderation/prefixCommands/functions/listCommands.ts +++ b/src/commands/moderation/prefixCommands/functions/listCommands.ts @@ -41,10 +41,10 @@ export async function handleListPrefixCommands(interaction: ChatInputCommandInte const embedFields: APIEmbedField[] = []; for (let i = 0; i < foundCommands.length; i++) { const command = foundCommands[i]; - const { id, name, aliases, isEmbed, embedColor } = command; + const { name, description, aliases, isEmbed, embedColor } = command; embedFields.push({ name: `${name} - ${aliases.join(',')} - ${isEmbed ? 'Embed' : 'No Embed'} - ${embedColor || 'No Color'}`, - value: `${id}`, + value: `${description}`, }); } try { diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index e29be2dd..2fe71365 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -36,7 +36,7 @@ const successEmbed = (command: string, commandId: string) => makeEmbed({ color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, aliases: string[], isEmbed: boolean, embedColor: string, commandId: string) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, aliases: string[], description: string, isEmbed: boolean, embedColor: string, commandId: string) => makeEmbed({ title: 'Prefix command modified', fields: [ { @@ -51,6 +51,10 @@ const modLogEmbed = (moderator: User, command: string, aliases: string[], isEmbe name: 'Aliases', value: aliases.join(','), }, + { + name: 'Description', + value: description, + }, { name: 'Is Embed', value: isEmbed ? 'Yes' : 'No', @@ -81,6 +85,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt const command = interaction.options.getString('command')!; const name = interaction.options.getString('name') || ''; const category = interaction.options.getString('category') || ''; + const description = interaction.options.getString('description') || ''; const aliasesString = interaction.options.getString('aliases') || ''; const aliases = aliasesString !== '' ? aliasesString.split(',') : []; const isEmbed = interaction.options.getBoolean('is_embed') || null; @@ -122,6 +127,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt const oldCommand = existingCommand.$clone(); existingCommand.name = name || existingCommand.name; existingCommand.categoryId = foundCategory?.id || existingCommand.categoryId; + existingCommand.description = description || existingCommand.description; existingCommand.aliases = aliases.length > 0 ? aliases : existingCommand.aliases; existingCommand.isEmbed = isEmbed !== null ? isEmbed : existingCommand.isEmbed; existingCommand.embedColor = embedColor || existingCommand.embedColor; @@ -132,7 +138,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt await interaction.followUp({ embeds: [successEmbed(name, commandId)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, aliases, isEmbed || false, embedColor || '', existingCommand.id)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, aliases, description, isEmbed || false, embedColor || '', existingCommand.id)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/prefixCommands.ts b/src/commands/moderation/prefixCommands/prefixCommands.ts index 269777ca..f89cccd9 100644 --- a/src/commands/moderation/prefixCommands/prefixCommands.ts +++ b/src/commands/moderation/prefixCommands/prefixCommands.ts @@ -263,6 +263,13 @@ const data = slashCommandStructure({ autocomplete: true, max_length: 32, }, + { + name: 'description', + description: 'Provide a description for the prefix command.', + type: ApplicationCommandOptionType.String, + required: true, + max_length: 255, + }, { name: 'aliases', description: 'Provide a comma separated list of aliases for the prefix command.', @@ -314,6 +321,13 @@ const data = slashCommandStructure({ autocomplete: true, max_length: 32, }, + { + name: 'description', + description: 'Provide a description for the prefix command.', + type: ApplicationCommandOptionType.String, + required: false, + max_length: 255, + }, { name: 'aliases', description: 'Provide a comma separated list of aliases for the prefix command.', @@ -504,7 +518,7 @@ const data = slashCommandStructure({ const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { const autoCompleteOption = interaction.options.getFocused(true); const { name: optionName, value: searchText } = autoCompleteOption; - let choices: ApplicationCommandOptionChoiceData[] = []; + const choices: ApplicationCommandOptionChoiceData[] = []; const conn = getConn(); @@ -532,9 +546,7 @@ const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { } break; case 'version': - choices = [ - { name: 'GENERIC', value: 'GENERIC' }, - ]; + choices.push({ name: 'GENERIC', value: 'GENERIC' }); if (!conn) { return interaction.respond(choices); } @@ -546,7 +558,7 @@ const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { } break; default: - choices = []; + break; } return interaction.respond(choices); diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts index b3644089..b85054e3 100644 --- a/src/lib/schemas/prefixCommandSchemas.ts +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -103,6 +103,7 @@ export interface IPrefixCommand extends Document { commandId: mongoose.Schema.Types.ObjectId; categoryId: mongoose.Schema.Types.ObjectId; name: string; + description: string; aliases: string[]; isEmbed: boolean; embedColor?: string; @@ -122,6 +123,7 @@ const prefixCommandSchema = new Schema({ required: true, unique: true, }, + description: String, aliases: [{ type: String }], isEmbed: Boolean, embedColor: String, From 32b6c536dbd07147cb1d538d21ccf450f04bb51a Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 13 Oct 2024 19:13:42 -0700 Subject: [PATCH 18/51] Made changes to Prefix Command Handler and Cache Manager: * Cache Manager: Moved cache prefixes to variables and export them * Handler: Use new cache prefix variables * Handler: Removed unnecessary footer checks (prefix commands have no footer) * Handler: Introduced expiration edit if version not selected in time * Handler: If a specific version is asked, but doesn't exist generic is given without options * Handler: Versions are now sorted to be consistent --- src/events/messageCreateHandler.ts | 76 ++++++++++++++++++++++-------- src/lib/cache/cacheManager.ts | 43 ++++++++++------- 2 files changed, 81 insertions(+), 38 deletions(-) diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index de9124a8..d958ebb8 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -1,5 +1,5 @@ import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder, Interaction, Message } from 'discord.js'; -import { event, getInMemoryCache, Logger, Events, constantsConfig, makeEmbed, makeLines } from '../lib'; +import { event, getInMemoryCache, memoryCachePrefixCommand, memoryCachePrefixVersion, memoryCachePrefixChannelDefaultVersion, Logger, Events, constantsConfig, makeEmbed, makeLines } from '../lib'; import { PrefixCommand, PrefixCommandVersion } from '../lib/schemas/prefixCommandSchemas'; const commandEmbed = (title: string, description: string, color: string, imageUrl: string = '') => makeEmbed({ @@ -12,13 +12,8 @@ const commandEmbed = (title: string, description: string, color: string, imageUr async function replyWithEmbed(msg: Message, embed: EmbedBuilder, buttonRow?: ActionRowBuilder) : Promise> { return msg.fetchReference() .then((res) => { - let existingFooterText = ''; - const existingFooter = embed.data.footer; - if (existingFooter) { - existingFooterText = `${existingFooter.text}\n\n`; - } embed = EmbedBuilder.from(embed.data); - embed.setFooter({ text: `${existingFooterText}Executed by ${msg.author.tag} - ${msg.author.id}` }); + embed.setFooter({ text: `Executed by ${msg.author.tag} - ${msg.author.id}` }); return res.reply({ embeds: [embed], components: buttonRow ? [buttonRow] : [], @@ -57,6 +52,30 @@ async function sendReply(message: Message, commandTitle: string, commandContent: } } +async function expireChoiceReply(message: Message, commandTitle: string, commandContent: string, isEmbed: boolean, embedColor: string, commandImage: string) : Promise> { + try { + if (isEmbed) { + const commandEmbedData = commandEmbed(commandTitle, commandContent, embedColor, commandImage); + const { footer } = message.embeds[0]; + const newFooter = footer?.text ? `${footer.text} - The choice has expired.` : 'The choice has expired.'; + commandEmbedData.setFooter({ text: newFooter }); + return message.edit({ embeds: [commandEmbedData], components: [] }); + } + + return message.edit({ + content: makeLines([ + `**${commandTitle}**`, + ...(commandContent ? [commandContent] : []), + '\n`The choice has expired.`', + ]), + components: [], + }); + } catch (error) { + Logger.error(error); + return message.reply('An error occurred while updating the message.'); + } +} + async function sendPermError(message: Message, errorText: string) { if (constantsConfig.prefixCommandPermissionDelay > 0) { errorText += `\n\nThis message & the original command message will be deleted in ${constantsConfig.prefixCommandPermissionDelay / 1000} seconds.`; @@ -91,7 +110,7 @@ export default event(Events.MessageCreate, async (_, message) => { const commandVersionExplicitGeneric = (commandText.toLowerCase() === 'generic'); // Step 1: Check if the command is actually a version alias - const commandCachedVersion = await inMemoryCache.get(`PF_VERSION:${commandText.toLowerCase()}`); + const commandCachedVersion = await inMemoryCache.get(`${memoryCachePrefixVersion}:${commandText.toLowerCase()}`); let commandVersionId; let commandVersionName; let commandVersionEnabled; @@ -106,7 +125,7 @@ export default event(Events.MessageCreate, async (_, message) => { // Step 2: Check if there's a default version for the channel if commandVersionName is GENERIC if (commandVersionName === 'GENERIC' && !commandVersionExplicitGeneric) { - const channelDefaultVersionCached = await inMemoryCache.get(`PF_CHANNEL_VERSION:${channelId}`); + const channelDefaultVersionCached = await inMemoryCache.get(`${memoryCachePrefixChannelDefaultVersion}:${channelId}`); if (channelDefaultVersionCached) { const channelDefaultVersion = PrefixCommandVersion.hydrate(channelDefaultVersionCached); ({ id: commandVersionId, name: commandVersionName, enabled: commandVersionEnabled } = channelDefaultVersion); @@ -125,7 +144,7 @@ export default event(Events.MessageCreate, async (_, message) => { } // Step 3: Check if the command exists itself and process it - const cachedCommandDetails = await inMemoryCache.get(`PF_COMMAND:${commandText.toLowerCase()}`); + const cachedCommandDetails = await inMemoryCache.get(`${memoryCachePrefixCommand}:${commandText.toLowerCase()}`); if (cachedCommandDetails) { const commandDetails = PrefixCommand.hydrate(cachedCommandDetails); const { name, contents, isEmbed, embedColor, permissions } = commandDetails; @@ -176,35 +195,44 @@ export default event(Events.MessageCreate, async (_, message) => { return; } - const commandContentData = contents.find(({ versionId }) => versionId === commandVersionId); + let commandContentData = contents.find(({ versionId }) => versionId === commandVersionId); + // If the version is not found, try to find the generic version + if (!commandContentData) { + commandContentData = contents.find(({ versionId }) => versionId === 'GENERIC'); + } + // If the generic version is not found, drop execution if (!commandContentData) { Logger.debug(`Prefix Command - Version "${commandVersionName}" not found for command "${name}" based on user command "${commandText}"`); return; } const { title: commandTitle, content: commandContent, image: commandImage } = commandContentData; - // If generic and multiple versions, show the selection + // If generic requested and multiple versions, show the selection + // Note that this only applies if GENERIC is the version explicitly requested + // Otherwise, the options are not shown if (commandVersionName === 'GENERIC' && contents.length > 1) { Logger.debug(`Prefix Command - Multiple versions found for command "${name}" based on user command "${commandText}", showing version selection`); - const versionSelectionButtons: ButtonBuilder[] = []; + const versionSelectionButtonData: { [key: string]: ButtonBuilder } = {}; for (const { versionId: versionIdForButton } of contents) { // eslint-disable-next-line no-await-in-loop - const versionCached = await inMemoryCache.get(`PF_VERSION:${versionIdForButton}`); + const versionCached = await inMemoryCache.get(`${memoryCachePrefixVersion}:${versionIdForButton}`); if (versionCached) { const version = PrefixCommandVersion.hydrate(versionCached); const { emoji } = version; - versionSelectionButtons.push( - new ButtonBuilder() - .setCustomId(`${versionIdForButton}`) - .setEmoji(emoji) - .setStyle(ButtonStyle.Primary), - ); + versionSelectionButtonData[emoji] = new ButtonBuilder() + .setCustomId(`${versionIdForButton}`) + .setEmoji(emoji) + .setStyle(ButtonStyle.Primary); } } + const versionSelectionButtons: ButtonBuilder[] = Object.keys(versionSelectionButtonData) + .sort() + .map((key: string) => versionSelectionButtonData[key]); const versionSelectButtonRow = new ActionRowBuilder().addComponents(versionSelectionButtons); const buttonMessage = await sendReply(message, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || '', versionSelectButtonRow); const filter = (interaction: Interaction) => interaction.user.id === authorId; const collector = buttonMessage.createMessageComponentCollector({ filter, time: 60_000 }); + let buttonClicked = false; collector.on('collect', async (collectedInteraction: ButtonInteraction) => { Logger.debug(`Prefix Command - User selected version "${collectedInteraction.customId}" for command "${name}" based on user command "${commandText}"`); await collectedInteraction.deferUpdate(); @@ -216,8 +244,16 @@ export default event(Events.MessageCreate, async (_, message) => { return; } const { title: commandTitle, content: commandContent, image: commandImage } = commandContentData; + buttonClicked = true; await sendReply(message, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || ''); }); + + collector.on('end', async () => { + if (!buttonClicked) { + Logger.debug(`Prefix Command - User did not select a version for command "${name}" based on user command "${commandText}"`); + await expireChoiceReply(buttonMessage, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || ''); + } + }); } else { Logger.debug(`Prefix Command - Executing version "${commandVersionName}" for command "${name}" based on user command "${commandText}"`); await sendReply(message, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || ''); diff --git a/src/lib/cache/cacheManager.ts b/src/lib/cache/cacheManager.ts index db423331..a3660c88 100644 --- a/src/lib/cache/cacheManager.ts +++ b/src/lib/cache/cacheManager.ts @@ -2,12 +2,19 @@ import { Cache, caching } from 'cache-manager'; import { getConn, IPrefixCommand, IPrefixCommandCategory, IPrefixCommandChannelDefaultVersion, IPrefixCommandVersion, Logger, PrefixCommand, PrefixCommandCategory, PrefixCommandChannelDefaultVersion, PrefixCommandVersion } from '../index'; let inMemoryCache: Cache; -const commandCachePrefix = 'PF_COMMAND'; -const commandVersionCachePrefix = 'PF_VERSION'; const cacheSize = 10000; const cacheRefreshInterval = process.env.CACHE_REFRESH_INTERVAL ? Number(process.env.CACHE_REFRESH_INTERVAL) : 1800; const cacheTTL = cacheRefreshInterval * 2 * 1000; +/** + * Cache Prefixes + */ + +export const memoryCachePrefixCommand = 'PF_COMMAND'; +export const memoryCachePrefixVersion = 'PF_VERSION'; +export const memoryCachePrefixCategory = 'PF_CATEGORY'; +export const memoryCachePrefixChannelDefaultVersion = 'PF_CHANNEL_VERSION'; + /** * Cache Management Functions */ @@ -47,9 +54,9 @@ export async function clearSinglePrefixCommandCache(command: IPrefixCommand) { Logger.debug(`Clearing cache for command or alias "${name}"`); for (const alias of aliases) { // eslint-disable-next-line no-await-in-loop - await inMemoryCache.del(`${commandCachePrefix}:${alias.toLowerCase()}`); + await inMemoryCache.del(`${memoryCachePrefixCommand}:${alias.toLowerCase()}`); } - await inMemoryCache.del(`${commandCachePrefix}:${name.toLowerCase()}`); + await inMemoryCache.del(`${memoryCachePrefixCommand}:${name.toLowerCase()}`); } export async function clearAllPrefixCommandsCache() { @@ -58,7 +65,7 @@ export async function clearAllPrefixCommandsCache() { const keys = await inMemoryCache.store.keys(); for (const key of keys) { - if (key.startsWith(`${commandCachePrefix}:`)) { + if (key.startsWith(`${memoryCachePrefixCommand}:`)) { Logger.debug(`Clearing cache for command or alias "${key}"`); // eslint-disable-next-line no-await-in-loop await inMemoryCache.del(key); @@ -72,10 +79,10 @@ export async function loadSinglePrefixCommandToCache(command: IPrefixCommand) { const { name, aliases } = command; Logger.debug(`Loading command ${name} to cache`); - await inMemoryCache.set(`${commandCachePrefix}:${name.toLowerCase()}`, command.toObject()); + await inMemoryCache.set(`${memoryCachePrefixCommand}:${name.toLowerCase()}`, command.toObject()); for (const alias of aliases) { // eslint-disable-next-line no-await-in-loop - await inMemoryCache.set(`${commandCachePrefix}:${alias.toLowerCase()}`, command.toObject()); + await inMemoryCache.set(`${memoryCachePrefixCommand}:${alias.toLowerCase()}`, command.toObject()); } } @@ -111,8 +118,8 @@ export async function clearSinglePrefixCommandVersionCache(version: IPrefixComma const { alias, _id: versionId } = version; Logger.debug(`Clearing cache for command version alias "${alias}"`); - await inMemoryCache.del(`${commandVersionCachePrefix}:${alias.toLowerCase()}`); - await inMemoryCache.del(`${commandVersionCachePrefix}:${versionId}`); + await inMemoryCache.del(`${memoryCachePrefixVersion}:${alias.toLowerCase()}`); + await inMemoryCache.del(`${memoryCachePrefixVersion}:${versionId}`); } export async function clearAllPrefixCommandVersionsCache() { @@ -121,7 +128,7 @@ export async function clearAllPrefixCommandVersionsCache() { const keys = await inMemoryCache.store.keys(); for (const key of keys) { - if (key.startsWith(`${commandVersionCachePrefix}:`)) { + if (key.startsWith(`${memoryCachePrefixVersion}:`)) { Logger.debug(`Clearing cache for command version alias/id "${key}"`); // eslint-disable-next-line no-await-in-loop await inMemoryCache.del(key); @@ -135,8 +142,8 @@ export async function loadSinglePrefixCommandVersionToCache(version: IPrefixComm const { alias, _id: versionId } = version; Logger.debug(`Loading version with alias ${alias} to cache`); - await inMemoryCache.set(`${commandVersionCachePrefix}:${alias.toLowerCase()}`, version.toObject()); - await inMemoryCache.set(`${commandVersionCachePrefix}:${versionId}`, version.toObject()); + await inMemoryCache.set(`${memoryCachePrefixVersion}:${alias.toLowerCase()}`, version.toObject()); + await inMemoryCache.set(`${memoryCachePrefixVersion}:${versionId}`, version.toObject()); } export async function loadAllPrefixCommandVersionsToCache() { @@ -171,7 +178,7 @@ export async function clearSinglePrefixCommandCategoryCache(category: IPrefixCom const { name } = category; Logger.debug(`Clearing cache for command category "${name}"`); - await inMemoryCache.del(`PF_CATEGORY:${name.toLowerCase()}`); + await inMemoryCache.del(`${memoryCachePrefixCategory}:${name.toLowerCase()}`); } export async function clearAllPrefixCommandCategoriesCache() { @@ -180,7 +187,7 @@ export async function clearAllPrefixCommandCategoriesCache() { const keys = await inMemoryCache.store.keys(); for (const key of keys) { - if (key.startsWith('PF_CATEGORY:')) { + if (key.startsWith(`${memoryCachePrefixCategory}:`)) { Logger.debug(`Clearing cache for command category "${key}"`); // eslint-disable-next-line no-await-in-loop await inMemoryCache.del(key); @@ -194,7 +201,7 @@ export async function loadSinglePrefixCommandCategoryToCache(category: IPrefixCo const { name } = category; Logger.debug(`Loading category ${name} to cache`); - await inMemoryCache.set(`PF_CATEGORY:${name.toLowerCase()}`, category.toObject()); + await inMemoryCache.set(`${memoryCachePrefixCategory}:${name.toLowerCase()}`, category.toObject()); } export async function loadAllPrefixCommandCategoriesToCache() { @@ -229,7 +236,7 @@ export async function clearSinglePrefixCommandChannelDefaultVersionCache(channel const { channelId } = channelDefaultVersion; Logger.debug(`Clearing cache for channel default version for channel "${channelId}"`); - await inMemoryCache.del(`PF_CHANNEL_VERSION:${channelId}`); + await inMemoryCache.del(`${memoryCachePrefixChannelDefaultVersion}:${channelId}`); } export async function clearAllPrefixCommandChannelDefaultVersionsCache() { @@ -238,7 +245,7 @@ export async function clearAllPrefixCommandChannelDefaultVersionsCache() { const keys = await inMemoryCache.store.keys(); for (const key of keys) { - if (key.startsWith('PF_CHANNEL_VERSION:')) { + if (key.startsWith(`${memoryCachePrefixChannelDefaultVersion}:`)) { Logger.debug(`Clearing cache for channel default version for channel "${key}"`); // eslint-disable-next-line no-await-in-loop await inMemoryCache.del(key); @@ -254,7 +261,7 @@ export async function loadSinglePrefixCommandChannelDefaultVersionToCache(channe const version = await PrefixCommandVersion.findById(versionId); if (version) { Logger.debug(`Loading default version for channel ${channelId} to cache`); - await inMemoryCache.set(`PF_CHANNEL_VERSION:${channelId}`, version.toObject()); + await inMemoryCache.set(`${memoryCachePrefixChannelDefaultVersion}:${channelId}`, version.toObject()); } } From 8224fab18abb56fbf7e9109b5e4e57a9b189ba16 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 13 Oct 2024 19:17:53 -0700 Subject: [PATCH 19/51] Introducing prefix help to show list of prefix commands per category --- src/commands/index.ts | 2 + src/commands/utils/prefixHelp.ts | 154 +++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/commands/utils/prefixHelp.ts diff --git a/src/commands/index.ts b/src/commands/index.ts index 7e0b060d..244171fe 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -34,6 +34,7 @@ import locate from './utils/locate/locate'; import prefixCommands from './moderation/prefixCommands/prefixCommands'; import prefixCommandPermissions from './moderation/prefixCommands/prefixCommandPermissions'; import prefixCommandCacheUpdate from './moderation/prefixCommands/prefixCommandCacheUpdate'; +import prefixHelp from './utils/prefixHelp'; const commandArray: SlashCommand[] = [ ping, @@ -71,6 +72,7 @@ const commandArray: SlashCommand[] = [ prefixCommands, prefixCommandPermissions, prefixCommandCacheUpdate, + prefixHelp, ]; export default commandArray; diff --git a/src/commands/utils/prefixHelp.ts b/src/commands/utils/prefixHelp.ts new file mode 100644 index 00000000..89ae548f --- /dev/null +++ b/src/commands/utils/prefixHelp.ts @@ -0,0 +1,154 @@ +import { ApplicationCommandOptionChoiceData, ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'; +import { makeEmbed, createPaginatedEmbedHandler, slashCommand, slashCommandStructure, getInMemoryCache, memoryCachePrefixCommand, AutocompleteCallback, memoryCachePrefixCategory, makeLines, memoryCachePrefixVersion, Logger } from '../../lib'; +import { PrefixCommand, PrefixCommandVersion, PrefixCommandCategory, IPrefixCommand } from '../../lib/schemas/prefixCommandSchemas'; + +const data = slashCommandStructure({ + name: 'prefix-help', + description: 'Display a list of all the prefix commands matching an optional search.', + type: ApplicationCommandType.ChatInput, + options: [{ + name: 'category', + description: 'The category to show the prefix commands for.', + type: ApplicationCommandOptionType.String, + max_length: 32, + autocomplete: true, + required: true, + }, + { + name: 'search', + description: 'The search term to filter the prefix commands by.', + type: ApplicationCommandOptionType.String, + max_length: 32, + autocomplete: true, + required: false, + }], +}); + +const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { + const autoCompleteOption = interaction.options.getFocused(true); + const { name: optionName, value: searchText } = autoCompleteOption; + const choices: ApplicationCommandOptionChoiceData[] = []; + + const inMemoryCache = getInMemoryCache(); + + switch (optionName) { + case 'category': + if (inMemoryCache) { + const foundCategories = await inMemoryCache.store.keys(); + for (const key of foundCategories) { + if (key.startsWith(memoryCachePrefixCategory) && key.includes(searchText.toLowerCase())) { + // eslint-disable-next-line no-await-in-loop + const categoryCached = await inMemoryCache.get(key); + if (categoryCached) { + const category = PrefixCommandCategory.hydrate(categoryCached); + const { name } = category; + choices.push({ name, value: name }); + } + } + } + } + break; + case 'search': + if (inMemoryCache) { + const foundCommands = await inMemoryCache.store.keys(); + for (const key of foundCommands) { + if (key.startsWith(memoryCachePrefixCommand) && key.includes(searchText.toLowerCase())) { + // Explicitly does not use the cache to hydrate the command to also capture aliases, resulting in commands + const commandName = key.split(':')[1]; + choices.push({ name: commandName, value: commandName }); + } + } + } + break; + default: + break; + } + + return interaction.respond(choices); +}; + +export default slashCommand(data, async ({ interaction }) => { + await interaction.deferReply({ ephemeral: true }); + + const categoryName = interaction.options.getString('category')!; + const search = interaction.options.getString('search') || ''; + + const inMemoryCache = getInMemoryCache(); + if (!inMemoryCache) { + return interaction.reply({ + content: 'An error occurred while fetching commands.', + ephemeral: true, + }); + } + + const categoryCached = await inMemoryCache.get(`${memoryCachePrefixCategory}:${categoryName.toLowerCase()}`); + if (!categoryCached) { + return interaction.reply({ + content: 'Invalid category, please select an existing category.', + ephemeral: true, + }); + } + const category = PrefixCommandCategory.hydrate(categoryCached); + + const commands: { [key: string]: IPrefixCommand } = {}; + const keys = await inMemoryCache.store.keys(); + for (const key of keys) { + if (key.startsWith(memoryCachePrefixCommand) && key.includes(search.toLowerCase())) { + // eslint-disable-next-line no-await-in-loop + const commandCached = await inMemoryCache.get(key); + if (commandCached) { + const command = PrefixCommand.hydrate(commandCached); + const { name, categoryId: commandCategoryId } = command; + const { _id: categoryId } = category; + if (commandCategoryId.toString() === categoryId.toString() && !(name in commands)) { + commands[name] = command; + } + } + } + } + + const sortedCommands = Object.values(commands).sort((a, b) => a.name.localeCompare(b.name)); + + const pageLimit = 10; + const embeds = []; + for (let page = 0; page * pageLimit < sortedCommands.length; page++) { + const startIndex = page * pageLimit; + const endIndex = startIndex + pageLimit; + const currentCommands = sortedCommands.slice(startIndex, endIndex); + const totalPages = Math.ceil(sortedCommands.length / pageLimit); + + const descriptionLines: string[] = []; + for (const command of currentCommands) { + const { name, aliases, description, contents } = command; + const versionEmojis = []; + for (const content of contents) { + const { versionId } = content; + if (versionId !== 'GENERIC') { + Logger.debug(`Fetching version ${versionId} for command ${name}`); + // eslint-disable-next-line no-await-in-loop + const versionCached = await inMemoryCache.get(`${memoryCachePrefixVersion}:${versionId}`); + if (versionCached) { + const version = PrefixCommandVersion.hydrate(versionCached); + const { emoji } = version; + Logger.debug(`Found version ${versionId} for command ${name} with emoji ${emoji}`); + versionEmojis.push(emoji); + } + } + } + const sortedVersionEmojis = versionEmojis.sort((a, b) => a.localeCompare(b)); + descriptionLines.push(`**${name}** ${sortedVersionEmojis.join(', ')}`); + descriptionLines.push(description); + if (aliases.length > 0) descriptionLines.push(`Aliases: ${aliases.join(', ')}`); + descriptionLines.push(''); + } + + const embed = makeEmbed({ + title: `Prefix Commands - ${category.name} (${page + 1}/${totalPages})`, + description: makeLines(descriptionLines), + }); + + embeds.push(embed); + } + + return createPaginatedEmbedHandler(interaction, embeds); +}, autocompleteCallback); From 68c8de9ec8a849c56d7e6cab9868cd456181db61 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 13 Oct 2024 20:24:51 -0700 Subject: [PATCH 20/51] Use appropriate language for blocklist --- .../functions/setCommandPermissionSettings.ts | 20 ++++++------- .../functions/showCommandPermissions.ts | 14 ++++----- .../prefixCommandPermissions.ts | 8 ++--- src/events/messageCreateHandler.ts | 30 +++++++++---------- src/lib/schemas/prefixCommandSchemas.ts | 8 ++--- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts b/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts index 98bd9175..99ec7a39 100644 --- a/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts +++ b/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts @@ -24,7 +24,7 @@ const successEmbed = (command: string) => makeEmbed({ color: Colors.Green, }); -const modLogEmbed = (moderator: User, command: string, rolesBlacklist: boolean, channelsBlacklist: boolean, quietErrors: boolean, verboseErrors: boolean) => makeEmbed({ +const modLogEmbed = (moderator: User, command: string, rolesBlocklist: boolean, channelsBlocklist: boolean, quietErrors: boolean, verboseErrors: boolean) => makeEmbed({ title: 'Prefix command version added', fields: [ { @@ -32,12 +32,12 @@ const modLogEmbed = (moderator: User, command: string, rolesBlacklist: boolean, value: command, }, { - name: 'Roles Blacklist', - value: rolesBlacklist ? 'Enabled' : 'Disabled', + name: 'Roles Blocklist', + value: rolesBlocklist ? 'Enabled' : 'Disabled', }, { - name: 'Channels Blacklist', - value: channelsBlacklist ? 'Enabled' : 'Disabled', + name: 'Channels Blocklist', + value: channelsBlocklist ? 'Enabled' : 'Disabled', }, { name: 'Quiet Errors', @@ -72,8 +72,8 @@ export async function handleSetPrefixCommandPermissionSettings(interaction: Chat } const command = interaction.options.getString('command')!; - const rolesBlacklist = interaction.options.getBoolean('roles-blacklist') || false; - const channelsBlacklist = interaction.options.getBoolean('channels-blacklist') || false; + const rolesBlocklist = interaction.options.getBoolean('roles-blocklist') || false; + const channelsBlocklist = interaction.options.getBoolean('channels-blocklist') || false; const quietErrors = interaction.options.getBoolean('quiet-errors') || false; const verboseErrors = interaction.options.getBoolean('verbose-errors') || false; const moderator = interaction.user; @@ -98,8 +98,8 @@ export async function handleSetPrefixCommandPermissionSettings(interaction: Chat if (!foundCommand.permissions) { foundCommand.permissions = new PrefixCommandPermissions(); } - foundCommand.permissions.rolesBlacklist = rolesBlacklist; - foundCommand.permissions.channelsBlacklist = channelsBlacklist; + foundCommand.permissions.rolesBlocklist = rolesBlocklist; + foundCommand.permissions.channelsBlocklist = channelsBlocklist; foundCommand.permissions.quietErrors = quietErrors; foundCommand.permissions.verboseErrors = verboseErrors; try { @@ -108,7 +108,7 @@ export async function handleSetPrefixCommandPermissionSettings(interaction: Chat await interaction.followUp({ embeds: [successEmbed(command)], ephemeral: true }); if (modLogsChannel) { try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, rolesBlacklist, channelsBlacklist, quietErrors, verboseErrors)] }); + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, command, rolesBlocklist, channelsBlocklist, quietErrors, verboseErrors)] }); } catch (error) { Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } diff --git a/src/commands/moderation/prefixCommands/functions/showCommandPermissions.ts b/src/commands/moderation/prefixCommands/functions/showCommandPermissions.ts index 3bd8489e..b64bb990 100644 --- a/src/commands/moderation/prefixCommands/functions/showCommandPermissions.ts +++ b/src/commands/moderation/prefixCommands/functions/showCommandPermissions.ts @@ -19,7 +19,7 @@ const failedEmbed = (command: string) => makeEmbed({ color: Colors.Red, }); -const permissionEmbed = (command: string, roles: string[], rolesBlacklist: boolean, channels: string[], channelsBlacklist: boolean, quietErrors: boolean, verboseErrors: boolean) => makeEmbed({ +const permissionEmbed = (command: string, roles: string[], rolesBlocklist: boolean, channels: string[], channelsBlocklist: boolean, quietErrors: boolean, verboseErrors: boolean) => makeEmbed({ title: `Prefix Commands - Show Command Permissions - ${command}`, fields: [ { @@ -27,16 +27,16 @@ const permissionEmbed = (command: string, roles: string[], rolesBlacklist: boole value: roles.length > 0 ? roles.join(', ') : 'None', }, { - name: 'Roles Blacklist', - value: rolesBlacklist ? 'Enabled' : 'Disabled', + name: 'Roles Blocklist', + value: rolesBlocklist ? 'Enabled' : 'Disabled', }, { name: 'Channels', value: channels.length > 0 ? channels.join(', ') : 'None', }, { - name: 'Channels Blacklist', - value: channelsBlacklist ? 'Enabled' : 'Disabled', + name: 'Channels Blocklist', + value: channelsBlocklist ? 'Enabled' : 'Disabled', }, { name: 'Quiet Errors', @@ -71,7 +71,7 @@ export async function handleShowPrefixCommandPermissions(interaction: ChatInputC const [foundCommand] = foundCommands; const { permissions } = foundCommand; - const { roles, rolesBlacklist, channels, channelsBlacklist, quietErrors, verboseErrors } = permissions; + const { roles, rolesBlocklist, channels, channelsBlocklist, quietErrors, verboseErrors } = permissions; const roleNames = []; const channelNames = []; if (roles) { @@ -96,7 +96,7 @@ export async function handleShowPrefixCommandPermissions(interaction: ChatInputC } try { - await interaction.followUp({ embeds: [permissionEmbed(command, roleNames, rolesBlacklist || false, channelNames, channelsBlacklist || false, quietErrors || false, verboseErrors || false)], ephemeral: false }); + await interaction.followUp({ embeds: [permissionEmbed(command, roleNames, rolesBlocklist || false, channelNames, channelsBlocklist || false, quietErrors || false, verboseErrors || false)], ephemeral: false }); } catch (error) { Logger.error(`Failed to show prefix command content for command ${command}: ${error}`); await interaction.followUp({ embeds: [failedEmbed(command)], ephemeral: true }); diff --git a/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts b/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts index 03f9410a..9a7badd1 100644 --- a/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts +++ b/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts @@ -50,14 +50,14 @@ const data = slashCommandStructure({ max_length: 32, }, { - name: 'roles-blacklist', - description: 'Enable or disable the role blacklist.', + name: 'roles-blocklist', + description: 'Enable or disable the role blocklist.', type: ApplicationCommandOptionType.Boolean, required: false, }, { - name: 'channels-blacklist', - description: 'Enable or disable the channel blacklist.', + name: 'channels-blocklist', + description: 'Enable or disable the channel blocklist.', type: ApplicationCommandOptionType.Boolean, required: false, }, diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index d958ebb8..9bdc32d2 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -148,31 +148,31 @@ export default event(Events.MessageCreate, async (_, message) => { if (cachedCommandDetails) { const commandDetails = PrefixCommand.hydrate(cachedCommandDetails); const { name, contents, isEmbed, embedColor, permissions } = commandDetails; - const { roles: permRoles, rolesBlacklist, channels: permChannels, channelsBlacklist, quietErrors, verboseErrors } = permissions; + const { roles: permRoles, rolesBlocklist, channels: permChannels, channelsBlocklist, quietErrors, verboseErrors } = permissions; const authorMember = await guild.members.fetch(authorId); // Check permissions const hasAnyRole = permRoles && permRoles.some((role) => authorMember.roles.cache.has(role)); const isInChannel = permChannels && permChannels.includes(channelId); const meetsRoleRequirements = !permRoles || permRoles.length === 0 - || (hasAnyRole && !rolesBlacklist) - || (!hasAnyRole && rolesBlacklist); + || (hasAnyRole && !rolesBlocklist) + || (!hasAnyRole && rolesBlocklist); const meetsChannelRequirements = !permChannels || permChannels.length === 0 - || (isInChannel && !channelsBlacklist) - || (!isInChannel && channelsBlacklist); + || (isInChannel && !channelsBlocklist) + || (!isInChannel && channelsBlocklist); if (!meetsRoleRequirements) { Logger.debug(`Prefix Command - User does not meet role requirements for command "${name}" based on user command "${commandText}"`); if (quietErrors) return; let errorText = ''; - if (verboseErrors && !rolesBlacklist) { + if (verboseErrors && !rolesBlocklist) { errorText = `You do not have the required role to execute this command. Required roles: ${permRoles.map((role) => guild.roles.cache.get(role)?.name).join(', ')}.`; - } else if (verboseErrors && rolesBlacklist) { - errorText = `You have a blacklisted role for this command. Blacklisted roles: ${permRoles.map((role) => guild.roles.cache.get(role)?.name).join(', ')}.`; - } else if (!verboseErrors && !rolesBlacklist) { + } else if (verboseErrors && rolesBlocklist) { + errorText = `You have a blocklisted role for this command. Blocklisted roles: ${permRoles.map((role) => guild.roles.cache.get(role)?.name).join(', ')}.`; + } else if (!verboseErrors && !rolesBlocklist) { errorText = 'You do not have the required role to execute this command.'; } else { - errorText = 'You have a blacklisted role for this command.'; + errorText = 'You have a blocklisted role for this command.'; } await sendPermError(message, errorText); return; @@ -182,14 +182,14 @@ export default event(Events.MessageCreate, async (_, message) => { Logger.debug(`Prefix Command - Message does not meet channel requirements for command "${name}" based on user command "${commandText}"`); if (quietErrors) return; let errorText = ''; - if (verboseErrors && !channelsBlacklist) { + if (verboseErrors && !channelsBlocklist) { errorText = `This command is not available in this channel. Required channels: ${permChannels.map((channel) => guild.channels.cache.get(channel)?.toString()).join(', ')}.`; - } else if (verboseErrors && channelsBlacklist) { - errorText = `This command is blacklisted in this channel. Blacklisted channels: ${permChannels.map((channel) => guild.channels.cache.get(channel)?.toString()).join(', ')}.`; - } else if (!verboseErrors && !channelsBlacklist) { + } else if (verboseErrors && channelsBlocklist) { + errorText = `This command is blocklisted in this channel. Blocklisted channels: ${permChannels.map((channel) => guild.channels.cache.get(channel)?.toString()).join(', ')}.`; + } else if (!verboseErrors && !channelsBlocklist) { errorText = 'This command is not available in this channel.'; } else { - errorText = 'This command is blacklisted in this channel.'; + errorText = 'This command is blocklisted in this channel.'; } await sendPermError(message, errorText); return; diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts index b85054e3..8828ae03 100644 --- a/src/lib/schemas/prefixCommandSchemas.ts +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -83,18 +83,18 @@ const prefixCommandContentSchema = new Schema({ export interface IPrefixCommandPermissions extends Document { roles?: string[], - rolesBlacklist?: boolean, + rolesBlocklist?: boolean, channels?: string[], - channelsBlacklist?: boolean, + channelsBlocklist?: boolean, quietErrors?: boolean, verboseErrors?: boolean, } const prefixCommandPermissionsSchema = new Schema({ roles: [String], - rolesBlacklist: Boolean, + rolesBlocklist: Boolean, channels: [String], - channelsBlacklist: Boolean, + channelsBlocklist: Boolean, quietErrors: Boolean, verboseErrors: Boolean, }); From de2356e0e2cccc1fc879a6ea8619c6fd99827056 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 13 Oct 2024 22:32:48 -0700 Subject: [PATCH 21/51] Making the max length more standardized --- .../moderation/prefixCommands/functions/setContent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index 3d366a14..6047d14a 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -126,9 +126,9 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman .setLabel('Content') .setPlaceholder('Provide the content for the command.') .setStyle(TextInputStyle.Paragraph) - .setMaxLength(2047) + .setMaxLength(2048) .setMinLength(0) - .setRequired(true) + .setRequired(false) .setValue(foundContent && foundContent.content ? foundContent.content : ''); const commandContentImageUrl = new TextInputBuilder() From 9fbbf3370a24f772ac150168e23bd0a23a20599e Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 13 Oct 2024 22:33:12 -0700 Subject: [PATCH 22/51] Show the emoji of a category --- src/commands/utils/prefixHelp.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/utils/prefixHelp.ts b/src/commands/utils/prefixHelp.ts index 89ae548f..613df0db 100644 --- a/src/commands/utils/prefixHelp.ts +++ b/src/commands/utils/prefixHelp.ts @@ -142,8 +142,9 @@ export default slashCommand(data, async ({ interaction }) => { descriptionLines.push(''); } + const { name: categoryName, emoji: categoryEmoji } = category; const embed = makeEmbed({ - title: `Prefix Commands - ${category.name} (${page + 1}/${totalPages})`, + title: `${categoryEmoji || ''}${categoryName} Commands (${page + 1}/${totalPages})`, description: makeLines(descriptionLines), }); From 9238f4266a60d66fc83bd418ca0ea9aa424f9933 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 13 Oct 2024 22:33:51 -0700 Subject: [PATCH 23/51] Show the buttons for generic in case the version specific content does not exist --- src/events/messageCreateHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index 9bdc32d2..f1369d99 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -199,6 +199,7 @@ export default event(Events.MessageCreate, async (_, message) => { // If the version is not found, try to find the generic version if (!commandContentData) { commandContentData = contents.find(({ versionId }) => versionId === 'GENERIC'); + commandVersionName = 'GENERIC'; } // If the generic version is not found, drop execution if (!commandContentData) { From 152d834bbf361683892b2397a7e2656271d0ed81 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 13 Oct 2024 23:44:08 -0700 Subject: [PATCH 24/51] Fixing bug when there is no CDV set for a channel --- .../prefixCommands/functions/showChannelDefaultVersion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/moderation/prefixCommands/functions/showChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/showChannelDefaultVersion.ts index 4f8ca271..03846f87 100644 --- a/src/commands/moderation/prefixCommands/functions/showChannelDefaultVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/showChannelDefaultVersion.ts @@ -53,7 +53,7 @@ export async function handleShowPrefixCommandChannelDefaultVersion(interaction: const channel = interaction.options.getChannel('channel')!; const { id: channelId, name: channelName } = channel; const foundChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find({ channelId }); - if (!foundChannelDefaultVersions || foundChannelDefaultVersions.length > 1) { + if (!foundChannelDefaultVersions || foundChannelDefaultVersions.length === 0 || foundChannelDefaultVersions.length > 1) { await interaction.followUp({ embeds: [noChannelDefaultVersionEmbed(channelName)], ephemeral: true }); return; } From fb5db693f504fb1b7bb0ba046ccd11b035bee130 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Mon, 14 Oct 2024 00:18:25 -0700 Subject: [PATCH 25/51] Adding initial documentation for prefix-commands --- docs/prefix-commands.md | 522 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 docs/prefix-commands.md diff --git a/docs/prefix-commands.md b/docs/prefix-commands.md new file mode 100644 index 00000000..0df7546d --- /dev/null +++ b/docs/prefix-commands.md @@ -0,0 +1,522 @@ +# Prefix Commands + +This documentation is meant for Discord server admins/moderators and for Developers. It'll first cover the General Concepts, then the Use for admins/moderators and finally the design so Developers understand how things have been set up. + +## Table of Content + +- [Prefix Commands](#prefix-commands) + - [Table of Content](#table-of-content) + - [Overview](#overview) + - [Detailed Concepts](#detailed-concepts) + - [Command](#command) + - [Command Content](#command-content) + - [Category](#category) + - [Version](#version) + - [Version Behavior](#version-behavior) + - [Channel Default Version](#channel-default-version) + - [Permission](#permission) + - [Permission Behavior](#permission-behavior) + - [Errors](#errors) + - [Managing Prefix Commands](#managing-prefix-commands) + - [Requirements and Bot Setup](#requirements-and-bot-setup) + - [Management Capabilities](#management-capabilities) + - [Managing Categories](#managing-categories) + - [Listing Categories](#listing-categories) + - [Adding a Category](#adding-a-category) + - [Modifying a Category](#modifying-a-category) + - [Deleting a Category](#deleting-a-category) + - [Managing Versions](#managing-versions) + - [Listing Versions](#listing-versions) + - [Adding a Version](#adding-a-version) + - [Modifying a Version](#modifying-a-version) + - [Deleting a Version](#deleting-a-version) + - [Managing Commands](#managing-commands) + - [Listing commands](#listing-commands) + - [Adding a command](#adding-a-command) + - [Modifying a command](#modifying-a-command) + - [Deleting a command](#deleting-a-command) + - [Managing Content](#managing-content) + - [Showing Content](#showing-content) + - [Setting Content](#setting-content) + - [Deleting Content](#deleting-content) + - [Managing Command Permissions](#managing-command-permissions) + - [Showing Permissions](#showing-permissions) + - [Setting Permissions](#setting-permissions) + - [Managing Channels](#managing-channels) + - [Managing Roles](#managing-roles) + - [Managing Channel Default Versions](#managing-channel-default-versions) + - [Showing the Channel Default Version](#showing-the-channel-default-version) + - [Setting the Channel Default Version](#setting-the-channel-default-version) + - [Deleting the Channel Default Version](#deleting-the-channel-default-version) + - [Showing Commands for a Category](#showing-commands-for-a-category) + - [Design and Development Overview](#design-and-development-overview) + - [In-Memory Cache](#in-memory-cache) + - [Cache Design](#cache-design) + - [Command Cache](#command-cache) + - [Versions Cache](#versions-cache) + - [Categories Cache](#categories-cache) + - [Channel Default Version Cache](#channel-default-version-cache) + - [Cache Refresh](#cache-refresh) + - [Message Handler](#message-handler) + +## Overview + +Prefix Commands are dynamically controlled commands that are accessed by using a prefix. For example, if the prefix is configured to be `.`, and the command is `hello`, the user in Discord would execute `.hello`. + +These commands are not hard coded in the bot, and instead they are configured through typical `/`-commands and then stored in the MongoDB. + +Additionally, there's a set of features that make these commands very flexible in use: + +- **Categories** + Commands are categorized for identifying the purpose or use of the command. Categories are manage dynamically through `/`-commands and stored in MongoDB. + +- **Versions** + The content of commands are defined per Version. A version can be used to provide different content based on the context in which the command is executed. By default, there is a hard-coded `GENERIC` version available, more can be added and managed dynamically through `/`-commands and stored in MongoDB. If multiple versions exist, and no version is specified during the execution, the `GENERIC` version is shown, with buttons to select the version to be shown. + +- **Content** + For each version, it is possible (but not a must) to set the content of a command. The content is static information that is managed with `/`-commands and stored in MongoDB. Depending on the version requested, the content is loaded and shown. Content contains a Title, Body and Image. + +- **Permissions** + Two types of permissions exist: Channel permissions and Role permissions. Using permissions it is possible to block or allow the use of a command in certain channels or by certain roles. + +- **Channel Default Versions** + For every channel, a specific version can be set as the default. In this case, even if there are multiple versions, if the command is executed without a version specified, the version set as the default for that channel is shown. + +### Detailed Concepts + +#### Command + +Commands only contain the basic information that is needed to use them: + +- `name` + A command has a name, this is the main way to execute the command, in combination with the configured prefix. This is a required attribute. +- `category` + The category a command belongs to, a command can only belong to a single category. This is a required attribute. +- `description` + The description of commands gives a short and brief overview of what the command is used for. This is a required attribute. +- `aliases` + A comma separated list for aliases for this command, each alias can be used to call the command instead of the name of the command. This is an optional attribute. Default value is empty. +- `is_embed` + A boolean attribute that identifies if the output should be posted as an Embed. If set to `False`, it will be shown as a regular text output, which is useful for simple image commands, or for simple links. This is an optional attribute. Default value is `False`. +- `embed_color` + Embeds in Discord have a color to the left, which can be used to give it a special look. By setting this value, you can change the default `FBW_CYAN` to other special colors. Colors are defined in the Config JSON file. This is an optional attribute. Default value is `FBW_CYAN` (only value currently in the `production.json` config file). + +Note that a Command does not contain any information about the content itself. This is because the content is specific per version and is described below. + +#### Command Content + +Content contains the actual information that is shown to the user when executing the command. Depending on the configuration of the command itself, this content will be displayed as an Embed or as standard text. The following attributes are available: + +- `version` + The version this content applies to, this is a reference to one of the existing versions in the bot. This is a required attribute. +- `title` + The title of the content, this will be shown as the title of the Embed, or as the first bold line of the text output in case the command is not an embed. This is a required attribute. +- `content` + A markdown capable string that identifies the actual content of the specified version. It can be up to 2048 Unicode characters, including emojis. This is an optional attribute. The default value is an empty (`null`), in which case it will not be shown as part of the output. +- `image` + A URL that refers to an image that should be shown as part of the content if the command is an Embed. For text-style commands, the URL should be part of the content itself so Discord automatically loads it as a preview. This is an optional attribute. The default value is empty (`null`). + +The `image` behavior can be surprising, but is chosen because it is not possible to just 'add' a message to a text-style response, unless if it is uploaded, and the decision was made to not upload an image very time the command is executed. What can be done is use markdown to load the image using the link syntax. Discord will then automatically load it as a preview. + +#### Category + +Categories are used to group commands together. This is mostly useful for when the help command is called to get the list of available commands for a specific category. It groups them together and makes it easy to identify. Categories have the following attributes: + +- `name` + The name as how it should be shown to the users and in any output. This is a required attribute. +- `emoji` + An emoji to identify the category, this will be shown next to the category whenever shown. This is an optional attribute. The default value is empty (`null`), in which case it isn't shown. + +#### Version + +Versions are useful if you want the same command to have different contents based on the context in which it is executed. An example use for FlyByWire Simulations is to use it to have a single command that gives different output based on the product for which it is requested. Later the impact of Versions will be described more. Versions have the following attributes: + +- `name` + A name for the version, this is how the version is identified in the different commands on how to manage the content of commands. This is a required attribute. +- `emoji` + The emoji associated with the version. When a command is executed without any version context, a GENERIC content will be displayed that offers the user the choice to get the details for a specific version. It does so by showing the emojis as buttons for the user to select. This is a required attribute. +- `alias` + This is a command alias for the version. By executing ` `, the user can get the content for a specific version directly, instead of going through the GENERIC content. This is a required attribute. +- `is_enabled` + A boolean attribute that can enable or disable a version. When this is set to `False`, the version will not be exposed to users. It will not show up in the selection of versions for the GENERIC content, and the alias will not work. This allows for versions and the content for those versions to be created ahead of enabling them. This is an optional attribute. The default value is `False`. + +#### Version Behavior + +Users can execute commands in two different ways, and they each result in different behavior. Any time a non-GENERIC version is mentioned, it must be enabled. Disable versions will never show up: + +- `` + This is the direct way of executing a command, depending on the available content, several things might happen: + - If no Channel Default Version is configured for the channel in which the command is executed: + - GENERIC version and one or more other versions have content: + The GENERIC content is shown and the user is given a choice underneath it with buttons containing the emjois of the other versions with content. When the user clicks on one of the buttons, the GENERIC content is removed and a new message is send with the content of the selected version. + - Only GENERIC version has content: + The GENERIC content is shown, and the user is not given a choice of other versions, as there is no choice available. + - No GENERIC content is set: + No response is given to the user: + - No content is set at all: + No response is given to the user. + - If a Channel Default Version is configured for the channel in which the command is executed: + - Content is set for the Channel Default Version: + The content for that version is shown to the user directly. + - No content is set for the Channel Default Version, but it does exist for the GENERIC version: + The content for the GENERIC version is shown, but no selection buttons are shown. + - No content is set for the Channel Default version, and no content exists for the GENERIC version: + No response is given to the user. + - No content is set at all: + No response is given to the user. +- ` ` + This directly requests the content for the specified version: + - Content is set for the specified version: + The content for the specified version is shown to the user directly. + - No content is set for the specified version, GENERIC version and one or more other versions have content: + The GENERIC content is shown and the user is given a choice underneath it with buttons containing the emjois of the other versions with content. When the user clicks on one of the buttons, the GENERIC content is removed and a new message is send with the content of the selected version. + - Content is not set for the specified version and only GENERIC version has content: + The GENERIC content is shown, and the user is not given a choice of other versions, as there is no choice available. + - No content is set for the specified version and no GENERIC content is set: + No response is given to the user. + - No content is set at all: + No response is given to the user. + +#### Channel Default Version + +It is possible to set a version as the default for a specific channel. By doing so, whenever someone executes the command directly (`` in that channel, it will automatically default to that version and not first post the GENERIC version with choices. This bypasses the choice menu and allows for an optimized experience for users. Two attributes need to be provided during configuration: + +- `channel` + The Discord channel to which the version should be defaulted to. This is a required attribute. +- `version` + The version that should be the default for this channel. It is possible to select a disabled version for this, but if you do so and the command is executed in the channel, no output will be shown. This is a required attribute. + +#### Permission + +Permissions can be set on commands so there are limitations to who can use the command and/or in which channels they can be used. The permission behavior is described below. Permissions have the following attributes: + +- `roles` + A list of Discord roles that either have access or do not have access to the command. This is an optional attribute. The default is an empty list, which results in the roles of the user not being checked. +- `role-blocklist` + A boolean attribute that identifies if the list of roles is blocked from using the command (`True`) or allowed to use the command (`False`). This is an optional attribute. The default value is `False`, meaning that if a `roles` list is set, only users with at least one of those roles can execute the command. +- `channels` + A list of Disord channels in which the command can either be executed or not executed. This is an optional attirubute. The default is an empty list, which results in the channel not being checked. +- `channel-blocklist` + A boolean attribute that identifies if the list of channels is blocked from command execution (`True`) or allowed to execute the command in (`False`). This is an optional attribute. The default value is `False`, meaning that if a `channels` list is set, the command is only allowed to be executed in one of those channels. +- `quiet-errors` + A boolean attribute, which if set to `True` will not display any warning to the user and will quietly fail the command execution if the permissions do not allow the execution of the command. This is an optional attribute. The default value is `False`. +- `verbose-errors` + A boolean attribute, which if set to `True` will show detailed output about the permission violated and who (role violation) or where (channel violation) the command can be executed. This is an optional attribute. The default value is `False`. + +##### Permission Behavior + +Permissions are checked in the following flow: + +- If there is a list of `roles` defined, the user is checked if they have any of the roles. + - If `role-blocklist` is `False` and the user *does not* have any of the roles, the execution of the command is blocked. + - If `role-blocklist` is `True` and the user *does* have any of the roles, the execution of the command is blocked. +- If there is a list of `channels` defined, the list is checked to see if the channel in which the command is executed, is part of the list. + - If `channel-blocklist` is `False` and the command *is not* executed in any of the channels, the execution of the command is blocked. + - If `channel-blocklist` is `True` and the command *is* executed in any of the channels, the execution of the command is blocked. + +##### Errors + +By default, when a command is blocked from execution, a message is shown that the execution is blocked either because of a role permission, or a channel permission. These messages are generic and do not reveal any detail about which role or channel needs to be present (blocklist is `False`), or needs to be absent (blocklist is `True`). + +When `verbose-errors` is `True`, the message is enriched with information about which roles or channels are allowed (blocklist is `False`), or blocked (blocklist is `True`). + +When `quiet-errors` is `True`, no message is send at all. + +If both are set to `True`, `quiet-errors` takes precedence. + +## Managing Prefix Commands + +### Requirements and Bot Setup + +The bot will require a MongoDB environment set up. There are a few settings to configure: + +- **Config JSON**: + - `prefixCommandPrefix` + A String that will be the prefix for command that will be used by the user to execute commands. For example `.` to execute commands like `.help` + - `prefixCommandPermissionDelay` + A number in milliseconds that identifies the delay before the message about invalid permissions is deleted. + +- **Environment Variable**: + - `CACHE_REFRESH_INTERVAL` + A number in milliseconds that is used to automatically refresh the cache from the database, to make sure the cache remains up to date. + +### Management Capabilities + +High level, the following capabilities exist from a management perspective, each with their own `/`-command: + +- Manage Categories + - `/prefix-commands categories list [search_text]` + - `/prefix-commands categories add [emoji]` + - `/prefix-commands categories modify [name] [emoji]` + - `/prefix-commands categories delete ` +- Manage Versions + - `/prefix-commands versions list [search_text]` + - `/prefix-commands versions add [is_enabled]` + - `/prefix-commands versions modify [name] [emoji] [alias] [is_enabled]` + - `/prefix-commands versions delete [force]` +- Manage Commands + - `/prefix-commands commands list [search_text]` + - `/prefix-commands commands add [aliases] [is_embed] [embed_color]` + - `/prefix-commands commands modify [name] [category] [description] [aliases] [is_embed] [embed_color]` + - `/prefix-commands commands ` +- Manage Content for Commands + - `/prefix-commands content show ` + - `/prefix-commands content set ` + - `/prefix-commands content delete ` +- Manage Permissions for Commands + - `/prefix-command-permissions show ` + - `/prefix-command-permissions settings [roles-blocklist] [channels-blocklist] [quiet-errors] [verbose-errors]` + - `/prefix-command-permissions channels add ` + - `/prefix-command-permissions channels remove ` + - `/prefix-command-permissions roles add ` + - `/prefix-command-permissions roles remove ` +- Manage Channel Default Versions for Channels + - `/prefix-commands channel-default-version show ` + - `/prefix-commands channel-default-version set ` + - `/prefix-commands channel-default-version delete ` +- Showing available Commands per Category + - `/prefix-help [search]` + +Below is a deeper dive in each of those. The deep-dives will not contain any details on the attributes themselves, those have been described above. + +### Managing Categories + +#### Listing Categories + +It is possible to get a list of all categories, or a list of filtered categories, using the following command: + +`/prefix-commands categories list [search_text]` + +In this command, if the `search_text` is empty, all categories are shown. If it is set, any category containing the provided string will be listed. + +#### Adding a Category + +Adding a category is done using the command below. A name must be provided, and must be unique. An emoji can optionally be provided to have a nice representation of the category. + +`/prefix-commands categories add [emoji]` + +#### Modifying a Category + +To modify a category, use the command below. When executing the command, you have to select the category you want to modify. This is an automatically generated list from the existing categories. Then you provide an optional new name and new emoji. If you do not provide a new value, the original setting is kept. + +`/prefix-commands categories modify [name] [emoji]` + +#### Deleting a Category + +You can delete a category by using the below command. This will fail if there's still commands part of the category. + +`/prefix-commands categories delete ` + +### Managing Versions + +#### Listing Versions + +It is possible to get a list of all versions, or a list of filtered versions, using the following command: + +`/prefix-commands versions list [search_text]` + +In this command, if the `search_text` is empty, all versions are shown. If it is set, any version containing the provided string will be listed. + +#### Adding a Version + +Adding a version is done using the command below. A name, emoji and alias must be provided, and they must be unique. A version can optionally be enabled. + +`/prefix-commands versions add [is_enabled]` + +#### Modifying a Version + +To modify a version, use the command below. When executing the command, you have to select the version you want to modify. This is an automatically generated list from the existing versions. Then you provide an optional new name, new emoji, new alias and if you want to enable or disable it. If you do not provide a new value, the original setting is kept. + +`/prefix-commands versions modify [name] [emoji] [alias] [is_enabled]` + +#### Deleting a Version + +You can delete a version by using the below command. This will fail if there's still content or channel default versions configured of the version unless force is enabled. Deleting a version with force will automatically delete any content and channel default versions referencing it. + +`/prefix-commands versions delete [force]` + +### Managing Commands + +#### Listing commands + +It is possible to get a list of all commands, or a list of filtered commands, using the following command: + +`/prefix-commands commands list [search_text]` + +In this command, if the `search_text` is empty, all commands are shown. If it is set, any command containing the provided string will be listed. + +#### Adding a command + +Adding a command is done using the command below. A name, category and description must be provided. The name must be unique. A command can optionally have aliases. By default, a command is not an Embed, but this can be enabled and a color for the embed can be selected. + +`/prefix-commands commands add [aliases] [is_embed] [embed_color]` + +#### Modifying a command + +To modify a command, use the command below. When executing the command, you have to select the command you want to modify. This is an automatically generated list from the existing commands. Then you provide an optional new name, new category, new description, new aliases, if you want to make it an embed or not, and what color the embed should have. If you do not provide a new value, the original setting is kept. + +`/prefix-commands commands modify [name] [category] [description] [aliases] [is_embed] [embed_color]` + +#### Deleting a command + +You can delete a command by using the below command. Deleting a command will automatically delete all content for it, as well as its permissions. + +`/prefix-commands commands ` + +### Managing Content + +### Showing Content + +It is possible to show to show the content of a command and version by using the below command. You must provide a command and version. + +`/prefix-commands content show ` + +### Setting Content + +To set the content of a command, execute the below command. You must provide a command and version. + +`/prefix-commands content set ` + +When executing this command, a window will pop open that will have a form asking for the title (required), content (optional) and image URL (optional). If the content was already set, it will be prefilled so it is easy to modify. You will have 2 minutes to fill in the fields. + +It is strongly advised that you prepare the content and just copy/paste it into the form, instead of typing it out in the form itself, as it might time out. + +**It is best to always create content for the GENERIC version for every command, as it is a fallback and default in many cases.** + +#### Deleting Content + +If you want to remove the content for a command and version entirely, you can do so using the below command. You must provide a command and version. + +`/prefix-commands content delete ` + +### Managing Command Permissions + +#### Showing Permissions + +To show the permission settings of a command, execute the following command. You must select a command. It will show an embed with all the settings, roles and channels that are configured for the command. + +`/prefix-command-permissions show ` + +#### Setting Permissions + +You can set the different settings for a command using the following command. You must select a command, but the rest of the attributes are optional. If you do not provide a new value, the original setting is kept. + +`/prefix-command-permissions settings [roles-blocklist] [channels-blocklist] [quiet-errors] [verbose-errors]` + +#### Managing Channels + +The list of channels to which the permissions apply can be set using the following commands. For simplicity, managing the list is done by adding or removing channels. You must provide the command and channel. If you try to remove a channel that isn't in the list, or try to add a channel that is already in the list, nothing will happen. + +`/prefix-command-permissions channels add ` +`/prefix-command-permissions channels remove ` + +#### Managing Roles + +The list of roles to which the permissions apply can be set using the following commands. For simplicity, managing the list is done by adding or removing roles. You must provide the command and channel. If you try to remove a channel that isn't in the list, or try to add a channel that is already in the list, nothing will happen. + +`/prefix-command-permissions roles add ` +`/prefix-command-permissions roles remove ` + +### Managing Channel Default Versions + +#### Showing the Channel Default Version + +To show the channel default version for a specific channel, execute the following command. A channel must be provided. If no channel default version is set, it will let you know. + +`/prefix-commands channel-default-version show ` + +#### Setting the Channel Default Version + +Setting the default version for a channel can be done with the below command. You must provide the channel and the version. + +`/prefix-commands channel-default-version set ` + +#### Deleting the Channel Default Version + +To remove a channel default version, and return a channel to use the generic version again by default, you can execute the below command. A channel must be provided. + +`/prefix-commands channel-default-version delete ` + +### Showing Commands for a Category + +It is possible for any user, regardless of role and permissions, to list all the existing prefix commands for a specific category, by using the below command. This command has an optional search, any command within the category that matches the search, will be shown with it's name, versions, aliases and description. + +`/prefix-help [search]` + +## Design and Development Overview + +A few items should be highlighted in how the design of this solution works, most importantly around the caching system and the message handling. + +### In-Memory Cache + +As all prefix commands are stored in MongoDB, if there was no local in-memory cache, for every time someone started a message with the prefix, the bot would need to go to MongoDB to check if it existed, retrieve the necessary details, including version details, content information and permissions. This would cause a lot of activity towards the database, and given the database is not hosted together with the bot, this could cause significant delays in handling the commands. + +To avoid these problems, an in-memory cache is set up when the bot starts up. During startup, it will fill the cache with all the data from the database. The following objects are currently cached: + +- Commands, including + - Content + - Permissions +- Versions +- Categories +- Channel Default Versions + +#### Cache Design + +The in-memory cache is a simple key/value pair cache that is fully stored in memory. To distinguish between the different types of objects, the key is prefixed with a specific string that identifies the type of object. + +##### Command Cache + +The prefix for commands is set as `PF_COMMAND`. For each command, the command is stored potentially multiple times: + +- Once for the name of the command, in lower case (`PF_COMMAND:`). +- Once for every alias of the command, in lower case (`PF_COMMAND:`). + +In each case, the entire command object is stored as the value. This is done for the efficient retrieval of the command when executed by a user, either by its name, or by its alias. + +Every time the command, content or permissions for a command is added, modified or deleted, the cache is refreshed for that command. + +##### Versions Cache + +The prefix for versions is set as `PF_VERSION`. For each version the version is stored twice: + +- Once for the name of the version, in lower case (`PF_VERSION:`). +- Once for the MongoDB ID of the version (`PF_VERSION:<_id>`). + +In each case, the entire version object is stored as the value. The latter case is done as in several cases, a version will be referenced by ID and this optimizes the access to versions. + +Every time a version is added, modified or deleted, the cache is refreshed for that version. + +#### Categories Cache + +The prefix for categories is set as `PF_CATEGORY`. For each category the category is stored once for the name of the category, in lower case (`PF_CATEGORY:`). + +In each case, the entire category object is stored as the value. + +Every time a category is added, modified or deleted, the cache is refreshed for that category. + +#### Channel Default Version Cache + +The prefix for channel default versions is set as `PF_CHANNEL_VERSION`. For each channel default version the channel default version is stored once for the channel ID of the channel (`PF_CHANNEL_VERSION:`). + +In each case, the entire channel default version object is stored as the value. + +Every time a channel default version is added, modified or deleted, the cache is refreshed for that channel default version. + +#### Cache Refresh + +Periodically, the cache will be updated automatically using the scheduler. The interval is configurable using an environment variable `CACHE_REFRESH_INTERVAL`. + +When doing so, first all entries are purged to make sure items that no longer exist, do not remain. Then all items are recreated. + +TODO: This is not an ideal solution and has the risk that by emptying the cache first, prefix commands might temporarily not work. Therefor this will be adjusted in the short term with a mechanism where the following actions are taken in steps, for each type of object cached: + +- Fetch all existing items from the DB. +- Fetch all keys from the cache. +- Update or Add all objects from the DB. +- Compare all keys found in the cache, and check if they still exist in the DB data, if not, remove them from the cache. + +### Message Handler + +A new Message Handler has been created that listens for all message creations. It takes the following steps: + +TODO: Add diagram - to be drawn From 5b867cf685a8e27d2a8b496a82559b0489bf0b21 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Mon, 14 Oct 2024 22:46:04 -0700 Subject: [PATCH 26/51] Refactor cache management for Channel Default Versions --- .../functions/setChannelDefaultVersion.ts | 6 +-- src/lib/cache/cacheManager.ts | 54 +++++++++++-------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts index fcb7ec6a..ea8a34ae 100644 --- a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommandChannelDefaultVersion, Logger, makeEmbed, refreshSinglePrefixCommandChannelDefaultVersionCache } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, PrefixCommandChannelDefaultVersion, Logger, makeEmbed, loadSinglePrefixCommandChannelDefaultVersionToCache } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Set Default Channel Version - No Connection', @@ -90,9 +90,7 @@ export async function handleSetPrefixCommandChannelDefaultVersion(interaction: C channelDefaultVersion.versionId = versionId; try { await channelDefaultVersion.save(); - if (foundVersion) { - await refreshSinglePrefixCommandChannelDefaultVersionCache(channelDefaultVersion, channelDefaultVersion); - } + await loadSinglePrefixCommandChannelDefaultVersionToCache(channelDefaultVersion); await interaction.followUp({ embeds: [successEmbed(channelName, version, emoji)], ephemeral: true }); if (modLogsChannel) { try { diff --git a/src/lib/cache/cacheManager.ts b/src/lib/cache/cacheManager.ts index a3660c88..ba787a74 100644 --- a/src/lib/cache/cacheManager.ts +++ b/src/lib/cache/cacheManager.ts @@ -239,20 +239,6 @@ export async function clearSinglePrefixCommandChannelDefaultVersionCache(channel await inMemoryCache.del(`${memoryCachePrefixChannelDefaultVersion}:${channelId}`); } -export async function clearAllPrefixCommandChannelDefaultVersionsCache() { - const inMemoryCache = getInMemoryCache(); - if (!inMemoryCache) return; - - const keys = await inMemoryCache.store.keys(); - for (const key of keys) { - if (key.startsWith(`${memoryCachePrefixChannelDefaultVersion}:`)) { - Logger.debug(`Clearing cache for channel default version for channel "${key}"`); - // eslint-disable-next-line no-await-in-loop - await inMemoryCache.del(key); - } - } -} - export async function loadSinglePrefixCommandChannelDefaultVersionToCache(channelDefaultVersion: IPrefixCommandChannelDefaultVersion) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; @@ -278,12 +264,38 @@ export async function loadAllPrefixCommandChannelDefaultVersionsToCache() { } } -export async function refreshSinglePrefixCommandChannelDefaultVersionCache(oldChannelDefaultVersion: IPrefixCommandChannelDefaultVersion, newChannelDefaultVersion: IPrefixCommandChannelDefaultVersion) { - await clearSinglePrefixCommandChannelDefaultVersionCache(oldChannelDefaultVersion); - await loadSinglePrefixCommandChannelDefaultVersionToCache(newChannelDefaultVersion); -} - export async function refreshAllPrefixCommandChannelDefaultVersionsCache() { - await clearAllPrefixCommandChannelDefaultVersionsCache(); - await loadAllPrefixCommandChannelDefaultVersionsToCache(); + const conn = getConn(); + const inMemoryCache = getInMemoryCache(); + if (!conn || !inMemoryCache) return; + + // Step 1: Get all channel default versions from the database + const prefixCommandChannelDefaultVersions = await PrefixCommandChannelDefaultVersion.find(); + // Step 2: Get all channel default versions from the cache + const cacheKeys = await inMemoryCache.store.keys(); + // Step 3: Loop over cached channel default versions + for (const key of cacheKeys) { + if (key.startsWith(`${memoryCachePrefixChannelDefaultVersion}:`)) { + const channelId = key.split(':')[1]; + // Step 3.a: Check if cached channel default version exists in the database list + let found = false; + for (const dbChannelDefaultVersion of prefixCommandChannelDefaultVersions) { + if (dbChannelDefaultVersion.channelId.toString().toLocaleLowerCase() === channelId.toLocaleLowerCase()) { + found = true; + break; + } + } + // Step 3.b: If not found, remove from cache + if (!found) { + Logger.debug(`Removing channel default version for channel ${channelId} from cache`); + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.del(key); + } + } + } + // Step 4: Loop over database channel default versions and update cache + for (const dbChannelDefaultVersion of prefixCommandChannelDefaultVersions) { + // eslint-disable-next-line no-await-in-loop + await loadSinglePrefixCommandChannelDefaultVersionToCache(dbChannelDefaultVersion); + } } From a4351c76dd0788afcf41dc3357c0b1267420762f Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Mon, 14 Oct 2024 23:30:57 -0700 Subject: [PATCH 27/51] Refactoring cache --- src/lib/cache/cacheManager.ts | 153 +++++++++++++++++++++++----------- 1 file changed, 104 insertions(+), 49 deletions(-) diff --git a/src/lib/cache/cacheManager.ts b/src/lib/cache/cacheManager.ts index ba787a74..2002aea2 100644 --- a/src/lib/cache/cacheManager.ts +++ b/src/lib/cache/cacheManager.ts @@ -59,20 +59,6 @@ export async function clearSinglePrefixCommandCache(command: IPrefixCommand) { await inMemoryCache.del(`${memoryCachePrefixCommand}:${name.toLowerCase()}`); } -export async function clearAllPrefixCommandsCache() { - const inMemoryCache = getInMemoryCache(); - if (!inMemoryCache) return; - - const keys = await inMemoryCache.store.keys(); - for (const key of keys) { - if (key.startsWith(`${memoryCachePrefixCommand}:`)) { - Logger.debug(`Clearing cache for command or alias "${key}"`); - // eslint-disable-next-line no-await-in-loop - await inMemoryCache.del(key); - } - } -} - export async function loadSinglePrefixCommandToCache(command: IPrefixCommand) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; @@ -104,8 +90,40 @@ export async function refreshSinglePrefixCommandCache(oldCommand: IPrefixCommand } export async function refreshAllPrefixCommandsCache() { - await clearAllPrefixCommandsCache(); - await loadAllPrefixCommandsToCache(); + const conn = getConn(); + const inMemoryCache = getInMemoryCache(); + if (!conn || !inMemoryCache) return; + + // Step 1: Get all commands from the database + const prefixCommands = await PrefixCommand.find(); + // Step 2: Get all commands from the cache + const cacheKeys = await inMemoryCache.store.keys(); + // Step 3: Loop over cached commands + for (const key of cacheKeys) { + if (key.startsWith(`${memoryCachePrefixCommand}:`)) { + const checkCommand = key.split(':')[1]; + // Step 3.a: Check if cached command exists in the database list + let found = false; + for (const dbCommand of prefixCommands) { + const { name: dbCommandName, aliases: dbCommandAliases } = dbCommand; + if (dbCommandName.toLowerCase() === checkCommand.toLowerCase() || dbCommandAliases.includes(checkCommand)) { + found = true; + break; + } + } + // Step 3.b: If not found, remove from cache + if (!found) { + Logger.debug(`Removing command or alias ${checkCommand} from cache`); + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.del(key); + } + } + } + // Step 4: Loop over database commands and update cache + for (const dbCommand of prefixCommands) { + // eslint-disable-next-line no-await-in-loop + await loadSinglePrefixCommandToCache(dbCommand); + } } /** @@ -122,20 +140,6 @@ export async function clearSinglePrefixCommandVersionCache(version: IPrefixComma await inMemoryCache.del(`${memoryCachePrefixVersion}:${versionId}`); } -export async function clearAllPrefixCommandVersionsCache() { - const inMemoryCache = getInMemoryCache(); - if (!inMemoryCache) return; - - const keys = await inMemoryCache.store.keys(); - for (const key of keys) { - if (key.startsWith(`${memoryCachePrefixVersion}:`)) { - Logger.debug(`Clearing cache for command version alias/id "${key}"`); - // eslint-disable-next-line no-await-in-loop - await inMemoryCache.del(key); - } - } -} - export async function loadSinglePrefixCommandVersionToCache(version: IPrefixCommandVersion) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; @@ -164,8 +168,40 @@ export async function refreshSinglePrefixCommandVersionCache(oldVersion: IPrefix } export async function refreshAllPrefixCommandVersionsCache() { - await clearAllPrefixCommandVersionsCache(); - await loadAllPrefixCommandVersionsToCache(); + const conn = getConn(); + const inMemoryCache = getInMemoryCache(); + if (!conn || !inMemoryCache) return; + + // Step 1: Get all versions from the database + const prefixCommandVersions = await PrefixCommandVersion.find(); + // Step 2: Get all versions from the cache + const cacheKeys = await inMemoryCache.store.keys(); + // Step 3: Loop over cached versions + for (const key of cacheKeys) { + if (key.startsWith(`${memoryCachePrefixVersion}:`)) { + const checkVersion = key.split(':')[1]; + // Step 3.a: Check if cached version exists in the database list + let found = false; + for (const dbVersion of prefixCommandVersions) { + const { _id: dbVersionId, alias } = dbVersion; + if (dbVersionId.toString().toLowerCase() === checkVersion.toLowerCase() || alias.toLowerCase() === checkVersion.toLowerCase()) { + found = true; + break; + } + } + // Step 3.b: If not found, remove from cache + if (!found) { + Logger.debug(`Removing version with id ${checkVersion} from cache`); + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.del(key); + } + } + } + // Step 4: Loop over database versions and update cache + for (const dbVersion of prefixCommandVersions) { + // eslint-disable-next-line no-await-in-loop + await loadSinglePrefixCommandVersionToCache(dbVersion); + } } /** @@ -181,20 +217,6 @@ export async function clearSinglePrefixCommandCategoryCache(category: IPrefixCom await inMemoryCache.del(`${memoryCachePrefixCategory}:${name.toLowerCase()}`); } -export async function clearAllPrefixCommandCategoriesCache() { - const inMemoryCache = getInMemoryCache(); - if (!inMemoryCache) return; - - const keys = await inMemoryCache.store.keys(); - for (const key of keys) { - if (key.startsWith(`${memoryCachePrefixCategory}:`)) { - Logger.debug(`Clearing cache for command category "${key}"`); - // eslint-disable-next-line no-await-in-loop - await inMemoryCache.del(key); - } - } -} - export async function loadSinglePrefixCommandCategoryToCache(category: IPrefixCommandCategory) { const inMemoryCache = getInMemoryCache(); if (!inMemoryCache) return; @@ -222,8 +244,40 @@ export async function refreshSinglePrefixCommandCategoryCache(oldCategory: IPref } export async function refreshAllPrefixCommandCategoriesCache() { - await clearAllPrefixCommandCategoriesCache(); - await loadAllPrefixCommandCategoriesToCache(); + const conn = getConn(); + const inMemoryCache = getInMemoryCache(); + if (!conn || !inMemoryCache) return; + + // Step 1: Get all catagories from the database + const prefixCommandCategories = await PrefixCommandCategory.find(); + // Step 2: Get all categories from the cache + const cacheKeys = await inMemoryCache.store.keys(); + // Step 3: Loop over cached categories + for (const key of cacheKeys) { + if (key.startsWith(`${memoryCachePrefixCategory}:`)) { + const categoryName = key.split(':')[1]; + // Step 3.a: Check if cached category exists in the database list + let found = false; + for (const dbCategory of prefixCommandCategories) { + const { name: dbCategoryName } = dbCategory; + if (dbCategoryName.toLowerCase() === categoryName.toLowerCase()) { + found = true; + break; + } + } + // Step 3.b: If not found, remove from cache + if (!found) { + Logger.debug(`Removing category ${categoryName} from cache`); + // eslint-disable-next-line no-await-in-loop + await inMemoryCache.del(key); + } + } + } + // Step 4: Loop over database categories and update cache + for (const dbCategory of prefixCommandCategories) { + // eslint-disable-next-line no-await-in-loop + await loadSinglePrefixCommandCategoryToCache(dbCategory); + } } /** @@ -280,7 +334,8 @@ export async function refreshAllPrefixCommandChannelDefaultVersionsCache() { // Step 3.a: Check if cached channel default version exists in the database list let found = false; for (const dbChannelDefaultVersion of prefixCommandChannelDefaultVersions) { - if (dbChannelDefaultVersion.channelId.toString().toLocaleLowerCase() === channelId.toLocaleLowerCase()) { + const { channelId: dbChannelId } = dbChannelDefaultVersion; + if (dbChannelId.toString().toLowerCase() === channelId.toLowerCase()) { found = true; break; } From 984b1db6d84d8d74cb73368ff70f387e86d3b1be Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Mon, 14 Oct 2024 23:34:31 -0700 Subject: [PATCH 28/51] Updating docs on cachi auto refresh --- docs/prefix-commands.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/prefix-commands.md b/docs/prefix-commands.md index 0df7546d..c44ca048 100644 --- a/docs/prefix-commands.md +++ b/docs/prefix-commands.md @@ -506,14 +506,13 @@ Every time a channel default version is added, modified or deleted, the cache is Periodically, the cache will be updated automatically using the scheduler. The interval is configurable using an environment variable `CACHE_REFRESH_INTERVAL`. -When doing so, first all entries are purged to make sure items that no longer exist, do not remain. Then all items are recreated. +When the cache is refreshed, it will do the following steps: -TODO: This is not an ideal solution and has the risk that by emptying the cache first, prefix commands might temporarily not work. Therefor this will be adjusted in the short term with a mechanism where the following actions are taken in steps, for each type of object cached: - -- Fetch all existing items from the DB. -- Fetch all keys from the cache. -- Update or Add all objects from the DB. -- Compare all keys found in the cache, and check if they still exist in the DB data, if not, remove them from the cache. +1. Fetch all existing items from the DB. +2. Fetch all keys from the cache. +3. Loop over all keys found in cache. + 1. Verify the item still exists in the items from the DB. If not, delete the cache key. +4. Update or Add all objects from the DB. ### Message Handler From 2df024c71e14d58fcf8a00028788b4d940d86453 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Mon, 14 Oct 2024 23:42:33 -0700 Subject: [PATCH 29/51] Adding CHANGELOG entry --- .github/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index c6c3769a..a6a6aae3 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,7 @@ Update _ October 2024 +- feat: Prefix Command Management (23/10/2024) - fix: role assignment typo for server announcements (22/10/2024) Update _ August 2024 From 23ce2f6cfcd268b208725a8e1b6af88e1f179afb Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Tue, 15 Oct 2024 20:15:39 -0700 Subject: [PATCH 30/51] Disabling autocreate for unnecessary collections --- src/lib/schemas/prefixCommandSchemas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts index 8828ae03..3b89dabc 100644 --- a/src/lib/schemas/prefixCommandSchemas.ts +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -79,7 +79,7 @@ const prefixCommandContentSchema = new Schema({ }, content: String, image: String, -}); +}, { autoCreate: false }); export interface IPrefixCommandPermissions extends Document { roles?: string[], @@ -97,7 +97,7 @@ const prefixCommandPermissionsSchema = new Schema({ channelsBlocklist: Boolean, quietErrors: Boolean, verboseErrors: Boolean, -}); +}, { autoCreate: false }); export interface IPrefixCommand extends Document { commandId: mongoose.Schema.Types.ObjectId; From 72fa602c14f9c2b2a7dc25c8277c9ea767859fa4 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Tue, 15 Oct 2024 22:48:47 -0700 Subject: [PATCH 31/51] Fixing bug where initial permissions were not properly set --- .../moderation/prefixCommands/functions/addCommand.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts index c92f2df4..2c7adb02 100644 --- a/src/commands/moderation/prefixCommands/functions/addCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -130,8 +130,14 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera isEmbed, embedColor, contents: [], - channelPermissions: [], - rolePermissions: [], + permissions: { + roles: [], + rolesBlocklist: false, + channels: [], + channelsBlocklist: false, + quietErrors: false, + verboseErrors: false, + }, }); try { await prefixCommand.save(); From 035b415ed1d0459ffe3210e7b08cfa525f420576 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Tue, 15 Oct 2024 23:09:12 -0700 Subject: [PATCH 32/51] Fixing issues: * Allowing empty title * Fixing messageCreateHandler in case of unset permissions on command * --- .../prefixCommands/functions/setContent.ts | 2 +- .../prefixCommands/functions/showContent.ts | 2 +- src/events/messageCreateHandler.ts | 30 ++++++++++++------- src/lib/schemas/prefixCommandSchemas.ts | 5 +--- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index 6047d14a..fabb40b5 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -118,7 +118,7 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman .setStyle(TextInputStyle.Short) .setMaxLength(255) .setMinLength(0) - .setRequired(true) + .setRequired(false) .setValue(foundContent ? foundContent.title : ''); const commandContentContent = new TextInputBuilder() diff --git a/src/commands/moderation/prefixCommands/functions/showContent.ts b/src/commands/moderation/prefixCommands/functions/showContent.ts index 5de6c2f5..286d7997 100644 --- a/src/commands/moderation/prefixCommands/functions/showContent.ts +++ b/src/commands/moderation/prefixCommands/functions/showContent.ts @@ -94,7 +94,7 @@ export async function handleShowPrefixCommandContent(interaction: ChatInputComma } const { id: contentId, title, content, image } = foundContent; try { - await interaction.followUp({ embeds: [contentEmbed(command, version, `${title}`, `${content}`, `${image}`, `${commandId}`, `${versionId}`, `${contentId}`)], ephemeral: false }); + await interaction.followUp({ embeds: [contentEmbed(command, version, title || '', content || '', image || '', `${commandId}`, `${versionId}`, `${contentId}`)], ephemeral: false }); } catch (error) { Logger.error(`Failed to show prefix command content for command ${command} and version ${version}: ${error}`); await interaction.followUp({ embeds: [failedEmbed(command, version)], ephemeral: true }); diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index f1369d99..b86c27c9 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -1,6 +1,6 @@ import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder, Interaction, Message } from 'discord.js'; import { event, getInMemoryCache, memoryCachePrefixCommand, memoryCachePrefixVersion, memoryCachePrefixChannelDefaultVersion, Logger, Events, constantsConfig, makeEmbed, makeLines } from '../lib'; -import { PrefixCommand, PrefixCommandVersion } from '../lib/schemas/prefixCommandSchemas'; +import { PrefixCommand, PrefixCommandPermissions, PrefixCommandVersion } from '../lib/schemas/prefixCommandSchemas'; const commandEmbed = (title: string, description: string, color: string, imageUrl: string = '') => makeEmbed({ title, @@ -42,10 +42,14 @@ async function sendReply(message: Message, commandTitle: string, commandContent: if (isEmbed) { return replyWithEmbed(message, commandEmbed(commandTitle, commandContent, embedColor, commandImage), versionButtonRow); } - return replyWithMsg(message, makeLines([ - `**${commandTitle}**`, - ...(commandContent ? [commandContent] : []), - ]), versionButtonRow); + const content: string[] = []; + if (commandTitle) { + content.push(`**${commandTitle}**`); + } + if (commandContent) { + content.push(commandContent); + } + return replyWithMsg(message, makeLines(content), versionButtonRow); } catch (error) { Logger.error(error); return message.reply('An error occurred while processing the command.'); @@ -62,12 +66,16 @@ async function expireChoiceReply(message: Message, commandTitle: string, command return message.edit({ embeds: [commandEmbedData], components: [] }); } + const content: string[] = []; + if (commandTitle) { + content.push(`**${commandTitle}**`); + } + if (commandContent) { + content.push(commandContent); + } + content.push('\n`The choice has expired.`'); return message.edit({ - content: makeLines([ - `**${commandTitle}**`, - ...(commandContent ? [commandContent] : []), - '\n`The choice has expired.`', - ]), + content: makeLines(content), components: [], }); } catch (error) { @@ -148,7 +156,7 @@ export default event(Events.MessageCreate, async (_, message) => { if (cachedCommandDetails) { const commandDetails = PrefixCommand.hydrate(cachedCommandDetails); const { name, contents, isEmbed, embedColor, permissions } = commandDetails; - const { roles: permRoles, rolesBlocklist, channels: permChannels, channelsBlocklist, quietErrors, verboseErrors } = permissions; + const { roles: permRoles, rolesBlocklist, channels: permChannels, channelsBlocklist, quietErrors, verboseErrors } = permissions ?? new PrefixCommandPermissions(); const authorMember = await guild.members.fetch(authorId); // Check permissions diff --git a/src/lib/schemas/prefixCommandSchemas.ts b/src/lib/schemas/prefixCommandSchemas.ts index 3b89dabc..c4dee8b5 100644 --- a/src/lib/schemas/prefixCommandSchemas.ts +++ b/src/lib/schemas/prefixCommandSchemas.ts @@ -73,10 +73,7 @@ const prefixCommandContentSchema = new Schema({ type: String, required: true, }, - title: { - type: String, - required: true, - }, + title: String, content: String, image: String, }, { autoCreate: false }); From 26973ba94702d9ed43ba4c8c851e183d8e13b214 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Tue, 15 Oct 2024 23:10:50 -0700 Subject: [PATCH 33/51] Fixing maximum number of callback items --- .../prefixCommands/prefixCommandPermissions.ts | 4 +++- .../moderation/prefixCommands/prefixCommands.ts | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts b/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts index 9a7badd1..b5f33950 100644 --- a/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts +++ b/src/commands/moderation/prefixCommands/prefixCommandPermissions.ts @@ -188,7 +188,9 @@ const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { if (!conn) { return interaction.respond(choices); } - const foundCommands = await PrefixCommand.find({ name: { $regex: searchText, $options: 'i' } }); + const foundCommands = await PrefixCommand.find({ name: { $regex: searchText, $options: 'i' } }) + .sort({ name: 1 }) + .limit(25); for (let i = 0; i < foundCommands.length; i++) { const command = foundCommands[i]; const { name } = command; diff --git a/src/commands/moderation/prefixCommands/prefixCommands.ts b/src/commands/moderation/prefixCommands/prefixCommands.ts index f89cccd9..6662056d 100644 --- a/src/commands/moderation/prefixCommands/prefixCommands.ts +++ b/src/commands/moderation/prefixCommands/prefixCommands.ts @@ -527,7 +527,9 @@ const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { if (!conn) { return interaction.respond(choices); } - const foundCategories = await PrefixCommandCategory.find({ name: { $regex: searchText, $options: 'i' } }); + const foundCategories = await PrefixCommandCategory.find({ name: { $regex: searchText, $options: 'i' } }) + .sort({ name: 1 }) + .limit(25); for (let i = 0; i < foundCategories.length; i++) { const category = foundCategories[i]; const { name } = category; @@ -538,7 +540,9 @@ const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { if (!conn) { return interaction.respond(choices); } - const foundCommands = await PrefixCommand.find({ name: { $regex: searchText, $options: 'i' } }); + const foundCommands = await PrefixCommand.find({ name: { $regex: searchText, $options: 'i' } }) + .sort({ name: 1 }) + .limit(25); for (let i = 0; i < foundCommands.length; i++) { const command = foundCommands[i]; const { name } = command; @@ -550,7 +554,9 @@ const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { if (!conn) { return interaction.respond(choices); } - const foundVersions = await PrefixCommandVersion.find({ name: { $regex: searchText, $options: 'i' } }); + const foundVersions = await PrefixCommandVersion.find({ name: { $regex: searchText, $options: 'i' } }) + .sort({ name: 1 }) + .limit(25); for (let i = 0; i < foundVersions.length; i++) { const version = foundVersions[i]; const { name } = version; From d3e0165284b03ded2a9f014921f3d07fde0a2f9e Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Tue, 15 Oct 2024 23:25:39 -0700 Subject: [PATCH 34/51] Fix if there's no content at all --- src/events/messageCreateHandler.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index b86c27c9..f3d091b5 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -39,16 +39,18 @@ async function replyWithMsg(msg: Message, text: string, buttonRow?:ActionRowBuil async function sendReply(message: Message, commandTitle: string, commandContent: string, isEmbed: boolean, embedColor: string, commandImage: string, versionButtonRow?: ActionRowBuilder) : Promise> { try { + let actualCommandContent = commandContent; + if (!commandTitle && !commandContent && !commandImage) { + actualCommandContent = 'No content available.'; + } if (isEmbed) { - return replyWithEmbed(message, commandEmbed(commandTitle, commandContent, embedColor, commandImage), versionButtonRow); + return replyWithEmbed(message, commandEmbed(commandTitle, actualCommandContent, embedColor, commandImage), versionButtonRow); } const content: string[] = []; if (commandTitle) { content.push(`**${commandTitle}**`); } - if (commandContent) { - content.push(commandContent); - } + content.push(actualCommandContent); return replyWithMsg(message, makeLines(content), versionButtonRow); } catch (error) { Logger.error(error); @@ -58,8 +60,12 @@ async function sendReply(message: Message, commandTitle: string, commandContent: async function expireChoiceReply(message: Message, commandTitle: string, commandContent: string, isEmbed: boolean, embedColor: string, commandImage: string) : Promise> { try { + let actualCommandContent = commandContent; + if (!commandTitle && !commandContent && !commandImage) { + actualCommandContent = 'No content available.'; + } if (isEmbed) { - const commandEmbedData = commandEmbed(commandTitle, commandContent, embedColor, commandImage); + const commandEmbedData = commandEmbed(commandTitle, actualCommandContent, embedColor, commandImage); const { footer } = message.embeds[0]; const newFooter = footer?.text ? `${footer.text} - The choice has expired.` : 'The choice has expired.'; commandEmbedData.setFooter({ text: newFooter }); @@ -70,9 +76,7 @@ async function expireChoiceReply(message: Message, commandTitle: string, command if (commandTitle) { content.push(`**${commandTitle}**`); } - if (commandContent) { - content.push(commandContent); - } + content.push(actualCommandContent); content.push('\n`The choice has expired.`'); return message.edit({ content: makeLines(content), From 98eabc8c5b1118c1cceec71ab27661c8e2be5856 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Tue, 15 Oct 2024 23:26:01 -0700 Subject: [PATCH 35/51] Fix boolean handling --- .../moderation/prefixCommands/functions/modifyCommand.ts | 2 +- .../moderation/prefixCommands/functions/modifyVersion.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index 2fe71365..029fad06 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -88,7 +88,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt const description = interaction.options.getString('description') || ''; const aliasesString = interaction.options.getString('aliases') || ''; const aliases = aliasesString !== '' ? aliasesString.split(',') : []; - const isEmbed = interaction.options.getBoolean('is_embed') || null; + const isEmbed = interaction.options.getBoolean('is_embed'); const embedColor = interaction.options.getString('embed_color') || ''; const moderator = interaction.user; diff --git a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts index c27e029e..fce02763 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts @@ -77,7 +77,7 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom const name = interaction.options.getString('name') || ''; const emoji = interaction.options.getString('emoji') || ''; const alias = interaction.options.getString('alias') || ''; - const enabled = interaction.options.getBoolean('is_enabled') || null; + const enabled = interaction.options.getBoolean('is_enabled'); const moderator = interaction.user; const nameRegex = /^[\w\d-_]+$/; From 8076f0b4346b573bfc759c0a2f49139a08068d16 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 16 Oct 2024 21:34:29 -0700 Subject: [PATCH 36/51] Fix: Cache update with additional caches and proper mod output --- .../prefixCommands/prefixCommandCacheUpdate.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/commands/moderation/prefixCommands/prefixCommandCacheUpdate.ts b/src/commands/moderation/prefixCommands/prefixCommandCacheUpdate.ts index a90e3910..a80d5ad4 100644 --- a/src/commands/moderation/prefixCommands/prefixCommandCacheUpdate.ts +++ b/src/commands/moderation/prefixCommands/prefixCommandCacheUpdate.ts @@ -1,5 +1,5 @@ -import { ApplicationCommandType, Colors, EmbedField, TextChannel } from 'discord.js'; -import { constantsConfig, slashCommand, slashCommandStructure, makeEmbed, refreshAllPrefixCommandsCache, refreshAllPrefixCommandVersionsCache } from '../../../lib'; +import { ApplicationCommandType, Colors, EmbedField, TextChannel, User } from 'discord.js'; +import { constantsConfig, slashCommand, slashCommandStructure, makeEmbed, refreshAllPrefixCommandsCache, refreshAllPrefixCommandVersionsCache, refreshAllPrefixCommandCategoriesCache, refreshAllPrefixCommandChannelDefaultVersionsCache } from '../../../lib'; const data = slashCommandStructure({ name: 'prefix-commands-cache-update', @@ -22,10 +22,10 @@ const noChannelEmbed = (channelName: string) => makeEmbed({ color: Colors.Yellow, }); -const cacheUpdateEmbedField = (moderator: string, duration: string): EmbedField[] => [ +const cacheUpdateEmbedField = (moderator: User, duration: string): EmbedField[] => [ { name: 'Moderator', - value: moderator, + value: `${moderator}`, inline: true, }, { @@ -41,15 +41,17 @@ export default slashCommand(data, async ({ interaction }) => { const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; const start = new Date().getTime(); - await refreshAllPrefixCommandsCache(); await refreshAllPrefixCommandVersionsCache(); + await refreshAllPrefixCommandCategoriesCache(); + await refreshAllPrefixCommandsCache(); + await refreshAllPrefixCommandChannelDefaultVersionsCache(); const duration = ((new Date().getTime() - start) / 1000).toFixed(2); await interaction.editReply({ embeds: [cacheUpdateEmbed( cacheUpdateEmbedField( - interaction.user.tag, + interaction.user, duration, ), Colors.Green, @@ -60,7 +62,7 @@ export default slashCommand(data, async ({ interaction }) => { await modLogsChannel.send({ embeds: [cacheUpdateEmbed( cacheUpdateEmbedField( - interaction.user.tag, + interaction.user, duration, ), Colors.Green, From dc11d6e073a0cd26560d6194ce4fa7f4c6d8f4b2 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 16 Oct 2024 21:38:03 -0700 Subject: [PATCH 37/51] Fix: Improvements based on review feedback --- .../prefixCommands/functions/addChannelPermission.ts | 4 ++-- .../moderation/prefixCommands/functions/addCommand.ts | 2 +- .../moderation/prefixCommands/functions/addRolePermission.ts | 2 +- .../moderation/prefixCommands/functions/addVersion.ts | 2 +- .../moderation/prefixCommands/functions/modifyCommand.ts | 4 ++-- .../moderation/prefixCommands/functions/modifyVersion.ts | 2 +- .../prefixCommands/functions/setChannelDefaultVersion.ts | 2 +- .../prefixCommands/functions/setCommandPermissionSettings.ts | 2 +- src/events/messageCreateHandler.ts | 5 ++--- 9 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts index c96bc667..823f277c 100644 --- a/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts +++ b/src/commands/moderation/prefixCommands/functions/addChannelPermission.ts @@ -14,7 +14,7 @@ const noCommandEmbed = (command: string) => makeEmbed({ }); const failedEmbed = (command: string, channel: string) => makeEmbed({ - title: 'Prefix Commands - Add Channel Permission - Failed', + title: 'Prefix Commands - Add Channel - Failed', description: `Failed to add the prefix command channel <#${channel}> for command ${command}.`, color: Colors.Red, }); @@ -26,7 +26,7 @@ const alreadyExistsEmbed = (command: string, channel: string) => makeEmbed({ }); const successEmbed = (command: string, channel: string) => makeEmbed({ - title: `Prefix command channel <#${channel}> added for command ${command}.}`, + title: `Prefix command channel <#${channel}> added for command ${command}.`, color: Colors.Green, }); diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts index 2c7adb02..d5f5ec3a 100644 --- a/src/commands/moderation/prefixCommands/functions/addCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -93,7 +93,7 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera const embedColor = interaction.options.getString('embed_color') || ''; const moderator = interaction.user; - const nameRegex = /^[\w\d-_]+$/; + const nameRegex = /^[\w-]+$/; if (!nameRegex.test(name)) { await interaction.followUp({ embeds: [wrongFormatEmbed(name)], ephemeral: true }); return; diff --git a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts index 74756669..946b24ce 100644 --- a/src/commands/moderation/prefixCommands/functions/addRolePermission.ts +++ b/src/commands/moderation/prefixCommands/functions/addRolePermission.ts @@ -26,7 +26,7 @@ const alreadyExistsEmbed = (command: string, roleName: string) => makeEmbed({ }); const successEmbed = (command: string, roleName: string) => makeEmbed({ - title: `Prefix command role ${roleName} added for command ${command}.}`, + title: `Prefix command role ${roleName} added for command ${command}.`, color: Colors.Green, }); diff --git a/src/commands/moderation/prefixCommands/functions/addVersion.ts b/src/commands/moderation/prefixCommands/functions/addVersion.ts index d2c7dabe..0aa2bcbd 100644 --- a/src/commands/moderation/prefixCommands/functions/addVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/addVersion.ts @@ -80,7 +80,7 @@ export async function handleAddPrefixCommandVersion(interaction: ChatInputComman const enabled = interaction.options.getBoolean('is_enabled') || false; const moderator = interaction.user; - const nameRegex = /^[\w\d-_]+$/; + const nameRegex = /^[\w-]+$/; if (!nameRegex.test(name)) { await interaction.followUp({ embeds: [wrongFormatEmbed(name)], ephemeral: true }); return; diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index 029fad06..12ed37b9 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -92,7 +92,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt const embedColor = interaction.options.getString('embed_color') || ''; const moderator = interaction.user; - const nameRegex = /^[\w\d-_]+$/; + const nameRegex = /^[\w-]+$/; if (name && !nameRegex.test(name)) { await interaction.followUp({ embeds: [wrongFormatEmbed(name)], ephemeral: true }); return; @@ -133,7 +133,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt existingCommand.embedColor = embedColor || existingCommand.embedColor; try { await existingCommand.save(); - const { name, aliases, isEmbed, embedColor } = existingCommand; + const { name, description, aliases, isEmbed, embedColor } = existingCommand; await refreshSinglePrefixCommandCache(oldCommand, existingCommand); await interaction.followUp({ embeds: [successEmbed(name, commandId)], ephemeral: true }); if (modLogsChannel) { diff --git a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts index fce02763..97b9c2a4 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts @@ -80,7 +80,7 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom const enabled = interaction.options.getBoolean('is_enabled'); const moderator = interaction.user; - const nameRegex = /^[\w\d-_]+$/; + const nameRegex = /^[\w-]+$/; if (name && !nameRegex.test(name)) { await interaction.followUp({ embeds: [wrongFormatEmbed(name)], ephemeral: true }); return; diff --git a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts index ea8a34ae..c6ad2252 100644 --- a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts @@ -25,7 +25,7 @@ const successEmbed = (channel: string, version: string, emoji: string) => makeEm }); const modLogEmbed = (moderator: User, channel: string, version: string, emoji: string) => makeEmbed({ - title: 'Prefix command version added', + title: 'Prefix channel default version set', fields: [ { name: 'Channel', diff --git a/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts b/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts index 99ec7a39..8ca7c1c6 100644 --- a/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts +++ b/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts @@ -25,7 +25,7 @@ const successEmbed = (command: string) => makeEmbed({ }); const modLogEmbed = (moderator: User, command: string, rolesBlocklist: boolean, channelsBlocklist: boolean, quietErrors: boolean, verboseErrors: boolean) => makeEmbed({ - title: 'Prefix command version added', + title: 'Prefix command permission set', fields: [ { name: 'Command', diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index f3d091b5..dea5af8e 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -1,6 +1,5 @@ import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder, Interaction, Message } from 'discord.js'; -import { event, getInMemoryCache, memoryCachePrefixCommand, memoryCachePrefixVersion, memoryCachePrefixChannelDefaultVersion, Logger, Events, constantsConfig, makeEmbed, makeLines } from '../lib'; -import { PrefixCommand, PrefixCommandPermissions, PrefixCommandVersion } from '../lib/schemas/prefixCommandSchemas'; +import { event, getInMemoryCache, memoryCachePrefixCommand, memoryCachePrefixVersion, memoryCachePrefixChannelDefaultVersion, Logger, Events, constantsConfig, makeEmbed, makeLines, PrefixCommand, PrefixCommandPermissions, PrefixCommandVersion } from '../lib'; const commandEmbed = (title: string, description: string, color: string, imageUrl: string = '') => makeEmbed({ title, @@ -249,7 +248,7 @@ export default event(Events.MessageCreate, async (_, message) => { collector.on('collect', async (collectedInteraction: ButtonInteraction) => { Logger.debug(`Prefix Command - User selected version "${collectedInteraction.customId}" for command "${name}" based on user command "${commandText}"`); await collectedInteraction.deferUpdate(); - buttonMessage.delete(); + await buttonMessage.delete(); const { customId: selectedVersionId } = collectedInteraction; const commandContentData = contents.find(({ versionId }) => versionId === selectedVersionId); if (!commandContentData) { From f00ed5c95bf5d409a1fc1715dcde1731f8dc3936 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 16 Oct 2024 21:39:00 -0700 Subject: [PATCH 38/51] Fix: review issues and bug on missing max number of choices --- src/commands/utils/prefixHelp.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/commands/utils/prefixHelp.ts b/src/commands/utils/prefixHelp.ts index 613df0db..da313819 100644 --- a/src/commands/utils/prefixHelp.ts +++ b/src/commands/utils/prefixHelp.ts @@ -1,6 +1,5 @@ import { ApplicationCommandOptionChoiceData, ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'; -import { makeEmbed, createPaginatedEmbedHandler, slashCommand, slashCommandStructure, getInMemoryCache, memoryCachePrefixCommand, AutocompleteCallback, memoryCachePrefixCategory, makeLines, memoryCachePrefixVersion, Logger } from '../../lib'; -import { PrefixCommand, PrefixCommandVersion, PrefixCommandCategory, IPrefixCommand } from '../../lib/schemas/prefixCommandSchemas'; +import { makeEmbed, createPaginatedEmbedHandler, slashCommand, slashCommandStructure, getInMemoryCache, memoryCachePrefixCommand, AutocompleteCallback, memoryCachePrefixCategory, makeLines, memoryCachePrefixVersion, Logger, PrefixCommand, PrefixCommandVersion, PrefixCommandCategory, IPrefixCommand } from '../../lib'; const data = slashCommandStructure({ name: 'prefix-help', @@ -43,6 +42,9 @@ const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { const category = PrefixCommandCategory.hydrate(categoryCached); const { name } = category; choices.push({ name, value: name }); + if (choices.length >= 25) { + break; + } } } } @@ -56,6 +58,9 @@ const autocompleteCallback: AutocompleteCallback = async ({ interaction }) => { // Explicitly does not use the cache to hydrate the command to also capture aliases, resulting in commands const commandName = key.split(':')[1]; choices.push({ name: commandName, value: commandName }); + if (choices.length >= 25) { + break; + } } } } From 573862d9a16d15c680b50cb6957a704c6837a8a6 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 16 Oct 2024 21:39:48 -0700 Subject: [PATCH 39/51] Change order of cache update to be in line with command --- src/lib/schedulerJobs/refreshInMemoryCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/schedulerJobs/refreshInMemoryCache.ts b/src/lib/schedulerJobs/refreshInMemoryCache.ts index de020150..fcb6a6ed 100644 --- a/src/lib/schedulerJobs/refreshInMemoryCache.ts +++ b/src/lib/schedulerJobs/refreshInMemoryCache.ts @@ -24,9 +24,9 @@ export async function refreshInMemoryCache(job: Job) { const start = new Date().getTime(); try { - await refreshAllPrefixCommandsCache(); await refreshAllPrefixCommandVersionsCache(); await refreshAllPrefixCommandCategoriesCache(); + await refreshAllPrefixCommandsCache(); await refreshAllPrefixCommandChannelDefaultVersionsCache(); } catch (error) { Logger.error('Failed to refresh the in memory cache:', error); From 5bfc280ab5897ace0e873fda67f24daee32104c4 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 16 Oct 2024 22:15:33 -0700 Subject: [PATCH 40/51] Fix: Fixing a potential race condition to set the clicked immediately --- src/events/messageCreateHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index dea5af8e..dd7676f4 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -246,8 +246,9 @@ export default event(Events.MessageCreate, async (_, message) => { const collector = buttonMessage.createMessageComponentCollector({ filter, time: 60_000 }); let buttonClicked = false; collector.on('collect', async (collectedInteraction: ButtonInteraction) => { - Logger.debug(`Prefix Command - User selected version "${collectedInteraction.customId}" for command "${name}" based on user command "${commandText}"`); + buttonClicked = true; await collectedInteraction.deferUpdate(); + Logger.debug(`Prefix Command - User selected version "${collectedInteraction.customId}" for command "${name}" based on user command "${commandText}"`); await buttonMessage.delete(); const { customId: selectedVersionId } = collectedInteraction; const commandContentData = contents.find(({ versionId }) => versionId === selectedVersionId); @@ -256,7 +257,6 @@ export default event(Events.MessageCreate, async (_, message) => { return; } const { title: commandTitle, content: commandContent, image: commandImage } = commandContentData; - buttonClicked = true; await sendReply(message, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || ''); }); From 1b771fb67da33a6a2b6c0a10aa2739f2b83ff628 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 16 Oct 2024 22:24:01 -0700 Subject: [PATCH 41/51] Fix: Actually fixes the race condition properly by checking for the right event --- src/events/messageCreateHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index dd7676f4..16587c2f 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -260,8 +260,8 @@ export default event(Events.MessageCreate, async (_, message) => { await sendReply(message, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || ''); }); - collector.on('end', async () => { - if (!buttonClicked) { + collector.on('end', async (_: ButtonInteraction, reason: string) => { + if (!buttonClicked && reason === 'time') { Logger.debug(`Prefix Command - User did not select a version for command "${name}" based on user command "${commandText}"`); await expireChoiceReply(buttonMessage, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || ''); } From 539eae2fb56592329d5764ebcfddcb823d36e3a2 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Wed, 16 Oct 2024 22:38:37 -0700 Subject: [PATCH 42/51] Disabling buttons if it is a fallback to GENERIC version --- src/events/messageCreateHandler.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index 16587c2f..c7cfe59b 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -207,10 +207,12 @@ export default event(Events.MessageCreate, async (_, message) => { } let commandContentData = contents.find(({ versionId }) => versionId === commandVersionId); + let enableButtons = true; // If the version is not found, try to find the generic version if (!commandContentData) { commandContentData = contents.find(({ versionId }) => versionId === 'GENERIC'); commandVersionName = 'GENERIC'; + enableButtons = false; } // If the generic version is not found, drop execution if (!commandContentData) { @@ -221,7 +223,7 @@ export default event(Events.MessageCreate, async (_, message) => { // If generic requested and multiple versions, show the selection // Note that this only applies if GENERIC is the version explicitly requested // Otherwise, the options are not shown - if (commandVersionName === 'GENERIC' && contents.length > 1) { + if (enableButtons && commandVersionName === 'GENERIC' && contents.length > 1) { Logger.debug(`Prefix Command - Multiple versions found for command "${name}" based on user command "${commandText}", showing version selection`); const versionSelectionButtonData: { [key: string]: ButtonBuilder } = {}; for (const { versionId: versionIdForButton } of contents) { From 0b2119ba485858e756d8ee896b97dc20d1223e61 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 20 Oct 2024 09:59:51 -0700 Subject: [PATCH 43/51] Fix bugs - crash for unexisting version in setContext and missing db returns --- src/commands/moderation/prefixCommands/functions/addCategory.ts | 2 +- src/commands/moderation/prefixCommands/functions/addCommand.ts | 1 - src/commands/moderation/prefixCommands/functions/addVersion.ts | 1 - .../moderation/prefixCommands/functions/modifyCommand.ts | 1 + .../prefixCommands/functions/setChannelDefaultVersion.ts | 1 - .../prefixCommands/functions/setCommandPermissionSettings.ts | 1 - src/commands/moderation/prefixCommands/functions/setContent.ts | 2 ++ 7 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/addCategory.ts b/src/commands/moderation/prefixCommands/functions/addCategory.ts index 58709c1f..07890e46 100644 --- a/src/commands/moderation/prefixCommands/functions/addCategory.ts +++ b/src/commands/moderation/prefixCommands/functions/addCategory.ts @@ -54,9 +54,9 @@ export async function handleAddPrefixCommandCategory(interaction: ChatInputComma await interaction.deferReply({ ephemeral: true }); const conn = getConn(); - if (!conn) { await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; } const name = interaction.options.getString('name')!; diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts index d5f5ec3a..db1c751d 100644 --- a/src/commands/moderation/prefixCommands/functions/addCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -78,7 +78,6 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera await interaction.deferReply({ ephemeral: true }); const conn = getConn(); - if (!conn) { await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); return; diff --git a/src/commands/moderation/prefixCommands/functions/addVersion.ts b/src/commands/moderation/prefixCommands/functions/addVersion.ts index 0aa2bcbd..e1b4e2eb 100644 --- a/src/commands/moderation/prefixCommands/functions/addVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/addVersion.ts @@ -68,7 +68,6 @@ export async function handleAddPrefixCommandVersion(interaction: ChatInputComman await interaction.deferReply({ ephemeral: true }); const conn = getConn(); - if (!conn) { await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); return; diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index 12ed37b9..7ebbca24 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -80,6 +80,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt const conn = getConn(); if (!conn) { await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); + return; } const command = interaction.options.getString('command')!; diff --git a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts index c6ad2252..662c933c 100644 --- a/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/setChannelDefaultVersion.ts @@ -53,7 +53,6 @@ export async function handleSetPrefixCommandChannelDefaultVersion(interaction: C await interaction.deferReply({ ephemeral: true }); const conn = getConn(); - if (!conn) { await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); return; diff --git a/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts b/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts index 8ca7c1c6..888d2310 100644 --- a/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts +++ b/src/commands/moderation/prefixCommands/functions/setCommandPermissionSettings.ts @@ -65,7 +65,6 @@ export async function handleSetPrefixCommandPermissionSettings(interaction: Chat await interaction.deferReply({ ephemeral: true }); const conn = getConn(); - if (!conn) { await interaction.followUp({ embeds: [noConnEmbed], ephemeral: true }); return; diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index fabb40b5..7289044e 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -69,6 +69,8 @@ const noModLogs = makeEmbed({ }); export async function handleSetPrefixCommandContent(interaction: ChatInputCommandInteraction<'cached'>) { + await interaction.deferReply({ ephemeral: true }); + const conn = getConn(); if (!conn) { await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); From f26c5ae059f98a0179fa7e07c4eac15621eb9c93 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 20 Oct 2024 11:32:31 -0700 Subject: [PATCH 44/51] Fix bugs: * Showing buttons for disabled versions on generic content * Not showing anything if channel default is disabled --- src/events/messageCreateHandler.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index c7cfe59b..f463f769 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -135,19 +135,27 @@ export default event(Events.MessageCreate, async (_, message) => { } // Step 2: Check if there's a default version for the channel if commandVersionName is GENERIC + let channelDefaultVersionUsed = false; if (commandVersionName === 'GENERIC' && !commandVersionExplicitGeneric) { const channelDefaultVersionCached = await inMemoryCache.get(`${memoryCachePrefixChannelDefaultVersion}:${channelId}`); if (channelDefaultVersionCached) { const channelDefaultVersion = PrefixCommandVersion.hydrate(channelDefaultVersionCached); ({ id: commandVersionId, name: commandVersionName, enabled: commandVersionEnabled } = channelDefaultVersion); + channelDefaultVersionUsed = true; } } - // Drop execution if the version is disabled - if (!commandVersionEnabled) { + // Drop execution if the version is disabled and we aren't using the default version for a channel + if (!commandVersionEnabled && !channelDefaultVersionUsed) { Logger.debug(`Prefix Command - Version "${commandVersionName}" is disabled - Not executing command "${commandText}"`); return; } + // If the version is disabled and we are using the default version for a channel, switch to the generic version + if (!commandVersionEnabled && channelDefaultVersionUsed) { + commandVersionId = 'GENERIC'; + commandVersionName = 'GENERIC'; + commandVersionEnabled = true; + } // Step 2.5: If the first command was actually a version alias, take the actual command as CommandText if ((commandCachedVersion || commandVersionExplicitGeneric) && commandTextMatch[2]) { @@ -231,11 +239,13 @@ export default event(Events.MessageCreate, async (_, message) => { const versionCached = await inMemoryCache.get(`${memoryCachePrefixVersion}:${versionIdForButton}`); if (versionCached) { const version = PrefixCommandVersion.hydrate(versionCached); - const { emoji } = version; - versionSelectionButtonData[emoji] = new ButtonBuilder() - .setCustomId(`${versionIdForButton}`) - .setEmoji(emoji) - .setStyle(ButtonStyle.Primary); + const { emoji, enabled } = version; + if (enabled) { + versionSelectionButtonData[emoji] = new ButtonBuilder() + .setCustomId(`${versionIdForButton}`) + .setEmoji(emoji) + .setStyle(ButtonStyle.Primary); + } } } const versionSelectionButtons: ButtonBuilder[] = Object.keys(versionSelectionButtonData) From ffd38884f20a9ce55544c18454799681dfcb78f1 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 20 Oct 2024 14:14:49 -0700 Subject: [PATCH 45/51] Fix bug: Add name and aliases uniqueness --- .../prefixCommands/functions/addCommand.ts | 99 +++++++++++-------- .../prefixCommands/functions/modifyCommand.ts | 57 ++++++++++- .../prefixCommands/functions/modifyVersion.ts | 2 +- 3 files changed, 114 insertions(+), 44 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts index db1c751d..7f1a1d24 100644 --- a/src/commands/moderation/prefixCommands/functions/addCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandCategory, loadSinglePrefixCommandToCache } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandCategory, loadSinglePrefixCommandToCache, PrefixCommandVersion } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Add Command - No Connection', @@ -25,9 +25,9 @@ const categoryNotFoundEmbed = (category: string) => makeEmbed({ color: Colors.Red, }); -const alreadyExistsEmbed = (command: string) => makeEmbed({ +const alreadyExistsEmbed = (command: string, reason: string) => makeEmbed({ title: 'Prefix Commands - Add Command - Already exists', - description: `The prefix command ${command} already exists. Not adding again.`, + description: `The prefix command ${command} can not be added: ${reason}`, color: Colors.Red, }); @@ -83,10 +83,10 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera return; } - const name = interaction.options.getString('name')!; + const name = interaction.options.getString('name')?.toLowerCase()!; const category = interaction.options.getString('category')!; const description = interaction.options.getString('description')!; - const aliasesString = interaction.options.getString('aliases') || ''; + const aliasesString = interaction.options.getString('aliases')?.toLowerCase() || ''; const aliases = aliasesString !== '' ? aliasesString.split(',') : []; const isEmbed = interaction.options.getBoolean('is_embed') || false; const embedColor = interaction.options.getString('embed_color') || ''; @@ -105,6 +105,30 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera } } + // Check if command name and alias are unique, additionally check if they do not exist as a version alias. + const foundCommandName = await PrefixCommand.findOne({ + $or: [ + { name }, + { name: { $in: aliases } }, + { aliases: name }, + { aliases: { $in: aliases } }, + ], + }); + if (foundCommandName) { + await interaction.followUp({ embeds: [alreadyExistsEmbed(name, `${name} already exists as a command or alias, or one of the aliases already exists as a command or alias.`)], ephemeral: true }); + return; + } + const foundVersion = await PrefixCommandVersion.findOne({ + $or: [ + { alias: name }, + { alias: { $in: aliases } }, + ], + }); + if (foundVersion) { + await interaction.followUp({ embeds: [alreadyExistsEmbed(name, `${name} already exists as a version alias, or one of the aliases already exists as a version alias.`)], ephemeral: true }); + return; + } + //Check if the mod logs channel exists const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (!modLogsChannel) { @@ -118,42 +142,37 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera } const { id: categoryId } = foundCategory; Logger.info(`categoryId: ${categoryId}`); - const existingCommand = await PrefixCommand.findOne({ name }); - - if (!existingCommand) { - const prefixCommand = new PrefixCommand({ - name, - categoryId, - aliases, - description, - isEmbed, - embedColor, - contents: [], - permissions: { - roles: [], - rolesBlocklist: false, - channels: [], - channelsBlocklist: false, - quietErrors: false, - verboseErrors: false, - }, - }); - try { - await prefixCommand.save(); - await loadSinglePrefixCommandToCache(prefixCommand); - await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); - if (modLogsChannel) { - try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, aliases, description, isEmbed, embedColor, prefixCommand.id)] }); - } catch (error) { - Logger.error(`Failed to post a message to the mod logs channel: ${error}`); - } + + const prefixCommand = new PrefixCommand({ + name, + categoryId, + aliases, + description, + isEmbed, + embedColor, + contents: [], + permissions: { + roles: [], + rolesBlocklist: false, + channels: [], + channelsBlocklist: false, + quietErrors: false, + verboseErrors: false, + }, + }); + try { + await prefixCommand.save(); + await loadSinglePrefixCommandToCache(prefixCommand); + await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, aliases, description, isEmbed, embedColor, prefixCommand.id)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } - } catch (error) { - Logger.error(`Failed to add a prefix command ${name}: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(name)], ephemeral: true }); } - } else { - await interaction.followUp({ embeds: [alreadyExistsEmbed(name)], ephemeral: true }); + } catch (error) { + Logger.error(`Failed to add a prefix command ${name}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(name)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index 7ebbca24..162778e1 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandCategory, refreshSinglePrefixCommandCache } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommand, Logger, makeEmbed, PrefixCommandCategory, refreshSinglePrefixCommandCache, PrefixCommandVersion } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Modify Command - No Connection', @@ -31,6 +31,12 @@ const doesNotExistsEmbed = (command: string) => makeEmbed({ color: Colors.Red, }); +const alreadyExistsEmbed = (command: string, reason: string) => makeEmbed({ + title: 'Prefix Commands - Modify Command - Already exists', + description: `The prefix command ${command} can not be modified: ${reason}`, + color: Colors.Red, +}); + const successEmbed = (command: string, commandId: string) => makeEmbed({ title: `Prefix command ${command} (${commandId}) was modified successfully.`, color: Colors.Green, @@ -84,10 +90,10 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt } const command = interaction.options.getString('command')!; - const name = interaction.options.getString('name') || ''; + const name = interaction.options.getString('name')?.toLowerCase() || ''; const category = interaction.options.getString('category') || ''; const description = interaction.options.getString('description') || ''; - const aliasesString = interaction.options.getString('aliases') || ''; + const aliasesString = interaction.options.getString('aliases')?.toLowerCase() || ''; const aliases = aliasesString !== '' ? aliasesString.split(',') : []; const isEmbed = interaction.options.getBoolean('is_embed'); const embedColor = interaction.options.getString('embed_color') || ''; @@ -105,6 +111,51 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt return; } } + // Check if command name and alias are unique, additionally check if they do not exist as a version alias. + if (name) { + const foundCommandName = await PrefixCommand.findOne({ + name: { $ne: command }, + $or: [ + { name }, + { aliases: name }, + ], + }); + if (foundCommandName) { + await interaction.followUp({ embeds: [alreadyExistsEmbed(command, `${name} already exists as a different command or alias.`)], ephemeral: true }); + return; + } + const foundVersion = await PrefixCommandVersion.findOne({ + $or: [ + { alias: name }, + ], + }); + if (foundVersion) { + await interaction.followUp({ embeds: [alreadyExistsEmbed(command, `${name} already exists as a version alias.`)], ephemeral: true }); + return; + } + } + if (aliases.length > 0) { + const foundCommandName = await PrefixCommand.findOne({ + name: { $ne: command }, + $or: [ + { name: { $in: aliases } }, + { aliases: { $in: aliases } }, + ], + }); + if (foundCommandName) { + await interaction.followUp({ embeds: [alreadyExistsEmbed(command, 'The new aliases contain an alias that already exists as a different command or alias.')], ephemeral: true }); + return; + } + const foundVersion = await PrefixCommandVersion.findOne({ + $or: [ + { alias: { $in: aliases } }, + ], + }); + if (foundVersion) { + await interaction.followUp({ embeds: [alreadyExistsEmbed(command, 'The new aliases contain an alias that already exists as a version alias.')], ephemeral: true }); + return; + } + } //Check if the mod logs channel exists const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; diff --git a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts index 97b9c2a4..71373996 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts @@ -76,7 +76,7 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom const version = interaction.options.getString('version')!; const name = interaction.options.getString('name') || ''; const emoji = interaction.options.getString('emoji') || ''; - const alias = interaction.options.getString('alias') || ''; + const alias = interaction.options.getString('alias')?.toLowerCase() || ''; const enabled = interaction.options.getBoolean('is_enabled'); const moderator = interaction.user; From 551b7ebd3b6d11b52f1a4a531dafcff610c3f86d Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 20 Oct 2024 15:40:44 -0700 Subject: [PATCH 46/51] Fix bugs in Content management * Not properly checking commands and versions * Not prorperly using defer/reply/followUp --- .../prefixCommands/functions/deleteContent.ts | 37 ++++++++++++++++--- .../prefixCommands/functions/setContent.ts | 9 ++--- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/deleteContent.ts b/src/commands/moderation/prefixCommands/functions/deleteContent.ts index a54704bc..72a07de1 100644 --- a/src/commands/moderation/prefixCommands/functions/deleteContent.ts +++ b/src/commands/moderation/prefixCommands/functions/deleteContent.ts @@ -13,6 +13,18 @@ const noContentEmbed = (command: string, version: string) => makeEmbed({ color: Colors.Red, }); +const noCommandEmbed = (command: string) => makeEmbed({ + title: 'Prefix Commands - Delete Content - No Command', + description: `Failed to delete command content for command ${command} as the command does not exist or there are more than one matching.`, + color: Colors.Red, +}); + +const noVersionEmbed = (version: string) => makeEmbed({ + title: 'Prefix Commands - Delete Content - No Version', + description: `Failed to delete command content for version ${version} as the version does not exist or there are more than one matching.`, + color: Colors.Red, +}); + const failedEmbed = (version: string) => makeEmbed({ title: 'Prefix Commands - Delete Content - Failed', description: `Failed to delete the prefix command content with version ${version}.`, @@ -52,7 +64,7 @@ const modLogEmbed = (moderator: User, commandName: string, versionName: string, value: `${moderator}`, }, ], - color: Colors.Green, + color: Colors.Red, }); const noModLogs = makeEmbed({ @@ -80,10 +92,25 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - const foundVersion = await PrefixCommandVersion.findOne({ name: version }); - const { versionId } = foundVersion ?? { versionId: 'GENERIC' }; const foundCommand = await PrefixCommand.findOne({ name: command }); - const [existingContent] = foundCommand?.contents.filter((content) => content.versionId === versionId) ?? []; + if (!foundCommand) { + await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); + return; + } + let versionId = ''; + let foundVersions = null; + if (version === 'GENERIC' || version === 'generic') { + versionId = 'GENERIC'; + } else { + foundVersions = await PrefixCommandVersion.find({ name: version }); + if (foundVersions && foundVersions.length === 1) { + [{ _id: versionId }] = foundVersions; + } else { + await interaction.followUp({ embeds: [noVersionEmbed(version)], ephemeral: true }); + return; + } + } + const existingContent = foundCommand.contents.find((content) => content.versionId.toString() === versionId.toString()); if (foundCommand && existingContent) { const { title, content, image } = existingContent; @@ -97,7 +124,7 @@ export async function handleDeletePrefixCommandContent(interaction: ChatInputCom versionName = foundVersion.name || ''; } try { - foundCommand.contents.find((con) => con.versionId === versionId)?.deleteOne(); + foundCommand.contents.find((con) => con.versionId.toString() === versionId.toString())?.deleteOne(); await foundCommand.save(); await refreshSinglePrefixCommandCache(foundCommand, foundCommand); await interaction.followUp({ embeds: [successEmbed(`${commandName}`, `${versionName}`)], ephemeral: true }); diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index 7289044e..adb4d71a 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -69,8 +69,6 @@ const noModLogs = makeEmbed({ }); export async function handleSetPrefixCommandContent(interaction: ChatInputCommandInteraction<'cached'>) { - await interaction.deferReply({ ephemeral: true }); - const conn = getConn(); if (!conn) { await interaction.reply({ embeds: [noConnEmbed], ephemeral: true }); @@ -86,7 +84,7 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman foundCommands = await PrefixCommand.find({ aliases: { $in: [command] } }); } if (!foundCommands || foundCommands.length !== 1) { - await interaction.followUp({ embeds: [noCommandEmbed(command)], ephemeral: true }); + await interaction.reply({ embeds: [noCommandEmbed(command)], ephemeral: true }); return; } @@ -101,13 +99,12 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman if (foundVersions && foundVersions.length === 1) { [{ _id: versionId }] = foundVersions; } else { - await interaction.followUp({ embeds: [noVersionEmbed(version)], ephemeral: true }); + await interaction.reply({ embeds: [noVersionEmbed(version)], ephemeral: true }); return; } } - const foundContent = foundCommand.contents.find((c) => c.versionId === versionId); - + const foundContent = foundCommand.contents.find((c) => c.versionId.toString() === versionId.toString()); const contentModal = new ModalBuilder({ customId: 'commandContentModal', title: `Content for ${command} - ${version}`, From 556caeec6fe7a6f71e48c95d4a68bfb450bdd842 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 20 Oct 2024 15:45:21 -0700 Subject: [PATCH 47/51] Fix: Checking for empty content --- .../prefixCommands/functions/setContent.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/setContent.ts b/src/commands/moderation/prefixCommands/functions/setContent.ts index adb4d71a..20089239 100644 --- a/src/commands/moderation/prefixCommands/functions/setContent.ts +++ b/src/commands/moderation/prefixCommands/functions/setContent.ts @@ -166,14 +166,21 @@ export async function handleSetPrefixCommandContent(interaction: ChatInputComman time: 120000, }); + title = modalSubmitInteraction.fields.getTextInputValue('commandContentTitle').trim(); + content = modalSubmitInteraction.fields.getTextInputValue('commandContentContent').trim(); + image = modalSubmitInteraction.fields.getTextInputValue('commandContentImageUrl').trim(); + + if (!title && !content && !image) { + await modalSubmitInteraction.reply({ + content: 'You did not provide any content information and the change was not made.', + ephemeral: true, + }); + return; + } await modalSubmitInteraction.reply({ content: 'Processing command content data.', ephemeral: true, }); - - title = modalSubmitInteraction.fields.getTextInputValue('commandContentTitle').trim(); - content = modalSubmitInteraction.fields.getTextInputValue('commandContentContent').trim(); - image = modalSubmitInteraction.fields.getTextInputValue('commandContentImageUrl').trim(); } catch (error) { //Handle the error if the user does not respond in time Logger.error(error); From 1ddc66e35bba1e2d6a3840dfac88aef941da9abe Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 20 Oct 2024 16:11:15 -0700 Subject: [PATCH 48/51] Fix verifying generic does not get created/used for versions or commands. And version aliases do not overlap with commands --- .../prefixCommands/functions/addCommand.ts | 6 +- .../prefixCommands/functions/addVersion.ts | 72 +++++++++++-------- .../prefixCommands/functions/modifyCommand.ts | 8 +-- .../prefixCommands/functions/modifyVersion.ts | 40 ++++++++++- 4 files changed, 90 insertions(+), 36 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/addCommand.ts b/src/commands/moderation/prefixCommands/functions/addCommand.ts index 7f1a1d24..ebcbe8a6 100644 --- a/src/commands/moderation/prefixCommands/functions/addCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/addCommand.ts @@ -83,10 +83,10 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera return; } - const name = interaction.options.getString('name')?.toLowerCase()!; + const name = interaction.options.getString('name')?.toLowerCase().trim()!; const category = interaction.options.getString('category')!; const description = interaction.options.getString('description')!; - const aliasesString = interaction.options.getString('aliases')?.toLowerCase() || ''; + const aliasesString = interaction.options.getString('aliases')?.toLowerCase().trim() || ''; const aliases = aliasesString !== '' ? aliasesString.split(',') : []; const isEmbed = interaction.options.getBoolean('is_embed') || false; const embedColor = interaction.options.getString('embed_color') || ''; @@ -124,7 +124,7 @@ export async function handleAddPrefixCommand(interaction: ChatInputCommandIntera { alias: { $in: aliases } }, ], }); - if (foundVersion) { + if (foundVersion || name.toLowerCase() === 'generic' || aliases.includes('generic')) { await interaction.followUp({ embeds: [alreadyExistsEmbed(name, `${name} already exists as a version alias, or one of the aliases already exists as a version alias.`)], ephemeral: true }); return; } diff --git a/src/commands/moderation/prefixCommands/functions/addVersion.ts b/src/commands/moderation/prefixCommands/functions/addVersion.ts index e1b4e2eb..8126ccdc 100644 --- a/src/commands/moderation/prefixCommands/functions/addVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/addVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, loadSinglePrefixCommandVersionToCache } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, loadSinglePrefixCommandVersionToCache, PrefixCommand } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Add Version - No Connection', @@ -19,9 +19,9 @@ const wrongFormatEmbed = (invalidString: string) => makeEmbed({ color: Colors.Red, }); -const alreadyExistsEmbed = (version: string) => makeEmbed({ +const alreadyExistsEmbed = (version: string, reason: string) => makeEmbed({ title: 'Prefix Commands - Add Version - Already exists', - description: `The prefix command version ${version} already exists. Not adding again.`, + description: `The prefix command version ${version} already exists: ${reason}`, color: Colors.Red, }); @@ -75,7 +75,7 @@ export async function handleAddPrefixCommandVersion(interaction: ChatInputComman const name = interaction.options.getString('name')!; const emoji = interaction.options.getString('emoji')!; - const alias = interaction.options.getString('alias')!; + const alias = interaction.options.getString('alias')!.toLowerCase(); const enabled = interaction.options.getBoolean('is_enabled') || false; const moderator = interaction.user; @@ -90,37 +90,53 @@ export async function handleAddPrefixCommandVersion(interaction: ChatInputComman return; } + // Check if a command or version exists with the same name or alias + const foundCommandName = await PrefixCommand.findOne({ + $or: [ + { name: alias }, + { aliases: alias }, + ], + }); + if (foundCommandName) { + await interaction.followUp({ embeds: [alreadyExistsEmbed(name, `${alias} already exists as a command or alias.`)], ephemeral: true }); + return; + } + const foundVersion = await PrefixCommandVersion.findOne({ + $or: [ + { name }, + { alias }, + ], + }); + if (foundVersion || name.toLowerCase() === 'generic' || alias === 'generic') { + await interaction.followUp({ embeds: [alreadyExistsEmbed(name, `${alias} already exists as a version alias.`)], ephemeral: true }); + return; + } + //Check if the mod logs channel exists const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (!modLogsChannel) { await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); } - const existingVersion = await PrefixCommandVersion.findOne({ name }); - - if (!existingVersion) { - const prefixCommandVersion = new PrefixCommandVersion({ - name, - emoji, - enabled, - alias, - }); - try { - await prefixCommandVersion.save(); - await loadSinglePrefixCommandVersionToCache(prefixCommandVersion); - await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); - if (modLogsChannel) { - try { - await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, emoji, alias, enabled, prefixCommandVersion.id)] }); - } catch (error) { - Logger.error(`Failed to post a message to the mod logs channel: ${error}`); - } + const prefixCommandVersion = new PrefixCommandVersion({ + name, + emoji, + enabled, + alias, + }); + try { + await prefixCommandVersion.save(); + await loadSinglePrefixCommandVersionToCache(prefixCommandVersion); + await interaction.followUp({ embeds: [successEmbed(name)], ephemeral: true }); + if (modLogsChannel) { + try { + await modLogsChannel.send({ embeds: [modLogEmbed(moderator, name, emoji, alias, enabled, prefixCommandVersion.id)] }); + } catch (error) { + Logger.error(`Failed to post a message to the mod logs channel: ${error}`); } - } catch (error) { - Logger.error(`Failed to add a prefix command category ${name}: ${error}`); - await interaction.followUp({ embeds: [failedEmbed(name)], ephemeral: true }); } - } else { - await interaction.followUp({ embeds: [alreadyExistsEmbed(name)], ephemeral: true }); + } catch (error) { + Logger.error(`Failed to add a prefix command category ${name}: ${error}`); + await interaction.followUp({ embeds: [failedEmbed(name)], ephemeral: true }); } } diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index 162778e1..d26348de 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -90,10 +90,10 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt } const command = interaction.options.getString('command')!; - const name = interaction.options.getString('name')?.toLowerCase() || ''; + const name = interaction.options.getString('name')?.toLowerCase().trim() || ''; const category = interaction.options.getString('category') || ''; const description = interaction.options.getString('description') || ''; - const aliasesString = interaction.options.getString('aliases')?.toLowerCase() || ''; + const aliasesString = interaction.options.getString('aliases')?.toLowerCase().trim() || ''; const aliases = aliasesString !== '' ? aliasesString.split(',') : []; const isEmbed = interaction.options.getBoolean('is_embed'); const embedColor = interaction.options.getString('embed_color') || ''; @@ -129,7 +129,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt { alias: name }, ], }); - if (foundVersion) { + if (foundVersion || name === 'generic') { await interaction.followUp({ embeds: [alreadyExistsEmbed(command, `${name} already exists as a version alias.`)], ephemeral: true }); return; } @@ -151,7 +151,7 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt { alias: { $in: aliases } }, ], }); - if (foundVersion) { + if (foundVersion || aliases.includes('generic')) { await interaction.followUp({ embeds: [alreadyExistsEmbed(command, 'The new aliases contain an alias that already exists as a version alias.')], ephemeral: true }); return; } diff --git a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts index 71373996..93b6992f 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyVersion.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyVersion.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, Colors, TextChannel, User } from 'discord.js'; -import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, refreshSinglePrefixCommandVersionCache } from '../../../../lib'; +import { constantsConfig, getConn, PrefixCommandVersion, Logger, makeEmbed, refreshSinglePrefixCommandVersionCache, PrefixCommand } from '../../../../lib'; const noConnEmbed = makeEmbed({ title: 'Prefix Commands - Modify Version - No Connection', @@ -25,6 +25,12 @@ const doesNotExistsEmbed = (version: string) => makeEmbed({ color: Colors.Red, }); +const alreadyExistsEmbed = (version: string, reason: string) => makeEmbed({ + title: 'Prefix Commands - Add Version - Already exists', + description: `The prefix command version ${version} already exists: ${reason}`, + color: Colors.Red, +}); + const successEmbed = (version: string, versionId: string) => makeEmbed({ title: `Prefix command version ${version} (${versionId}) was modified successfully.`, color: Colors.Green, @@ -90,6 +96,38 @@ export async function handleModifyPrefixCommandVersion(interaction: ChatInputCom await interaction.followUp({ embeds: [wrongFormatEmbed(alias)], ephemeral: true }); return; } + if (name) { + const foundVersion = await PrefixCommandVersion.findOne({ + name: { + $ne: version, + $eq: name, + }, + }); + if (foundVersion || name.toLowerCase() === 'generic') { + await interaction.followUp({ embeds: [alreadyExistsEmbed(version, `${name} already exists as a version.`)], ephemeral: true }); + return; + } + } + if (alias) { + const foundVersion = await PrefixCommandVersion.findOne({ + name: { $ne: version }, + alias, + }); + if (foundVersion || alias === 'generic') { + await interaction.followUp({ embeds: [alreadyExistsEmbed(version, `${alias} already exists as a version alias.`)], ephemeral: true }); + return; + } + const foundCommandName = await PrefixCommand.findOne({ + $or: [ + { name: alias }, + { aliases: alias }, + ], + }); + if (foundCommandName) { + await interaction.followUp({ embeds: [alreadyExistsEmbed(version, `${alias} already exists as a command or command alias.`)], ephemeral: true }); + return; + } + } //Check if the mod logs channel exists const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; From d07d7be3bfa8eec16ebc15f27c3b08e4974aebe4 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 20 Oct 2024 16:43:18 -0700 Subject: [PATCH 49/51] Fix bug where if no content for enabled versions was found it could crash --- src/events/messageCreateHandler.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index f463f769..91e0ed1e 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -147,6 +147,9 @@ export default event(Events.MessageCreate, async (_, message) => { // Drop execution if the version is disabled and we aren't using the default version for a channel if (!commandVersionEnabled && !channelDefaultVersionUsed) { + if ((commandCachedVersion || commandVersionExplicitGeneric) && commandTextMatch[2]) { + [commandText] = commandTextMatch.slice(2); + } Logger.debug(`Prefix Command - Version "${commandVersionName}" is disabled - Not executing command "${commandText}"`); return; } @@ -252,6 +255,13 @@ export default event(Events.MessageCreate, async (_, message) => { .sort() .map((key: string) => versionSelectionButtonData[key]); const versionSelectButtonRow = new ActionRowBuilder().addComponents(versionSelectionButtons); + + if (versionSelectButtonRow.components.length < 1) { + Logger.debug(`Prefix Command - No enabled versions found for command "${name}" based on user command "${commandText}"`); + Logger.debug(`Prefix Command - Executing version "${commandVersionName}" for command "${name}" based on user command "${commandText}"`); + await sendReply(message, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || ''); + return; + } const buttonMessage = await sendReply(message, commandTitle, commandContent || '', isEmbed || false, embedColor || constantsConfig.colors.FBW_CYAN, commandImage || '', versionSelectButtonRow); const filter = (interaction: Interaction) => interaction.user.id === authorId; From b650b38f88bd4c9cdc254dc34d046b3a4983af4f Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Sun, 20 Oct 2024 16:46:17 -0700 Subject: [PATCH 50/51] Clarifying button --- src/events/messageCreateHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events/messageCreateHandler.ts b/src/events/messageCreateHandler.ts index 91e0ed1e..a5885922 100644 --- a/src/events/messageCreateHandler.ts +++ b/src/events/messageCreateHandler.ts @@ -270,7 +270,7 @@ export default event(Events.MessageCreate, async (_, message) => { collector.on('collect', async (collectedInteraction: ButtonInteraction) => { buttonClicked = true; await collectedInteraction.deferUpdate(); - Logger.debug(`Prefix Command - User selected version "${collectedInteraction.customId}" for command "${name}" based on user command "${commandText}"`); + Logger.debug(`Prefix Command - User selected button "${collectedInteraction.customId}" for command "${name}" based on user command "${commandText}"`); await buttonMessage.delete(); const { customId: selectedVersionId } = collectedInteraction; const commandContentData = contents.find(({ versionId }) => versionId === selectedVersionId); From f3893d0f0655567de5d55c055e68ba5fb4546da1 Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Tue, 22 Oct 2024 20:34:09 -0700 Subject: [PATCH 51/51] Fix modchannel checks --- src/commands/moderation/prefixCommands/functions/addCategory.ts | 1 - .../moderation/prefixCommands/functions/modifyCommand.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/commands/moderation/prefixCommands/functions/addCategory.ts b/src/commands/moderation/prefixCommands/functions/addCategory.ts index 07890e46..73645c6f 100644 --- a/src/commands/moderation/prefixCommands/functions/addCategory.ts +++ b/src/commands/moderation/prefixCommands/functions/addCategory.ts @@ -67,7 +67,6 @@ export async function handleAddPrefixCommandCategory(interaction: ChatInputComma const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (!modLogsChannel) { await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); - return; } const existingCategory = await PrefixCommandCategory.findOne({ name }); diff --git a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts index d26348de..e05fb258 100644 --- a/src/commands/moderation/prefixCommands/functions/modifyCommand.ts +++ b/src/commands/moderation/prefixCommands/functions/modifyCommand.ts @@ -161,7 +161,6 @@ export async function handleModifyPrefixCommand(interaction: ChatInputCommandInt const modLogsChannel = interaction.guild.channels.resolve(constantsConfig.channels.MOD_LOGS) as TextChannel; if (!modLogsChannel) { await interaction.followUp({ embeds: [noModLogs], ephemeral: true }); - return; } let foundCategory;