diff --git a/.eslintrc b/.eslintrc index 6598da8..c142c1f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,9 +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 }] + "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals" : true }], + "semi": ["error", "always"] } } \ No newline at end of file diff --git a/src/handler/logger.ts b/src/handler/logger.ts index f4cc0bc..8fd122d 100644 --- a/src/handler/logger.ts +++ b/src/handler/logger.ts @@ -3,40 +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 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 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), {}), - ); - } + 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), {}), + ); + } } diff --git a/src/handler/sern.ts b/src/handler/sern.ts index 1611a57..e8a8feb 100644 --- a/src/handler/sern.ts +++ b/src/handler/sern.ts @@ -14,222 +14,222 @@ import Logger from './logger'; */ export class Handler { - private wrapper: Wrapper; + private wrapper: Wrapper; - /** - * @constructor - * @param {Wrapper} wrapper The data that is required to run sern handler - */ + /** + * @constructor + * @param {Wrapper} wrapper The data that is required to run sern handler + */ - constructor(wrapper: Wrapper) { - this.wrapper = wrapper; - this.client + constructor(wrapper: Wrapper) { + this.wrapper = wrapper; + this.client - /** - * On ready, builds command data and registers them all - * from command directory - **/ + /** + * On ready, builds command data and registers them all + * 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('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) => { - if (isBot(message) || !hasPrefix(message, this.prefix)) return; - if (message.channel.type === 'DM') return; // TODO: Handle dms + .on('messageCreate', async (message: Message) => { + if (isBot(message) || !hasPrefix(message, this.prefix)) return; + if (message.channel.type === 'DM') return; // TODO: Handle dms - const tryFmt = fmt(message, this.prefix); - 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(' ')); - if (cmdResult === undefined) return; + const tryFmt = fmt(message, this.prefix); + 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(' ')); + if (cmdResult === undefined) return; - message.channel.send(cmdResult); - }) + message.channel.send(cmdResult); + }) - .on('interactionCreate', async (interaction) => { - if (!interaction.isCommand()) return; - const module = Files.Commands.get(interaction.commandName); - const res = await this.interactionResult(module, interaction); - if (res === undefined) return; - await interaction.reply(res); - }); - } - - /** - * - * @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 { - if (module === undefined) return 'Unknown slash 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 - * @returns Takes return value and replies it, if possible input - */ - - 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 !== module.mod.test) { - const msg = `This command is only available on test servers.`; // TODO: Customizable private message - - return msg; - } + .on('interactionCreate', async (interaction) => { + if (!interaction.isCommand()) return; + const module = Files.Commands.get(interaction.commandName); + const res = await this.interactionResult(module, interaction); + if (res === undefined) return; + await interaction.reply(res); + }); } - const context = { - message: Some(message), - 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 - */ + /** + * + * @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 registerModules( - modArr: { - 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 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 - await this.client.application!.commands.create({ - name: cmdName, - description: mod.desc, - options, - }); - } + private async interactionResult( + module: Files.CommandVal | undefined, + interaction: CommandInteraction, + ): Promise { + if (module === undefined) return 'Unknown slash 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 + * @returns Takes return value and replies it, if possible input + */ + + 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 !== module.mod.test) { + const msg = `This command is only available on test servers.`; // TODO: Customizable private message + + return msg; } - } - break; - default: - throw Error(`SernHandlerError: ${name} with ${mod.visibility} is not a valid module type.`); - } - - if (mod.alias.length > 0) { - for (const alias of mod.alias) { - Files.Alias.set(alias, { mod, options: [] }); } - } + const context = { + message: Some(message), + 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; } - } - /** - * - * @param {string} cmdName name of command - * @param {string} description description of command - * @param {ApplicationCommandOptionData[]} options any options for the slash command - */ + /** + * This function chains `Files.buildData` + * @param {{name: string, mod: Module, absPath: string}} modArr module information + */ - private async reloadSlash( - cmdName: string, - description: string, - options: ApplicationCommandOptionData[], - ): Promise { - for (const { id } of this.privateServers) { - const guild = await this.client.guilds.fetch(id); + private async registerModules( + modArr: { + 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 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 + await this.client.application!.commands.create({ + name: cmdName, + description: mod.desc, + options, + }); + } + } + } + break; + default: + throw Error(`SernHandlerError: ${name} with ${mod.visibility} is not a valid module type.`); + } - guild.commands.create({ - name: cmdName, - description, - options, - }); + if (mod.alias.length > 0) { + for (const alias of mod.alias) { + Files.Alias.set(alias, { mod, options: [] }); + } + } + } } - } - /** - * @readonly - * @returns {string} The prefix used for legacy commands - */ + /** + * + * @param {string} cmdName name of command + * @param {string} description description of command + * @param {ApplicationCommandOptionData[]} options any options for the slash command + */ - get prefix(): string { - return this.wrapper.prefix; - } + private async reloadSlash( + cmdName: string, + description: string, + options: ApplicationCommandOptionData[], + ): Promise { + for (const { id } of this.privateServers) { + const guild = await this.client.guilds.fetch(id); - /** - * @readonly - * @returns {string} Directory of the commands folder - */ + guild.commands.create({ + name: cmdName, + description, + options, + }); + } + } - get commandDir(): string { - return this.wrapper.commands; - } + /** + * @readonly + * @returns {string} The prefix used for legacy commands + */ - /** - * @readonly - * @returns {Client} the discord.js client (DiscordJS#Client)); - */ + get prefix(): string { + return this.wrapper.prefix; + } - get client(): Client { - return this.wrapper.client; - } + /** + * @readonly + * @returns {string} Directory of the commands folder + */ - /** - * @readonly - * @returns {{test: boolean, id: string}[]} Private server ID for testing or personal use - */ + get commandDir(): string { + return this.wrapper.commands; + } - get privateServers(): { test: boolean; id: string }[] { - return this.wrapper.privateServers; - } + /** + * @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 }[] { + return this.wrapper.privateServers; + } } /** @@ -242,11 +242,11 @@ 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 }[]; } /** @@ -260,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; } /** @@ -274,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 index 6050734..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 index ad1dfdd..722baba 100644 --- a/src/handler/utilities/preprocessors/args.ts +++ b/src/handler/utilities/preprocessors/args.ts @@ -15,9 +15,9 @@ export type ArgType = Result; */ export function parseInt(arg: string, onFailure: possibleOutput): ArgType { - const val = Number.parseInt(arg); + const val = Number.parseInt(arg); - return val === NaN ? Err(onFailure) : Ok(val); + return val === NaN ? Err(onFailure) : Ok(val); } /** @@ -28,20 +28,20 @@ export function parseInt(arg: string, onFailure: possibleOutput): ArgType { - if (arg.match(regexes.yesRegex)) return Ok(true); - if (arg.match(regexes.noRegex)) return Ok(false); + if (arg.match(regexes.yesRegex)) return Ok(true); + if (arg.match(regexes.noRegex)) return Ok(false); - return Err(onFailure); + return Err(onFailure); } /** @@ -52,7 +52,7 @@ export function parseBool( */ export function toArr(arg: string, sep = ' '): ArgType { - return Ok(arg.split(sep)); + return Ok(arg.split(sep)); } /** @@ -63,7 +63,7 @@ export function toArr(arg: string, sep = ' '): 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)); } /** @@ -73,5 +73,5 @@ export function toPositiveInt(arg: string, onFailure: possibleOutput): ArgType} */ export function toNegativeInt(arg: string, onFailure: possibleOutput): ArgType { - return parseInt(arg, onFailure).andThen((num) => Ok(num > 0 ? -num : num)); -} \ No newline at end of file + 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 index f8d694b..981e64b 100644 --- a/src/handler/utilities/readFile.ts +++ b/src/handler/utilities/readFile.ts @@ -7,8 +7,8 @@ import { basename, join } from 'path'; import type * as Sern from '../sern'; export type CommandVal = { - mod: Sern.Module; - options: ApplicationCommandOptionData[]; + mod: Sern.Module; + options: ApplicationCommandOptionData[]; }; export const Commands = new Map(); @@ -16,17 +16,17 @@ 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)); + 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; } - } catch (err) { - throw err; - } - return arrayOfFiles; + return arrayOfFiles; } export const fmtFileName = (n: string) => n.substring(0, n.length - 3); @@ -37,20 +37,20 @@ export const fmtFileName = (n: string) => n.substring(0, n.length - 3); */ export async function buildData(handler: Sern.Handler): Promise< - { - name: string; - mod: Sern.Module; - absPath: string; - }[] + { + 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 }; - }), - ); + 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)); + return readPath(join(process.cwd(), dir)); } diff --git a/src/types/handler.ts b/src/types/handler.ts index 340620f..0a79e85 100644 --- a/src/types/handler.ts +++ b/src/types/handler.ts @@ -1,11 +1,11 @@ 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'; @@ -18,14 +18,14 @@ 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; + [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 }>;