Slash Commands in Discord.js

Registrare gli Slash Commands Discord mette a disposizione degli sviluppatori l’opzione di creare degli slash commands integrati direttamente con il client. In questa lezione vedremo come registrare gli slash commands […]

Avatar di gbfactory
gbfactory 6 Agosto 2022

Registrare gli Slash Commands

Discord mette a disposizione degli sviluppatori l'opzione di creare degli slash commands integrati direttamente con il client. In questa lezione vedremo come registrare gli slash commands utilizzando discord.js.

Slash Commands del Server (Guild Commands)

I comandi del server, chiamati guild commands, sono degli slash commands disponibili solamente all'interno del server in cui sono stati registrati, ovviamente solo se all'applicazione è stato assegnato lo scopo applications.commands.

In questa sezione utilizzeremo il procedimento già visto in precedenza per registrare gli slash commands, insieme alla struttura del command handler creata in precedenza.

Come prima cosa installiamo il modulo @discordjs/rest eseguendo il seguente comando nel terminale, posizionandoci nella cartella del nostro progetto:

npm install @discordjs/rest

Poi eseguiamo il seguente script per registrare gli slash commands all'interno del server di cui abbiamo specificato l'id:

const { REST } = require('@discordjs/rest');
const { Routes } = require('discord.js');
const { token } = require('./config.json');
const fs = require('node:fs');

const commands = [];
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));

// Inserisci qui l'id del client e del server
const clientId = '123456789012345678';
const guildId = '876543210987654321';

for (const file of commandFiles) {
  const command = require(`./commands/${file}`);
  commands.push(command.data.toJSON());
}

const rest = new REST({ version: '10' }).setToken(token);

(async () => {
  try {
    console.log('Iniziato aggiornamento degli slash commands.');

    await rest.put(
      Routes.applicationGuildCommands(clientId, guildId),
      { body: commands },
    );

    console.log('Slash commands aggiornati con successo.');
  } catch (error) {
    console.error(error);
  }
})();

Slash Commands Globali

Gli slash commands globali, dopo la registrazione, saranno disponibili in tutti i server in cui è presente il tuo bot e in cui è stato autorizzato lo scopo applications.commands. Sono disponibili anche nei messaggi privati (DM).

Per registrare degli slash commands globali puoi utilizzare lo script illustrato nella sezione precedente, impostando il metodo della rotta a .applicationCommands(clientId):

await rest.put(
  Routes.applicationCommands(clientId),
  { body: commands },
);

Opzioni degli Slash Commands

Tutti gli slash commands possono avere delle opzioni, chiamate options, che possono essere immaginate come i parametri di una funzione. Possiamo specificarle in fase di registrazione di un comando nel seguente modo:

const { SlashCommandBuilder } = require('discord.js');

const data = new SlashCommandBuilder()
  .setName('echo')
  .setDescription('Risponde con l\'input fornito!')
  .addStringOption(option =>
    option.setName('input')
      .setDescription('L\'input che verrà inviato')
      .setRequired(true));

Il metodo .setRequired(true) viene utilizzato all'interno dell'option builder per obbligare l'utente a inserite un valore per quell'opzione. L'utente non potrà inviare quel comando senza prima aver scritto qualcosa.

Tipi delle Opzioni

Come visto nell'esempio precedente, possiamo specificare il tipo (type) dell'opzione di un commando (ApplicationCommandOption). Di seguito sono elencati tutti i possibili valori che possono essere passati come tipi dell'opzione (ApplicationCommandOptionType):

  • Subcommand rende l'opzione un sotto-comando;
  • SubcommandGroup rende l'opzione un gruppo di sotto-comandi;
  • String richiede che il valore dell'opzione sia una stringa;
  • Intger richiede che il valore dell'opzione sia un numero intero;
  • Number richiede che il valore dell'opzione sia un numero decimale (floating point);
  • Boolean richiede che il valore dell'opzione sia booleano (vero o falso);
  • User richiede che venga menzionato un utente o inserito uno snowflake;
  • Channel richiede che venga menzionato un canale o inserito uno snowflake;
  • Role richiede che venga menzionato un ruolo o inserito uno snowflake;
  • Mentionable richiede che venga menzionato un utente o un ruolo, oppure inserito uno snowflake;
  • Attachment richiede che venga inserito un file allegato.

