diff --git a/.eslintrc b/.eslintrc index 3dac692..c142c1f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,10 +1,10 @@ { "parser": "@typescript-eslint/parser", "extends": ["plugin:@typescript-eslint/recommended"], - "parserOptions": { "ecmaVersion": "esnext", "sourceType": "" }, + "parserOptions": { "ecmaVersion": "latest", "sourceType": "script" }, "rules": { "@typescript-eslint/no-non-null-assertion": "off", "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals" : true }], - "semi" : ["error", "always"] + "semi": ["error", "always"] } } \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 3203c2e..2f31668 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,5 @@ "trailingComma": "all", "singleQuote": true, "printWidth": 120, - "tabWidth": 2 -} \ No newline at end of file + "tabWidth": 4 +} diff --git a/src/handler/Utilities/readFile.ts b/src/handler/Utilities/readFile.ts deleted file mode 100644 index 9fc78ba..0000000 --- a/src/handler/Utilities/readFile.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { - ApplicationCommandOptionData -} from 'discord.js'; - -import { - readdirSync, - statSync -} from 'fs'; - -import { - basename, - join -} from 'path'; - -import type * as Sern from '../sern'; - -export type CommandVal = { - mod: Sern.Module, - options: ApplicationCommandOptionData[], -} - -export const Commands = new Map(); -export const Alias = new Map(); - -// Courtesy of Townsy#0001 on Discord -async function readPath(dir: string, arrayOfFiles: string[] = []): Promise { - try { - const files = readdirSync(dir); - for (const file of files) { - if (statSync(dir + '/' + file).isDirectory()) - await readPath(dir + '/' + file, arrayOfFiles); - else - arrayOfFiles.push(join(dir, '/', file)); - } - } catch (err) { - throw err; - } - - return arrayOfFiles; -} - -export const fmtFileName = (n: string) => n.substring(0, n.length - 3); - -/** - * @param {Sern.Handler} handler an instance of Sern.Handler - * @returns {Promise<{ name: string; mod: Sern.Module; absPath: string; }[]>} data from command files -*/ - -export async function buildData(handler: Sern.Handler) - : Promise<{ - name: string; - mod: Sern.Module; - absPath: string; - }[]> { - const commandDir = handler.commandDir; - return Promise.all((await getCommands(commandDir)) - .map(async absPath => { - return { name: basename(absPath), mod: (await import(absPath)).default as Sern.Module, absPath }; - })); -} - -export async function getCommands(dir: string): Promise { - return readPath(join(process.cwd(), dir)); -} diff --git a/src/handler/logger.ts b/src/handler/logger.ts index d325452..8fd122d 100644 --- a/src/handler/logger.ts +++ b/src/handler/logger.ts @@ -3,39 +3,40 @@ import Timezone from 'dayjs/plugin/timezone'; import UTC from 'dayjs/plugin/timezone'; enum sEvent { - GLOBAL_SLASH, - LOCAL_SLASH, - DM, - CRASH, - TEXT_CMD, + GLOBAL_SLASH, + LOCAL_SLASH, + DM, + CRASH, + TEXT_CMD, } export default class Logger { - - public clear() { console.clear(); } - - public log(e : T, message: string) { - dayJS.extend(UTC); - dayJS.extend(Timezone); - dayJS.tz.guess(); - // add colored logging? - const tz = dayJS().format(); - console.log(`[${`${tz}`}][${sEvent[e]}] :: ${message}`); - } + public clear() { + console.clear(); + } + + public log(e: T, message: string) { + dayJS.extend(UTC); + dayJS.extend(Timezone); + dayJS.tz.guess(); + // add colored logging? + const tz = dayJS().format(); + console.log(`[${`${tz}`}][${sEvent[e]}] :: ${message}`); + } + + /** + * Utilizes console.table() to print out memory usage of current process. + * Optional at startup. + * + */ - /** - * Utilizes console.table() to print out memory usage of current process. - * Optional at startup. - * - */ - public tableRam() { - console.table( - Object.entries(process.memoryUsage()) - .map(([k, v]: [string, number]) => { - return { [k]: `${((Math.round(v) / 1024 / 1024 * 100) / 100).toFixed(2)} MBs` }; - }) - .reduce(((r, c) => Object.assign(r, c)), {}) - ); + console.table( + Object.entries(process.memoryUsage()) + .map(([k, v]: [string, number]) => { + return { [k]: `${(((Math.round(v) / 1024 / 1024) * 100) / 100).toFixed(2)} MBs` }; + }) + .reduce((r, c) => Object.assign(r, c), {}), + ); } } diff --git a/src/handler/sern.ts b/src/handler/sern.ts index 2a01e30..c40cb0c 100644 --- a/src/handler/sern.ts +++ b/src/handler/sern.ts @@ -1,23 +1,13 @@ import * as Files from './utilities/readFile'; import type * as Utils from './utilities/preprocessors/args'; -import type { - Arg, - Context, - Visibility, - possibleOutput -} from '../types/handler'; +import type { Arg, Context, Visibility, possibleOutput } from '../types/handler'; -import type { - ApplicationCommandOptionData, - Awaitable, - Client, - CommandInteraction, - Message -} from 'discord.js'; +import type { ApplicationCommandOptionData, Awaitable, Client, CommandInteraction, Message } from 'discord.js'; import { Ok, Result, None, Some } from 'ts-results'; import { isBot, hasPrefix, fmt } from './utilities/messageHelpers'; +import Logger from './logger'; /** * @class @@ -28,24 +18,23 @@ export class Handler { /** * @constructor - * @param {Wrapper} wrapper The data that is required to run sern handler + * @param {Wrapper} wrapper The data that is required to run sern handler */ - - constructor( - wrapper: Wrapper, - ) { + + constructor(wrapper: Wrapper) { this.wrapper = wrapper; this.client - + /** * On ready, builds command data and registers them all - * from command directory + * from command directory **/ - + .on('ready', async () => { Files.buildData(this) .then(data => this.registerModules(data)); if (wrapper.init !== undefined) wrapper.init(this); + new Logger().tableRam(); }) .on('messageCreate', async (message: Message) => { @@ -53,16 +42,16 @@ export class Handler { if (message.channel.type === 'DM') return; // TODO: Handle dms const tryFmt = fmt(message, this.prefix); - const module = this.findCommand(tryFmt.shift()!); + const commandName = tryFmt.shift()!; + const module = Files.Commands.get(commandName) ?? Files.Alias.get(commandName); if (module === undefined) { message.channel.send('Unknown legacy command'); return; } - const cmdResult = (await this.commandResult(module, message, tryFmt.join(' '))); + const cmdResult = await this.commandResult(module, message, tryFmt.join(' ')); if (cmdResult === undefined) return; message.channel.send(cmdResult); - }) .on('interactionCreate', async (interaction) => { @@ -75,58 +64,63 @@ export class Handler { } /** - * - * @param {Files.CommandVal | undefined} module Command file information - * @param {CommandInteraction} interaction The Discord.js command interaction (DiscordJS#CommandInteraction)) + * + * @param {Files.CommandVal | undefined} module Command file information + * @param {CommandInteraction} interaction The Discord.js command interaction (DiscordJS#CommandInteraction)) * @returns {possibleOutput | undefined} Takes return value and replies it, if possible input */ - + private async interactionResult( module: Files.CommandVal | undefined, - interaction: CommandInteraction): Promise { - + interaction: CommandInteraction, + ): Promise { if (module === undefined) return 'Unknown slash command!'; - const name = this.findCommand(interaction.commandName); - if (name === undefined) `${interaction.commandName} is not a valid command!`; + const name = Array.from(Files.Commands.keys()).find((it) => it === interaction.commandName); + if (name === undefined) return `Could not find ${interaction.commandName} command!`; + if (module.mod.type < CommandType.SLASH) return 'This is not a slash command'; - const context = { message: None, interaction: Some(interaction) }; const parsedArgs = module.mod.parse?.(context, ['slash', interaction.options]) ?? Ok(''); - + if (parsedArgs.err) return parsedArgs.val; - + return (await module.mod.delegate(context, parsedArgs))?.val; } /** - * + * * @param {Files.CommandVal | undefined} module Command file information * @param {Message} message The message object - * @param {string} args Anything after the command + * @param {string} args Anything after the command * @returns Takes return value and replies it, if possible input */ - - private async commandResult(module: Files.CommandVal | undefined, message: Message, args: string): Promise { + + private async commandResult( + module: Files.CommandVal | undefined, + message: Message, + args: string, + ): Promise { if (module?.mod === undefined) return 'Unknown legacy command'; if (module.mod.type === CommandType.SLASH) return `This may be a slash command and not a legacy command`; if (module.mod.visibility === 'private') { const checkIsTestServer = this.privateServers.find(({ id }) => id === message.guildId!)?.test; - if (checkIsTestServer === undefined) return 'This command has the private modifier but is not registered under Handler#privateServers'; + if (checkIsTestServer === undefined) + return 'This command has the private modifier but is not registered under Handler#privateServers'; if (checkIsTestServer !== module.mod.test) { - const msg = `This command is only available on test servers.`; // TODO: Customizable private message - - return msg; + const msg = `This command is only available on test servers.`; // TODO: Customizable private message + + return msg; } } const context = { message: Some(message), - interaction: None + interaction: None, }; const parsedArgs = module.mod.parse?.(context, ['text', args]) ?? Ok(''); if (parsedArgs.err) return parsedArgs.val; return (await module.mod.delegate(context, parsedArgs))?.val; } - + /** * This function chains `Files.buildData` * @param {{name: string, mod: Module, absPath: string}} modArr module information @@ -134,37 +128,40 @@ export class Handler { private async registerModules( modArr: { - name: string, - mod: Module, - absPath: string - }[] + name: string; + mod: Module; + absPath: string; + }[], ) { for await (const { name, mod, absPath } of modArr) { const cmdName = Files.fmtFileName(name); switch (mod.type) { - case 1: Files.Commands.set(cmdName, { mod, options: [] }); break; + case 1: + Files.Commands.set(cmdName, { mod, options: [] }); + break; case 2: - case (1 | 2): { - const options = ((await import(absPath)).options as ApplicationCommandOptionData[]); - Files.Commands.set(cmdName, { mod, options: options ?? [] }); - switch (mod.visibility) { - case 'private': { - // Reloading guild slash commands - await this.reloadSlash(cmdName, mod.desc, options); - } - case 'public': { - // Creating global commands - // TODO : warn user they will be creating a public command - await this.client.application!.commands - .create({ + case 1 | 2: + { + const options = (await import(absPath)).options as ApplicationCommandOptionData[]; + Files.Commands.set(cmdName, { mod, options: options ?? [] }); + switch (mod.visibility) { + case 'private': { + // Reloading guild slash commands + await this.reloadSlash(cmdName, mod.desc, options); + } + case 'public': { + // Creating global commands + await this.client.application!.commands.create({ name: cmdName, description: mod.desc, - options + options, }); + } } } - } break; - default: throw Error(`SernHandlerError: ${name} with ${mod.visibility} is not a valid module type.`); + break; + default: + throw Error(`SernHandlerError: ${name} with ${mod.visibility} is not a valid module type.`); } if (mod.alias.length > 0) { @@ -174,34 +171,26 @@ export class Handler { } } } - /** - * - * @param {string} name name of possible command - * @returns {Files.CommandVal | undefined} - */ - private findCommand(name : string) : Files.CommandVal | undefined { - return Files.Commands.get(name) ?? Files.Alias.get(name); - } /** - * + * * @param {string} cmdName name of command - * @param {string} description description of command - * @param {ApplicationCommandOptionData[]} options any options for the slash command + * @param {string} description description of command + * @param {ApplicationCommandOptionData[]} options any options for the slash command */ - + private async reloadSlash( cmdName: string, description: string, - options: ApplicationCommandOptionData[] - ) : Promise { + options: ApplicationCommandOptionData[], + ): Promise { for (const { id } of this.privateServers) { - const guild = (await this.client.guilds.fetch(id)); + const guild = await this.client.guilds.fetch(id); guild.commands.create({ name: cmdName, description, - options + options, }); } } @@ -210,16 +199,16 @@ export class Handler { * @readonly * @returns {string} The prefix used for legacy commands */ - + get prefix(): string { return this.wrapper.prefix; } - + /** - * @readonly - * @returns {string} Directory of the commands folder - */ - + * @readonly + * @returns {string} Directory of the commands folder + */ + get commandDir(): string { return this.wrapper.commands; } @@ -228,23 +217,23 @@ export class Handler { * @readonly * @returns {Client} the discord.js client (DiscordJS#Client)); */ - + get client(): Client { return this.wrapper.client; } - + /** * @readonly * @returns {{test: boolean, id: string}[]} Private server ID for testing or personal use */ - - get privateServers(): { test: boolean, id: string }[] { + + get privateServers(): { test: boolean; id: string }[] { return this.wrapper.privateServers; } } /** - * An object to be passed into Sern.Handler constructor. + * An object to be passed into Sern.Handler constructor. * @typedef {object} Wrapper * @property {readonly Client} client * @property {readonly string} prefix @@ -253,15 +242,15 @@ export class Handler { * @property {readonly {test: boolean, id: string}[]} privateServers */ export interface Wrapper { - readonly client: Client, - readonly prefix: string, - readonly commands: string - init?: (handler: Handler) => void, - readonly privateServers: { test: boolean, id: string }[], + readonly client: Client; + readonly prefix: string; + readonly commands: string; + init?: (handler: Handler) => void; + readonly privateServers: { test: boolean; id: string }[]; } /** - * An object that gets imported and acts as a command. + * An object that gets imported and acts as a command. * @typedef {object} Module * @property {string} desc * @property {Visibility} visibility @@ -271,13 +260,13 @@ export interface Wrapper { */ export interface Module { - alias: string[], - desc: string, - visibility: Visibility, - type: CommandType, - test : boolean, - delegate: (eventParams: Context, args: Ok) => Awaitable | void> - parse?: (ctx: Context, args: Arg) => Utils.ArgType + alias: string[]; + desc: string; + visibility: Visibility; + type: CommandType; + test: boolean; + delegate: (eventParams: Context, args: Ok) => Awaitable | void>; + parse?: (ctx: Context, args: Arg) => Utils.ArgType; } /** @@ -285,6 +274,6 @@ export interface Module { */ export enum CommandType { - TEXT = 1, - SLASH = 2, + TEXT = 1, + SLASH = 2, } diff --git a/src/handler/Utilities/messageHelpers.ts b/src/handler/utilities/messageHelpers.ts similarity index 54% rename from src/handler/Utilities/messageHelpers.ts rename to src/handler/utilities/messageHelpers.ts index cfad6dc..85d43cf 100644 --- a/src/handler/Utilities/messageHelpers.ts +++ b/src/handler/utilities/messageHelpers.ts @@ -1,13 +1,13 @@ import type { Message } from 'discord.js'; export function isBot(message: Message) { - return message.author.bot; + return message.author.bot; } export function hasPrefix(message: Message, prefix: string) { - return (message.content.slice(0, prefix.length).toLowerCase().trim()) === prefix; + return message.content.slice(0, prefix.length).toLowerCase().trim() === prefix; } export function fmt(msg: Message, prefix: string): string[] { - return msg.content.slice(prefix.length).trim().split(/\s+/g); + return msg.content.slice(prefix.length).trim().split(/\s+/g); } diff --git a/src/handler/Utilities/Preprocessors/args.ts b/src/handler/utilities/preprocessors/args.ts similarity index 74% rename from src/handler/Utilities/Preprocessors/args.ts rename to src/handler/utilities/preprocessors/args.ts index 2cd02ab..722baba 100644 --- a/src/handler/Utilities/Preprocessors/args.ts +++ b/src/handler/utilities/preprocessors/args.ts @@ -8,8 +8,8 @@ import type { possibleOutput } from '../../../types/handler'; export type ArgType = Result; /** - * - * @param {string} arg - command arguments + * + * @param {string} arg - command arguments * @param {possibleOutput} onFailure - if `Number.parseInt` returns NaN * @returns {ArgType} Attempts to use `Number.parseInt()` on `arg` */ @@ -21,9 +21,9 @@ export function parseInt(arg: string, onFailure: possibleOutput): ArgType } attemps to parse `args` as a boolean */ @@ -31,12 +31,12 @@ export function parseBool( arg: string, onFailure: possibleOutput = `Cannot parse '${arg}' as a boolean`, regexes: { - yesRegex: RegExp, - noRegex: RegExp + yesRegex: RegExp; + noRegex: RegExp; } = { yesRegex: /^(?:y(?:es)?|👍)$/i, - noRegex: /^(?:n(?:o)?|👎)$/i - } + noRegex: /^(?:n(?:o)?|👎)$/i, + }, ): ArgType { if (arg.match(regexes.yesRegex)) return Ok(true); if (arg.match(regexes.noRegex)) return Ok(false); @@ -45,8 +45,8 @@ export function parseBool( } /** - * - * @param {string} arg - command arguments + * + * @param {string} arg - command arguments * @param {string} sep - default separator = ' ' * @returns {Ok} */ @@ -56,22 +56,22 @@ export function toArr(arg: string, sep = ' '): ArgType { } /** - * - * @param {string} arg - command arguments - * @param {possibleOutput} onFailure - delegates `Utils.parseInt` + * + * @param {string} arg - command arguments + * @param {possibleOutput} onFailure - delegates `Utils.parseInt` * @returns {ArgType} */ export function toPositiveInt(arg: string, onFailure: possibleOutput): ArgType { - return parseInt(arg, onFailure).andThen(num => Ok(num > 0 ? num : -num)); + return parseInt(arg, onFailure).andThen((num) => Ok(num > 0 ? num : -num)); } /** - * - * @param {string} arg - command arguments - * @param {possibleOutput} onFailure - delegates `parseInt` + * + * @param {string} arg - command arguments + * @param {possibleOutput} onFailure - delegates `parseInt` * @returns {ArgType} */ export function toNegativeInt(arg: string, onFailure: possibleOutput): ArgType { - return parseInt(arg, onFailure).andThen(num => Ok(num > 0 ? -num : num)); + return parseInt(arg, onFailure).andThen((num) => Ok(num > 0 ? -num : num)); } diff --git a/src/handler/utilities/readFile.ts b/src/handler/utilities/readFile.ts new file mode 100644 index 0000000..981e64b --- /dev/null +++ b/src/handler/utilities/readFile.ts @@ -0,0 +1,56 @@ +import type { ApplicationCommandOptionData } from 'discord.js'; + +import { readdirSync, statSync } from 'fs'; + +import { basename, join } from 'path'; + +import type * as Sern from '../sern'; + +export type CommandVal = { + mod: Sern.Module; + options: ApplicationCommandOptionData[]; +}; + +export const Commands = new Map(); +export const Alias = new Map(); + +// Courtesy of Townsy#0001 on Discord +async function readPath(dir: string, arrayOfFiles: string[] = []): Promise { + try { + const files = readdirSync(dir); + for (const file of files) { + if (statSync(dir + '/' + file).isDirectory()) await readPath(dir + '/' + file, arrayOfFiles); + else arrayOfFiles.push(join(dir, '/', file)); + } + } catch (err) { + throw err; + } + + return arrayOfFiles; +} + +export const fmtFileName = (n: string) => n.substring(0, n.length - 3); + +/** + * @param {Sern.Handler} handler an instance of Sern.Handler + * @returns {Promise<{ name: string; mod: Sern.Module; absPath: string; }[]>} data from command files + */ + +export async function buildData(handler: Sern.Handler): Promise< + { + name: string; + mod: Sern.Module; + absPath: string; + }[] +> { + const commandDir = handler.commandDir; + return Promise.all( + (await getCommands(commandDir)).map(async (absPath) => { + return { name: basename(absPath), mod: (await import(absPath)).default as Sern.Module, absPath }; + }), + ); +} + +export async function getCommands(dir: string): Promise { + return readPath(join(process.cwd(), dir)); +} diff --git a/src/types/handler.ts b/src/types/handler.ts index d1797ad..0a79e85 100644 --- a/src/types/handler.ts +++ b/src/types/handler.ts @@ -1,34 +1,34 @@ import type { Option } from 'ts-results'; import type { - CommandInteraction, - CommandInteractionOptionResolver, - Message, - MessagePayload, - MessageOptions + CommandInteraction, + CommandInteractionOptionResolver, + Message, + MessagePayload, + MessageOptions, } from 'discord.js'; import type * as Sern from '../handler/sern'; export type Visibility = 'private' | 'public'; // Anything that can be sent in a `#send` or `#reply` -export type possibleOutput = T | MessagePayload & MessageOptions; +export type possibleOutput = T | (MessagePayload & MessageOptions); export type Nullable = T | null; export type delegate = Sern.Module['delegate']; // Thanks @cursorsdottsx export type ParseType = { - [K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never; -} [keyof T]; + [K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never; +}[keyof T]; // A Sern.Module['delegate'] will carry a Context Parameter export type Context = { - message: Option, - interaction: Option -} + message: Option; + interaction: Option; +}; + +export type Arg = ParseType<{ text: string; slash: SlashOptions }>; -export type Arg = ParseType<{ text: string, slash: SlashOptions }>; - // TypeAlias for interaction.options export type SlashOptions = Omit;