Panini Bot — Making a Discord Bot with Replit, UptimeRobot, Node.js, Express, and Eris

Abbey Perini - Jun 2 '21 - - Dev Community

As soon as I saw Emi's tweet about the open source Pandera package, I knew it had to be a Discord bot, so here's how I built a bot that will insert a new word starting with 'p' for every instance of 'pandemic' in a Discord message.

Cover image alt text: "Replacing the word "pandemic" with incorrect variations is 90% of how I've been coping with this panasonic" - Vax Een

I'd already used replit and UptimeRobot to make a Python Discord bot that sends my friends pictures of dogs, so I fired up a Node.js repl and named it Panini. You can view the live code here.

Making a Discord bot with Replit, UptimeRobot, Node.js, Express, and Eris

For UptimeRobot to keep my bot alive, the first thing I need in my Node.js repl is an Express server. After running npm install express in the console, I added the following code to index.js:

// server imports
const express = require('express');
const app = express();
const port = 3000;
// create main route
app.get('/', (req, res) => res.send('Hello World!'));
// instantiate server
app.listen(port, () => console.log(`App is listening at http://localhost:${port}`));
Enter fullscreen mode Exit fullscreen mode

When I hit run, I should see a browser window pop up above the console with "Hello World!" and "App is listening at http://localhost:3000" in the console. I'll need the URL at the top of the window to connect UptimeRobot and keep Panini Bot alive.

After logging into my UptimeRobot account and clicking 'Add New Monitor' in my dashboard, I set it to HTTP(s) Monitor Type, give it a Friendly Name, enter the URL from my repl browser window, and set the Monitoring Interval to every 5 minutes. It will then ping my Panini Bot server regularly. You can even have your robot email you every time it pings your server and the server is down!

a screenshot of the UptimeRobot Edit Monitor form with monitor type, friendly name, URL (or IP) and monitoring interval settings

Next, I created a bot in Discord, installed the Eris package and brought my Panini Bot online. 

To create a bot, I logged into Discord and navigated to https://discord.com/developers/applications. I clicked 'new application' and gave my bot application a name.

Discord developer portal after a new application is created. There's information about the new bot application including the application ID.

I saved the application ID for later - this is the bot token. Afterwards, I still had to click on 'Bot' in the navigation bar on the left. After adding a bot, I know I'm done when I see a Bot screen with "A wild bot has appeared!"

The bot screen the Discord developer portal after a new bot is created. A green banner with "A wild bot has appeared!" and information about the bot

My bot is live and ready to be used. If you want to be sure no one else adds your bot to a server, toggle off 'Public Bot.'

 
A repl has a secrets tab that works just like a dotenv file or secrets manager. All you have to do is click on the lock in the navigation bar on the left and you should see this screen:

The secrets tab in a repl. A form with a box for key and value with  "add new secret" button and button for putting your secret code into your files.

I added my Discord application id to the value field. The key is the variable name you want to use. I used bot_id, so I access my bot token with process.env['bot_id']. Repl will even insert the code you need to access your secret into your file for you.

 
Next I ran npm install eris and added the initial bot code:

const eris = require('eris');
const bot = new eris.CommandClient(process.env['bot_id'], {}, {
  description: "Returns your message with the word 'pandemic' replaced by a random word starting with the letter 'p.'",
  owner: "Abbey Perini",
  prefix: '!'
});
// what the bot does when it's ready
bot.on('ready', () => {
  console.log('Connected and ready');
});
// error 'handling'
bot.on("error", (err) => {
  console.error(err);
});
// instantiate bot
bot.connect();
Enter fullscreen mode Exit fullscreen mode

Now when I hit run, I see 'Connected and ready' in the console. This will also connect my code to my new Discord bot, prefix all commands with an exclamation point, and the !help command will return a message including the description and owner properties defined above.

Full disclosure: I originally wrote this bot with eris.Client() and chained async functions for the !Panini command. After my first round of user testing - aka asking my friends to break it - I leaned into the tools provided by Eris, and there are a few cool benefits to using eris.CommandClient() over new eris.Client()

The description and fullDescription define descriptive text for the !help command to send to users. You only have to return whatever you want sent as a message in response to the command being used. The built in arguments are also pretty neat. Not only do you get a msg object with useful properties like username and id, but also the args argument, an array of the entire message after the command split by space. 

