Base de bot de discordResponders

Parâmetros

Transforme o customId de componentes e modais em rotas http com parâmetros

O que são CustomID params?

Vamos um pouco além do convencional, nesta base um recurso presente em diversas ferramentas de servidor API foi implementado para permitir que sistemas mais avançados possam ser desenvolvidos com mais praticidade.

Igual em rotas HTTP, podemos passar parâmetros para os customIds de botões, menus de seleção e modais. Veja como é simples:

Primeiro vamos criar um comando de contexto de usuário:

src/discord/commands/context/manage.ts

createCommand({
    name: "Gerenciar",
    type: ApplicationCommandType.User,
    async run(interaction){
        const { targetUser } = interaction;

        const embed = new EmbedBuilder({ description: `Gerenciar ${targetUser}` });
        const row = createRow(
            new ButtonBuilder({ 
                customId: `/manage/user/${targetUser.id}/kick`, 
                label: "Expulsar", style: ButtonStyle.Secondary 
            }),
            new ButtonBuilder({ 
                customId: `/manage/user/${targetUser.id}/ban`, 
                label: "Banir", style: ButtonStyle.Danger 
            }),
            new ButtonBuilder({ 
                customId: `/manage/user/${targetUser.id}/timeout`, 
                label: "Castigo", style: ButtonStyle.Danger 
            }),
            new ButtonBuilder({ 
                customId: `/manage/user/${targetUser.id}/alert`, 
                label: "Alertar", style: ButtonStyle.Primary 
            })
        );

        interaction.reply({ flags: ["Ephemeral"], embeds: [embed], components: [row] });
    }
});

Com isso podemos criar um Responder que espera qualquer componente de botão que siga esse padrão no customId.

Ele deve começar com /manage/user logo depois deve ter mais dois segmentos de parâmetros separados por / e começando com :, onde o primeiro será o id do usuário e o segundo vai ser uma ação.

Ficaria dessa maneira: /manage/user/:userId/:action

Então podemos definir esse padrão e qualquer botão onde o customId seguir ele, será respondido pela função definida. Você pode obter os parâmetros no segundo argumento da função.

src/discord/responders/manage.ts
// Dynamic button component function
createResponder({
    customId: "/manage/user/:userId/:action",
    types: [ComponentType.Button], cache: "cached",
    async run(interaction, params) {
        const { action, userId } = params;
        const targetMember = await interaction.guild.members.fetch(userId);

        switch(action){
            case "kick": {
                targetMember.kick();
                // do things ...
                break;
            }
            case "ban": {
                targetMember.ban();
                // do things ...
                break;
            }
            case "timeout": {
                targetMember.timeout(60000);
                // do things ...
                break;
            }
            case "alert": {
                targetMember.send({ /* ... */ });
                // do things ...
                break;
            }
        }
    },
});

Você pode usar esse recurso com qualquer tipo de Responder, mas não esqueça que o discord tem um limite de 100 caracteres nos customIds.

Transformando parâmetros de CustomID

A função createResponder tem uma opção onde você pode especificar uma forma de transformar o objeto de parâmetros que inicalmente tanto as chaves, quantos os valores são somente strings. Confira:

Transforme o parâmetro value em um número, assim podendo usar funções numéricas

Botão
new Button({
    customId: "/count/1",
    label: "Contagem", 
    style: ButtonStyle.Primary 
})
Responder
createResponder({
    customId: "/count/:value", cache: "cached",
    types: [ComponentType.Button],
    parse: params => ({ 
        value: Number.parseInt(params.value) 
    }), 
    async run(interaction, { value }) {
        console.log(value + 1); // 2
        console.log(value.toFixed(2)); // "1.00"
    }
});

Você pode passar datas ISO string também:

Botão
new Button({
    customId:`/remind/${new Date().toISOString()}`
    // customId: `/remind/2018-11-30T12:20:00.000Z`,
    label: "Lembrar", 
    style: ButtonStyle.Success 
});
Responder
createResponder({
    customId: "/remind/:date", cache: "cached",
    types: [ComponentType.Button],
    parse: params => ({ 
        date: new Date(params.date) 
    }), 
    async run(interaction, { date }) {
        console.log(date.getHours()) // 9
        console.log(date.getMinutes()) // 20
        console.log(date.getDate()); // 30
        console.log(date.getMonth()); // 10
        console.log(date.getFullYear()); // 2018
    }
});