Tieni presente che lo Slash Commands Builder ha un metodo da utilizzare per ognuno di questi tipi appena elencati. Puoi fare riferimento alla documentazione dell'API di Discord per una spiegazione dettagliata sui tipo SUB_COMMAND e SUB_COMMAND_GROUP.

Scelte degli Slash Commands

I tipi delle opzioni String, Number e Intger possono avere delle scelte, chiamate choises. Queste scelte sono un insieme di valori pre-configurati tra cui l'utente può scegliere mentre sta selezionado l'opzione che li contiene.

Tieni presente che quando vai a specificare delle scelte per un'opzione, quelli saranno gli unici valori validi tra cui l'utente potrà scegliere.

Puoi specificare le scelte per una determinata opzione utilizzando il metodo .addChoices() fornito dallo slash command builder:

const { SlashCommandBuilder } = require('discord.js');

const data = new SlashCommandBuilder()
  .setName('gif')
  .setDescription('Invia un GIF estratta casualmente!')
  .addStringOption(option =>
    option.setName('category')
      .setDescription('La categoria della GIF')
      .setRequired(true)
      .addChoices(
        { name: 'Divertente', value: 'gif_funny' },
      	{ name: 'Meme', value: 'gif_meme' },
        { name: 'Film', value: 'gif_movie' },
  ));

Sotto-Comandi degli Slash Commands

I sotto-comandi possono essere aggiunti utilizzando il metodo .addSubcommand():

const { SlashCommandBuilder } = require('discord.js');

const data = new SlashCommandBuilder()
  .setName('info')
  .setDescription('Informazioni su un utente o sul server!')
  .addSubcommand(subcommand =>
    subcommand
      .setName('user')
      .setDescription('Informazioni su un utente')
      .addUserOption(option => option.setName('target').setDescription('L\'utente su cui visualizzare le informazioni')))
  .addSubcommand(subcommand =>
    subcommand
      .setName('server')
      .setDescription('Informazioni sul server'));

Rispondere agli Slash Commands

In questa sezione della lezione vedremo come inviare una risposta in chat ai comandi registrati in precedenza. Tieni presente che dovrai avere almeno un comando registrato sulla tua applicazione per poter continuare a seguire questa lezione.

Intercettare le Interazioni

Ogni slash command è un'interazione, chiamata interaction. Quindi, per rispondere ad un comando, dobbiamo predisporre un event listener che andrà ad eseguire del codice quando la nostra applicazione riceverà un'interazione:

client.on('interactionCreate', interaction => {
  console.log(interaction);
});

Tuttavia, non tutte le interazioni sono slash commands (es: MessageComponent). Dobbiamo quindi essere sicuri di considerare solamente gli slash commands utilizzando il metodo isChatInputCommand() della classe BaseInteraction:

client.on('interactionCreate', interaction => {
  if (!interaction.isChatInputCommand()) return;

  console.log(interaction);
});

Rispondere ad uno Slash Command

Ci sono vari modi di rispondere ad uno slash command, e in questa sezione della guida li vedremo tutti. Il metodo più diffuso consiste nell'inviare una risposta utilizzando il metodo .reply() della classe BaseInteraction:

client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'ping') {
    await interaction.reply('Pong!');
  }
});

Tieni presente che il token di un'interazione è valido solamente per tre secondi, quindi quello è il tempo entro cui possiamo utilizzare il metodo .reply(). Le risposte che richiedono più tempo (risposte differite) sono trattate più avanti nella lezione.

Dopo aver riavviato il bot ed eseguito il comando in un canale testuale a cui il bot ha accesso, se tutto è andato bene dovresti vedere qualcosa di questo tipo:

Abbiamo inviato con successo la risposta ad uno slash command! Questo è solamente l'inizio, ci sono altri sistemi per rispondere ad uno slash commands che ora vedremo.

Risposte Effimere agli Slash Commands

Non sempre potrebbe essere desiderato che tutti gli utenti che hanno accesso al canale vedano la risposta che è stata data da un comando. Fortunatamente Discord ha implementato un sistema per nascondere le risposte a tutti tranne che all'esecutore del comando. Questi messaggi vengono definiti effimeri (ephemeral) e possono essere impostati impostando ephemeral: true all'interno delle InteractionReplyOptions come mostrato:

client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'ping') {
    await interaction.reply({ content: 'Pong!', ephemeral: true });
  }
});

Ora se proviamo ad eseguire nuovamente il comando, dovremmo vedere qualcosa come questo:

Modificare le Risposte agli Slash Commands