Next, I registered my first command and tested that the bot was online:

const paniniSummon = bot.registerCommand("Panini", (msg, args) => {
  return "Reporting for duty!";
}, {
     description: "Summon Panini Bot",
     fullDescription: "If you want Panini Bot to replace the word 'pandemic' for you, start your message with this command."
    })
Enter fullscreen mode Exit fullscreen mode

I created an invite link and invited Panini Bot to my bot test server. I suggest making a server or channel for bot testing, especially in the beginning. After adding my bot, I sent a message with '!Panini' and it responded back with a message saying 'Reporting for duty!'

Making Panini Bot

I want Panini Bot to see a message with the !Panini command, delete that message, and then send a new message. The new message should mention the user, followed by their original message, but a brand new word starting with 'p' will replace every instance of 'pandemic' or 'Pandemic.' When I originally wrote my createMessage function, it replaced all instances of 'pandemic' with the same random p-word. For the purposes of this blog, we'll skip ahead to Panini Bot 2.1.

 
After user testing, I realized I needed to alias commands so users could also use '!panini.' Furthermore, I quickly found I'd need Panini Bot to send an error message if there was no message body after the command or it didn't include at least one instance of 'pandemic.' After struggling a little bit to get the bot up and running in my own established server, I wanted to add a permissions error message. Most importantly, my friends immediately turned it into a Mad Libs bot, so I knew I had to rewrite createMessage so that each instance of 'pandemic' would be replaced by a different p-word.

First the new command aliases:

// Alias !halp to !help
bot.registerCommandAlias("halp", "help");
// Alias !panini to !Panini
bot.registerCommandAlias("panini", "Panini")
Enter fullscreen mode Exit fullscreen mode

Then, I installed Pandera ( npm install pandera ) and added my import statement:

const pandera = require('pandera');
Enter fullscreen mode Exit fullscreen mode

When I call pandera(), it will return 1 random p-word. 

Next I wrote an array so the bot could check if 'pandemic' was in the message body:

const pandemicsArray = ['pandemic', 'Pandemic', 'pandemic!', 'Pandemic!', 'pandemic?', 'Pandemic?', 'pandemic.', 'Pandemic.', 'pandemic,', 'Pandemic,', 'pandemic:', 'Pandemic:', 'pandemic;', 'Pandemic;', '"pandemic"', '"Pandemic"', "'pandemic'", "'Pandemic'"];
Enter fullscreen mode Exit fullscreen mode

Now let's rewrite the paniniSummon command:

const paniniSummon = bot.registerCommand("Panini", (msg, args) => {
  console.log(msg.id, msg.content, msg.author.username, msg.channel.id);
  let includesPan = false;
  for (i = 0; i < args.length; i++) {
    if (pandemicsArray.includes(args[i])) {
      includesPan = true;
    }
  }
  if (args.length === 0) {
    return "No message to edit. Use the command '!help' for more information.";
  } else if (!includesPan) {
    return "No instance of 'pandemic' to replace. Use the command '!help' for more information.";
  }
  deleteOldMessage(msg)
  return formatNewMessage(msg, args);
}, {
    description: "Summon Panini Bot",
    fullDescription: "If you want Panini Bot to replace the word 'pandemic' for you, start your message with this command."
    })
Enter fullscreen mode Exit fullscreen mode

After a couple rounds of user testing, I started console.log-ing information about the message before anything is replaced and the message body after it was formatted. includesPan is my check for the word 'pandemic.' Because args is an array of words, it's easy to iterate through and check each one against my pandemicsArray

The if block checks first if there is any message after the command then if my check turned includesPan true. If either is not true, an error message with more information for the user is returned. Next I call deleteOldMessage and pass the entire msg object. Then I call formatNewMessage and pass the msg object and the args array. Whatever I get back from formatNewMessage will be returned as a new Discord message.

deleteOldMessage stayed pretty much the same from Panini Bot 1.0 to Panini Bot 2.0:

async function deleteOldMessage(msg) {
  const channel = msg.channel.id;
  await bot.deleteMessage(channel, msg.id, "replace the word 'Pandemic'")
    .catch((error) => { // error message
      if (error.message === "Missing Permissions") {
        sendPermissionsError(channel);
      }
    });
}
Enter fullscreen mode Exit fullscreen mode

