DEV Community

Elijah Trillionz
Elijah Trillionz

Posted on • Updated on

Build A Modern Discord Bot from Scratch. Learn the basics

Discord bots help you interact with members of a server as well as moderate the server. A discord bot can send messages on the server, message a user directly (DM), ban a user, promote and demote a user and so much more.

As a server owner, you are not always going to be present to monitor your server, but a bot can, and it does it way faster.

You may not be a server owner, but you want to create a bot for a server you belong to or maybe for public use (available for other servers) this article will help you do that.

Before we jump right into code, let's see how Discord bot works.

Audience Intended for
This article is mainly focused on beginners who don't know how Discord bots work, and how to build them. So if you're already familiar with building discord bots, you may not find something new here.

Though it's for beginners I do expect you to know a little about working with NodeJS and npm.

How Discord bot works

If you're in a server where there are bots you may have noticed that these bots are similar to users account.

They usually have these bot-looking profile pictures, seem to always be online, reply to messages very fast. These are cool, but how do all these things work?

There is a type of user dedicated for automation called bot accounts. They look a lot like the user's account.

The bot accounts are authenticated using a token (rather than a username, password), and this token gives these accounts full access to all Discord API routes.

So basically,

  1. We create a bot on Discord developers website (more details soon)
  2. Assign roles to the bot i.e granting permissions to the bot
  3. Create an OAuth scope for the bot (simply, a link for authentication)
  4. Add the bot to one of our servers
  5. Boom! The bot starts performing magic like replying to messages.

Pretty easy. Though I must mention before the bots start to perform magic you'd need to have connected to Discord API, and logged the bot in. This is how we will create this bot

  1. Create the bot in Discord
  2. Create permissions for our bot
  3. Generate an OAuth link and use it to connect to our discord server
  4. We will then create a folder for the bot in our computer, open VSCode
  5. Install some dependencies, write some code to connect to Discord API
  6. With that our bot is online

Don't fret if you don't get it now. More will be explained in detail later.

What can you build with a discord bot?

Discord bots can span from a hubby-friendly bot to a very powerful bot. You can build anything with a discord bot. But here are some ideas.

  • A YouTube video fetcher
  • Interesting Tweet fetcher
  • A meme fetcher from Reddit
  • A game
  • A scheduler with a calendar
  • A music player and song fetcher
  • Server manager
  • Quiz bot

And so much more. Here are some more Discord bot ideas

About bot

The bot we will create for this article is going to be very basic, but it will contain almost all you need to build that super bot of yours.

With this bot, we will be able to reply to messages (commands), view message history, send DM's.

So try to follow along as much as you can. I will use my discord server for this project.

If you don't have a server you own or manage, you should create one.

Let's Create Our First Bot

Just a quick reminder that this is a follow along with this article. So try to do what I do/did as you read.

Create Bot

The first step we will take is to create the bot on Discord developers page. To create a bot you first need to create an application.

  1. So head up to https://discord.com/developers/applications, click create New Application at the top right corner.
  2. Enter the name of the app. You can name it whatever you want, but for the sake of this tutorial, I'll name it Buddy

Hurray! You just created your first discord application. Now let's create the bot.

  1. Click Bot in the left side nav
  2. Now click Add Bot
  3. A modal will pop up, simply click the blue button to continue

Yahoo! A wild bot has appeared! Ready to give this bot life?.

Bot Permissions and OAuth

Now we need to define some permissions for this bot, but to do this we have to create an OAuth scope first. It's simple

Click OAuth2 in the left sidenav.

Here you will find some checkboxes with a sub-heading called "SCOPES".

Look for bot in the middle column, tick it.

Defining Permissions

Another set of checkboxes under a sub-heading called "BOT PERMISSIONS" will display (only if you clicked tick in the first set of checkboxes)

Now select the permissions you want for your bot, again for the sake of this tutorial we will select.

  1. View channels (this is required).
  2. Send messages.
  3. Embed links.
  4. Manage messages.
  5. Read message history.

That would be all the permissions we need for this bot.

Once you're done, scroll back to the first set of checkboxes ("SCOPES") and copy the link below.

Open a new tab in your browser and paste that link, next thing is to select the server you want the bot in. Then click Continue.

Next, you will see a list of permissions that we selected, you can simply click Authorize to move on, verify you are a human and that will be all.

If you check the Discord server you invited this bot into, you'd see that the bot is there but offline. Now it's time to make it come alive.

Connecting to Discord API

I believe you already have a folder set up on your local machine. If not do that now.

