As of today, Discord is one of the most popular chat apps, especially for gamers and devs. People love it because it’s free, it’s efficient, it’s cross-platform, it’s… well, you guessed it, it’s awesome. One of the great things you can do on Discord is to integrate bots on servers in order to make it more interactive. As an example, you may have encountered one of them that congratulates you and upgrades your level after you’ve sent many messages in the server. Or another one that allows you to listen to music using commands, to kick or ban members, etc.
As I’m a developer, I wondered how to build a bot. How do I create commands? Listen to messages? Deploy a bot? Turns out it’s easier than what I’ve thought. So, without further ado, let’s get started on how to create a Discord bot.
Note: I work mainly with JS. So you guessed it, this bot will be written in JS!
For this tutorial, you need Node.js and npm or yarn installed.
As usual, you always have to do some setup when starting out a project. Here are the four main things we will do:
Easy peasy!
All right, no more talking. First, you will need to create a folder and initialize it:
mkdir my-bot
cd my-bot
npm init -y # generates you a minimal package.json file
Then, we will need to import a library that allows us to interact with Discord’s API. And luckily for us, there is one great JS wrapper called discord.js. Let’s add it to our project:
npm install discord.js
If we take a look at their website, we can see they already provide us some code to get started, how nice is this. We’ll shamelessly copy-paste their example.
In your folder, create a new file called index.js
and paste the following:
const Discord = require('discord.js')
const client = new Discord.Client()
client.on('ready', () => {
console.log(`Logged in as ${client.user.tag}!`)
})
client.on('message', msg => {
if (msg.content === 'ping') {
msg.reply('Pong!')
}
})
client.login('token')
What does the code above do? Not so many things.
discord.js
’s library and initialize it by calling Client()
.ready
and message
via the on
method and we tell how to handle these events with a callback function.login
. It establishes a websocket connection to Discord. But, as you can see, we need to provide a token to that method. Indeed, Discord requires you to have a key/token in order to use their API. Thus, we’ll grab one to make it work.index.js
as a parameter of the login
method. Voilà! You are now the happy owner of a Discord bot token.client.login('YOUR_TOKEN')
So far, we’ve done the setup, grabbed the token. There are two steps remaining: add our bot to a server and test it.
Now, we are going to add the bot to a server, but for that, we need at least one server. If you don’t have yet created a server, here is how to do it (I recommend you to create a test server):
Great. Now we’re ready to add our bot to our server:
Our bot is on our server but it’s offline. So let’s make it alive. For development purposes, we will add nodemon
. It will allow us to reload our code as soon as it changes:
npm install nodemon --save-dev
Then, we will add some scripts to our package.json
to make our life easier. One will be called start
and the other one dev
. Paste the following in your package.json
file:
{
"name": "my-bot",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
"dependencies": {
"discord.js": "^11.4.2"
},
"devDependencies": {
"nodemon": "^1.18.9"
}
}
Ladies and gentlemen, this is the moment you’ve been waiting for. Your bot is going to be brought online! Run this command:
npm run dev
If all went well, you should see this in your terminal:
Go back to your Discord test server, you should see your bot online. Send ping in the general channel, your bot will reply Pong!. Amazing!
When you grabbed your token, you may have noticed that Discord hid it by default. They did it because it’s a sensitive data. Indeed, you can do whatever you want with the bot with that token. So if somebody steals it, bad things can happen. As a consequence, we need to hide our token from the source code in case you push it on GitHub for example.
To hide data, we use what we call environmental variables. Basically, we put what we want to hide or configuration variables into a file whose name is .env
. This file should never be pushed on a public repository if it contains sensitive data. Create a new file called .env
and paste the following:
BOT_TOKEN=YOUR_TOKEN_HERE
Of course, don’t literally paste YOUR_TOKEN_HERE
. Paste your own token.
The next step is to load this variable into our app. Indeed, you may have added a .env
file, it will mean nothing to Node when it will run your index.js
file. For that, we will add a package called dotenv
. As their docs say, dotenv is a module that loads environment variables from a .env
file into process.env.
:
npm install dotenv
index.js
file, add this line:require('dotenv').config()
process.env.BOT_TOKEN
:client.login(process.env.BOT_TOKEN)
Go back to your server and make sure your bot is still online. Alternatively, you can also check your terminal and double-check you didn’t get any errors.
So far, our bot replies Pong! whenever someone says ping. Let’s face it, it’s totally useless. One of the common features of bots is to welcome members and give them instructions as soon as they join the server. Therefore we will code our bot so that whenever someone joins the server, it will send a welcome message!
There is a special event emitted when a user joins the server whose name is guildMemberAdd
. The member that just joined the server is passed as an argument and it has a loooot of methods including an interesting one called send
. It will allow us to create a direct message between the bot and the new member. Beneath the last listener (message
), add the following:
client.on('guildMemberAdd', member => {
member.send(
`Welcome on the server! Please be aware that we won't tolerate troll, spam or harassment. Have fun 😀`
)
})
To verify that everything works properly, invite a friend (or create another Discord account) and verify that they get the welcome message:
Our bot starts to become interesting. Let’s add a moderation feature: the ability to kick users by mentioning them. A simple !kick @user
and BAM! The user is kicked out of the server. If we think of this feature, what should we do?
!kick
.!kick
. So we need to verify we’ve got a real member.kickable
property exist on the GuildMember
object to know it.kick()
method. It returns a Promise. If the Promise is resolved (the kick was successful), the bot will reply it has successfully kicked the user. If not, the bot will say it was unable to do so.Here is the full code (I got rid of the ping-pong feature):
client.on('message', message => {
if (message.content.startsWith('!kick')) {
const member = message.mentions.members.first()
if (!member) {
return message.reply(
`Who are you trying to kick? You must mention a user.`
)
}
if (!member.kickable) {
return message.reply(`I can't kick this user. Sorry!`)
}
return member
.kick()
.then(() => message.reply(`${member.user.tag} was kicked.`))
.catch(error => message.reply(`Sorry, an error occured.`))
}
})
Wait. I tried to kick someone and that stupid bot keep saying me it can’t kick that user!
That’s normal. The bot doesn’t have yet the permission to kick someone. Therefore, we’ll create a bot role that gives the permission to kick members and we’ll assign this role to our favorite little bot.
Now, you’re good to go. Go back to your Discord app and make sure you can kick users. Try to kick yourself and admire how the bot tells you you can’t kick yourself.
As your app grows, you won’t keep all your code in the index.js
file. So, in this part, we will refactor the code and prepare the code for the next features.
Currently we have three events: ready
when the bot is ready, message
each time a message is sent to the server, guildMemberAdd
whenever a user joins the server. There are a lot of other events that you can find here.
To modularize our code, we will create an events
folder. This folder will contain .js
files whose name will match the different events name discord.js
listens to:
events
folder.ready.js
, message.js
and guildMemberAdd.js
.Get ready for the following, it’s harder than what you’ve seen until here.
For each .js
file in the events
folder, we will export a function which will be our event handler, that is to say, the function that runs each time the corresponding event is emitted. To do that, we will need to pass arguments to these event handlers such as message
, member
, etc.
index.js
, you have to import the fs
module at the top of your fileconst fs = require('fs')
const client = new Discord.Client()
), read all the files of the events folder using fs.readdir
. The callback has two arguments: err
and files
where files
is an array of the filenames in the directory.fs.readdir('./events/', (err, files) => {})
fs.readdir('./events/', (err, files) => {
files.forEach(file => {
const eventHandler = require(`./events/${file}`)
})
})
ready.js
instead of just ready
. So we must get rid of that extension.fs.readdir('./events/', (err, files) => {
files.forEach(file => {
const eventHandler = require(`./events/${file}`)
const eventName = file.split('.')[0]
client.on(eventName, arg => eventHandler(client, arg))
})
})
ready.js
module.exports = client => {
console.log(`Logged in as ${client.user.tag}!`)
}
message.js
module.exports = (client, message) => {
if (message.content.startsWith('!kick')) {
const member = message.mentions.members.first()
if (!member) {
return message.reply(
`Who are you trying to kick? You must mention a user.`
)
}
if (!member.kickable) {
return message.reply(`I can't kick this user. Sorry!`)
}
return member
.kick()
.then(() => message.reply(`${member.user.tag} was kicked.`))
.catch(error => message.reply(`Sorry, an error occured.`))
}
}
guildMemberAdd.js
module.exports = (client, member) => {
member.send(
`Welcome on the server! Please be aware that we won't tolerate troll, spam or harassment. Have fun 😀`
)
}
fs.readdir('./events/', (err, files) => {
files.forEach(file => {
const eventHandler = require(`./events/${file}`)
const eventName = file.split('.')[0]
client.on(eventName, (...args) => eventHandler(client, ...args))
})
})
Few lines of code but not so easy to understand. Pheeeew!
Bare with me, we’re nearly finished with the bot. Look at message.js
, only one command has been implemented and yet the file has already grown big. Imagine now if you add the ban feature or a music feature, it will become really complicated. Thus, the last step of the refactorization is to create a commands
folder. This folder will contain the actions of the bot.
commands
folder at the root folder.kick.js
file and move all the code of message.js
that is inside the if statement:kick.js
module.exports = message => {
const member = message.mentions.members.first()
if (!member) {
return message.reply(`Who are you trying to kick? You must mention a user.`)
}
if (!member.kickable) {
return message.reply(`I can't kick this user. Sorry!`)
}
return member
.kick()
.then(() => message.reply(`${member.user.tag} was kicked.`))
.catch(error => message.reply(`Sorry, an error occured.`))
}
message.js
and replace the code you’ve just pasted in kick.js
:const kick = require('../commands/kick')
module.exports = (client, message) => {
if (message.content.startsWith('!kick')) {
return kick(message)
}
}
Much more readable, isn’t it?
Last part: deploying the bot. You’re not going to let your computer always on just to keep the bot running, don’t you? So we’ll make sure our bot will be always-on thanks to Heroku:
Procfile
at the root folder and insert the following content:worker: node index.js
web
process and switch on the worker
process:Note: We do so because we’re running a background task.
Now, go back to your server and make sure you stopped running your bot locally. Is your bot online? Yes? Congratulations, you just built and deployed your first Discord bot.
If you miss something or if you want to browse the full code, head over this GitHub repository: discord-bot-example
You can do endless things with that bot from there: ban users, play music, rank users, find GIFS, integrate it with many services (Google APIs, Unsplash for images, …). Have fun with your bot! 😃