Dopo aver inviato una risposta iniziale, potremmo voler modificare quella risposta per varie motivazioni. Questo può essere fatto utilizzando il metodo editReply() della classe BaseInteraction:

const wait = require('node:timers/promises').setTimeout;

client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'ping') {
    await interaction.reply('Pong!');
    await wait(2000);
    await interaction.editReply('Ancora Pong!');
  }
});

Tieni presente che dopo la risposta iniziale, il token di un'interazione è valido per 15 minuti. Questo è quindi il tempo che abbiamo a disposizione per modificare la risposta oppure per mandare ulteriori messaggi di seguito (follow up).

Risposte in differita degli Slash Commands

Come menzionato in precedenza, abbiamo solamente tre secondi per rispondere ad un'interazione prima che il suo token venga invalidato. Ma nel caso in cui il comando debba svolgere un'operazione che richieda vari secondi per poi fornire una risposta?

In questo caso è possibile utilizzare il metodo deferReply() della classe BaseInteraction, che triggera il messaggio automatico "L'applicazione sta pensando..." per fornire una risposta iniziale. Questo ci consente di avere a disposizione 15 minuti per completare il processo prima di rispondere:

const wait = require('node:timers/promises').setTimeout;

client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'ping') {
    await interaction.deferReply();
    await wait(4000);
    await interaction.editReply('Pong!');
  }
});

Se hai un comando che richiede molto tempo prima di poter inviare la risposta, assicurati di richiamare il metodo deferReply() il prima possibile.

Puoi anche impostare la risposta come effimera fin da subito, impostando il flag ephemeral a true nelle opzioni della risposta in differita (InteractionDeferOptions):

await interaction.deferReply({ ephemeral: true });

Risposte multiple agli Slash Commands (Follow-ups)

Cosa possiamo fare se vogliamo mandare più risposte per un singolo slash commands e non una soltanto? In questo caso entrano in gioco i follow-ups, in italiano messaggi supplementari, che è possibile inviare all'utente utilizzando il metodo followUp():

client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'ping') {
    await interaction.reply('Pong!');
    await interaction.followUp('Pong again!');
  }
});

Tieni presente che dopo la risposta iniziale, il token di un'interazione è valido per 15 minuti. Questo è quindi il tempo che abbiamo a disposizione per modificare eventualmente la risposta oppure per mandare ulteriori messaggi di seguito (follow up).

Se provi ad avviare il bot ed eseguire il codice appena scritto, la risposta da parte del bot dovrebbe essere più o meno così:

Puoi anche rendere il messaggio effimero passando il flag ephemeral all'interno delle opzioni del messaggio supplementare (InteractionReplyOptions), in questo modo:

await interaction.followUp({ content: 'Pong again!', ephemeral: true });

Fine della sezione

Complimenti! Questo è tutto quello che c'è da sapere su come rispondere agli slash commands con discord.js versione 14.

