From f5982f743da1c92d879c2bfd3faf21d996eed082 Mon Sep 17 00:00:00 2001 From: fernandogonzalez11 Date: Wed, 5 May 2021 20:00:53 -0600 Subject: [PATCH] 1.0.0 Initial commit --- .env | 1 + LICENSE.md | 21 +++++ README.md | 21 +++++ colors.json | 35 +++++++++ config.json | 4 + index.js | 43 +++++++++++ .../Configuration & Management/eval.js | 56 ++++++++++++++ src/commands/Utilities/help.js | 76 +++++++++++++++++++ src/commands/Utilities/ping.js | 25 ++++++ src/events/client/ready.js | 13 ++++ src/events/guild/message.js | 39 ++++++++++ src/handlers/command.js | 46 +++++++++++ src/handlers/event.js | 19 +++++ 13 files changed, 399 insertions(+) create mode 100644 .env create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 colors.json create mode 100644 config.json create mode 100644 index.js create mode 100644 src/commands/Configuration & Management/eval.js create mode 100644 src/commands/Utilities/help.js create mode 100644 src/commands/Utilities/ping.js create mode 100644 src/events/client/ready.js create mode 100644 src/events/guild/message.js create mode 100644 src/handlers/command.js create mode 100644 src/handlers/event.js diff --git a/.env b/.env new file mode 100644 index 0000000..8141df0 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DISCORD_TOKEN= \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..cb6addd --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 TeraBaito + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ddb6a4c --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Discord Bot Skeleton - By TeraBytes +This is a skeleton code with all the common code I (like to) use for Discord Bots. It includes +* Command/Event Handler +* Extended Client structure (for Intellisense and more organization) +* Necessary, ready-to-fill config files +* A colors.json file with a **lot** of color HEX values +* `message` and `ready` events +* `ping`, `help` and `eval` commands + +## Installation +I will be sharing compressed artifacts of new releases every now and then, make sure to get those by going [https://github.com/TeraBaito/discord-bot-skeleton/releases] + +1. Unzip the file (you can safely remove `README.md` and `LICENSE.md`) +1. Run `npm init` (`npm init -y` to skip prompts) +1. Run `npm i ascii-table beautify chalk common-tags discord.js dotenv` +1. Fill in the `DISCORD_TOKEN=` parameter on `.env` file +1. Go to config.json, and fill in the properties accordingly + +Then it's all yours! + +Made by TeraBytes, 2021, MIT License \ No newline at end of file diff --git a/colors.json b/colors.json new file mode 100644 index 0000000..2bcf097 --- /dev/null +++ b/colors.json @@ -0,0 +1,35 @@ +{ + "Red": "#FF0000", + "FireBrick": "#B22222", + "Pink": "#FFC0CB", + + "Blue": "#0000FF", + "Cyan": "#00FFFF", + "CornflowerBlue": "#6495ED", + "LightBlue": "#ADD8E6", + "PaleBlue": "#293749", + "SteelBlue": "#4682B4", + + "ForestGreen": "#228B22", + "Lime": "#00FF00", + "GreenYellow": "#ADFF2F", + "SeaGreen": "#2E8B57", + "Olive": "#808000", + + "Orange": "#EB8334", + "OrangeRed": "#FF4500", + + "Yellow": "#FFFF00", + "Gold": "#FFD700", + "GoldenRod": "#DAA520", + "Peru": "#CD853F", + "Maroon": "#800000", + + "Purple": "#800080", + "Lavender": "#E6E6FA", + "Magenta": "#FF00FF", + + "White": "#FFFFFF", + "Black": "#000000", + "Gray": "#808080" +} \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..8a4492b --- /dev/null +++ b/config.json @@ -0,0 +1,4 @@ +{ + "prefix": "", + "ownerID": "" +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..7646877 --- /dev/null +++ b/index.js @@ -0,0 +1,43 @@ +// Modules +const { Client, Collection } = require('discord.js'); +require('dotenv').config({ path: './.env'}); +const fs = require('fs'); +const chalk = require('chalk'); +const { stripIndents } = require('common-tags'); + +const Bot = class extends Client { + constructor() { + super(); + + this.commands = new Collection(); + this.aliases = new Collection(); + this.categories = fs.readdirSync('./src/commands'); + } +}; +module.exports = Bot; + +// Client +const bot = new Bot(); + +// Debugging +//bot.on('raw', console.log); +//bot.on('debug', m => console.log(`${chalk.cyan('[Debug]')} - ${m}`)); +bot.on('rateLimit', rl => console.warn( + stripIndents`${chalk.yellow('[Ratelimit]')} + Timeout: ${rl.timeout} + Limit: ${rl.limit} + Route: ${rl.route}`)); +bot.on('warn', w => console.warn(`${chalk.yellow('[Warn]')} - ${w}`)); +bot.on('error', e => console.error(`${chalk.redBright('[Error]')} - ${e.stack}`)); +process.on('uncaughtException', e => console.error(`${chalk.redBright('[Error]')} - ${e.stack}`)); +process.on('unhandledRejection', e => console.error(`${chalk.redBright('[Error]')} - ${e.stack}`)); +process.on('warning', e => console.warn(`${chalk.yellow('[Error]')} - ${e.stack}`)); + + +// Handlers' modules +['command', 'event'].forEach(handler => { + require(`./src/handlers/${handler}`)(bot); +}); + +// Login and turn on (default is DISCORD_TOKEN) +bot.login(); \ No newline at end of file diff --git a/src/commands/Configuration & Management/eval.js b/src/commands/Configuration & Management/eval.js new file mode 100644 index 0000000..410b61e --- /dev/null +++ b/src/commands/Configuration & Management/eval.js @@ -0,0 +1,56 @@ +const { Message, MessageEmbed } = require('discord.js'); +const Bot = require('../../../index'); +const beautify = require('beautify'); +const { ownerID } = require('../../../config.json'); +const colors = require('../../../colors.json'); + + +module.exports = { + name: 'eval', + helpName: 'Evaluate', + usage: 'eval [string]', + description: 'Evaluates JavaScript code inputed from args.\nOnwer Only Command\nSelfnote: don\'t use this next to many people idk they could take your token i guess lmao', + + /** + * @param {Bot} bot + * @param {Message} message + * @param {string[]} args + */ + run: async(bot, message, args) => { + if (message.author.id !== ownerID) { + return message.channel.send('No dude. I don\'t want anyone but my master mess with code in the bot...') + .then(m => setTimeout(() => { m.delete(); }, 5000)); + } + + if (!args[0]) { + return message.channel.send('Give me something to evaluate tho') + .then(m => setTimeout(() => { m.delete(); }, 5000)); + } + + try { + if (args.join(' ').toLowerCase().includes('token')) return message.channel.send('oh nononono you\'re not getting the token you\'re NOT GETTING IT IDNFIABGDJDNWIKG'); + + const toEval = args.join(' '); + const evaluated = eval(toEval); + + let embed = new MessageEmbed() + .setColor(colors.ForestGreen) + .setTimestamp() + .setTitle('Eval') + .addField('To Evaluate', `\`\`\`js\n${beautify(toEval, { format: 'js' })}\n\`\`\``) + .addField('Evaluated', evaluated) + .addField('Type of', typeof(evaluated)) + .setFooter(bot.user.username); + + message.channel.send(embed); + } catch (e) { + let embed = new MessageEmbed() + .setColor(colors.Red) + .setTitle('Error') + .setDescription(e) + .setFooter(bot.user.username); + + message.channel.send(embed); + } + } +}; \ No newline at end of file diff --git a/src/commands/Utilities/help.js b/src/commands/Utilities/help.js new file mode 100644 index 0000000..131a73f --- /dev/null +++ b/src/commands/Utilities/help.js @@ -0,0 +1,76 @@ +const { Message, MessageEmbed } = require('discord.js'); +const Bot = require('../../../index'); +const { readdirSync } = require('fs'); +const { prefix } = require('../../../config.json'); +const colors = require('../../../colors.json'); + + +module.exports = { + name: 'help', + aliases: ['commands'], + usage: 'help (command)', + description: 'Shows list of commands', + + /** + * @param {Bot} bot + * @param {Message} message + * @param {string[]} args + */ + run: async (bot, message, args) => { + if (args[0]) { + return getCmd(bot, message, args[0]); + } else { + return getAll(bot, message); + } + } +}; + +function getAll(bot, message) { + const embed = new MessageEmbed() + .setColor(colors.Orange) + .setFooter('Syntax: () = optional, [] = required, {a, b} = choose between a or b'); + + /* bot.categories is an array + Basically, this reads recursively each directory from src/commands + Then, for each category, it adds a field to the embed with the name and its commands */ + bot.categories.forEach(category => { + let filesArr = readdirSync(`./src/commands/${category}`) + .filter(file => file.endsWith('.js')); // Accepts only .js files + + embed.addField(category, + filesArr + .map(file => file.substring(0, file.length - 3)) // Removes the .js + .filter(cmd => !bot.commands.get(cmd).hidden) // Removes the ones with a hidden property + .map(str => `\`${str}\``) // Formats the names to include monospace + .join(' ')); // Joints them by spaces instead of newlines + + }); + + // After they're all added, send it + return message.channel.send(embed); +} + +function getCmd(bot, message, input) { + const embed = new MessageEmbed() + .setColor(colors.SteelBlue) + .setFooter('Syntax: () = optional; [] = required; {a, b} = choose between a or b'); + + // Fetching the command data through bot.commands or bot.aliases + const cmd = bot.commands.get(input.toLowerCase()) || bot.commands.get(bot.aliases.get(input.toLowerCase())); + + // If the command isn't found (likely doesn't exist) + if(!cmd) { + return message.channel.send(`**${input.toLowerCase()}** is not a command?`); + } + + // Adds its name based on helpName || uppercase name + if(cmd.name) embed.setDescription(`**${cmd.helpName ? cmd.helpName : cmd.name[0].toUpperCase() + cmd.name.slice(1)} Command**`); + // Adds aliases by mapping them + if(cmd.aliases) embed.addField('Aliases', `${cmd.aliases.map(a => `\`${a}\``).join(' ')}`); + // The description + if(cmd.description) embed.addField('Description', `${cmd.description}`); + // The usage + if(cmd.usage) embed.addField('Usage', `\`${prefix}${cmd.usage}\``); + + return message.channel.send(embed); +} \ No newline at end of file diff --git a/src/commands/Utilities/ping.js b/src/commands/Utilities/ping.js new file mode 100644 index 0000000..49f0292 --- /dev/null +++ b/src/commands/Utilities/ping.js @@ -0,0 +1,25 @@ +const { Message } = require('discord.js'); +const Bot = require('../../../index'); +const { stripIndents } = require('common-tags'); + + +module.exports = { + name: 'ping', + aliases: ['pingu', 'pong'], + usage: 'ping', + description: 'Checks the latency of the bot and message latency, and checks if bot is on', + + /** + * @param {Bot} bot + * @param {Message} message + * @param {string[]} args + */ + run: async (bot, message, args) => { + + const msg = await message.channel.send('Pinging...'); + + msg.edit(stripIndents`Pong! + Latency: ${Math.floor(msg.createdAt - message.createdAt)}ms + Discord API Latency: ${bot.ws.ping}ms`); + } +}; \ No newline at end of file diff --git a/src/events/client/ready.js b/src/events/client/ready.js new file mode 100644 index 0000000..ed7b451 --- /dev/null +++ b/src/events/client/ready.js @@ -0,0 +1,13 @@ +const Bot = require('../../../index'); +const chalk = require('chalk'); + +/** + * `ready` event. + * Triggers once the bot loads all the other events and goes online. + * Useful to show ready messages and do/set things at startup. + * + * @param {Client} bot + */ +module.exports = bot => { + console.info(`${chalk.green('[Info]')} - ${bot.user.username} online!`); +}; \ No newline at end of file diff --git a/src/events/guild/message.js b/src/events/guild/message.js new file mode 100644 index 0000000..7e93a04 --- /dev/null +++ b/src/events/guild/message.js @@ -0,0 +1,39 @@ +const { Message } = require('discord.js'); +const Bot = require('../../../index'); +const { prefix } = require('../../../config.json'); + +/** + * `message` event. + * + * Triggers each time any user sends any message in any channel the bot can look into. + * + * This event will include things to do whenever a command is triggered, a blacklisted word is said, etc. + * + * Honestly mostly everything that has to do with user input goes here. + * + * @param {Bot} bot The bot as a Client object + * @param {Message} message The Message object passed with the `message` event. + */ +module.exports = async (bot, message) => { + const args = message.content.slice(prefix.length).trim().split(/ +/g); + const cmd = args.shift().toLowerCase(); + + /* "\config prefix ?" in which: + \ = prefix + config = cmd + prefix,? = args (args[0],args[1]) */ + + // Command reading + if (message.author.bot) return; // Prevent from command loops or maymays from bot answers + if (!message.guild) return; // No DMs n stuff + if (!message.member) message.member = await message.guild.members.fetch(message); + if (cmd.length === 0) return; // Come on + + + // Command handler + let command = bot.commands.get(cmd); + if(!command) command = bot.commands.get(bot.aliases.get(cmd)); + if(command && message.content.startsWith(prefix)) { + command.run(bot, message, args); + } +}; \ No newline at end of file diff --git a/src/handlers/command.js b/src/handlers/command.js new file mode 100644 index 0000000..40c6e6c --- /dev/null +++ b/src/handlers/command.js @@ -0,0 +1,46 @@ +const Bot = require('../../index'); +const ascii = require('ascii-table'); +const { readdirSync } = require('fs'); + + +let table = new ascii(); +table.setHeading('Command', 'Load Status'); + + +/** + * Checks if a command has all what's needed for it to have. Return one string if it does, return other if it doesn't + */ +function checkData(bot, command, fileName) { + const + success = '✔ Loaded', + err = '✖ Missing Data'; + + const { name, usage, description, run } = command; + if ( + typeof name == 'string' && + typeof usage == 'string' && + typeof description == 'string' && + typeof run == 'function' + ) { + bot.commands.set(command.name.toLowerCase(), command); + return table.addRow(fileName, success); + } + return table.addRow(fileName, err); +} +/** + * Requires and triggers a command from the ./commands/ directory when it is inputed by a user next to the prefix. + * Not included in this file but in `index.js`, but there also is a collection with all commands at the time of node. + * If a user inputs a wrong command (incorrect command.name or command.aliases) it will not trigger anything. + * @param {Bot} bot The bot as a Client object + */ +module.exports = bot => { + readdirSync('./src/commands/').forEach(dir => { + const commands = readdirSync(`./src/commands/${dir}/`).filter(file => file.endsWith('.js')); + for (let file of commands) { + let pull = require(`../commands/${dir}/${file}`); + checkData(bot, pull, file); + if (pull.aliases && Array.isArray(pull.aliases)) pull.aliases.forEach(alias => bot.aliases.set(alias.toLowerCase(), pull.name.toLowerCase())); + } + }); + console.log(table.toString()); +}; \ No newline at end of file diff --git a/src/handlers/event.js b/src/handlers/event.js new file mode 100644 index 0000000..dab426b --- /dev/null +++ b/src/handlers/event.js @@ -0,0 +1,19 @@ +const Bot = require('../../index'); +const { readdirSync } = require('fs'); + +/** + * Event handler + * Requires and loads all events in the ./events/ directory with the correct parameters + * @param {Bot} bot The bot as a Client object + */ +module.exports = bot => { + const load = dirs => { + const events = readdirSync(`./src/events/${dirs}/`).filter(d => d.endsWith('.js')); + for(let file of events) { + const evt = require(`../events/${dirs}/${file}`); + let eName = file.split('.')[0]; + bot.on(eName, evt.bind(null, bot)); + } + }; + ['client', 'guild'].forEach(x=>load(x)); +}; \ No newline at end of file