Writing a Discord Bot with Eris (Slash Commands Edition)

canaris

DET171

Posted on September 11, 2022

Writing a Discord Bot with Eris (Slash Commands Edition)

Writing a Discord bot with slash commands using Eris

Introduction

Yep, the title says it all. I decided to do this as there hasn't been any written guide I'm aware of that shows how to write a Discord bot with slash commands using Eris.

Anyways, let's get started! You first need the following few things:

  • Your Discord bot's token (refer to this if you don't know how)
  • Node.js installed on your computer (preferrably v16 or higher)
  • A good code editor/IDE (optional, but recommended)

Now install the required dependencies:

yarn add eris dotenv eslint consola
# or using NPM
npm i eris dotenv eslint consola
Enter fullscreen mode Exit fullscreen mode

I'm also going to use ESLint to enforce code style and consola for nicer console messages.

And finally, add these scripts to your package.json:

"scripts": {
    "start": "node index.js",
    "lint": "eslint ."
}
Enter fullscreen mode Exit fullscreen mode

You also need to set type to module in your package.json file.

Writing the code

Now create a file called index.js and add the following code:

import Eris, { Constants, Collection, CommandInteraction } from 'eris';
import fs from 'fs';
import console from 'consola';
import * as dotenv from 'dotenv';
dotenv.config();
Enter fullscreen mode Exit fullscreen mode

We're importing Eris, the Constants and Collection from Eris, the native fs module, consola and dotenv.

Before I forget, you need to create a .env file and add your bot's token to it:

TOKEN=your-bot-token
Enter fullscreen mode Exit fullscreen mode

Now let's creat the client:

const client = new Eris(`${process.env.TOKEN}`, {
    intents: [
        Constants.Intents.guilds,
        Constants.Intents.guildMessages,
        Constants.Intents.guildMessageReactions,
        Constants.Intents.directMessages,
        Constants.Intents.directMessageReactions,
    ],
});
Enter fullscreen mode Exit fullscreen mode

Registering commands

Now let's register the commands. First, create a folder called commands and add a file called ping.js under it. Then add the following code:

export default {
    name: 'ping',
    description: 'Ping!',
    execute: (i) => {
        i.createMessage('Pong.');
    },
};
Enter fullscreen mode Exit fullscreen mode

We basically export an object with the command information and the function to execute when the command is used for later use.

In index.js, add a ready event listener:

client.on('ready', async () => {
    console.info(`Logged in as ${client.user.username}#${client.user.discriminator}`);
    console.info('Loading commands...');
});
Enter fullscreen mode Exit fullscreen mode

Inside the ready event listener, we're going to load commands:

client.on('ready', async () => {
    // ...

    // first create a collection to store commands
    client.commands = new Collection();
    // load all the js files under ./commands
    const commandFiles = fs.readdirSync('./src/commands').filter(file => file.endsWith('.js'));
});
Enter fullscreen mode Exit fullscreen mode

We now have an array of file names, so we can loop through them and register them:

client.on('ready', async () => {
    // ...

    for (const file of commandFiles) {
        const command = (await import(`./commands/${file}`)).default;
        client.commands.set(command.name, command);

        client.createCommand({
            name: command.name,
            description: command.description,
            options: command.options ?? [],
            type: Constants.ApplicationCommandTypes.CHAT_INPUT,
        });
    }

    console.info('Commands loaded!');
});
Enter fullscreen mode Exit fullscreen mode

Note that I used the dynamic import, as import ... from 'xxx' can only be used at the top level, and require() isn't available in ESM. The dynamic import retunrs an object with a default property, which is the exported object from the file, which is the reason for the .default at the end.

client.createCommand basically registers the command, and accepts an object with the command information. You can read more about it here.

Handling commands

Now, for the command handler! First, create an interactionCreate event listener:

client.on('interactionCreate', async (i) => {

});
Enter fullscreen mode Exit fullscreen mode

We also need to check if the interaction is a command interaction, and whether it exists or not:

client.on('interactionCreate', async (i) => {
    if (i instanceof CommandInteraction) {
        if (!client.commands.has(i.data.name)) return i.createMessage('This command does not exist.');
    }
});
Enter fullscreen mode Exit fullscreen mode

i instanceof CommandInteraction basically checks to see if i is a CommandInteraction object. If it is, we check if the command exists, and if it doesn't, we send a message saying that the command is not yet implemented. This is unlikely to happen as you aren't likely to register commands that don't exist yet, but in case you do (for certain reasons like testing or as a placeholder), this is a good way to handle it.

Remember the collection we created earlier on? We're gonna use it now:

client.on('interactionCreate', async (i) => {
    if (i instanceof CommandInteraction) {
        if (!client.commands.has(i.data.name)) return i.createMessage('This command does not exist.');

        try {
            await client.commands.get(i.data.name).execute(i);
        }
        catch (error) {
            console.error(error);
            await i.createMessage('There was an error while executing this command!');
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

We get the command from the collection and execute it, and if there's an error, we log it and send a message to the user.

Finally, don't forget to connect the client!

// ...
client.connect();
Enter fullscreen mode Exit fullscreen mode

You can now run npm run dev and test your bot!

Conclusion

That's it! You now have a bot that can handle slash commands. You can add more commands by adding more files under the commands folder. You can also read more about slash commands and Eris.

Final Code

index.js:

import Eris, { Constants, Collection, CommandInteraction } from 'eris';
import fs from 'fs';
import console from 'consola';
import * as dotenv from 'dotenv';
dotenv.config();

const client = new Eris(`${process.env.TOKEN}`, {
    intents: [
        Constants.Intents.guilds,
        Constants.Intents.guildMessages,
        Constants.Intents.guildMessageReactions,
        Constants.Intents.directMessages,
        Constants.Intents.directMessageReactions,
    ],
});


client.on('ready', async () => {
    console.info(`Logged in as ${client.user.username}#${client.user.discriminator}`);
    console.info('Loading commands...');
    client.commands = new Collection();

    const commandFiles = fs.readdirSync('./src/commands').filter(file => file.endsWith('.js'));
    for (const file of commandFiles) {
        const command = (await import(`./commands/${file}`)).default;
        client.commands.set(command.name, command);

        client.createCommand({
            name: command.name,
            description: command.description,
            options: command.options ?? [],
            type: Constants.ApplicationCommandTypes.CHAT_INPUT,
        });
    }
    console.info('Commands loaded!');
});

client.on('error', (err) => {
    console.error(err); // or your preferred logger
});

client.on('interactionCreate', async (i) => {
    if (i instanceof CommandInteraction) {
        if (!client.commands.has(i.data.name)) return i.createMessage('This command does not exist.');

        try {
            await client.commands.get(i.data.name).execute(i);
        }
        catch (error) {
            console.error(error);
            await i.createMessage('There was an error while executing this command!');
        }
    }
});

client.connect();
Enter fullscreen mode Exit fullscreen mode

commands/ping.js:

export default {
    name: 'ping',
    description: 'Pong!',
    execute: (i) => {
        await i.createMessage('Pong!');
    },
};
Enter fullscreen mode Exit fullscreen mode

If you have any errors, you may ask me via Discord (𝓐𝓭𝓢𝓲𝓻π“ͺ𝓡 𝓒π“ͺ𝓷π“ͺ𝓻𝓲𝓼#0340).

πŸ’– πŸ’ͺ πŸ™… 🚩
canaris
DET171

Posted on September 11, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related