Discord Bot using Eris
What is Discord?
Discord is a pretty widespread platform nowadays, so it's unlikely that someone has not heard of it, but here is a definition:Discord is a free, versatile communication app designed for text, voice, and video chat, allowing users to create or join private, organized, and topic-based virtual communities called "servers". Primarily used by gamers, it has expanded to include diverse groups for socializing, working, and streaming
What is Eris?
Eris is a library that allows us to interact with Discord's API. There are other popular libraries out there, like Discord.js, but I wanted to use Eris because from one quick look, it seemed simpler and much more intuitive to use. From what I've read, Eris also has the advantage of being faster - not that I was going to create a bot that would scale that big. The documentation is a bit worse than the one Discord.js provides, but it was something I was willing to accept. They provide us with a few examples to get started and then theirindex.d.ts and Constants.d.ts files handle the rest. I guess that is their documentation, on which they have done a very nice job.Why would anyone want to create a bot?
For starters, a Bot is an application that simulates a user (well, it is in fact a user, just not a real person) that moderates or performs other actions in a server or even in private messages. It can also be used in more creative ways. For example, combining it with other apps, IoT and/or robots gives you a fun little assistant that can notify you of various events on Discord, whether that event is saved on Google Calendar or my dog approaching the food bowl. Anything, really. This has no practical application that I can think of off the top of my head, but this is what I consider fun, so ...One day sometime ago I decided I wanted to learn how to create one myself. I wasn't that much into programming back then, and I barely had any knowledge, so everything just flew over my head. I followed the tutorial but I didn't really realize what I was doing, so even though it worked, I didn't understand what was going on. Funnily enough, that was one reason why I chose JavaScript as my first programming language. That and because there was a lot of documentation online.
Recently I decided I wanted to give it another go, now that I have a bit more experience in the field. I am not part of any large servers that need moderation, nor is there anything I care about automating in Discord. My reason for creating one is mostly to try something new on a platform that I like. And maybe revenge for back then.
How does a Bot function?
Before I write about Bots, we need to talk about Discord Applications first. I said earlier that Bots are applications, which is not 100% accurate.Discord lets us create 'apps' from their Developer Portal, that allow us to:
customize, extend, and enhance Discord for millions of users. Whether you're a developer interested in building an Activity, customizing servers, or integrating a game, apps are the container to bring your idea to life.Applications are cool, but it's the Bot that brings the application to life and adds interactivity. A Bot is a user that is registered on discord that brings all of the Application's magic into life:
Newly-created apps have a bot user enabled by default. Bot users allow your app to appear and behave similarly to other server members when it's installed to a server.So basically a Discord Bot is an application that executes some pre-defined commands in a server or in a user's DMs.
Application contexts
Apps can either be installed on a user or on a server. Installing an application on a server will make it accessible by all the members (as long as they have the proper permissions). Installing an app on a user will give the user access to that app from everywhere.Technical implementation
That is it for the general information regarding applications and bots. How do we actually create a bot? How does it work behind the scenes?Before creating a bot with Eris, I wanted to get a good grasp of what is going on. I mean, what is really happening when I use a library. Using Eris, creating a bot is as simple as
bot.js
const bot = new Eris();
bot.on("ready", () => {
console.log("connected");
});but that does not really help me understand anything. This is wonderful for someone who does not care how everything is handled, but not for me. So I decided that I first wanted to build a discord bot without using any libraries - by interacting with the Discord API on my own.
Discord API
The Discord API gives us two ways of exchanging information. One is through HTTP requests, and the other is through the Gateway API, as they call it. This is basically just connecting to a WebSocket server that Discord has up and running 24/7. This is the part that I found cool, since I had never done something similar.Connecting to the WebSocket is simple enough - just write a snippet of code that connects to a WebSocket server. I used the 'ws' library to do that:
bot.js
const WebSocket = require("ws");
require("dotenv").config();
const token = process.env.BOT_TOKEN;
const baseUrl = "wss://gateway.discord.gg";
const queryParams = "?v=10&encoding=json";
const gatewayUrl = `${baseUrl}${queryParams}`;
const ws = new WebSocket(gatewayUrl);
ws.on("open", () => {
console.log('Socket opened. Initializing connection ...');
})After the connection has been established, we need to start sending Discord heartbeats. Heartbeats are messages (pings) that we send at a set interval of time, showing Discord that our application is active.
To start sending heartbeats, we first need to wait for Discord to send us a Hello (opcode 10) event. That event's data will include the interval at which we need to send the pings.
To start sending heartbeats, we first need to wait for Discord to send us a Hello (opcode 10) event. That event's data will include the interval at which we need to send the pings.
bot.js
ws.on("message", e => {
const msg = JSON.parse(e.toString());
const { op, t, s, d } = msg; // the arguments of the message sent by discord
// s stands for 'sequence'. this is a number used to keep track of our 'conversation' and we use it to 'acknowledge' messages
if (s) {
last_sequence = s;
}
// confirm that connection is established - start heartbeat interval
switch (op) {
case 10: {
console.log(`Received opcode 10`);
// d stands for 'data'
heartBeatIntervalMS = d.heartbeat_interval;
heartbeat = startHeartbeat(ws, last_sequence, heartBeatIntervalMS);
sendIdentify(ws);
break;
}
}
})where
startHeartbeat and sendIdentify are functions defined as:functions.js
1const startHeartbeat = (socket, sequence, interval) => {
2 const int = setInterval(() => {
3 if (socket.readyState !== WebSocket.OPEN) return;
4 const d = sequence || null;
5 const payload = { op: 1, d };
6 socket.send(JSON.stringify(payload));
7 console.log('heartbeat sent');
8 }, interval);
9
10 return int;
11}functions.js
1const sendIdentify = (socket, token) => {
2 const payload = {
3 op: 2,
4 d: {
5 token,
6 intents:
7 INTENTS.GUILDS |
8 INTENTS.GUILD_MESSAGES |
9 INTENTS.GUILD_MESSAGE_REACTIONS |
10 INTENTS.DIRECT_MESSAGES |
11 INTENTS.DIRECT_MESSAGE_REACTIONS,
12 properties: {
13 os: "macintosh",
14 device: "pc"
15 }
16 }
17 };
18
19 socket.send(JSON.stringify(payload));
20}Token is the means of authorization and is provided by the Discord portal when we create the Bot.
The function is called
Intents are the permissions we give our application/bot: what it is allowed to access and do as a User in Discord. What we send is an integer, which when broken down to binary represents the permissions.
The function is called
sendIdentify because after we start sending heartbeats, we need to go through identification. The Identify event is an initial handshake with the Gateway that’s required before our app can begin sending or receiving most Gateway events.Intents are the permissions we give our application/bot: what it is allowed to access and do as a User in Discord. What we send is an integer, which when broken down to binary represents the permissions.
functions.js
1const INTENTS = {
2 GUILDS: 1 << 0,
3 GUILD_PRESENCES: 1 << 8,
4 GUILD_MESSAGES: 1 << 9,
5 GUILD_MESSAGE_REACTIONS: 1 << 10,
6 DIRECT_MESSAGES: 1 << 12,
7 DIRECT_MESSAGE_REACTIONS: 1 << 13,
8};Where I've used the bitwise OR operation to get the resulting integer for the intents field.
This is an interesting topic on its own, but in short:
means 'x shifted by y digits to the left'. 'x' is the binary representation of a number. So if 'x' was 4, the binary representation would be
(assuming 16-bit numbers), and for y = 8:
which is just 210=1024
So, for the
Bitwise OR
This is an interesting topic on its own, but in short:
x << ymeans 'x shifted by y digits to the left'. 'x' is the binary representation of a number. So if 'x' was 4, the binary representation would be
0000000000000100(assuming 16-bit numbers), and for y = 8:
0000010000000000which is just 210=1024
So, for the
GUILD_MESSAGES intent, 1 << 9 would be 512 (29)Bitwise OR
(|) between two numbers performs the OR operation on each bit of the two numbers.There are other opcodes for the various messages we receive from Discord, but these are the important ones.
Wrapping up
And that is it. Running this file will make the Bot user go online and show up as active on servers and DM's that it is a part of. Not that it does much yet, but this is the first step. Making the Bot interact with users is going to take quite a bit of work. This was the most interesting part for me though, which is why I wanted to do it without a library in the first place. The rest was creating commands and responding to messages, and it was not much different - executing API calls and talking through the socket connection with different codes. I decided to spare me some trouble and switch to Eris for these.Setting up commands with Eris
First things first, we have to initialize the bot. We useEris.CommandClient so we can support both prefix commands (like !echo) and slash commands. We pass the token, the intents we need (guilds, guild messages, message content, and direct messages), and a prefix for legacy commands:bot.js
1const Eris = require("eris");
2require("dotenv").config();
3
4const bot = new Eris.CommandClient(
5 `Bot ${process.env.BOT_TOKEN}`,
6 {
7 intents: [
8 "guilds",
9 "guildMessages",
10 "messageContent",
11 "directMessages",
12 ],
13 },
14 { prefix: "!" }
15);
16
17bot.on("ready", () => {
18 console.log("Bot is connected and ready.");
19});
20
21bot.on("messageCreate", (msg) => {
22 if (msg.author.bot) return;
23
24 if (msg.content === "!echo") {
25 bot.createMessage(msg.channel.id, "receiving");
26 }
27});
28
29bot.connect();Slash commands have to be registered with Discord before users can see them. We do that with
bot.createCommand, usually inside the ready event so we only register once the connection is up. Each command needs a name, a description, and a type. For a normal text slash command we use CHAT_INPUT:bot.js
1bot.on("ready", async () => {
2 console.log("Bot is connected and ready.");
3
4 await bot.createCommand({
5 name: "ping",
6 description: "Replies with Pong!",
7 type: Eris.Constants.ApplicationCommandTypes.CHAT_INPUT,
8 });
9
10 await bot.createCommand({
11 name: "hello",
12 description: "Greets you with your username.",
13 type: Eris.Constants.ApplicationCommandTypes.CHAT_INPUT,
14 });
15});When a user runs a slash command, Discord sends an interaction. We listen for it with
interactionCreate. We check that the interaction is a CommandInteraction (so we ignore button clicks, autocomplete, etc.), read interaction.data.name to see which command was used, and reply with interaction.createMessage. That is how the /ping and /hello commands are handled:bot.js
1bot.on("interactionCreate", async (interaction) => {
2 if (interaction instanceof Eris.CommandInteraction) {
3 const commandName = interaction.data.name;
4
5 if (commandName === "ping") {
6 await interaction.createMessage("Pong!");
7 }
8
9 if (commandName === "hello") {
10 const user = interaction.member?.user ?? interaction.user;
11 await interaction.createMessage(`Hello, ${user.username}!`);
12 }
13 }
14});With this in place, the bot will register the slash commands on startup and respond to
Important notice: Global commands can take up to an hour to appear everywhere; for faster iteration we can use
/ping with "Pong!" and to /hello with a greeting that includes the user's username. We can add more commands by extending the createCommand calls and the interactionCreate handler in the same way.Important notice: Global commands can take up to an hour to appear everywhere; for faster iteration we can use
bot.createGuildCommand(guildId, ...) to register commands for a single server only, which are registered instantly.Conclusion
That is quite a bit of information to go through, but this is it. Apart from setting up the app and creating the bot in the developer portal, this is the code needed to run a barebones discord bot.