From 1ff6324a9e2591c1a91b510d511b6dc4bc648b22 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 11 Apr 2026 20:16:02 +0200 Subject: [PATCH] refactor: handle checks in Command structure with bitflags --- src/commands/admin/CrashCommand.ts | 13 ++----- src/commands/admin/DevResetCommand.ts | 13 ++----- src/commands/admin/SyncCommand.ts | 13 +++---- src/commands/admin/UpdateGeminiKeyCommand.ts | 13 ++----- src/commands/moderation/BanCommand.ts | 13 +++---- src/commands/moderation/ClearCommand.ts | 11 ++---- src/commands/moderation/KickCommand.ts | 13 +++---- src/commands/moderation/LockCommand.ts | 13 +++---- src/commands/moderation/MuteCommand.ts | 13 +++---- src/commands/moderation/PurgeCommand.ts | 19 ++++------- src/commands/moderation/RoleCommand.ts | 13 +++---- src/commands/moderation/UnmuteCommand.ts | 11 ++---- src/commands/utils/PingCommand.ts | 3 +- src/structures/Command.ts | 36 ++++++++++++++++++++ 14 files changed, 83 insertions(+), 114 deletions(-) diff --git a/src/commands/admin/CrashCommand.ts b/src/commands/admin/CrashCommand.ts index d28ef8f..5fe82f7 100644 --- a/src/commands/admin/CrashCommand.ts +++ b/src/commands/admin/CrashCommand.ts @@ -1,5 +1,5 @@ import { PermissionsBitField } from 'discord.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { ExtendedClient } from '@structures/Client.js'; import { Embeds } from '@utils/Embeds.js'; import { Constants } from '@utils/Constants.js'; @@ -10,19 +10,12 @@ export default class CrashCommand extends Command { name: 'crash', description: 'Crash the bot', requiredPermissions: [PermissionsBitField.Flags.Administrator], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, }); } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const author = context.interaction?.user || context.message?.author; - - if (!author) { - await context.reply({ - embeds: [Embeds.error('Unable to identify the command author.')], - }); - - return; - } + const author = context.author!; if (!Constants.TRUSTED_USER_IDS.includes(author.id)) { await context.reply({ diff --git a/src/commands/admin/DevResetCommand.ts b/src/commands/admin/DevResetCommand.ts index 16300d7..a180246 100644 --- a/src/commands/admin/DevResetCommand.ts +++ b/src/commands/admin/DevResetCommand.ts @@ -1,5 +1,5 @@ import { PermissionsBitField } from 'discord.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { ExtendedClient } from '@structures/Client.js'; import { Embeds } from '@utils/Embeds.js'; import { Constants } from '@utils/Constants.js'; @@ -10,19 +10,12 @@ export default class DevResetCommand extends Command { name: 'devreset', description: 'Reset the chat bot', requiredPermissions: [PermissionsBitField.Flags.Administrator], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, }); } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const author = context.interaction?.user || context.message?.author; - - if (!author) { - await context.reply({ - embeds: [Embeds.error('Unable to identify the command author.')], - }); - - return; - } + const author = context.author!; if (!Constants.TRUSTED_USER_IDS.includes(author.id)) { await context.reply({ diff --git a/src/commands/admin/SyncCommand.ts b/src/commands/admin/SyncCommand.ts index 6bfc390..8bad4ea 100644 --- a/src/commands/admin/SyncCommand.ts +++ b/src/commands/admin/SyncCommand.ts @@ -1,8 +1,8 @@ import { PermissionFlagsBits } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; -import { Constants } from '@utils/Constants.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; +import { Constants } from '@utils/Constants.js'; export default class SyncCommand extends Command { constructor() { @@ -10,17 +10,12 @@ export default class SyncCommand extends Command { name: 'sync', description: "Cleanup each member's roles", requiredPermissions: [PermissionFlagsBits.Administrator], + checkFlags: CommandCheckFlags.Guild, }); } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; const members = await guild.members.fetch(); const communityRole = guild.roles.cache.get(Constants.ROLES.COMMUNITY); diff --git a/src/commands/admin/UpdateGeminiKeyCommand.ts b/src/commands/admin/UpdateGeminiKeyCommand.ts index a340b1a..e93f26c 100644 --- a/src/commands/admin/UpdateGeminiKeyCommand.ts +++ b/src/commands/admin/UpdateGeminiKeyCommand.ts @@ -1,6 +1,6 @@ import { MessageFlags, PermissionFlagsBits } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Argument } from '@structures/Argument.js'; import { Embeds } from '@utils/Embeds.js'; import { Constants } from '@utils/Constants.js'; @@ -11,6 +11,7 @@ export default class UpdateGeminiKeyCommand extends Command { name: 'updategeminikey', description: 'Update the Gemini API key', requiredPermissions: [PermissionFlagsBits.Administrator], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'key', @@ -23,15 +24,7 @@ export default class UpdateGeminiKeyCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const author = context.interaction?.user || context.message?.author; - - if (!author) { - await context.reply({ - embeds: [Embeds.error('Unable to identify the command author.')], - }); - - return; - } + const author = context.author!; if (!Constants.TRUSTED_USER_IDS.includes(author.id)) { await context.reply({ diff --git a/src/commands/moderation/BanCommand.ts b/src/commands/moderation/BanCommand.ts index c6d6fe6..1966e42 100644 --- a/src/commands/moderation/BanCommand.ts +++ b/src/commands/moderation/BanCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class BanCommand extends Command { name: 'ban', description: 'Ban a user from the server', requiredPermissions: [PermissionFlagsBits.BanMembers], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'user', @@ -28,14 +29,8 @@ export default class BanCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - const author = context.interaction?.user ?? context.message?.author; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; + const author = context.author!; const user = guild.members.cache.get(context.args.user as string); diff --git a/src/commands/moderation/ClearCommand.ts b/src/commands/moderation/ClearCommand.ts index 8eea35e..f5056c0 100644 --- a/src/commands/moderation/ClearCommand.ts +++ b/src/commands/moderation/ClearCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits, TextChannel } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class ClearCommand extends Command { name: 'clear', description: 'Clear messages from a channel', requiredPermissions: [PermissionFlagsBits.ManageMessages], + checkFlags: CommandCheckFlags.None, args: [ new Argument({ name: 'amount', @@ -22,14 +23,6 @@ export default class ClearCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } - const amount = context.args.amount as number; if (!amount || isNaN(amount) || amount < 1 || amount > 100) { diff --git a/src/commands/moderation/KickCommand.ts b/src/commands/moderation/KickCommand.ts index 942bbce..6b599b0 100644 --- a/src/commands/moderation/KickCommand.ts +++ b/src/commands/moderation/KickCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class KickCommand extends Command { name: 'kick', description: 'Kick a user from the server', requiredPermissions: [PermissionFlagsBits.KickMembers], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'user', @@ -28,14 +29,8 @@ export default class KickCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - const author = context.interaction?.user ?? context.message?.author; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; + const author = context.author!; const user = guild.members.cache.get(context.args.user as string); diff --git a/src/commands/moderation/LockCommand.ts b/src/commands/moderation/LockCommand.ts index 2762b46..94d1620 100644 --- a/src/commands/moderation/LockCommand.ts +++ b/src/commands/moderation/LockCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits, TextChannel } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class LockCommand extends Command { name: 'lock', description: 'Lock a channel', requiredPermissions: [PermissionFlagsBits.ManageChannels], + checkFlags: CommandCheckFlags.Guild, args: [ new Argument({ name: 'channel', @@ -22,16 +23,10 @@ export default class LockCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; const channel = context.args.channel - ? guild?.channels.cache.get(context.args.channel as string) + ? guild.channels.cache.get(context.args.channel as string) : (context.interaction?.channel ?? context.message?.channel); if (!channel || !channel.isTextBased()) { diff --git a/src/commands/moderation/MuteCommand.ts b/src/commands/moderation/MuteCommand.ts index 6557129..c63023c 100644 --- a/src/commands/moderation/MuteCommand.ts +++ b/src/commands/moderation/MuteCommand.ts @@ -1,6 +1,6 @@ import { Argument } from '@structures/Argument.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { PermissionsBitField } from 'discord.js'; import { Embeds } from '@utils/Embeds.js'; @@ -10,6 +10,7 @@ export default class MuteCommand extends Command { name: 'mute', description: 'Mute a user', requiredPermissions: [PermissionsBitField.Flags.ModerateMembers], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'user', @@ -34,14 +35,8 @@ export default class MuteCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - const author = context.interaction?.user ?? context.message?.author; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; + const author = context.author!; const user = guild.members.cache.get(context.args.user as string); diff --git a/src/commands/moderation/PurgeCommand.ts b/src/commands/moderation/PurgeCommand.ts index 7f99dc7..88019e6 100644 --- a/src/commands/moderation/PurgeCommand.ts +++ b/src/commands/moderation/PurgeCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits, TextChannel } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class PurgeCommand extends Command { name: 'purge', description: 'Purge and recreate a channel', requiredPermissions: [PermissionFlagsBits.ManageChannels], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'channel', @@ -22,17 +23,11 @@ export default class PurgeCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - const author = context.interaction?.user ?? context.message?.author; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; + const author = context.author!; const originalChannel = context.args.channel - ? guild?.channels.cache.get(context.args.channel as string) + ? guild.channels.cache.get(context.args.channel as string) : (context.interaction?.channel ?? context.message?.channel); if (!originalChannel || !originalChannel.isTextBased()) { @@ -56,10 +51,10 @@ export default class PurgeCommand extends Command { } await newChannel.setPosition(textChannel.position); - await textChannel.delete(`Purged by ${author?.tag}`); + await textChannel.delete(`Purged by ${author.tag}`); await newChannel.send({ - embeds: [Embeds.success(`Channel purged by ${author?.tag}`)], + embeds: [Embeds.success(`Channel purged by ${author.tag}`)], }); } } diff --git a/src/commands/moderation/RoleCommand.ts b/src/commands/moderation/RoleCommand.ts index ff64d7e..0ad988f 100644 --- a/src/commands/moderation/RoleCommand.ts +++ b/src/commands/moderation/RoleCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class RoleCommand extends Command { name: 'role', description: 'Give or remove a role from a user', requiredPermissions: [PermissionFlagsBits.ManageRoles], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'user', @@ -28,14 +29,8 @@ export default class RoleCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - const author = context.interaction?.user ?? context.message?.author; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; + const author = context.author!; const user = guild.members.cache.get(context.args.user as string); diff --git a/src/commands/moderation/UnmuteCommand.ts b/src/commands/moderation/UnmuteCommand.ts index 3be5b1d..fc361fc 100644 --- a/src/commands/moderation/UnmuteCommand.ts +++ b/src/commands/moderation/UnmuteCommand.ts @@ -1,6 +1,6 @@ import { Argument } from '@structures/Argument.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { PermissionsBitField } from 'discord.js'; import { Embeds } from '@utils/Embeds.js'; @@ -10,6 +10,7 @@ export default class UnmuteCommand extends Command { name: 'unmute', description: 'Unmute a user', requiredPermissions: [PermissionsBitField.Flags.ModerateMembers], + checkFlags: CommandCheckFlags.Guild, args: [ new Argument({ name: 'user', @@ -28,13 +29,7 @@ export default class UnmuteCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; const user = guild.members.cache.get(context.args.user as string); diff --git a/src/commands/utils/PingCommand.ts b/src/commands/utils/PingCommand.ts index 1365ad3..2478329 100644 --- a/src/commands/utils/PingCommand.ts +++ b/src/commands/utils/PingCommand.ts @@ -1,5 +1,5 @@ import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; export default class PingCommand extends Command { @@ -7,6 +7,7 @@ export default class PingCommand extends Command { super({ name: 'ping', description: 'Check the bot latency', + checkFlags: CommandCheckFlags.None, }); } diff --git a/src/structures/Command.ts b/src/structures/Command.ts index 842b675..940456e 100644 --- a/src/structures/Command.ts +++ b/src/structures/Command.ts @@ -2,6 +2,7 @@ import { ChatInputCommandInteraction, Guild, Message, + User, SlashCommandBuilder, MessageCreateOptions, InteractionReplyOptions, @@ -12,6 +13,7 @@ import { } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; import { Argument, ArgumentType } from '@structures/Argument.js'; +import { Embeds } from '@utils/Embeds.js'; import { Utils } from '@utils/Utils.js'; export interface CommandOptions { @@ -19,12 +21,22 @@ export interface CommandOptions { description: string; args?: Argument[]; requiredPermissions?: PermissionResolvable[]; + checkFlags?: CommandCheckFlags; +} + +export enum CommandCheckFlags { + None = 0, + Author = 1 << 0, + Guild = 1 << 1, + Default = Author | Guild, } export interface CommandContext { client: ExtendedClient; interaction?: ChatInputCommandInteraction; message?: Message; + author?: User; + guild?: Guild; args: Record; reply(content: string | MessageCreateOptions | InteractionReplyOptions): Promise; deferReply(ephemeral?: boolean): Promise; @@ -37,12 +49,14 @@ export abstract class Command { public readonly description: string; public readonly args: Argument[]; public readonly requiredPermissions: PermissionResolvable[]; + public readonly checkFlags: CommandCheckFlags; constructor(options: CommandOptions) { this.name = options.name; this.description = options.description; this.args = options.args ?? []; this.requiredPermissions = options.requiredPermissions ?? []; + this.checkFlags = options.checkFlags ?? CommandCheckFlags.Default; } public buildSlashCommand(): SlashCommandBuilder { @@ -61,6 +75,26 @@ export abstract class Command { return builder; } + public async run(client: ExtendedClient, context: CommandContext): Promise { + if (this.checkFlags & CommandCheckFlags.Author && !context.author) { + await context.reply({ + embeds: [Embeds.error('Unable to identify the command author.')], + }); + + return; + } + + if (this.checkFlags & CommandCheckFlags.Guild && !context.guild) { + await context.reply({ + embeds: [Embeds.error('This command can only be used in a server.')], + }); + + return; + } + + await this.execute(client, context); + } + private addArgumentToBuilder(builder: SlashCommandBuilder, arg: Argument): void { const methodName = this.getBuilderMethodName(arg.type); const methodKey = `add${methodName}Option` as const; @@ -164,6 +198,8 @@ export abstract class Command { client, interaction, message, + author: interaction?.user ?? message?.author, + guild: interaction?.guild ?? message?.guild ?? undefined, args, reply: async (content: string | MessageCreateOptions | InteractionReplyOptions) => { if (interaction) {