A discord bot for making pyramids!
Hey there! We'll be using Node.js, TypeScript, and Discord's JS API to create a pyramid bot that looks like this:
I'll be assuming you're already familiar with some of the basics of Discord and JavaScript. Let's get started!
Step 1: Boilerplate
The first thing you'll want to do is create a new directory for your project. Here's some commands you'll want to run:
# Clone a boilerplate for Node.JS
git clone https://github.com/safinsingh/node-ts.git pyramid
# Reset source control
rm -rf .git
git init
# Install dependencies
{pnpm/[yarn]/npm} install
Let's take a closer look at the directory structure of this boilerplate:
.
├── .eslintignore (linting ignores)
├── .eslintrc (linting config)
├── .github (continuous integration)
│ └── workflows
│ ├── format.yml
│ └── lint.yml
├── .gitignore (source control ignores)
├── .prettierignore (formatting ignores)
├── .prettierrc (formatting config)
├── package.json (package metadata)
├── src (code)
│ └── index.ts
├── tsconfig.json (typescript config)
└── yarn.lock (package lockfile)
Perfect! Now that we know all the files and directories are for, let's install some of the dependencies we need:
{pnpm/[yarn]/npm} install discord.js dotenv
discord.js
contains the Discord API module for JavaScript as well as types for TypeScript, and dotenv
is for loading our Discord API key without publishing to source control.
Step 2: Discord Bot Setup
First, create a Discord account if you don't have one at discord.com. Then, navigate to the Discord developer portal and create a new app:
From there, hit the Bot
tab and copy your bot token:
Then, just copy-and-paste that into a .env
file within your Pyramid directory like so:
TOKEN=YOUR-SUPER-SECURE-TOKEN-HERE
However, in the boilerplate, the .env
file isn't ignored by default. We can fix this by editing the .gitignore
file and adding .env
like so:
echo ".env" >> .gitignore
or by opening it with your text editor and changing it through that.
Finally, add it to your server by going to the OAuth2
tab and generating a URL with scopes
being bot
and with the permisssion bit 67584
:
Just copy this into your browser and you should be able to add your bot to your server! For now it'll be offline, so we'll not to set that up first.
Step 3: Hello World!
To get our bot up and running, let's add this in to the index.ts
file:
// Imports dotenv and discord modules
import dotenv from 'dotenv'
import Discord from 'discord.js'
// Read config from .env and login to the Discord API
dotenv.config()
const client = new Discord.Client()
client.login(process.env.TOKEN)
// Listen for a 'ready' event and execute a callback when it's fired
client.on('ready', () => {
console.log('Ready!')
})
// Listen for a 'message' event and execute a callback when it's fired
client.on('message', (msg) => {
const channel = msg.channel as Discord.TextChannel
channel.send('Hi there!')
})
Perfect! Now we can run {pnpm/[yarn]/npm} dev
to start our server. Our bot should be active and reply to us whenever we send a message!
Step 4: Error Handling
Now that we're able to run our bot, we need to start making some pyramids!
Before that, let's try to read and validate the command inputted by the user:
// Useful constants
const content = msg.content.split(' ')
const channel = msg.channel as Discord.TextChannel
// If the message starts with /pyramid
if (content[0] === '/pyramid') {
// Get the size of the pyramid and the repeatable text
const size = parseInt(content[1])
const toRepeat = content.slice(2).join(' ')
// Validate our message
const valid = isValid(msg)
if (!valid.isValid) {
// If not valid, tell them!
msg.reply(valid.error)
msg.react(valid.reaction as Discord.EmojiResolvable)
return
}
// Generate a pyramid from the text and size
const toSend = genPyramid(toRepeat, size)
// Send the message and catch an error
channel.send(toSend).catch((err) => msg.reply(err))
}
In isValid, we're going to add a couple functions to prevent bot abuse (the isValid
function):
// Create an interface for what a validCheck should look like
// Errors and reactions should be optional and only present if isValid is false
interface validCheck {
isValid: boolean
error?: string
reaction?: Discord.EmojiResolvable
}
// Determine whether the message will overflow the 2000 character limit imposed by Discord
const willOverflow = (msgArr: Array<string>): boolean => {
// Get the height of the pyramid
const iter = parseInt(msgArr[1]) + 1
// iter * (iter - 1) is the same thing as 2 * (n + (n - 1) + ... 1)
if (iter * (iter - 1) * msgArr.slice(2).join(' ').length > 1000) {
return true
}
return false
}
// Determine is a message is valid, and return a validCheck object
export const isValid = (msg: Discord.Message): validCheck => {
const msgArr = msg.content.split(' ')
// Make sure all the required arguments are present
if (msgArr.length < 3) {
console.log('1')
return {
isValid: false,
error: 'Invalid command, must have at least 3 arguments!',
reaction: '🗑️'
}
// Ensure that the height of the pyramid is actually a number
} else if (isNaN(parseInt(msgArr[1]))) {
return {
isValid: false,
error: 'Invalid number, must be an integer!',
reaction: '🗑️'
}
} else {
// Create a temporary storage variable
let toReturn: validCheck = {
isValid: true
}
// Loop through words to be pyramidified
msg.content
.split(' ')
.slice(1)
.forEach((e) => {
// Prevent disallowed keywords
if (e === '/pyramid') {
toReturn = {
isValid: false,
error: 'Recursiveness is not allowed!',
reaction: '😡'
}
} else if (e === '͔') {
toReturn = {
isValid: false,
error: "Sorry, but that character doesn't work :(",
reaction: '😔'
}
}
})
// If the message is invalid, return the temporary variable containing the most recent error
if (!toReturn.isValid) {
return toReturn
}
// Prevent character overflow
if (willOverflow(msgArr)) {
return {
isValid: false,
error: 'Whoops! Looks like that exceeds the maximum characters!',
reaction: '😔'
}
} else {
// Return correct message!
return {
isValid: true
}
}
}
}
Step 5: Make Pyramids
Finally we're ready to make pyramids! This is by far the simplest part of the bot. Let's take a look at the following algorithm:
// Define a pyramid generator with arguments for the repeater and the pyramid size
export const genPyramid = (toRepeat: string, size: number): string => {
let toSend = ''
for (let i = 0; i <= size; i++) {
// For line in pyramid
for (let z = 0; z < i; z++) {
// For entry in line
toSend += `${toRepeat} ` // Append to sending variable
}
toSend += '\n' // Create newline between pyramid rows
}
return toSend
}
This will produce a pattern like the following:
1
1 1
1 1 1
1 1 1 1
1 1 1 1 1
If you look closely, you'll notice that the number of 1
's in each row is equal to the row number:
Row 1: 1 (1 column)
Row 2: 1 1 (2 columns)
Row 3: 1 1 1 (3 columns)
Row 4: 1 1 1 1 (4 columns)
Row 5: 1 1 1 1 1 (5 columns)
Part 6: Putting it all together
Finally, now that we're done with all the utility functions, let's integrate the actual functions into the bot itself. Here are some polishing changes I added to the index.ts
file, explained thouroughly:
import dotenv from 'dotenv'
import Discord from 'discord.js'
// Abstract utility functions
import { isValid, genPyramid } from './util'
dotenv.config()
const client = new Discord.Client()
client.login(process.env.TOKEN)
// Set bot activity
client.on('ready', () => {
console.log('Ready!')
client?.user?.setActivity('making sideways pyramids')
})
client.on('message', (msg) => {
const content = msg.content.split(' ')
const channel = msg.channel as Discord.TextChannel
// Root checker
if (content[0] === '/pyramid') {
const size = parseInt(content[1])
const toRepeat = content.slice(2).join(' ')
const valid = isValid(msg)
if (!valid.isValid) {
msg.reply(valid.error)
msg.react(valid.reaction as Discord.EmojiResolvable)
return
}
// Create toSend
const toSend = genPyramid(toRepeat, size)
// Send the final message and catch an error
channel
.send(toSend)
.catch((err) =>
msg.reply(
`Nice! It looks like you've successfully hacked the Pyramid! Feel free to pen a pull request :). BTW, the error was: ${err}`
)
)
}
})
Woohoo 🎉! You've finally finished the Pyramid bot! You can now add some more finishing touches to it if you like it and personalize it!
If you have any suggestions or want to share your version of pyramid, open an issue or pull request to github.com/safinsingh/pyramid.
Next Steps
- Create a centered, upside-down, or right-facing pyramid
- Add other commands (e.g. a help/info command)
- The possibilities are endless! Have fun :)