Base de bot de discordComandos

Modularização

Como criar comandos de barra do discord

Este é um recurso exclusivo de comandos de barra!

Como modularizar

Conforme vamos desenvolvendo sistemas para o nosso bot, mais comandos serão necessários. A melhor forma de manter os comandos intuitívos, é usando grupos e sub comandos, mas isso pode trazer um pequeno problema para o código, veja o exemplo abaixo:

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;
            }
        }
    },
});

Mesmo que bem simples, este comando tem mais de 200 linhas de código. Imagine um comando real com mais complexidade, verificações, transformações, interatividade, ficaria muito maior. Isso pode ser ruim ao longo do tempo se você deseja manter seu projeto e atualizar sempre.

A função createCommand retorna um CommandInstance que contém dois métodos especiais: group e subcommand. Como você pode imaginar isso serve para criar grupos e sub comandos:

src/discord/commands/manage.ts
const command = createCommand({
    /* ... */                     
});

command.group({
    /* ... */
})

command.subcommand({
    /* ... */
})

Com isso em mente, vamos quebrar aquele comando gigante e separar em arquivos diferentes:

É recomendado criar uma pasta exclusiva para o seu comando gigante! Neste exemplo vamos criar a pasta manage. O caminho no projeto ficaria em ./src/discord/commands/manage/

command.ts
src/discord/commands/manage/command.ts
import { createCommand } from "#base";

export default createCommand({
    name: "manage",
    description: "Manage command",
    defaultMemberPermissions: ["Administrator"],
    dmPermission: false,
});

Agora dentro da pasta manage vamos criar outras pastas para os grupos de sub comandos. A estrutura deve ficar parecida com isso:

create.ts
delete.ts
group.ts
move.ts
create.ts
delete.ts
group.ts
permissions.ts
command.ts

Como exportamos o retorno da função createCommando usando export default, podemos importar nos arquivos group.ts das pastas channels e ** roles** para definir os grupos de sub comandos:

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",
});

Podemos exportar variáveis que serão usadas nos sub comandos desse grupo também

manage/roles/group.ts
import command from "../command.js";

export default command.group({
    name: "roles",
    description: "Manage roles"
});

Agora basta criar um arquivo para cada sub comando e o seu código já vai estar modularizado:

Sub comandos para o grupo channels:

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)}`
            }));
    },
})
manage/channels/delete.ts
import { ApplicationCommandOptionType, codeBlock } from "discord.js";
import group, { guildChannelTypes } from "./group.js";

group.subcommand({
    name: "delete",
    description: "Delete a channel",
    options: [
        {
            name: "channel",
            description: "Select the channel",
            type: ApplicationCommandOptionType.Channel,
            channelTypes: guildChannelTypes,
            required: true,
        },
    ],
    async run(interaction) {
        const { options } = interaction;

        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)}`
            }));
    },
})
manage/channels/move.ts
import { ApplicationCommandOptionType, ChannelType, codeBlock } from "discord.js";
import group, { guildChannelTypes } from "./group.js";

group.subcommand({
    name: "move",
    description: "Change channel category",
    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,
        },
    ],
    async run(interaction) {
        const { options } = interaction;

        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)}`
            }));
    },
})

Sub comandos para o grupo roles:

manage/roles/create.ts
import { ApplicationCommandOptionType, codeBlock } from "discord.js";
import group from "./group.js";

group.subcommand({
    name: "create",
    description: "Create new role",
    options: [
        {
            name: "name",
            description: "Role name",
            type: ApplicationCommandOptionType.String,
            required: true,
        },
    ],
    async run(interaction) {
        const { options, guild } = interaction;

        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)}`
            }));
    },
})
manage/roles/delete.ts
import { ApplicationCommandOptionType, codeBlock } from "discord.js";
import group from "./group.js";

group.subcommand({
    name: "delete",
    description: "Delete a role",
    options: [
        {
            name: "role",
            description: "Select the role",
            type: ApplicationCommandOptionType.Role,
            required: true,
        },
    ],
    async run(interaction) {
        const { options } = interaction;

        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)}`
            }));
    },
})
manage/roles/permissions.ts
import { ApplicationCommandOptionType, codeBlock, PermissionFlagsBits, PermissionsString } from "discord.js";
import group from "./group.js";

group.subcommand({
    name: "permissions",
    description: "Change role permissions",
    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,
        },
    ],
    async run(interaction) {
        const { options } = interaction;

        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)}`
            }));
    },
})

Transporte de dados

Você pode transportar dados entre os grupos e sub comandos simplesmente retornando eles nas funções run de cada um. Vamos ver um exemplo simples:

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
    },
});

Acima temos algo como uma consulta ao banco de dados para obter o documento do nosso sistema de loja. Quando retornamos algo dentro do run, ele será passado como o segundo argumento para o run dos grupos e sub comandos, tudo isso com tipagem inferida automaticamente!

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();
    },
});

Se você tiver grupos é possível transformar ou enviar novos dados para os sub comandos

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']
    }
})