For this tutorial, I will make use of NodeJS. You can use other languages like Python to build Discord bots too.

Setting up our environment

Since we have our folder ready, open up a terminal and run npm init -y.

For this to run you need to have NodeJS and NPM installed in your local machine (specifically NodeJS 16.6.0 or newer).

Installing Dependencies

We will need just two dependencies. - Discord.js: npm install discord.js - Nodemon (dev dependency): npm install -D nodemon
Run the commands above to install the dependencies.

Discord.js allows us to interact with the Discord API in NodeJS.

Nodemon restarts the app whenever will make and save new changes.

Moving on

Create a file called app.js. You can call it anything like bot.js or index.js.

Open your package.json file and change main to the name of the file you just created.

Next copy these JSON scripts into the scripts property in the package.json file

  "scripts": {
    "app": "nodemon app",
    "start": "node app"
  },
Enter fullscreen mode Exit fullscreen mode

Moving on

Create a folder called config and a file called default.js; we will store our secrets here.

Copy the following into config/default.js

const config = {
  DISCORD_TOKEN: 'YOUR TOKEN HERE',
};
module.exports = config;
Enter fullscreen mode Exit fullscreen mode

Replace 'YOUR TOKEN HERE' with your discord token.

You can find your discord token in the discord developers. Click your application, click Bot at the left side nav, now click Copy (close to the bot's profile pic).

Moving on

Create a file in the config folder, call it config.js. So you have config/config.js. In this file, we will have all of our configurations.

These configurations include commands, prefix(es), and Intents.

  • Commands are simply commands that the bot will respond to. So whenever a user types a command in the discord server, the bot will respond accordingly.

  • Prefix or prefixes (can vary) is a command prefix. For this bot, we will have just one prefix. A prefix is used just before a command, e.g !get-meme. ! Is a prefix while get-meme is the command.

You can as well call !get-meme as the command

  • Intents are new, they state the events you want your bot to receive based on what the bot does. Without these intents stated, Discord API will throw an error.

So let's get started.

Build a Discord Bot

Let's first make the bot come online.

Go to config/config.js and import Intents as

const { Intents } = require('discord.js');
Enter fullscreen mode Exit fullscreen mode

Copy and paste the code below afterward

const {
  DIRECT_MESSAGES,
  GUILD_MESSAGES,
  GUILDS,
} = Intents.FLAGS;
Enter fullscreen mode Exit fullscreen mode

These are the permissions we want our bot to have, so we are simply destructuring it from Intents.FLAGS provided by 'discord.js'.

Create an array, call it "botIntents", and copy-paste the variables above into it, so you have something like

const botIntents = [
  DIRECT_MESSAGES,
  GUILD_MESSAGES,
  GUILDS,
];
Enter fullscreen mode Exit fullscreen mode

Now export botIntents

module.exports = { botIntents };
Enter fullscreen mode Exit fullscreen mode

In app.js import the following

const { Client } = require('discord.js');
const { botIntents } = require('./config/config');
const config = require('./config/default');
Enter fullscreen mode Exit fullscreen mode

Then paste this

const client = new Client({
  intents: botIntents,
  partials: ['CHANNEL', 'MESSAGE'],
});
Enter fullscreen mode Exit fullscreen mode

Here we simply create a new client through the Client class from 'discord.js', and pass in some props.

The first prop is intents which are our botIntents, and the last is partials; an array, this is so our bot can be able to send direct messages. If you don't need this feature you can remove the prop

Moving on

Now we have access to the Discord API, we can now make listen for events.

The first event we will listen for is onready. In other words, when the bot is ready to go online

client.on('ready', () => {
  console.log('Logged in as ' + client.user.tag);
});
Enter fullscreen mode Exit fullscreen mode

We simply log to the console the name of the bot when the bot is ready to come online.

We are almost there. Before our bot will come online, we will need to log in with our Discord token.

At the bottom of app.js copy-paste this

client.login(config.DISCORD_TOKEN);
Enter fullscreen mode Exit fullscreen mode

Recall, the config file is an object that holds our Discord token.

Now run the app, go to your discord server and you'll see the bot online.

Though the bot is online, it cannot send any messages or reply to any messages. So let' work on that next.

Setting up Commands

I usually use RegEx to set up commands and use switch and case to check for what command was used. This is when the bot listens for different commands.

But this bot is a simple one, so we will keep things simple.

In config/config.js, let's register some commands. Create an object called commands and paste in the following like

const commands = {
  getName: 'get-name',
  tellJoke: 'tell-a-joke',
  sad: 'sad',
  lastMsgs: 'last-messages',
};
Enter fullscreen mode Exit fullscreen mode

So these are the commands our bot will listen for.

Before we export, create a variable and call it prefix, assign '!' to it. You can use any other prefix of your choice like '$'. So we have const prefix = '!';

Export both the commands and prefix as commands and prefix respectively.

In app.js, import commands and prefix from config/config.js. Simply add commands, prefix to the curly braces around botIntents.

Moving on

Copy-paste the following into app.js

client.on('messageCreate', (msg) => {
  if (msg.author.bot) return;
  if (!msg.content.startsWith(prefix)) return; // do nothing if command is not preceded with prefix

  const userCmd = msg.content.slice(prefix.length);

  if (userCmd === commands.getName) {
    msg.reply(msg.author.username);
  } else {
    msg.reply('I do not understand your command');
  }
});
Enter fullscreen mode Exit fullscreen mode

Oh wow, a lot is going on here. Let's break it down, shall we?

  • We listened for an event called messageCreate, there are others like messageDelete, messageReactionAdd, etc. Check the docs for all.
  • The messageCreate event returns a msg parameter containing the message info.
  • Next thing we did is check if the message is from a bot in msg.author.bot. Here we want to make sure we ignore messages that are from bots.
  • Also we ignore messages that do not contain our declared prefix ('!').
  • Next stop is to get the actual message without the prefix, that's why we slicing out the prefix. And then we assign it to userCmd (as in user command).
  • Finally, we checked if the content of the message (without the prefix now) is the same thing as our first command (i.e getName). If it is the same then
  • we replied to the user with his/her username using (msg.author.username). Find more on messages in the docs. If it's not the same
  • we replied with another message "I do not understand your command".

Save the changes. Go to your discord server, type in any message with the prefix and see the response. Now type in '!get-name' and see the response as well.

You can make the message a little nicer with Your discord username is ${msg.author.username}. This is not exactly useful in real life bot; returning the user's username. But at least it shows you what's possible.

Moving on
To add the rest commands, we will just add more else if to the initial if-chain. Like this

if (userCmd === commands.getName) {
  msg.reply(msg.author.username);
} else if (userCmd === commands.tellJoke) {
  msg.reply('HTML is a programming language'); // bad joke i guess, unless i don't have any jokes
} else if (userCmd === commands.sad) {
  msg.reply("Don't be sad! This is not the end of the road");
} else if (userCmd === commands.lastMsgs) {
  const reply = await getLastMsgs(msg);
  msg.reply(reply);
} else {
  msg.reply('I do not understand your command');
}
Enter fullscreen mode Exit fullscreen mode

To get the last messages we will create a function in app.js called getLastMsgs and pass in one argument.

Traditionally if each command your bot listens to has an ambiguous amount of things to do, it is often recommended to break these tasks into functions, for readability.

Also, you could put the functions in a separate file inside the same folder, you can call the folder actions or something.

Am not saying you should do this now, am just saying it's better to do it this way if the bot has a lot to do. But this bot doesn't do much so.

Here is an example. The bot's project was canceled though, but it should show you how bots with lots of tasks get structured.

Moving on
Copy-paste this into the getLastMsgs function, (You can create an asynchronous function if you haven't) like so

const getLastMsgs = async (msg) => {
  // fetching the last 10 messages
  const res = await msg.channel.messages.fetch({ limit: 10 });

  return 'Last ten messages';
};
Enter fullscreen mode Exit fullscreen mode

Technically we are passing the msg parameter we received from the onmessageCreate event. So in the current channel where the command was received (could be a DM or server), the last ten messages will be fetched.

The fetch method is provided by the Discord API, you should read about it after this.

The result of this is an array of ten messages, it's not like a traditional array that you can access each item using an index. For example, if you want to get the first message in the array, you'd have to use the .first() method.

So the first messages' content would be accessed like

res.first().content; // don't add this to the function, just a showcase
Enter fullscreen mode Exit fullscreen mode

Another good thing is, we can loop through each array item. So before the return statement in the getLastMsgs function, add the following

const lastTenMsgs = messages.map((message) => {
  return message.content;
});
Enter fullscreen mode Exit fullscreen mode

We can loop through with forEach or map, we also have access to the filter method

Now change the return statement to lastTenMsgs. In other words, your function should look like this

const getLastMsgs = async (msg) => {
  // fetching the last 10 messages
  const res = await msg.channel.messages.fetch({ limit: 10 });

  const lastTenMsgs = res.map((message) => {
    return message.content;
  });

  return lastTenMsgs;
};
Enter fullscreen mode Exit fullscreen mode

Before you save, remember to pass in async in your messageCreate event function. I.e

client.on('messageCreate', async (msg) => {});
Enter fullscreen mode Exit fullscreen mode

Now save the app, and test the new commands. The "!last-messages" command will throw an array, we will fix that soon. But for now, let's spice up the bot a little

First thing is first, not all messages would be replied, rather a message would be created by the bot. Let's do that with the "!tell-a-joke" command.

Instead of msg.reply, do this

msg.channel.send('HTML bla bla bla');
Enter fullscreen mode Exit fullscreen mode

You will know more of these when you study the docs, the docs is well written.

Another thing is, we said the bot should be able to send direct messages. So let's do that with the "!last-messages" command.

Instead of msg.reply, do this

msg.author.send(reply);
Enter fullscreen mode Exit fullscreen mode

This doesn't fix the error yet. We are getting to that now.

Lastly, you must have noticed some bots in Discord sending/replying messages with colors by the side, bold words, with footers and headers like it's a blog post.

Well, it's not difficult to do. But before we do that, I should let you know that you can make a word or text bold traditionally.

It's almost like it's markdown, but not all recognized markdown syntax can be used. Let's make the "!tell-a-joke" text bold with

msg.channel.send("**HTML** bla bla bla.\nI really don't have a joke");
Enter fullscreen mode Exit fullscreen mode

If you test the command, you'd notice HTML is now bold, and "I really don't have a joke" on a new line.

With that being said let's move on.

To make our messages like it's a blog post with nice colors, let's use the "!last-messages" command for this.

In app.js, first import MessageEmbed from 'discord.js'. So you have

const { Client, MessageEmbed } = require('discord.js');
Enter fullscreen mode Exit fullscreen mode

In the getLastMsgs function, add this

const embeds = [];

lastTenMsgs.forEach((msg, index) => {
  const embed = new MessageEmbed()
    .setColor('ORANGE') // can be hex like #3caf50
    .setTitle(`Message ${index + 1}`)
    .setDescription(`${msg}`)
    .setFooter('Buddy says Hi');

  embeds.push(embed);
});
return embeds;
Enter fullscreen mode Exit fullscreen mode

We are simply creating a new message embed and using some methods on it. For each message (from the ten messages), we will create an embed and push it to an array of embeds which we later returned.

The methods setColor, setTitle, etc are pretty descriptive. Learn more on embeds here.

Our reply for the "!last-messages" command will now change to

msg.author.send({ embeds: reply });
Enter fullscreen mode Exit fullscreen mode

We need to let discord know that it's an embed for it to work.

If it was just one embed you should also make sure you wrap it in an array i.e

msg.author.send({ embed: [onlyEmbed] });
Enter fullscreen mode Exit fullscreen mode

Now save the changes and test your command. Now the error is gone. Now that we have all of these working. Let's now publish the bot and make it online forever!

I will use Heroku's free plan for this. But the thing is, our Heroku's dyno will go to sleep after 30 minutes of inactivity.

The solution to that is Uptime robot. Uptime robot will keep your app alive. There is a side effect of doing this though, so usually, the best alternative to Heroku is Replit.

But whatever the case, you'd still need Uptime robot to keep the server alive, and you'd need a server (not a discord server).

So whether you are using Replit or Heroku, you need to have a server first and connect your bot to the server. So let's create a server in our local machine.

Since this is NodeJS let's use 'express'. Install express with npm i express.

Create a file in the root directory called server.js. In your package.json change your main to "server.js" and your scripts to point to "server.js" not "app.js".

In server.js paste the following;

const express = require('express');
const app = express();
const PORT = process.env.PORT || 5000;

app.get('/', (req, res) => {
  res.send('Buddy bot is running');
});

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Enter fullscreen mode Exit fullscreen mode

Not a lot going on here, we only just created a server with express.

We created just one route with a simple reply message. If you've never worked with express or NodeJS servers, trust me you really don't have much to worry about here.

Just copy-paste that in and you are good to go.

If you save. Rerun the program with npm run app and you'd see the log message 'Server running on port 5000'.

If you go to your web browser, open a new tab, and enter 'http://localhost:5000' you'd receive the message 'Buddy bot is running'.

Now the server is working fine. But the bot doesn't seem to be working with it. Let's fix this

In app.js, where we have client.login, create a function called startBot and wrap it around the client.login. So you have

const startBot = () => {
  client.login(config.DISCORD_TOKEN);
};

// export startBot as default
module.exports = startBot;
Enter fullscreen mode Exit fullscreen mode

In server.js, import startBot from './app.js'.

Now call the function just before the first route i.e

startBot();

// before app.get()
Enter fullscreen mode Exit fullscreen mode

You can call the function anywhere though, as long as it's before the listen method. But I prefer doing it before the routes.

Before you push, don't forget to great a .gitignore file to ignore node*modules. And be careful where you push to. If you're going to push to GitHub, add /config/default.js to _gitignore*.

Now push to Heroku or Replit. I already wrote an article on using uptime robot. So check that out.

Conclusion

I believe this is clear enough and can help you get started making bots for dozens of servers or just a server. If you have any challenges, just let me know in the comments.

The source code for this project is on GitHub, please give it a star, and you know give me a follow if you enjoyed this.

Finally, before I go, I really do make tweets daily on Twitter (@elijahtrillionz) on web development tips and resources. You should give me a follow, turn on notification, and let's stay connected.

Thanks for reading. I'll see you and your bot next time.

Discussion (17)

Collapse
lioness100 profile image
Lioness100

Very well written! Thanks for this great article. A few notes, though:

Intents are new, but they state the permissions your bot requires.

This isn't true. Intents allow you to subscribe to only the events you want to receive from the discord API to decrease memory usage. Also, why did you include the typing and reaction intents? It didn't seem like those were used in the guide code (and would in fact inflate memory usage unneededly).

if (!msg.content.includes(prefix)) return;

Especially since you're assuming the prefix is at the beginning when you slice it off of the message, you should probably use startsWith, not includes. For example, this would currently pass the "it's a command" check: sad is what I am!

Collapse
elijahtrillionz profile image
Elijah Trillionz Author • Edited on

Oh thanks for this. I never really understood the intents yet. Will check more on it and make the changes.

Also the intents I stated were originally to be used, but the article had grown to large already.

And thanks for the correction on the prefix. I checked the message properties, but that of content doesn't seem to have any prop (from the docs). Or is it msg.startsWith?, because i still can't find that.

Update: I just verified the startsWith method on VSCode, but I still can't find it in the docs though. It's like it's hidden or something

Collapse
lioness100 profile image
Lioness100 • Edited on

message.content is a string. You can use any string method on it. It wouldn't be listed on discord.js docs

developer.mozilla.org/en-US/docs/W...

Thread Thread
elijahtrillionz profile image
Elijah Trillionz Author

oh wow! I didn't know about this method before. Thanks for sharing

Collapse
hanatic profile image
Hannah Gray

Just like to stress that "YouTube fetchers" as I believe the author phrases it, are against YouTube's API terms of service and risk being sent a Cease and Desist letter (as happened in the cases of Groovy and Rythm).

Collapse
elijahtrillionz profile image
Elijah Trillionz Author

Ok let me explain what I mean by YouTube fetchers.

You can actually fetch new videos from a channel as the videos are uploaded. And it usually comes as a link to the video on youtube.

It could also be in an analytical sense, as a channel owner i can connect with the youtube api to see my analytics in the bot.

I really don't know about groovy so well, but from what I have read, I mean nothing like that in that statement.

Collapse
powerstm profile image
Powers

Thanks for this, got me up and running nicely.

One tweak:

// export startBot as default
module.export = startBot;

to

// export startBot as default
module.exports = startBot;

Collapse
elijahtrillionz profile image
Elijah Trillionz Author

Thanks for the correction.

Collapse
krishna_bollina profile image
BSK-git11

I get some errors from node modules files showing the use of "??=" in the place of "=",
Is any one facing the same?

Collapse
elijahtrillionz profile image
Elijah Trillionz Author

can you describe the issue on github, try pasting the error and how we can reproduce the error.
This is the repo

Collapse
krishna_bollina profile image
BSK-git11

Done

Collapse
elijahtrillionz profile image
Elijah Trillionz Author

Don't get your comment

Collapse
jscoder07 profile image
Jeremiah Adeboye

Great job. Thanks for sharing. Very well written

Collapse
elijahtrillionz profile image
Elijah Trillionz Author

Glad you liked it

Collapse
code913 profile image
code913

A music player and song fetcher

sad groovy noises

Collapse
elijahtrillionz profile image
Elijah Trillionz Author

Not all the time though.
I believe it depends on the purpose of the bot really.
There are some boys that are dedicated to lofi music which is great for coding in my opinion.