├── helpers └── .gitkeep ├── .gitignore ├── config.json ├── package.json ├── lib ├── debug.js └── bot.js ├── handlers ├── keywords │ └── lahee.js └── commands │ ├── talk.js │ └── help.js ├── dispatchers ├── keywordDispatch.js └── commandDispatch.js ├── LICENSE ├── index.js └── README.md /helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | data -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "!bot ", 3 | "token": "YOUR-DISCORD-BOT-TOKEN-HERE", 4 | "debug": true 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-discord-js-bot", 3 | "version": "2.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Morgan Breden @bredmor", 10 | "license": "MIT", 11 | "dependencies": { 12 | "discord.js": "^12.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | debugPrintMessages(client, message, formatCallback = null) { 4 | if(!client.botConfig.debug) { 5 | return; 6 | } 7 | 8 | if(message.author.bot) { 9 | return; 10 | } 11 | 12 | if(formatCallback) { 13 | console.log('msg} ' + formatCallback(message)) 14 | } 15 | 16 | console.log('msg} ', message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /handlers/keywords/lahee.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class responds to anyone that types "LAHEE" or any of the aliases listed below with the gif defined below. 3 | */ 4 | module.exports = { 5 | name: 'LAHEE', // The name of the keyword to react to, 6 | aliases: ['lahee', 'la hee', 'LA HEE'], // Other keywords to react to 7 | gifLink: 'https://tenor.com/view/soken-fan-fest-lahee-otamatone-hum-gif-21581907', // The gif we're responding with 8 | // We could respond with text, or any other type of file instead. 9 | execute(message) { 10 | return message.channel.send(this.gifLink); 11 | }, 12 | }; -------------------------------------------------------------------------------- /lib/bot.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const Discord = require('discord.js'); 3 | 4 | /** 5 | * This class autoloads handler functions from the given subdirectory 6 | */ 7 | module.exports = { 8 | loadHandlers(client, subdir) { 9 | // Gather a list of all of our individual handler functions 10 | const files = fs.readdirSync(`${client.botConfig.rootDir}/handlers/${subdir}`).filter(file => file.endsWith('.js')); 11 | 12 | // Creates an empty list in the client object to store all functions 13 | client[subdir] = new Discord.Collection(); 14 | 15 | // Loops over each file in the folder and sets the functions to respond to themselves 16 | for (const file of files) { 17 | const func = require(`${client.botConfig.rootDir}/handlers/${subdir}/${file}`); 18 | client[subdir].set(func.name, func); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /dispatchers/keywordDispatch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class searches for any keyword handlers with the message content as their name, or an alias and executes the 3 | * handler if found. 4 | */ 5 | module.exports = { 6 | handle(client, message) { 7 | const messageContent = message.content.trim(); 8 | const keyword = client.keywords.get(messageContent) 9 | || client.keywords.find(kwd => kwd.aliases && kwd.aliases.includes(messageContent)); 10 | if(keyword) { 11 | try { 12 | // Run the command 13 | keyword.execute(message).catch((err) => { 14 | console.error(`Failed running keyword handler ${keyword.name}."`, err) 15 | }); 16 | return true; 17 | } catch(error) { 18 | console.error(error); 19 | } 20 | 21 | return false; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /handlers/commands/talk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class responds to anyone that types !bot talk and chooses one of the phrases below to respond with at random. 3 | * 4 | */ 5 | module.exports = { 6 | name: 'talk', // The name of the command 7 | description: 'Random phrases', // The description of the command (for help text) 8 | args: false, // Specified that this command doesn't need any data other than the command 9 | usage: '', // Help text to explain how to use the command (if it had any arguments) 10 | execute(message, args) { 11 | 12 | // List of phrases to respond with 13 | var phrases = [ 14 | 'Hello, World.', 15 | 'The Quick Brown Fox Jumps Over The Lazy Dog', 16 | 'I am the very model of a modern major general.' 17 | ]; 18 | 19 | return message.reply(phrases[Math.floor(Math.random()*phrases.length)]); // Replies to the user with a random phrase 20 | }, 21 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Morgan Breden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /handlers/commands/help.js: -------------------------------------------------------------------------------- 1 | const { prefix } = require('../../config.json'); 2 | 3 | /** 4 | * Runs the help command, explaining each available command to the user. 5 | */ 6 | module.exports = { 7 | name: 'help', 8 | description: 'List all available commands, or info about a specific command.', 9 | aliases: ['commands'], 10 | usage: '[command name]', 11 | cooldown: 5, 12 | execute(message, args) { 13 | const data = []; 14 | const { commands } = message.client; 15 | 16 | // Send help data about ALL commands 17 | if(!args.length) { 18 | data.push('Here\'s a list of all my commands:'); 19 | data.push(commands.map(command => command.name).join(', ')); 20 | data.push(`\nYou can send \`${prefix}help [command name]\` to get info on a specific command!`); 21 | 22 | return message.author.send(data, { split: true }) 23 | .then(() => { 24 | if (message.channel.type === 'dm') return; 25 | message.reply('I\'ve sent you a DM with all my commands!'); 26 | }) 27 | .catch(error => { 28 | console.error(`Could not send help DM to ${message.author.tag}.\n`, error); 29 | message.reply('it seems like I can\'t DM you! Do you have DMs disabled?'); 30 | }); 31 | } 32 | 33 | // Send help data about the specific command 34 | const name = args[0].toLowerCase(); 35 | const command = commands.get(name) || commands.find(c => c.aliases && c.aliases.includes(name)); 36 | 37 | if (!command) { 38 | return message.reply('that\'s not a valid command!'); 39 | } 40 | 41 | data.push(`**Name:** ${command.name}`); 42 | 43 | if (command.aliases) data.push(`**Aliases:** ${command.aliases.join(', ')}`); 44 | if (command.description) data.push(`**Description:** ${command.description}`); 45 | if (command.usage) data.push(`**Usage:** ${prefix}${command.name} ${command.usage}`); 46 | 47 | data.push(`**Cooldown:** ${command.cooldown || 3} second(s)`); 48 | 49 | return message.channel.send(data, { split: true }); 50 | }, 51 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); // Loads the Filesystem library 2 | const Discord = require('discord.js'); // Loads the discord API library 3 | const Config = require('./config.json'); // Loads the configuration values 4 | const BotLib = require('./lib/bot.js'); 5 | const DebugLib = require('./lib/debug.js'); 6 | 7 | // Loads our dispatcher classes that figure out what handlers to use in response to events 8 | const Keywords = require('./dispatchers/keywordDispatch'); 9 | const Commands = require('./dispatchers/commandDispatch'); 10 | 11 | const client = new Discord.Client(); // Initiates the client 12 | client.botConfig = Config; // Stores the config inside the client object so it's auto injected wherever we use the client 13 | client.botConfig.rootDir = __dirname; // Stores the running directory in the config so we don't have to traverse up directories. 14 | 15 | // Loads our handler functions that do all the work 16 | BotLib.loadHandlers(client, 'commands'); 17 | BotLib.loadHandlers(client, 'keywords'); 18 | 19 | const cooldowns = new Discord.Collection(); // Creates an empty list for storing timeouts so people can't spam with commands 20 | 21 | if(client.botConfig.debug) { 22 | console.log('Config Loaded: ', client.botConfig); 23 | } 24 | 25 | // Starts the bot and makes it begin listening to events. 26 | client.on('ready', () => { 27 | console.log('Bot Online'); 28 | }); 29 | 30 | // Handle user messages 31 | client.on('message', message => { 32 | // Check for keywords that don't use a real command structure 33 | if(Keywords.handle(client, message)) { 34 | return; // If we handled a keyword, don't continue to handle events for the same message 35 | } 36 | 37 | // Check for structured commands 38 | if(Commands.handle(client, message, cooldowns)) { 39 | return; // If we handled a command, don't continue to handle events for the same message 40 | } 41 | 42 | // Register debug message printer 43 | DebugLib.debugPrintMessages(client, message); 44 | }); 45 | 46 | // Log the bot in using the token provided in the config file 47 | client.login(client.botConfig.token).catch((err) => { 48 | console.log(`Failed to authenticate with Discord network.`, err); 49 | }); 50 | -------------------------------------------------------------------------------- /dispatchers/commandDispatch.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | 3 | /** 4 | * This class searches for any command handlers with the message start as their name, or an alias and executes the 5 | * handler if found. 6 | */ 7 | module.exports = { 8 | handle(client, message, cooldowns) { 9 | // Ignore bot messages and messages that dont start with the prefix defined in the config file 10 | if(!message.content.startsWith(client.botConfig.prefix) || message.author.bot) return; 11 | 12 | // Split commands and arguments from message so they can be passed to functions 13 | const args = message.content.slice(client.botConfig.prefix.length).split(/ +/); 14 | const commandName = args.shift().toLowerCase(); 15 | 16 | // If the command isn't in the command folder, move on 17 | const command = client.commands.get(commandName) 18 | || client.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName)); 19 | if(!command) return; 20 | 21 | // If the command requires arguments, make sure they're there. 22 | if (command.args && !args.length) { 23 | let reply = 'That command requires more details!'; 24 | 25 | // If we have details on how to use the args, provide them 26 | if (command.usage) { 27 | reply += `\nThe proper usage would be: \`${client.botConfig.prefix}${command.name} ${command.usage}\``; 28 | } 29 | 30 | // Send a reply from the bot about any error encountered 31 | return message.channel.send(reply); 32 | } 33 | 34 | /** 35 | * The following block of code handles "cooldowns" making sure that users can only use a command every so often. 36 | * This is helpful for commands that require loading time or computation, like image requests. 37 | */ 38 | if(!cooldowns.has(command.name)) { 39 | cooldowns.set(command.name, new Discord.Collection()); 40 | } 41 | 42 | const now = Date.now(); 43 | const timestamps = cooldowns.get(command.name); 44 | const cooldownAmount = (command.cooldown || 3 ) * 1000; 45 | 46 | if(!timestamps.has(message.author.id)) { 47 | timestamps.set(message.author.id, now); 48 | setTimeout(() => timestamps.delete(message.author.id), cooldownAmount); 49 | } else { 50 | const expirationTime = timestamps.get(message.author.id) + cooldownAmount; 51 | 52 | if(now < expirationTime) { 53 | const timeLeft = (expirationTime - now) / 1000; 54 | return message.reply(`Whoa! You're sending commands too fast! Please wait ${timeLeft.toFixed(1)} more second(s) before running \`${command.name}\` again!`); 55 | } 56 | 57 | timestamps.set(message.author.id, now); 58 | setTimeout(() => timestamps.delete(message.author.id), cooldownAmount) 59 | } 60 | /** 61 | * End cooldown code 62 | */ 63 | 64 | try { 65 | // Run the command 66 | command.execute(message, args).catch((err) => { 67 | console.error(`Failed running command handler ${command.name}".`, err) 68 | }); 69 | } catch(error) { 70 | console.error(error); 71 | message.reply('Sorry! I ran into an error trying to do that!'); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Discord JS Bot 2 | A starter bot and "framework" to build Discord bots from. 3 | 4 | ## Version 2! 5 | This updated version adds some new functionality to allow for the easy creation of more advanced 6 | bots, and some updated help as well. The original simple-discord-js-bot code can be found [here on 7 | the v1.0 tag](https://github.com/bredmor/simple-discord-js-bot/tree/v1.0). 8 | 9 | ## Prerequisites 10 | * Node JS >= 12.0.0 11 | * NPM >= 6.9.0 12 | * Git 13 | 14 | ## Setup 15 | 1. Clone this repository: `git clone https://github.com/bredmor/simple-discord-js-bot.git` then navigate to the new 16 | folder named `simple-discord-js-bot` 17 | 2. Install dependencies with `npm i` 18 | 3. Edit `config.json`, replacing the placeholder values with your desired command prefix and your bot token (If you 19 | don't have one yet, see the note below on how to generate one.) 20 | 4. Start the bot with `node index.js` 21 | 5. Add the bot to the server of your choice by filling out the requisite permissions in the "OAuth2 URL Generator" form (start by checking "bot") on the OAuth2 section of the developer portal and navigating to the generated link. 22 | 23 | That's it! You can now try out the default commands like `!bot help`, or create your own and restart the bot to use them. 24 | 25 | >**Note:** 26 | If you don't already have a Discord bot application setup you can create one by going to the 27 | > [Discord Developer Portal](https://discord.com/developers/applications/me), then create a new application, give it a 28 | > name, go to the "Bot" tab, then click on "Add Bot", and you're good to go! 29 | 30 | 31 | ## Usage 32 | After adding the bot to a server, call its command via `!bot commandname` where "!bot" is the prefix you defined in 33 | config.js and "commandname" is the name of a command defined and exported in the `commands` folder. 34 | 35 | You can safely delete or modify the example command `talk.js` but it is recommended to keep `help.js`. 36 | 37 | ## Running the bot permanently 38 | It's recommended that you use a process monitor like [PM2](https://pm2.keymetrics.io/) to run the bot instead of 39 | just `node`, that way it can be restarted on crashes and monitored. 40 | 41 | If you don't want to keep your computer on 24/7 to host the bot, I recommend a $5/month droplet from 42 | [DigitalOcean](https://m.do.co/c/b96f8bd70573). 43 | 44 | ## Defining Commands 45 | Simply create a new js file in the `handlers/commands` subdirectory that exports at least a `name`, `description` 46 | and `usage` string, as well as an `execute()` method. 47 | 48 | You can additionally provide `aliases`, `cooldown` 49 | and `args`. There are examples for all of these keys and their usage in the 3 example commands. 50 | 51 | ## Other Handlers 52 | In addition to simple `!command` style handlers, you can easily program the bot to listen and respond to any kind 53 | of event. 54 | 55 | 1. Create a subdirectory for the type of handler you want to add, such as the example `keywords` subdirectory. 56 | 2. Define at least one handler in that subdirectory, such as the example `handlers/keywords/lahee.js` handler. 57 | This handler should export at least an `execute` function and `name` variable. 58 | 3. Instantiate a Loader for that subdirectory in `index.js`, for example: `BotLib.loadHandlers(client, 'keywords');` 59 | replacing `keywords` with the name of your subdirectory. This will create a collection on the `client` object for 60 | that handler type, named after your subdirectory. 61 | 3. Create a "dispatcher" in the `dispatchers` subdirectory that exports a `handle` function. This function should accept 62 | at least the `client` object, and probably the object representing the event you want to handle, such as a `message` 63 | or `userReaction` event. The function should find the appropriate handler from the collection created in step 2, and 64 | call the `execute()` function on the handler. The function should then return true, if an event was handled. 65 | 4. Register the handler in the appropriate event loop in `index.js`. For example, if the handler works on message 66 | events,you would add it to the `client.on('message', => {` listener. It can be helpful here to check if the handler 67 | executed and force a `return` to avoid unintentionally continuing to process other handlers on the same event. 68 | 69 | ### Notes 70 | Why is there a ridiculous level of comments? I wrote this package to help someone with little programming experience 71 | learn how to write a bot. My hope is that people with little experience programming, or just little experience using 72 | node.js will be able to easily create even advanced bots using this "framework". 73 | --------------------------------------------------------------------------------