When calling deleteOldMessage inside the command, instead of chained in then(), I needed the channel id from the msg object and I added the sendPermissionsError function which looks like this:

async function sendPermissionsError(channel) {
  await bot.createMessage(channel, "Panini Bot needs the Manage Messages permission to delete replaced messages. https://support.discord.com/hc/en-us/articles/206029707-How-do-I-set-up-Permissions");
}
Enter fullscreen mode Exit fullscreen mode

The Panini Bot will need the 'Manage Messages' permission so it can delete the old message before it is replaced. Any server Panini Bot is in will either have to let users manage other users' messages at a channel level or make Panini an admin. I also added the permission to my discord bot invite link using this calculator.

Finally, it's time to format Panini Bot's new message to send back - aka Pandemic Mad Libs.

 

function formatNewMessage(msg, args) {
  const author = msg.author.id;
  let numOfP = 0;
  for (i = 0; i < args.length; i++) {
    if (pandemicsArray.includes(args[i])) {
      numOfP++;
    }
  }
  let pArray = [];
  for (i = 0; i < numOfP; i++) {
    let newP = pandera();
    pArray.push(newP);
  }
  let pIndex = 0;
  for (i = 0; i < args.length; i++) {
    if (pandemicsArray.includes(args[i])) {
      let index = args.indexOf(args[i]);
      let oldArg = args[i];
      let newArg = oldArg.replace(/pandemic/gi, pArray[pIndex]);
      args[index] = newArg;
      pIndex++;
    }
  }
  let messageBody = args.join(" ");
  let fullMessage = `<@${author}> says "${messageBody}"`;
  console.log(fullMessage);
  return fullMessage;
}
Enter fullscreen mode Exit fullscreen mode

First, I want to mention the user, so I pull the user's id from the msg object. Discord mention syntax is <@userID>, so the template literal I'll return at the end of the function starts with <@${author}>.

 
Then, I made a for loop to get the number of instances of 'pandemic' in the message body using .includes() and our old buddy the pandemicsArray

Next, I wrote a for loop to populate an array of the same amount of new p-words. 

Finally, I wrote a for loop to replace each of the instances of 'pandemic' with a unique p-word. The regular expression, /pandemic/gi, will replace every instance of pandemic with a word from the pArray based on the pIndex counter and is case insensitive. This means that it will leave the user's punctuation even if args[i] is something like 'pandemic!' or 'pandemic.'

At this point it worked, but I was looking at three for loops and knew there had to be other ways. I reached out to two developers who are always willing to walk me through coding concepts, Kirk and Alex.

Kirk, functional programmer extraordinaire, reminded me that maps exist for a reason, and framed it as a transformation of the array. Here's his solution:

function formatNewMessage(msg, args) {
  const author = msg.author.id;
  const pandemicReplacer = (word) => {
    if (pandemicsArray.includes(word)) {
      return word.replace(/pandemic/gi, pandera())
    }
      return word
    }
  const messageBody = args.map(pandemicReplacer).join(" ");
  let fullMessage = `<@${author}> says "${messageBody}"`;

  return fullMessage;
}
Enter fullscreen mode Exit fullscreen mode

Alex, Senior DevOps Engineer and Cloud Infrastructure wizard, chose to work with the whole message as a string instead of the args array of words:

function formatMessageExAid(msg, args) {
  const author = msg.author.id;
  let messageBody = args.join(" ");
  let newMsg = `<@${author}> says "${messageBody}"`;
  while (newMsg.includes('pandemic')) {
    newMsg = newMsg.replace(/pandemic/i, pandera());
  }
  return newMsg;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

I enjoy working in a repl not only because I can check the bot from my phone, but also because I can invite other developers to collaborate. There are a few features I vacillate on adding to Panini Bot. I chose not to worry about capitalization, but I could add the option to turn off the permissions error message and/or deleteOldMessage. If you have thoughts about this or any other part of Panini Bot, share in the comments!

Panini Bot was so fun to build! Super special thanks to Emi and Yechiel for building Pandera in the first place. As always, thanks to Kirk and Alex for being encouraging and educational at the same time.

Add Panini Bot to your Discord server today!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player