Você não precisa espeficicar todas as propriedades dos parâmetros, vamos supor que você só queira transformar uma propriedade e manter todo o resto como string:

Botão
new Button({
    customId:`/embeds/1/title/create`
    label: "Criar", 
    style: ButtonStyle.Success 
});
Responder
createResponder({
    customId: "/embeds/:index/:menu/:action", cache: "cached",
    types: [ComponentType.Button],
    parse: params => ({ 
        ...params, 
        index: Number.parseInt(params.index) 
    }), 
    async run(interaction, { index, menu, action }) {
        const embed = embeds[index];

        switch(menu){
            // ...
        }
    }
});

Para customIds maiores você pode utilizar um esquema da lib zod, que já vem nas dependencias da base:

Botão
new Button({
    customId:`/menus/${user.id}/channels/create`
    label: "Criar novo canal", 
    style: ButtonStyle.Success 
});
Responder
import { z } from "zod"; 

createResponder({
    customId: "/menus/:userId/:menu/:action",
    types: [ResponderType.Button], cache: "cached",
    parse: z.object({ 
        userId: z.string(), 
        menu: z.enum(["channels", "roles", "parents"]), 
        action: z.enum(["create", "updated", "delete"]) 
    }).parse, 
    async run(interaction, { menu, action, userId }) {
        menu // "channels" | "roles" | "parents"
        action // "create" | "updated" | "delete"
    },
})

Wildcards

Você pode usar wildcards também se quiser responder a rotas com mais ou menos segmentos

Use ** para representar qualquer padrão de segmentos após a / (barra)

CustomIds que serão respondidos
- "/giveway"
- "/giveway/users"
- "/giveway/gifts/nitro"
- "/giveway/gifts/account/creator/expiresAt"
Responder
createResponder({
    customId: "/giveway/**", cache: "cached",
    types: [ComponentType.Button],
    async run(interaction, { _ }) {
        // /giveway _: ""
        // /giveway/users _: "users"
        // /giveway/gifts/nitro _:"gifts/nitro"
        // /giveway/gifts/account/creator/expiresAt _:"gifts/account/creator/expiresAt"
    }
});

De um nome para o wildcard ao invés de _, use : para definir um nome:

CustomIds que serão respondidos
- "/giveway/users"
- "/giveway/gifts/nitro"
- "/giveway/gifts/account/creator/expiresAt"
Responder
createResponder({
    customId: "/giveway/**:args", cache: "cached",
    types: [ComponentType.Button],
    async run(interaction, { args }) {
        // /giveway/users args: "users"
        // /giveway/gifts/nitro args:"gifts/nitro"
        // /giveway/gifts/account/creator/expiresAt args:"gifts/account/creator/expiresAt"
    }
});

Definindo um nome para o wildcard, agora se torna obrigatório o customId ter mais um segmento, ou seja, ele não responderá a apenas giveway, somente giveway/args....

Você pode usar o método parse para transformar o wildcard no que quiser, por exemplo um array

CustomIds que serão respondidos
- "/giveway"
- "/giveway/users"
- "/giveway/gifts/nitro"
- "/giveway/gifts/account/creator/expiresAt"
Responder
createResponder({
    customId: "/giveway/**", cache: "cached",
    types: [ComponentType.Button],
    parse: params => ({
        args: params._.split("/").filter(Boolean)
    }),
    async run(interaction, { args }) {
        // /giveway args: []
        // /giveway/users args: ["users"]
        // /giveway/gifts/nitro args: ["gifts", "nitro"]
        // /giveway/gifts/account/creator/expiresAt args: ["gifts", "account", "creator", "expiresAt"]
    }
});

Os exemplos dessa página foram todos com botões, mas este recurso pode ser utilizado com qualquer tipo de responder!