Come bonus finale tieni presente che le risposte degli slash commands possono contenere link mascherati (es: [testo] (https://example.com/)) e le emoji standard utilizzabili in tutte le chat Discord.

Parsing delle Opzioni degli Slash Commands

Opzioni dei Comandi

In questa sezione vedremo come accedere ai valori delle opzioni di un comando. Assumiamo di aver registrato un comando che contiene le seguenti opzioni:

const { SlashCommandBuilder } = require('discord.js');

const data = new SlashCommandBuilder()
  .setName('ping')
  .setDescription('Risponde con Pong!')
  .addStringOption(option => option.setName('input').setDescription('Inserisci una stringa'))
  .addIntegerOption(option => option.setName('int').setDescription('Inserisci un numero intero'))
  .addBooleanOption(option => option.setName('choice').setDescription('Seleziona un booleano'))
  .addUserOption(option => option.setName('target').setDescription('Seleziona un utente'))
  .addChannelOption(option => option.setName('destination').setDescription('Seleziona un canale'))
  .addRoleOption(option => option.setName('muted').setDescription('Seleziona un ruolo'))
  .addNumberOption(option => option.setName('num').setDescription('Inserisci un numero'))
  .addMentionableOption(option => option.setName('mentionable').setDescription('Menziona qualcuno o qualcosa'))
  .addAttachmentOption(option => option.setName('attachment').setDescription('Allega un file'));

Possiamo utilizzare il metodo get() per ottenere queste opzioni dal CommandInteractionOptionResolver come mostrato di seguito:

const string = interaction.options.getString('input');
const integer = interaction.options.getInteger('int');
const boolean = interaction.options.getBoolean('choice');
const user = interaction.options.getUser('target');
const member = interaction.options.getMember('target');
const channel = interaction.options.getChannel('destination');
const role = interaction.options.getRole('muted');
const number = interaction.options.getNumber('num');
const mentionable = interaction.options.getMentionable('mentionable');
const attachment = interaction.options.getAttachment('attachment');

console.log({ string, integer, boolean, user, member, channel, role, mentionable, attachment, number });

Nel caso volessi ottenere lo Snowflake o la struttura, recupera l'opzione con get() e poi accedi allo Snowflake utilizzando la proprietà value. Tieni presente che devi usare const { value: name } = ... per destrutturare e rinominare il valore ottenuto dalla struttura di CommandInteractionOption ed evitare possibili conflitti tra nomi di identificatori.

Sotto-comandi degli Slash Commands

Se hai un comando che contiene sotto-comandi, puoi effettuare il parsing in un modo molto simile agli esempi precedenti. Nel seguente blocco di codice è descritta la logica necessaria per lavorare con i sotto-comandi e rispondere di conseguenza utilizzando il metodo getSubcommand() della classe CommandInteractionOptionResolver:

client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'info') {
    if (interaction.options.getSubcommand() === 'user') {
      const user = interaction.options.getUser('target');

      if (user) {
        await interaction.reply(`Nome utente: ${user.username}\nID: ${user.id}`);
      } else {
        await interaction.reply(`Il tuo nome utente: ${interaction.user.username}\nIl tuo ID: ${interaction.user.id}`);
      }
    } else if (interaction.options.getSubcommand() === 'server') {
      await interaction.reply(`Nome del Server: ${interaction.guild.name}\nMembri totali: ${interaction.guild.memberCount}`);
    }
  }
});

Recuperare ed Eliminare le risposte

Oltre a rispondere ad uno slash commands, in un secondo momento potrebbe anche tornare utile eliminare la risposta iniziale. Per questo fine possiamo usare il metodo deleteReply() della classe ChatInputCommandInteraction:

await interaction.reply('Pong!');
await interaction.deleteReply();

Ricorda sempre che non è mai possibile eliminare un messaggio effimero!

Inoltre, potrebbe essere necessario recuperare l'oggetto Message di una risposta in un secondo momento per vari motivi, come per esempio per aggiungere delle reazioni. Possiamo farlo utilizzando il metodo fetchReply() della classe ChatInputCommandInteraction come mostrato di seguito:

await interaction.reply('Pong!');
const message = await interaction.fetchReply();
console.log(message);

Permessi degli Slash Commands

Gli slash commands hanno il proprio sistema di autorizzazioni, che consente di impostare i permessi necessari per utilizzare un comando.

I permessi degli slash commands locali (registrati all'interno di un server) sono impostati di default e possono essere modificati dagli amministratori del server.

Permesso di Utilizzo nei Messaggi Privati

Possiamo utilizzare il metodo setDMPermission() della classe ApplicationCommands per controllare se un comando globale può essere utilizzato all'interno dei messaggi privati (DM). Di default, tutti i comandi globali possono essere utilizzati anche all'interno dei messaggi privati.

onst { SlashCommandBuilder } = require('discord.js');

const data = new SlashCommandBuilder()
  .setName('ping')
  .setDescription('Risponde con Pong!')
  .setDMPermission(false);

Permessi degli Utenti

Possiamo utilizzare il metodo setDefaultMemberPermissions() della classe ApplicationCommand per impostare i permessi necessari affinché un utente possa eseguire il comando. Impostando il valore 0 si impedirà a chiunque, all'interno di un server, di eseguire il comando a meno che non sia configurata una sovrascrittura specifica o l'utente non disponga del permesso di amministratore.

const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');

const data = new SlashCommandBuilder()
  .setName('ban')
  .setDescription('Seleziona un membro e bannalo dal server.')
  .addUserOption(option =>
    option.setName('target').setDescription('Il membro da bannare'))
  .setDefaultMemberPermissions(PermissionFlagsBits.KickMembers | PermissionFlagsBits.BanMembers);

Se non sei familiare con l'operatore I, chiamato OR bit a bit, puoi consultare la pagina di Wikipedia sull'argomento.

Fine della sezione!

E questo era tutto quello che c'era da sapere sui permessi relativi agli Slash Commands!