Discord.JS Canvas Tutorial — Custom Welcome Images for Your Discord Bots

Alexzander Flores
The Startup
Published in
9 min readNov 8, 2020

Your bot may want to display a cool image when welcoming people to a Discord server. In this tutorial we’ll go over how to do that from scratch using Discord.JS and the WOKCommands package.

Corresponding Video Tutorial

The steps we will take

  1. Creating our project and connecting to MongoDB
  2. Creating a “!setWelcome” command to specify the welcome channel
  3. Creating a “!simJoin” command to simulate joins for testing
  4. Listening for new joins and sending a dynamic image

Creating our project and connecting to MongoDB

We’ll start off by creating a basic Node project, installing the required packages, and create the files/folders we need. I will be doing this through the command line with the following commands:

npm init -y
touch index.js .env
mkdir commands features models
npm install discord.js wokcommands dotenv mongoose canvas
npm install nodemon -g

At this stage we have a basic setup created, but we have to specify our bot’s token. If you don’t have a bot yet then follow the steps here to create one.

The .env file will contain our token like so:

TOKEN=YOUR_TOKEN_HERE
MONGO_URI=YOUR_MONGO_CONNECTION_STRING

We can then follow the WOKCommands documentation to setup our project:

const DiscordJS = require('discord.js')
const WOKCommands = require('wokcommands')
require('dotenv').config()

const client = new DiscordJS.Client()

client.on('ready', () => {
// Initialize WOKCommands with specific folders and MongoDB
new WOKCommands(client, {
commandsDir: 'commands',
featureDir: 'features'
})
.setMongoPath(process.env.MONGO_URI)
.setDefaultPrefix('!') // Change this to whatever you want
})

client.login(process.env.TOKEN)

We should now be able to run our bot with nodemon or node index.js

Creating a “!setWelcome” command to specify the welcome channel

Configuring our Discord bot should be as easy as possible for server owners. We can create a simple command to specify what channel is the welcome channel. This allows server owners to change the name however they want without interfering with our bot’s functionality.

To do this we first need to create a MongoDB model, and then create the command to save data to the specified collection within that model. Let’s start with the model:

// "welcome-channel.js" file within the "models" folderconst mongoose = require('mongoose')// We are using this multiple times so define
// it in an object to clean up our code
const reqString = {
type: String,
required: true,
}
const welcomeSchema = new mongoose.Schema({
_id: reqString, // Guild ID
channelId: reqString,
})
module.exports = mongoose.model('welcome-channel-tutorial', welcomeSchema)

We can now create our command with a basic boilerplate to see if it works:

// "setwelcome.js" file within the "commands" foldermodule.exports = {
requiredPermissions: ['ADMINISTRATOR'],
callback: async ({ message }) => {
message.reply('Works!')
},
}

If we run “!setwelcome” within a Discord server we see that it replies with “Works!” like so:

We can now add in our desired functionality now that we have a working command. First we have to import our model and then update the channel ID for the guild:

// "setwelcome.js" file within the "commands" folderconst welcomeChannelSchema = require('../models/welcome-channels')module.exports = {
requiredPermissions: ['ADMINISTRATOR'],
callback: async ({ message }) => {
// Destructure the guild and channel properties from the message object
const { guild, channel } = message
// Use find one and update to either update or insert the
// data depending on if it exists already
await welcomeChannelSchema.findOneAndUpdate(
{
_id: guild.id,
},
{
_id: guild.id,
channelId: channel.id,
},
{
upsert: true,
}
)
message.reply('Welcome channel set!')
},
}

After running the command we should now see the following object in our MongoDB database:

This means everything is working correctly so far. However we won’t want to read from the database every time someone joins, so it’s best for us to cache what guild is using what channel. We can easily do this with a map and an exported function to returns the channel ID. We also want to ensure we load our data when the bot starts up. Our file now looks like this:

// "setwelcome.js" file within the "commands" folderconst welcomeChannelSchema = require('../models/welcome-channels')const cache = new Map()// An async function to load the data
const loadData = async () => {
// Get all stored channel IDs
const results = await welcomeChannelSchema.find({})
// Loop through them all and set them in our map
for (const result of results) {
cache.set(result._id, result.channelId)
}
}
// Invoke this function when the bot starts up
loadData()
module.exports = {
requiredPermissions: ['ADMINISTRATOR'],
callback: async ({ message }) => {
// Destructure the guild and channel properties from the message object
const { guild, channel } = message
// Use find one and update to either update or insert the
// data depending on if it exists already
await welcomeChannelSchema.findOneAndUpdate(
{
_id: guild.id,
},
{
_id: guild.id,
channelId: channel.id,
},
{
upsert: true,
}
)
// Store the information in the cache
cache.set(guild.id, channel.id)
message.reply('Welcome channel set!')
},
}
module.exports.getChannelId = (guildId) => {
return cache.get(guildId)
}

Once we start listening for guild joins we can use the getChannelId function to get the desired channel for that guild.

Creating a “!simJoin” command to simulate joins for testing

We can create a command to simulate someone joining so we don’t have to ask our friends to help out. This is actually fairly simple to do with the following command:

// "simjoin.js" file within the "commands" foldermodule.exports = {
requiredPermissions: ['ADMINISTRATOR'],
callback: ({ message, args, text, client }) => {
client.emit('guildMemberAdd', message.member)
},
}

This will fire/run the “guildMemberAdd” event that we’ll be listening to soon.

Listening for new joins and sending a dynamic image

With the initial setup done we can now incorporate canvas within our project to create our dynamic image whenever someone joins. We’ll start by creating a welcome.js file inside of our “features” folder. This file will handle all of this logic for us:

// "welcome.js" file within the "features" foldermodule.exports = client => {
client.on('guildMemberAdd', member => {
console.log(member.user.tag)
})
}

Running “!simjoin” will now send a simple welcome message to the previously configured welcome channel. This is a nice proof of concept of loading the welcome channel for each guild and sending a message whenever someone joins. We can now move onto the canvas side of things. First off we need a background image. For simplicity we’ll use the one provided by the official Discord.JS documentation:

Be sure to save this as “background.png” in the root of your project folder like so:

It is easier to split this problem up into multiple smaller problems. So to start we’ll tackle just displaying the image whenever someone joins. To do that we need to first import canvas, create a canvas, and then attack the image to the canvas like so:

Note: Be sure your callback function is asynchronous!

// "welcome.js" file within the "features" folder// Note the capital 'C'
const Canvas = require('canvas')
const { MessageAttachment } = require('discord.js')
const path = require('path')
const { getChannelId } = require('../commands/setwelcome')
module.exports = (client) => {
client.on('guildMemberAdd', async (member) => {
// Async function
// Destructure the guild property from the member object
const { guild } = member
// Access the channel ID for this guild from the cache
const channelId = getChannelId(guild.id)
// Access the actual channel and send the message
const channel = guild.channels.cache.get(channelId)
// Create a canvas and access the 2d context
const canvas = Canvas.createCanvas(700, 250)
const ctx = canvas.getContext('2d')
// Load the background image and draw it to the canvas
const background = await Canvas.loadImage(
path.join(__dirname, '../background.png')
)
let x = 0
let y = 0
ctx.drawImage(background, x, y)
// Attach the image to a message and send it
const attachment = new MessageAttachment(canvas.toBuffer())
channel.send('', attachment)
})
}

At this stage when we run “!simjoin” it will now display the image inside of our welcome channel like so:

Now we can customize the other content we want to display on the canvas. This is personal preference, but I’ll be displaying the image and name of the user near the center of the canvas. I’m sure most of you will come up with more creative designs, but this will give us enough experience to know the basics. Let’s first start with displaying the user’s avatar at the exact center:

// "welcome.js" file within the "features" folder// Note the capital 'C'
const Canvas = require('canvas')
const { MessageAttachment } = require('discord.js')
const path = require('path')
const { getChannelId } = require('../commands/setwelcome')
module.exports = (client) => {
client.on('guildMemberAdd', async (member) => {
// Async function
// Destructure the guild property from the member object
const { guild } = member
// Access the channel ID for this guild from the cache
const channelId = getChannelId(guild.id)
// Access the actual channel and send the message
const channel = guild.channels.cache.get(channelId)
// Create a canvas and access the 2d context
const canvas = Canvas.createCanvas(700, 250)
const ctx = canvas.getContext('2d')
// Load the background image and draw it to the canvas
const background = await Canvas.loadImage(
path.join(__dirname, '../background.png')
)
let x = 0
let y = 0
ctx.drawImage(background, x, y)
// Load the user's profile picture and draw it
const pfp = await Canvas.loadImage(
member.user.displayAvatarURL({
format: 'png',
})
)
x = canvas.width / 2 - pfp.width / 2
y = canvas.height / 2 - pfp.height / 2
ctx.drawImage(pfp, x, y)
// Attach the image to a message and send it
const attachment = new MessageAttachment(canvas.toBuffer())
channel.send('', attachment)
})
}

We now see this when I run the “!simjoin” command:

How does the math work? The end goal here is to center the image. We can divide the width of the image by 2 and then subtract the width of the profile picture by 2. The same concept can be applied to the height to center it vertically too.

Next we need to move the image up to make room for some text below it. We can do this by simply using “25” as the y value:

We now have room to display some text below which we can do using the following:

// "welcome.js" file within the "features" folder// Note the capital 'C'
const Canvas = require('canvas')
const { MessageAttachment } = require('discord.js')
const path = require('path')
const { getChannelId } = require('../commands/setwelcome')
module.exports = (client) => {
client.on('guildMemberAdd', async (member) => {
// Async function
// Destructure the guild property from the member object
const { guild } = member
// Access the channel ID for this guild from the cache
const channelId = getChannelId(guild.id)
// Access the actual channel and send the message
const channel = guild.channels.cache.get(channelId)
// Create a canvas and access the 2d context
const canvas = Canvas.createCanvas(700, 250)
const ctx = canvas.getContext('2d')
// Load the background image and draw it to the canvas
const background = await Canvas.loadImage(
path.join(__dirname, '../background.png')
)
let x = 0
let y = 0
ctx.drawImage(background, x, y)
// Load the user's profile picture and draw it
const pfp = await Canvas.loadImage(
member.user.displayAvatarURL({
format: 'png',
})
)
x = canvas.width / 2 - pfp.width / 2
y = 25
ctx.drawImage(pfp, x, y)
// Display user text
ctx.fillStyle = '#ffffff' // White text
ctx.font = '35px sans-serif'
let text = `Welcome ${member.user.tag}!`
x = canvas.width / 2 - ctx.measureText(text).width / 2
ctx.fillText(text, x, 60 + pfp.height)
// Display member count
ctx.font = '30px sans-serif'
text = `Member #${guild.memberCount}`
x = canvas.width / 2 - ctx.measureText(text).width / 2
ctx.fillText(text, x, 100 + pfp.height)
// Attach the image to a message and send it
const attachment = new MessageAttachment(canvas.toBuffer())
channel.send('', attachment)
})
}

Running “!simjoin” we now get this result:

Again I’m sure most of you can come up with more creative ideas than this. Hopefully you can use this tutorial to modify and adjust the canvas to your liking.

--

--

Alexzander Flores
The Startup

Developer since 2008. I now create programming tutorials to help people learn the way I did.