From 7ad0d4bae439c339660a2c7f2a6e2711e837c7a4 Mon Sep 17 00:00:00 2001 From: Daryl Ronningen Date: Sun, 20 Jun 2021 02:39:44 -0700 Subject: [PATCH] feat(commands): added basic command handler --- config/default.json.example | 3 +- package.json | 13 +++++++- src/index.ts | 57 +++++++++++++++++++++++++++++++---- src/lib/structures/command.ts | 30 ++++++++++++++++++ src/lib/utils/defaults.ts | 8 +++-- src/lib/utils/logger.ts | 22 +++++++------- src/lib/utils/types.ts | 5 +++ src/lib/utils/utils.ts | 55 +++++++++++++++++++++++++++++++++ tsconfig.json | 3 ++ 9 files changed, 174 insertions(+), 22 deletions(-) create mode 100644 src/lib/structures/command.ts create mode 100644 src/lib/utils/utils.ts diff --git a/config/default.json.example b/config/default.json.example index ab13c68..24f5629 100644 --- a/config/default.json.example +++ b/config/default.json.example @@ -1,4 +1,5 @@ { "token": "", - "logLevel": "info" + "logLevel": "", + "prefix": "" } \ No newline at end of file diff --git a/package.json b/package.json index 620c85f..312ce36 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,16 @@ "license-header.txt" ], "no-case-declarations": "off", + "keyword-spacing": ["error", {"overrides": { + "if": { + "before": false, + "after": false + }, + "catch": { + "before": true, + "after": false + } + }}], "@typescript-eslint/no-non-null-assertion": "off" }, "reportUnusedDisableDirectives": true @@ -186,6 +196,7 @@ "@": "dist", "@src": "dist/src", "@lib": "dist/src/lib", - "@utils": "dist/src/lib/utils/" + "@utils": "dist/src/lib/utils/", + "@structures": "dist/src/lib/structures" } } diff --git a/src/index.ts b/src/index.ts index bd47fa0..9d1375b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,10 +23,16 @@ import config from 'config'; import chalk from 'chalk'; import { Validator } from 'jsonschema'; import { DateTime } from 'luxon'; -import { Client } from 'discord.js'; -import { debug, error, info, verbose } from '@utils/logger'; +import { Client, Collection } from 'discord.js'; +import { debug, error, fatal, info, verbose } from '@utils/logger'; import { ELoggingScope } from '@utils/types'; import { Defaults } from '@utils/defaults'; +import { walkDir } from '@utils/utils'; + +import type Command from '@structures/command'; + +let isBotReady = false; +const commands: Collection = new Collection(); info('Starting bot... Please wait!', ELoggingScope.Startup); debug('Checking config JSON schema', ELoggingScope.Startup); @@ -36,7 +42,7 @@ const mergedConfig = config.util.extendDeep(Defaults.config, config.util.loadFil const schemaValidator = new Validator(); const validate = schemaValidator.validate(mergedConfig, Defaults.configSchema); -if (validate.valid) { +if(validate.valid) { debug('Config matches JSON schema', ELoggingScope.Startup); } else { // Manually send fatal message in case someone messes up the logLevel config @@ -53,7 +59,7 @@ if (validate.valid) { } figlet('Argon Bot', (err, data) => { - if (err) error(`Figlet encountered an error!\n${err.message}`); + if(err) error(`Figlet encountered an error!\n${err.message}`); info(gradient.rainbow.multiline(`\n${data}`), ELoggingScope.Startup); }); @@ -62,11 +68,50 @@ const client = new Client({ intents: ['GUILDS', 'GUILD_MESSAGES'], }); -client.on('ready', () => { +client.on('message', (msg) => { + if(!isBotReady) return; + + if(msg.author.bot) return; + if(!msg.content.startsWith(config.get('prefix'))) return; + + const args = msg.content.slice((config.get('prefix') as string).length).trim().split(/ +/); + const command = args.shift()!.toLowerCase(); + + const findCommand = commands.find((com) => com.options.name === command); + + if(!findCommand) return; + else findCommand.run(msg, ...args); +}); + +client.on('ready', async () => { + info('Loading commands...', ELoggingScope.Startup); + + try { + const files = await walkDir(`${__dirname}/commands`); + + files?.forEach(async (file) => { + if(file.endsWith('js')) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const fileCommand = require(file).default; + + const command = new fileCommand(); + + commands.set(command.options.name, command); + + debug(`Loaded ${command.options.name}`, ELoggingScope.Startup); + } + }); + + info(`Finished loading commands! Found ${commands.size} commands.`, ELoggingScope.Startup); + } catch(err) { + fatal(`An error has occurred while attempting to load command files! Please see error below\n${err.message}`, ELoggingScope.Startup); + } + + info('Bot is ready!', ELoggingScope.Startup); debug(`Total number of Servers: ${client.guilds.cache.size}`, ELoggingScope.Startup); debug(`Total number of Users: ${client.users.cache.size}`, ELoggingScope.Startup); - info('Bot is ready!', ELoggingScope.Startup); + isBotReady = true; }); client.on('raw', (payload) => { diff --git a/src/lib/structures/command.ts b/src/lib/structures/command.ts new file mode 100644 index 0000000..47e96c8 --- /dev/null +++ b/src/lib/structures/command.ts @@ -0,0 +1,30 @@ +/* + * This file is part of ArgonBot + * + * ArgonBot is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ArgonBot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ArgonBot. If not, see . + */ +import type { Client, Message } from 'discord.js'; +import type { ICommandOptions } from '@utils/types'; + +export default abstract class Command { + public readonly client: Client; + public readonly options: ICommandOptions + + public constructor(client: Client, options: ICommandOptions) { + this.client = client; + this.options = options; + } + + public abstract run(message: Message, ...args:string[]): void; +} diff --git a/src/lib/utils/defaults.ts b/src/lib/utils/defaults.ts index 6d06d76..5818a18 100644 --- a/src/lib/utils/defaults.ts +++ b/src/lib/utils/defaults.ts @@ -19,9 +19,7 @@ export const Defaults = { logLevel: 'info', }, configSchema: { - $id: 'http://example.com/example.json', - $schema: 'http://json-schema.org/draft-07/schema', - required: ['token', 'logLevel'], + required: ['token', 'logLevel', 'prefix'], type: 'object', properties: { token: { @@ -40,6 +38,10 @@ export const Defaults = { 'fatal', ], }, + prefix: { + $id: '#/properties/prefix', + type: 'string', + }, }, additionalProperties: false, }, diff --git a/src/lib/utils/logger.ts b/src/lib/utils/logger.ts index f85b496..30d1452 100644 --- a/src/lib/utils/logger.ts +++ b/src/lib/utils/logger.ts @@ -70,8 +70,8 @@ export function verbose(message: string, scope?: ELoggingScope): void { const splitMultiline = message.split('\n'); splitMultiline.forEach((val) => { - if (verboseLevel) - if (scope) + if(verboseLevel) + if(scope) console.log(chalk`{grey (${date})} {magenta.bold ${scope}} {white.bold [VERBOSE]}: {white ${val}}`); else console.log(chalk`{grey (${date})} {white.bold [VERBOSE]}: {white ${val}}`); @@ -83,8 +83,8 @@ export function debug(message: string, scope?: ELoggingScope): void { const splitMultiline = message.split('\n'); splitMultiline.forEach((val) => { - if (debugLevel) - if (scope) + if(debugLevel) + if(scope) console.log(chalk`{grey (${date})} {magenta.bold ${scope}} {blue.bold [DEBUG]}: {blue ${val}}`); else console.log(chalk`{grey (${date})} {blue.bold [DEBUG]}: {blue ${val}}`); @@ -96,7 +96,7 @@ export function info(message: string, scope?: ELoggingScope): void { const splitMultiline = message.split('\n'); splitMultiline.forEach((val) => { - if (infoLevel) + if(infoLevel) if(scope) console.log(chalk`{grey (${date})} {magenta.bold ${scope}} {green.bold [INFO]}: {green ${val}}`); else @@ -109,8 +109,8 @@ export function warn(message: string, scope?: ELoggingScope): void { const splitMultiline = message.split('\n'); splitMultiline.forEach((val) => { - if (warnLevel) - if (scope) + if(warnLevel) + if(scope) console.log(chalk`{grey (${date})} {magenta.bold ${scope}} {yellow.bold [WARN]}: {yellow ${val}}`); else console.log(chalk`{grey (${date})} {yellow.bold [WARN]}: {yellow ${val}}`); @@ -122,8 +122,8 @@ export function error(message: string, scope?: ELoggingScope): void { const splitMultiline = message.split('\n'); splitMultiline.forEach((val) => { - if (errorLevel) - if (scope) + if(errorLevel) + if(scope) console.log(chalk`{grey (${date})} {magenta.bold ${scope}} {bold.underline.rgb(255, 165, 0) [ERROR]}: {underline.rgb(255, 165, 0) ${val}}`); else console.log(chalk`{grey (${date})} {bold.underline.rgb(255, 165, 0) [ERROR]}: {underline.rgb(255, 165, 0) ${val}}`); @@ -135,8 +135,8 @@ export function fatal(message: string, scope?: ELoggingScope): void { const splitMultiline = message.split('\n'); splitMultiline.forEach((val) => { - if (fatalLevel) - if (scope) + if(fatalLevel) + if(scope) console.log(chalk`{grey (${date})} {magenta.bold ${scope}} {red.bold.underline [FATAL]}: {red.underline ${val}}`); else console.log(chalk`{grey (${date})} {red.bold.underline [FATAL]}: {red.underline ${val}}`); diff --git a/src/lib/utils/types.ts b/src/lib/utils/types.ts index 1b3a7f1..0058146 100644 --- a/src/lib/utils/types.ts +++ b/src/lib/utils/types.ts @@ -23,5 +23,10 @@ export enum ELoggingScope { } // Interfaces +export interface ICommandOptions { + name: string; + shortDescription: string; + extendedDescription: string; +} // Type Aliases diff --git a/src/lib/utils/utils.ts b/src/lib/utils/utils.ts new file mode 100644 index 0000000..2a2feae --- /dev/null +++ b/src/lib/utils/utils.ts @@ -0,0 +1,55 @@ +/* + * This file is part of ArgonBot + * + * ArgonBot is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ArgonBot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ArgonBot. If not, see . + */ +import fs from 'fs'; +import path from 'path'; + +export const walkDir = async (dir: string): Promise => { + let results: string[] = []; + + return new Promise((resolve, reject) => { + fs.readdir(dir, (err, list) => { + if(err) return reject(err); + + let pending = list.length; + + if(!pending) return resolve(results); + + list.forEach((file) => { + file = path.resolve(dir, file); + + fs.stat(file, async (err, stat) => { + if(err) return reject(err); + + if(stat && stat.isDirectory()) { + try { + const dir = await walkDir(file); + + results = results.concat(dir as unknown as string); + + if(!--pending) resolve(results); + } catch(err) { + reject(err); + } + } else { + results.push(file); + if(!--pending) resolve(results); + } + }); + }); + }); + }); +}; diff --git a/tsconfig.json b/tsconfig.json index f0a619a..d490560 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -65,6 +65,9 @@ ], "@utils/*": [ "src/lib/utils/*" + ], + "@structures/*": [ + "src/lib/structures/*" ] } },