├── .eslintignore ├── .env.example ├── .prettierignore ├── .gitignore ├── src ├── types │ ├── index.ts │ └── bot │ │ └── Bot.ts ├── index.ts ├── config │ └── config.ts ├── events │ ├── Ready.ts │ └── Message.ts ├── utils │ └── Logger.ts ├── commands │ └── Ping.ts ├── Client.ts ├── managers │ └── ActionManager.ts └── Command.ts ├── .prettierrc ├── tsconfig.json ├── LICENSE ├── package.json ├── .eslintrc.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | BOT_TOKEN=TOKEN -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | .env -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bot/Bot'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "trailingComma": "none" 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import * as dotenv from 'dotenv'; 3 | import { Container } from 'typedi'; 4 | import { Client } from './Client'; 5 | 6 | dotenv.config(); 7 | 8 | // Initialize the Client using the IoC. 9 | Container.get(Client); 10 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | import { BotSettings } from '../types/bot/Bot'; 2 | 3 | export const settings: BotSettings = { 4 | presence: { 5 | activity: { 6 | name: '!help for commands', 7 | type: 'PLAYING' 8 | } 9 | }, 10 | prefix: '!', 11 | paths: { 12 | commands: 'src/commands', 13 | events: 'src/events' 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/events/Ready.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '../Client'; 2 | import { Logger } from '../utils/Logger'; 3 | import { BotEvent } from '../types'; 4 | 5 | export default class Ready implements BotEvent { 6 | constructor(private client: Client) {} 7 | 8 | public async run(): Promise { 9 | if (this.client.user) { 10 | Logger.info(`${this.client.user.username} is running.`); 11 | this.client.user.setPresence(this.client.settings.presence); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, transports as Transports, format } from 'winston'; 2 | 3 | const { printf, combine, timestamp, colorize } = format; 4 | const colorizer = colorize(); 5 | 6 | export const Logger = createLogger({ 7 | transports: new Transports.Console(), 8 | format: combine( 9 | timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 10 | printf(({ message, level, timestamp }) => 11 | colorizer.colorize(level, `[${timestamp}]: ${message}`) 12 | ) 13 | ) 14 | }); 15 | -------------------------------------------------------------------------------- /src/commands/Ping.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'discord.js'; 2 | import { Command } from '../Command'; 3 | import { BotClient } from '../types'; 4 | 5 | export default class Ping extends Command { 6 | constructor(client: BotClient) { 7 | super(client, { 8 | name: 'ping', 9 | description: 'Pings the bot.', 10 | category: 'Information', 11 | usage: client.settings.prefix.concat('ping'), 12 | cooldown: 1000, 13 | requiredPermissions: ['SEND_MESSAGES'] 14 | }); 15 | } 16 | 17 | public async run(message: Message): Promise { 18 | await super.respond(message.channel, 'Pong!'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "types": ["node"], 5 | "typeRoots": ["node_modules/@types"], 6 | "module": "commonjs", 7 | "outDir": "dist", 8 | "rootDir": "./", 9 | "removeComments": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "inlineSourceMap": true, 15 | "moduleResolution": "node", 16 | "sourceRoot": "src", 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true 19 | }, 20 | "include": [ 21 | "src" 22 | ], 23 | "exclude": [ 24 | "dist", 25 | "node_modules" 26 | ] 27 | } -------------------------------------------------------------------------------- /src/events/Message.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | import { Message as DiscordMessage } from 'discord.js'; 3 | import { Client } from '../Client'; 4 | import { BotEvent } from '../types'; 5 | 6 | export default class Message implements BotEvent { 7 | constructor(private client: Client) {} 8 | 9 | public async run(args: any): Promise { 10 | const [message] = args; 11 | 12 | if (message.author.bot || !message.content.startsWith(this.client.settings.prefix)) return; 13 | 14 | const argus = message.content.split(/\s+/g); 15 | const command = argus.shift()!.slice(this.client.settings.prefix.length); 16 | const cmd = this.client.commands.get(command); 17 | 18 | if (!cmd) return; 19 | if (!cmd.canRun(message.author, message)) return; 20 | 21 | await cmd.run(message, argus); 22 | 23 | if (message.guild) cmd.setCooldown(message.author, message.guild); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cornayy 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 | -------------------------------------------------------------------------------- /src/types/bot/Bot.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Client, 3 | TextChannel, 4 | DMChannel, 5 | PermissionString, 6 | PresenceData, 7 | ClientOptions, 8 | MessageEmbed, 9 | Guild, 10 | User, 11 | Collection, 12 | NewsChannel 13 | } from 'discord.js'; 14 | import { Command } from '../../Command'; 15 | 16 | export interface BotClient extends Client { 17 | settings: BotSettings; 18 | commands: Collection; 19 | } 20 | 21 | export interface CommandOptions { 22 | name: string; 23 | description?: string; 24 | usage?: string; 25 | category?: string; 26 | cooldown: number; 27 | requiredPermissions: PermissionString[]; 28 | } 29 | 30 | export interface BotSettings { 31 | presence: PresenceData; 32 | clientOptions?: ClientOptions; 33 | token?: string; 34 | prefix: string; 35 | paths: { 36 | commands: string; 37 | events: string; 38 | }; 39 | } 40 | 41 | export interface BotEvent { 42 | run(args?: any[]): void; 43 | } 44 | 45 | export interface UserCooldown { 46 | user: User; 47 | guild: Guild; 48 | } 49 | 50 | export type AnyChannel = TextChannel | DMChannel | NewsChannel; 51 | export type EmbedOrMessage = MessageEmbed | string; 52 | -------------------------------------------------------------------------------- /src/Client.ts: -------------------------------------------------------------------------------- 1 | import { Collection, Client as DiscordClient } from 'discord.js'; 2 | import { Service } from 'typedi'; 3 | import { Logger } from './utils/Logger'; 4 | import { BotSettings, BotClient } from './types'; 5 | import { Command } from './Command'; 6 | import { ActionManager } from './managers/ActionManager'; 7 | import { settings as configuration } from './config/config'; 8 | 9 | @Service() 10 | export class Client extends DiscordClient implements BotClient { 11 | public settings: BotSettings; 12 | 13 | constructor(private actionManager: ActionManager) { 14 | super(configuration.clientOptions || {}); 15 | this.settings = configuration; 16 | this.settings.token = process.env.BOT_TOKEN; 17 | this.initialize(); 18 | } 19 | 20 | private async initialize(): Promise { 21 | try { 22 | this.actionManager.initializeCommands(this); 23 | this.actionManager.initializeEvents(this); 24 | await this.login(configuration.token); 25 | } catch (e) { 26 | Logger.error(`Could not initialize bot: ${e}`); 27 | } 28 | } 29 | 30 | public get commands(): Collection { 31 | return this.actionManager.commands; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-discord-bot-boilerplate", 3 | "version": "1.0.0", 4 | "description": "A TypeScript Discord bot.", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "start": "tsc && node dist/src/index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Cornayy/ts-discord-bot-boilerplate.git" 12 | }, 13 | "author": "Cornayy", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Cornayy/ts-discord-bot-boilerplate/issues" 17 | }, 18 | "homepage": "https://github.com/Cornayy/ts-discord-bot-boilerplate#readme", 19 | "dependencies": { 20 | "discord.js": "^12.3.1", 21 | "dotenv": "^8.0.0", 22 | "reflect-metadata": "^0.1.13", 23 | "typedi": "^0.8.0", 24 | "typescript": "^3.6.2", 25 | "winston": "^3.3.3" 26 | }, 27 | "devDependencies": { 28 | "@types/dotenv": "^6.1.1", 29 | "@types/node": "^12.7.3", 30 | "@types/winston": "^2.4.4", 31 | "@typescript-eslint/eslint-plugin": "^2.0.0", 32 | "@typescript-eslint/parser": "^2.0.0", 33 | "eslint": "^6.1.0", 34 | "eslint-config-airbnb-typescript": "^4.0.1", 35 | "eslint-config-node": "^4.0.0", 36 | "eslint-config-prettier": "^6.0.0", 37 | "eslint-plugin-import": "^2.14.0", 38 | "eslint-plugin-node": "^9.1.0", 39 | "eslint-plugin-prettier": "^3.1.0", 40 | "prettier": "^1.18.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "airbnb-typescript/base", 8 | "prettier/@typescript-eslint", 9 | "plugin:prettier/recommended", 10 | "plugin:node/recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "plugins": ["prettier"], 14 | "parserOptions": { 15 | "parser": "@typescript-eslint/parser", 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "rules": { 20 | "prettier/prettier": "error", 21 | "no-unused-vars": ["warn", { "args": "none" }], 22 | "no-unused-expressions": [ 23 | "warn", 24 | { 25 | "allowTernary": true 26 | } 27 | ], 28 | "import/no-cycle": "off", 29 | "no-console": "off", 30 | "func-names": "off", 31 | "object-shorthand": "off", 32 | "class-methods-use-this": "off", 33 | "import/no-dynamic-require": "off", 34 | "global-require": "off", 35 | "no-shadow": "off", 36 | "no-useless-constructor": "off", 37 | "node/no-unsupported-features/es-syntax": "off", 38 | "lines-between-class-members": "off", 39 | "import/prefer-default-export": "off", 40 | "import/named": "off", 41 | "@typescript-eslint/no-explicit-any": "off", 42 | "@typescript-eslint/interface-name-prefix": "off" 43 | } 44 | } -------------------------------------------------------------------------------- /src/managers/ActionManager.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from 'discord.js'; 2 | import { Service } from 'typedi'; 3 | import { join } from 'path'; 4 | import { readdir, statSync } from 'fs'; 5 | import { BotClient } from '../types/bot/Bot'; 6 | import { Command } from '../Command'; 7 | import { Logger } from '../utils/Logger'; 8 | 9 | @Service() 10 | export class ActionManager { 11 | public commands: Collection = new Collection(); 12 | 13 | /** 14 | * Parses files into commands from the configured command path. 15 | * @param {BotClient} client The original client, for access to the configuration. 16 | * @returns {Collection} A dictionary of every command in a [name, object] pair. 17 | */ 18 | public initializeCommands(client: BotClient): void { 19 | const { commands } = client.settings.paths; 20 | 21 | readdir(commands, (err, files) => { 22 | if (err) Logger.error(err); 23 | 24 | files.forEach(cmd => { 25 | if (statSync(join(commands, cmd)).isDirectory()) { 26 | this.initializeCommands(client); 27 | } else { 28 | const Command: any = require(join( 29 | __dirname, 30 | '../../', 31 | `${commands}/${cmd.replace('ts', 'js')}` 32 | )).default; 33 | const command = new Command(client); 34 | 35 | this.commands.set(command.conf.name, command); 36 | } 37 | }); 38 | }); 39 | } 40 | 41 | /** 42 | * Initializes every event from the configured event path. 43 | * @param {BotClient} client The original client, for access to the configuration. 44 | */ 45 | public initializeEvents(client: BotClient): void { 46 | const { events } = client.settings.paths; 47 | 48 | readdir(events, (err, files) => { 49 | if (err) Logger.error(err); 50 | 51 | files.forEach(evt => { 52 | const Event: any = require(join( 53 | __dirname, 54 | '../../', 55 | `${events}/${evt.replace('ts', 'js')}` 56 | )).default; 57 | 58 | const event = new Event(client); 59 | const eventName = evt.split('.')[0]; 60 | 61 | client.on( 62 | eventName.charAt(0).toLowerCase() + eventName.slice(1), 63 | (...args: string[]) => event.run(args) 64 | ); 65 | }); 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts-discord-bot-boilerplate 2 | 3 | Get started with a new Discord bot using `discord.js` fast. 4 | 5 | ## Usage 6 | 7 | This section contains information about where to define new functionality for your Discord bot. 8 | 9 | ### Events 10 | 11 | Trigger actions on a new event by adding a new file that has the same name as a specific **event name** defined in the event section of the [documentation](https://discord.js.org/#/docs/main/stable/class/Client). 12 | 13 | To properly initialize the event, this file has to be added to the event directory you specified in the configuration file of the bot: `config.ts`. 14 | 15 | *Example of Event: **Ready*** 16 | 17 | *Ready.ts* 18 | 19 | ```typescript 20 | import { Client } from '../Client'; 21 | import { Logger } from '../utils/Logger'; 22 | import { BotEvent } from '../types'; 23 | 24 | export default class Ready implements BotEvent { 25 | public client: Client; 26 | 27 | constructor(client: Client) { 28 | this.client = client; 29 | } 30 | 31 | public async run(): Promise { 32 | Logger.info('Execute an action when the Ready event is triggered'); 33 | } 34 | } 35 | ``` 36 | 37 | ### Commands 38 | 39 | Execute commands when a specific keyword including the command prefix has been sent in a Discord channel. 40 | 41 | To properly initialize the command, this file has to be added to the command directory you specified in the configuration file of the bot: `config.ts`. 42 | 43 | *Example of Command: **Ping*** 44 | 45 | *Ping.ts* 46 | 47 | ```typescript 48 | import { Message } from 'discord.js'; 49 | import { Command } from '../Command'; 50 | import { BotClient } from '../types'; 51 | 52 | export default class Ping extends Command { 53 | constructor(client: BotClient) { 54 | super(client, { 55 | name: 'ping', 56 | description: 'Pings the bot.', 57 | category: 'Information', 58 | usage: client.settings.prefix.concat('ping'), 59 | cooldown: 1000, 60 | requiredPermissions: ['SEND_MESSAGES'] 61 | }); 62 | } 63 | 64 | public async run(message: Message): Promise { 65 | await super.respond(message.channel, 'Pong!'); 66 | } 67 | } 68 | ``` 69 | 70 | ## Running 71 | 72 | To run the bot, first create an application on the Discord developer portal. Then copy the `.env.example` and rename it into `.env`. A proper `.env` file should look like this: 73 | 74 | *.env* 75 | ``` 76 | BOT_TOKEN=RANDOMTOKENYOURECEIVEDFROMTHEDEVELOPERPORTAL 77 | ``` 78 | 79 | After everything is configured, you can run the bot locally by executing the `npm start` command. 80 | 81 | ## Documentation 82 | 83 | During development, refer to the documentation served by [discord.js](https://discord.js.org/#/docs/main/stable/general/welcome). 84 | 85 | ## License 86 | [MIT](LICENSE) -------------------------------------------------------------------------------- /src/Command.ts: -------------------------------------------------------------------------------- 1 | import { User, Message, Guild } from 'discord.js'; 2 | import { AnyChannel, BotClient, CommandOptions, EmbedOrMessage, UserCooldown } from './types'; 3 | 4 | export abstract class Command { 5 | public conf: CommandOptions; 6 | public cooldowns: Set; 7 | 8 | constructor(protected client: BotClient, options: CommandOptions) { 9 | this.conf = { 10 | name: options.name, 11 | description: options.description || 'No information specified.', 12 | usage: options.usage || 'No usage specified.', 13 | category: options.category || 'Information', 14 | cooldown: options.cooldown || 1000, 15 | requiredPermissions: options.requiredPermissions || ['READ_MESSAGES'] 16 | }; 17 | this.cooldowns = new Set(); 18 | } 19 | 20 | /** 21 | * Checks if the user has permission to run the command. 22 | * @param {User} user A Discord user. 23 | * @param {Message} message The original message that was sent. 24 | * @returns {boolean} Whether the user can run the command. 25 | */ 26 | public canRun(user: User, message: Message): boolean { 27 | const onCooldown = 28 | [...this.cooldowns].filter(cd => cd.user === user && cd.guild === message.guild) 29 | .length > 0; 30 | const hasPermission = message.member 31 | ? message.member.hasPermission(this.conf.requiredPermissions, { 32 | checkAdmin: true, 33 | checkOwner: true 34 | }) 35 | : false; 36 | 37 | if (!hasPermission || onCooldown) { 38 | message.channel.send( 39 | 'You do not have permission for this command or you are on cooldown.' 40 | ); 41 | return false; 42 | } 43 | 44 | return true; 45 | } 46 | 47 | /** 48 | * Sets the cooldown on a command for a Discord user. 49 | * @param {User} user The user that will receive a cooldown. 50 | * @param {Guild} guild The Discord server where the original message was sent. 51 | */ 52 | public setCooldown(user: User, guild: Guild): void { 53 | this.cooldowns.add({ user, guild }); 54 | 55 | setTimeout(() => { 56 | const cooldown = [...this.cooldowns].filter( 57 | cd => cd.user === user && cd.guild === guild 58 | )[0]; 59 | this.cooldowns.delete(cooldown); 60 | }, this.conf.cooldown); 61 | } 62 | 63 | /** 64 | * Sends the message in the specified channel. 65 | * @param {AnyChannel} channel Any Discord channel. 66 | * @param {EmbedOrMessage} message The message or embed that will be sent. 67 | * @returns {Promise} The original command, supports method chaining. 68 | */ 69 | public async respond(channel: AnyChannel, message: EmbedOrMessage): Promise { 70 | await channel.send(message); 71 | 72 | return this; 73 | } 74 | 75 | /** 76 | * The abstract run method for every command. 77 | * @param {Message} message The original message object that triggered the command. 78 | * @param {string[]} args The arguments that got sent with the message. 79 | */ 80 | public abstract async run(message: Message, args: string[]): Promise; 81 | } 82 | --------------------------------------------------------------------------------