Modularization
Como criar comandos de barra do discord
This is a unique feature of slash commands!
How to Modularize
As we develop systems for our bot, more commands will be needed. The best way to keep commands intuitive is to use groups and subcommands, but this can cause minor problems in the code. See the example below:
src/discord/commands/manage.ts
import { createCommand } from "#base";
import { ApplicationCommandOptionType, CategoryChannelType, ChannelType, codeBlock, PermissionFlagsBits, PermissionsString } from "discord.js";
const guildChannelTypes = [
ChannelType.GuildAnnouncement,
ChannelType.GuildForum,
ChannelType.GuildText,
ChannelType.GuildVoice,
] as const;
createCommand({
name: "manage",
description: "Manage command",
defaultMemberPermissions: ["Administrator"],
dmPermission: false,
options: [
{
name: "roles",
description: "Manage roles",
type: ApplicationCommandOptionType.SubcommandGroup,
options: [
{
name: "create",
description: "Create new role",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "name",
description: "Role name",
type: ApplicationCommandOptionType.String,
required: true,
},
],
},
{
name: "permissions",
description: "Change role permissions",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "role",
description: "Select the role",
type: ApplicationCommandOptionType.Role,
required: true,
},
{
name: "action",
description: "Select the action",
type: ApplicationCommandOptionType.String,
choices: [
{ name: "Add", value: "add" },
{ name: "Remove", value: "remove" },
],
required: true,
},
{
name: "permission",
description: "Select the permission",
type: ApplicationCommandOptionType.String,
choices: Object.keys(PermissionFlagsBits)
.map(value => ({ name: value, value })),
required: true,
},
],
},
{
name: "delete",
description: "Delete a role",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "role",
description: "Select the role",
type: ApplicationCommandOptionType.Role,
required: true,
},
],
}
]
},
{
name: "channels",
description: "Manage channels",
type: ApplicationCommandOptionType.SubcommandGroup,
options: [
{
name: "create",
description: "Create new channel",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "name",
description: "Channel name",
type: ApplicationCommandOptionType.String,
required: true,
},
{
name: "category",
description: "Select a channel category",
type: ApplicationCommandOptionType.Channel,
channelTypes: [ChannelType.GuildCategory]
},
{
name: "type",
type: ApplicationCommandOptionType.Integer,
choices: [
{ name: "Text", value: ChannelType.GuildText },
{ name: "Announcement", value: ChannelType.GuildAnnouncement },
{ name: "Forum", value: ChannelType.GuildForum },
{ name: "Voice", value: ChannelType.GuildVoice },
]
}
],
},
{
name: "move",
description: "Change channel category",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "channel",
description: "Select the channel",
type: ApplicationCommandOptionType.Channel,
channelTypes: guildChannelTypes,
required: true,
},
{
name: "category",
description: "Select the new category",
type: ApplicationCommandOptionType.Channel,
channelTypes: [ChannelType.GuildCategory],
required: true,
},
],
},
{
name: "delete",
description: "Delete a channel",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: "channel",
description: "Select the channel",
type: ApplicationCommandOptionType.Channel,
channelTypes: guildChannelTypes,
required: true,
},
],
}
]
}
],
async run(interaction) {
const { options, guild } = interaction;
const group = options.getSubcommandGroup();
const subcommand = options.getSubcommand();
switch (group) {
case "roles": {
switch (subcommand) {
case "create": {
const name = options.getString("name", true);
await interaction.reply({
flags: ["Ephemeral"],
content: "Creating role..."
});
await guild.roles.create({ name })
.then(role => interaction.editReply({
content: `Role created ${role}`
}))
.catch(err => interaction.editReply({
content: `Error: ${codeBlock(err)}`
}));
return;
}
case "permissions": {
const role = options.getRole("role", true);
const action = options.getString("action", true) as "add" | "remove";
const permission = options.getString("permission") as PermissionsString;
await interaction.reply({
flags: ["Ephemeral"],
content: "Update permissions..."
});
const permissions = role.permissions[action](permission).toArray();
role.edit({ permissions })
.then(role => interaction.editReply({
content: `${action} permissions: ${role}`
}))
.catch(err => interaction.editReply({
content: `Error: ${codeBlock(err)}`
}));
return;
}
case "delete": {
const role = options.getRole("role", true);
await interaction.reply({
flags: ["Ephemeral"],
content: "Deleting role..."
});
await role.delete()
.then(() => interaction.editReply({
content: "Channel deleted!"
}))
.catch(err => interaction.editReply({
content: `Error: ${codeBlock(err)}`
}));
return;
}
}
return;
}
case "channels": {
switch (subcommand) {
case "create": {
const name = options.getString("name", true);
const parent = options.getChannel("category", false, [ChannelType.GuildCategory]);
const type = options.getInteger("type") as CategoryChannelType;
await interaction.reply({
flags: ["Ephemeral"],
content: "Creating channel..."
});
await guild.channels.create({ name, type, parent })
.then(channel => interaction.editReply({
content: `Channel created ${channel.url}`
}))
.catch(err => interaction.editReply({
content: `Error: ${codeBlock(err)}`
}));
return;
}
case "move": {
const channel = options.getChannel("channel", true, guildChannelTypes);
const parent = options.getChannel("category", true, [ChannelType.GuildCategory]);
await interaction.reply({
flags: ["Ephemeral"],
content: "Moving channel..."
});
await channel.setParent(parent)
.then(channel => interaction.editReply({
content: `Channel moved ${channel.url}`
}))
.catch(err => interaction.editReply({
content: `Error: ${codeBlock(err)}`
}));
return;
}
case "delete": {
const channel = options.getChannel("channel", true, guildChannelTypes);
await interaction.reply({
flags: ["Ephemeral"],
content: "Deleting channel..."
});
await channel.delete()
.then(() => interaction.editReply({
content: "Channel deleted!"
}))
.catch(err => interaction.editReply({
content: `Error: ${codeBlock(err)}`
}));
return;
}
}
return;
}
}
},
});
Even though it's quite simple, this command has over 200 lines of code. Imagine a real command with more complexity, checks, transformations, and interactivity—it would be much larger. This could be detrimental over time if you want to maintain your project and keep updating it.
The createCommand
function returns a CommandInstance
that contains two special methods: group
and subcommand
. As you can imagine, this is used to create groups and subcommands:
src/discord/commands/manage.ts
const command = createCommand({
/* ... */
});
command.group({
/* ... */
})
command.subcommand({
/* ... */
})
With that in mind, let's break that giant command down and separate it into different files:
It's recommended to create a dedicated folder for your giant command! In this example, we'll create the manage folder. The path in the project would be ./src/discord/commands/manage/
src/
└─ discord/
└─ commands/
└─ manage/
src/discord/commands/manage/command.ts
import { createCommand } from "#base";
export default createCommand({
name: "manage",
description: "Manage command",
defaultMemberPermissions: ["Administrator"],
dmPermission: false,
});
Now, within the manage folder, we'll create other folders for the subcommand groups. The structure should look something like this:
<!-- Relative path from the manage/ folder -->
manage/
├─ channels/
│ ├─ create.ts
│ ├─ delete.ts
│ ├─ group.ts
│ └─ move.ts
├─ roles/
│ ├─ create.ts
│ ├─ delete.ts
│ ├─ group.ts
│ └─ permissions.ts
└─ command.ts
Since we exported the return of the createCommando
function using export default
, we can import it into the group.ts
files in the channels and roles folders to define the subcommand groups:
manage/channels/group.ts
import { ChannelType } from "discord.js";
import command from "../command.js";
export const guildChannelTypes = [
ChannelType.GuildAnnouncement,
ChannelType.GuildForum,
ChannelType.GuildText,
ChannelType.GuildVoice,
] as const;
export default command.group({
name: "channels",
description: "Manage channels",
});
We can export variables that will be used in the sub commands of this group as well.
Now just create a file for each sub command and your code will be modularized:
Sub commands for the channels group:
manage/channels/create.ts
import { ApplicationCommandOptionType, CategoryChannelType, ChannelType, codeBlock } from "discord.js";
import group from "./group.js";
group.subcommand({
name: "create",
description: "Create new channel",
options: [
{
name: "name",
description: "Channel name",
type: ApplicationCommandOptionType.String,
required: true,
},
{
name: "category",
description: "Select a channel category",
type: ApplicationCommandOptionType.Channel,
channelTypes: [ChannelType.GuildCategory]
},
{
name: "type",
type: ApplicationCommandOptionType.Integer,
choices: [
{ name: "Text", value: ChannelType.GuildText },
{ name: "Announcement", value: ChannelType.GuildAnnouncement },
{ name: "Forum", value: ChannelType.GuildForum },
{ name: "Voice", value: ChannelType.GuildVoice },
]
}
],
async run(interaction) {
const { options, guild } = interaction;
const name = options.getString("name", true);
const parent = options.getChannel("category", false, [ChannelType.GuildCategory]);
const type = options.getInteger("type") as CategoryChannelType;
await interaction.reply({
flags: ["Ephemeral"],
content: "Creating channel..."
});
await guild.channels.create({ name, type, parent })
.then(channel => interaction.editReply({
content: `Channel created ${channel.url}`
}))
.catch(err => interaction.editReply({
content: `Error: ${codeBlock(err)}`
}));
},
})
Data Transport
You can transport data between groups and subcommands simply by returning them in each run function. Let's look at a simple example:
store/command.ts
import { createCommand } from "#base";
import { db } from "#database";
export default createCommand({
name: "store",
async run(interaction) {
const { guild } = interaction;
await interaction.deferReply({ flags: ["Ephemeral"] });
const storeData = await db.store.findOne({
guildId: guild.id
});
return storeData // StoreDocument
},
});
Above, we have something like a database query to get the document from our store system. When we return something within run
, it will be passed as the second argument to run
of the groups and subcommands, all with automatically inferred typing!
store/setup.ts
import command from "./command.js";
import { codeBlock } from "discord.js";
command.subcommand({
name: "setup",
async run(interaction, data /* StoreDocument */) {
const { channelId, description } = data;
const channel = await interaction.guild.channels
.fetch(channelId)
.catch(() => null);
if (!channel || !channel.isSendable()) {
await interaction.editReply("Store channel not found");
return;
}
const message = await channel.send({ content: description })
.catch(err => {
interaction.editReply({
content: `Unable to setup store: ${codeBlock(err)}`
});
return null;
});
if (!message) return;
data.set("panelMessageURL", message.url);
await data.save();
},
});
If you have groups it is possible to transform or send new data to the sub commands
command.ts
export default createCommand({
/* ... */
async run(){
// ...
return "Constatic";
}
})
group.ts
import command from "./command.js";
export default command.group({
/* ... */
async run(_, text /* string */){
// ...
console.log(text)
// "Constatic"
return text.split("");
}
})
subcommand.ts
import group from "./command.js";
group.subcommand({
/* ... */
async run(_, words /* string[] */){
// ...
console.log(worlds)
// ['C', 'o', 'n', 's', 't', 'a', 't', 'i', 'c']
}
})