Command System
Command System
The command system provides a unified way to handle both slash commands and traditional message-based commands with type safety and automatic command registration.
Table of Contents
- Overview
- Creating Commands
- Command Options
- Command Execution
- Unified Argument Handling
- Command Cooldowns
- Permissions
- Type Safety
Overview
The command system features:
- Unified handling of both slash commands and message commands
- Built-in cooldown management
- Command categories for organization
- Command aliases for message commands
- Automatic command registration with Discord
- Type-safe command arguments and responses
Creating Commands
Commands are created in the src/commands/
directory, organized by category folders. Each command is a separate file that exports a command definition.
Basic Command
A command that works with both slash commands and message prefixes:
// src/commands/Utility/ping.ts
import { SlashCommandBuilder } from "discord.js";
import { commandFile } from "../../utils/types/commandManager.js";
export const data = commandFile({
data: new SlashCommandBuilder()
.setName("ping")
.setDescription("Check the bot's latency"),
options: {
cooldown: 5,
aliases: ["latency"],
category: "Utility"
},
execute: async (cmdExecutor) => {
const startTime = Date.now();
const msg = await cmdExecutor.reply("Pinging...");
const endTime = Date.now();
const ping = endTime - startTime;
const apiPing = cmdExecutor.client.ws.ping;
const response = `Pong! 🏓\nBot Latency: ${ping}ms\nAPI Latency: ${apiPing}ms`;
// withMode option
await cmdExecutor.withMode({
interaction: (exec) => exec.editReply(response),
message: (exec) => exec.editReply(msg, response)
});
// Alternatively, you can use type guards
// if (cmdExecutor.isInteraction()) {
// await cmdExecutor.editReply(response);
// } else {
// await cmdExecutor.editReply(msg, response);
// }
}
});
Slash-Only Command
A command that only works with slash commands:
// src/commands/Admin/ban.ts
import { SlashCommandBuilder } from "discord.js";
import { commandFile } from "../../utils/types/commandManager.js";
export const data = commandFile({
data: new SlashCommandBuilder()
.setName("ban")
.setDescription("Ban a user from the server")
.addUserOption(option =>
option.setName("user")
.setDescription("The user to ban")
.setRequired(true))
.addStringOption(option =>
option.setName("reason")
.setDescription("The reason for the ban")),
options: {
slashOnly: true, // Only available as a slash command
cooldown: 10,
},
execute: async (cmdExecutor) => {
// With slashOnly: true, cmdExecutor is typed as CommandExecutor<"slash">
const user = cmdExecutor.interaction.options.getUser("user", true);
const reason = cmdExecutor.interaction.options.getString("reason") || "No reason provided";
// Ban logic here...
await cmdExecutor.reply(`Banned ${user.tag} for reason: ${reason}`);
}
});
Message-Only Command
A command that only works with message prefix:
// src/commands/Fun/quote.ts
import { SlashCommandBuilder } from "discord.js";
import { commandFile } from "../../utils/types/commandManager.js";
export const data = commandFile({
data: new SlashCommandBuilder()
.setName("quote")
.setDescription("Quote a message by ID"),
options: {
messageOnly: true, // Only available with message prefix
aliases: ["q"],
},
execute: async (cmdExecutor) => {
// With messageOnly: true, cmdExecutor is typed as CommandExecutor<"message">
const messageId = cmdExecutor.arguments[0];
if (!messageId) {
return cmdExecutor.reply("Please provide a message ID to quote.");
}
// Quote logic here...
await cmdExecutor.reply(`Quoting message ${messageId}`);
}
});
Commands with Arguments
For slash commands and message commands, you should use the unified argument methods for consistent handling:
// src/commands/Utility/userinfo.ts
import { SlashCommandBuilder } from "discord.js";
import { commandFile } from "../../utils/types/commandManager.js";
export const data = commandFile({
data: new SlashCommandBuilder()
.setName("userinfo")
.setDescription("Get information about a user")
.addUserOption(option =>
option.setName("user")
.setDescription("The user to get info about")
.setRequired(false)),
execute: async (cmdExecutor) => {
// Use the unified method - works for both slash and message commands
const user = cmdExecutor.getUser("user") || cmdExecutor.getAuthor;
// Create and send user info embed
await cmdExecutor.reply({
embeds: [{
title: `User Info: ${user.tag}`,
thumbnail: { url: user.displayAvatarURL({ size: 256 }) },
fields: [
{ name: 'ID', value: user.id, inline: true },
{ name: 'Created', value: `<t:${Math.floor(user.createdTimestamp / 1000)}:R>`, inline: true },
],
color: 0x3498db
}]
});
}
});
Commands with Subcommands
For more complex commands, you can use subcommands:
// src/commands/Admin/config.ts
import { SlashCommandBuilder } from "discord.js";
import { commandFile } from "../../utils/types/commandManager.js";
export const data = commandFile({
data: new SlashCommandBuilder()
.setName("config")
.setDescription("Configure bot settings")
.addSubcommand(subcommand =>
subcommand
.setName("view")
.setDescription("View current configuration"))
.addSubcommand(subcommand =>
subcommand
.setName("set")
.setDescription("Set a configuration value")
.addStringOption(option =>
option.setName("key")
.setDescription("The configuration key")
.setRequired(true)
.addChoices(
{ name: 'Welcome Channel', value: 'welcomeChannel' },
{ name: 'Prefix', value: 'prefix' },
{ name: 'Log Level', value: 'logLevel' }
))
.addStringOption(option =>
option.setName("value")
.setDescription("The value to set")
.setRequired(true))),
options: {
slashOnly: true,
},
execute: async (cmdExecutor) => {
const subcommand = cmdExecutor.interaction.options.getSubcommand();
if (subcommand === "view") {
// Handle view subcommand
await cmdExecutor.reply("Here's the current configuration...");
} else if (subcommand === "set") {
const key = cmdExecutor.interaction.options.getString("key", true);
const value = cmdExecutor.interaction.options.getString("value", true);
// Handle set subcommand
await cmdExecutor.reply(`Setting ${key} to ${value}...`);
}
}
});
Command Options
Commands can have various options:
options: {
// Command category (defaults to folder name)
category: "Admin",
// Cooldown in seconds (0 = no cooldown)
cooldown: 5,
// Aliases for message commands (ignored for slash-only commands)
aliases: ["a", "alias1", "alias2"],
// Command mode options (choose one)
slashOnly: true, // Only available as a slash command
messageOnly: true, // Only available as a message command
// Permission settings
permissions: {
// Permissions the bot needs to execute the command
botPermissions: ["KickMembers", "BanMembers"],
// Permissions the user needs to execute the command
userPermissions: ["KickMembers"],
// If true, only the bot owner can use this command
ownerOnly: false,
// If true, command can only be used in servers
guildOnly: true,
// If true, command can only be used in DMs
dmOnly: false,
// Specific role IDs that can use this command
roleIds: ["123456789012345678"]
},
// Custom permission check function (optional)
permissionCheck?: async (cmdExecutor) => {
// Custom permission logic
return {
allowed: true,
reason: "Permission denied message if allowed is false"
};
}
}
Command Execution
The execute
function receives a CommandExecutor
object that provides a unified interface for both slash commands and message commands:
execute: async (cmdExecutor) => {
// Check the execution mode
if (cmdExecutor.isInteraction()) {
// Slash command specific logic
const option = cmdExecutor.interaction.options.getString("option");
} else if (cmdExecutor.isMessage()) {
// Message command specific logic
const arg = cmdExecutor.arguments[0];
}
// Common logic that works for both
await cmdExecutor.reply("This works in both modes!");
// Defer, edit, follow up, etc.
await cmdExecutor.deferReply();
await cmdExecutor.editReply("Updated message");
await cmdExecutor.followUp("Follow-up message");
}
Unified Argument Handling
The CommandExecutor
provides unified methods to access arguments in a consistent way for both slash commands and message commands:
execute: async (cmdExecutor) => {
// These methods work for both slash commands and message commands
const name = cmdExecutor.getString("name"); // Get a string argument
const user = cmdExecutor.getUser("user"); // Get a user argument
const amount = cmdExecutor.getNumber("amount"); // Get a number argument
const enabled = cmdExecutor.getBoolean("enabled"); // Get a boolean argument
const channel = cmdExecutor.getChannel("channel"); // Get a channel argument
const role = cmdExecutor.getRole("role"); // Get a role argument
const mentionable = cmdExecutor.getMentionable("mention"); // Get a user or role
const attachment = cmdExecutor.getAttachment("file"); // Get an attachment (slash only)
const integer = cmdExecutor.getInteger("count"); // Get an integer
// You can specify if an argument is required with generic type parameters
const requiredUser = cmdExecutor.getUser("user", true); // Will throw error if missing
// For message commands, these methods use the parsed arguments
// For slash commands, they use the Discord.js interaction options
// Full access to all command options as an object
const allOptions = cmdExecutor.getOptions();
// Helper methods for context
const member = cmdExecutor.getMember(); // GuildMember object for the command executor
const guild = cmdExecutor.getGuild(); // The guild where the command was executed
}
The unified argument methods provide better type safety and consistent error handling:
- For slash commands, they use Discord.js's built-in option methods
- For message commands, they use the argument parser that converts arguments to the right types
- They support type generics and required flags for better TypeScript support
This means you can write command logic once that works for both slash commands and message commands without dealing with the internal differences.
Command Cooldowns
Cooldowns are managed automatically through the CooldownManager
:
// src/commands/Economy/daily.ts
export const data = commandFile({
data: new SlashCommandBuilder()
.setName("daily")
.setDescription("Claim your daily reward"),
options: {
cooldown: 86400, // 24 hours in seconds
},
execute: async (cmdExecutor) => {
// This command can only be used once every 24 hours per user
// Give reward...
await cmdExecutor.reply("You've claimed your daily reward!");
}
});
If a user tries to use the command before the cooldown expires, they'll automatically receive a cooldown message.
Permissions
The command system provides a comprehensive permissions system that controls who can execute commands. It supports both standard Discord permissions and custom permission logic.
Permission Types
The permission system includes several ways to control access:
- Discord Permissions: Standard Discord permission flags for both users and the bot
- Command Restriction Flags:
guildOnly
: Commands that can only be used in servers, not DMsdmOnly
: Commands that can only be used in DMs, not serversownerOnly
: Commands that can only be used by the bot owner
- Role-Based Access: Restrict commands to users with specific role IDs
- Custom Permission Checks: Custom functions that implement complex permission logic
Setting Up Permissions
You can define permissions in the command options:
// src/commands/Admin/kick.ts
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
import { commandFile } from "../../utils/types/commandManager.js";
export const data = commandFile({
data: new SlashCommandBuilder()
.setName("kick")
.setDescription("Kick a user from the server")
.addUserOption(option =>
option.setName("user")
.setDescription("The user to kick")
.setRequired(true))
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),
options: {
// Standard permission requirements
permissions: {
// Permissions the bot needs to execute the command
botPermissions: ["KickMembers"],
// Permissions the user needs to execute the command
userPermissions: ["KickMembers"],
// Restriction flags
guildOnly: true,
ownerOnly: false,
// Role IDs that can use this command
roleIds: ["123456789012345678", "987654321098765432"]
}
},
execute: async (cmdExecutor) => {
// Command implementation...
}
});
Custom Permission Checks
For more complex permission logic, you can implement a custom permission check function:
// src/commands/Moderation/warn.ts
export const data = commandFile({
data: new SlashCommandBuilder()
.setName("warn")
.setDescription("Warn a user"),
options: {
// Custom permission check function
permissionCheck: async (cmdExecutor) => {
// Get the member executing the command
const member = cmdExecutor.getMember();
if (!member) return {
allowed: false,
reason: "This command can only be used in a server."
};
// Check if user is a moderator (custom role check)
const isModerator = member.roles.cache.some(role =>
role.name.toLowerCase().includes("moderator") ||
role.name.toLowerCase().includes("admin")
);
// Check server-specific permission in database (example)
let hasServerPermission = false;
if (cmdExecutor.getGuild()) {
try {
// This is just an example of how you could check permissions from a database
const guildId = cmdExecutor.getGuild().id;
const userId = cmdExecutor.getAuthor.id;
// hasServerPermission = await db.checkModPermission(guildId, userId);
} catch (error) {
// Handle errors
}
}
// Return the permission check result
return {
allowed: isModerator || hasServerPermission,
reason: "You must be a moderator or have warning permissions to use this command."
};
}
},
execute: async (cmdExecutor) => {
// Command implementation...
}
});
Permission Check Order
When a command is executed, permissions are checked in this order:
-
First, the custom permission check is evaluated if defined. If it returns
{allowed: false}
, the command is denied regardless of other permissions. -
Then, if the custom check passes or isn't defined, the standard permissions are checked:
- Owner-only restriction
- Guild-only or DM-only restrictions
- Role-based permissions
- Discord user permissions
- Discord bot permissions
If any permission check fails, the command execution is stopped and an error message is sent to the user.
Permission Error Handling
When a user doesn't have permission to use a command, they receive a permission denied message with the reason:
// The reason comes from either:
// - The custom permission check's reason field
// - A standard message for built-in permission checks
You can customize the appearance of permission denied messages by modifying the permissionDeniedEmbed
function in your embeds.
Using Permissions in Command Logic
Beyond the automatic permission checks, you can also perform additional permission checks during command execution:
execute: async (cmdExecutor) => {
const targetUser = cmdExecutor.getUser("user", true);
const guild = cmdExecutor.getGuild();
if (!guild) {
return cmdExecutor.reply("This command must be used in a server.");
}
const member = cmdExecutor.getMember();
const targetMember = await guild.members.fetch(targetUser.id);
// Check role hierarchy (can't moderate users with higher roles)
if (targetMember.roles.highest.position >= member.roles.highest.position) {
return cmdExecutor.reply({
content: "You cannot moderate members with a higher or equal role.",
ephemeral: true
});
}
// Continue command execution...
}
Type Safety
The command system provides extensive type safety:
- For slash-only commands,
cmdExecutor
is typed asCommandExecutor<"slash">
- For message-only commands,
cmdExecutor
is typed asCommandExecutor<"message">
- For dual-mode commands, you get full type checking when using
isInteraction()
orisMessage()
This ensures you can't accidentally access slash command properties in message commands or vice versa:
// This will cause a TypeScript error if slashOnly: true isn't set
const option = cmdExecutor.interaction.options.getString("option");
// This will cause a TypeScript error if messageOnly: true isn't set
const argument = cmdExecutor.arguments[0];