Format & improve src files

This commit is contained in:
xxDeveloper
2022-02-12 22:41:55 +03:00
parent cdda518ed9
commit 3b15b27803
6 changed files with 151 additions and 92 deletions

View File

@@ -1,15 +1,29 @@
import type { Arg, Context, Visibility } from "../types/handler";
import * as Files from "./utils/readFile"
import type { ApplicationCommandOptionData, Awaitable, Client, CommandInteraction, Message } from "discord.js";
import type { possibleOutput } from "../types/handler"
import { Ok, Result, None, Some } from "ts-results";
import type * as Utils from "./utils/preprocessors/args";
import { isBot, hasPrefix, fmt } from "./utils/messageHelpers";
import * as Files from './utils/readFile'
import type * as Utils from './utils/preprocessors/args';
import type {
Arg,
Context,
Visibility,
possibleOutput
} from '../types/handler';
import type {
ApplicationCommandOptionData,
Awaitable,
Client,
CommandInteraction,
Message
} from 'discord.js';
import { Ok, Result, None, Some } from 'ts-results';
import { isBot, hasPrefix, fmt } from './utils/messageHelpers';
/**
* @class
*/
export class Handler {
private wrapper: Wrapper;
/**
@@ -21,64 +35,72 @@ export class Handler {
) {
this.wrapper = wrapper;
this.client
/**
* On ready, builds command data and registers them all
* from command directory
**/
.on("ready", async () => {
.on('ready', async () => {
Files.buildData(this)
.then( data => this.registerModules(data))
.then(data => this.registerModules(data))
if (wrapper.init !== undefined) wrapper.init(this);
})
.on("messageCreate", async message => {
.on('messageCreate', async (message: {
channel: {
type: string; send: (arg0: string) => void;
};
}) => {
if (isBot(message) || !hasPrefix(message, this.prefix)) return;
if (message.channel.type === "DM") return; //todo, handle dms
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")
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 => {
.on('interactionCreate', async (interaction: {
isCommand: () => boolean; commandName: string; reply: (arg0: any) => any;
}) => {
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 a Discord.js command interaction
* @returns {possibleOutput | undefined} takes return value and replies it, if possible input
*/
private async interactionResult(
module: Files.CommandVal | undefined,
interaction: CommandInteraction): Promise<possibleOutput | undefined> {
if (module === undefined) return "Unknown slash command!";
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";
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("");
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
@@ -86,25 +108,33 @@ export class Handler {
* @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<possibleOutput | undefined> {
if (module?.mod === undefined) return "Unknown legacy command";
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") {
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.testOnly) {
return "This private command is a testing command";
const msg = `This command is only available on test servers.`; // TODO: Customizable private message
return msg;
}
}
const context = { message: Some(message), interaction: None }
const parsedArgs = module.mod.parse?.(context, ["text", args]) ?? Ok("");
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<unknown>, absPath: string}} modArr module information
*/
private async registerModules(
modArr: {
name: string,
@@ -121,22 +151,22 @@ export class Handler {
const options = ((await import(absPath)).options as ApplicationCommandOptionData[])
Files.Commands.set(cmdName, { mod, options: options ?? [], testOnly });
switch (mod.visibility) {
case "private": {
// loading guild slash commands only
case 'private': {
// Loading guild slash commands only
await this.reloadSlash(cmdName, mod.desc, options)
}
case "public": {
// creating global commands!
await this.client.application!.commands
case 'public': {
// Creating global commands
await this.client.application!.commands
.create({
name: cmdName,
description: mod.desc,
options
})
});
}
}
} break;
default: throw Error(`${name} with ${mod.visibility} is not a valid module type.`);
default: throw Error(`SernHandlerError: ${name} with ${mod.visibility} is not a valid module type.`);
}
if (mod.alias.length > 0) {
@@ -146,12 +176,14 @@ export class Handler {
}
}
}
/**
*
* @param {string} cmdName name of command
* @param {string} description description of command
* @param {ApplicationCommandOptionData[]} options any options for the slash command
*/
private async reloadSlash(
cmdName: string,
description: string,
@@ -167,36 +199,42 @@ export class Handler {
})
}
}
/**
* @readonly
* @returns {string} prefix used for legacy commands
* @returns {string} The prefix used for legacy commands
*/
get prefix(): string {
return this.wrapper.prefix;
}
/**
* @readonly
* @returns {string} directory of your commands folder
* @returns {string} Directory of the commands folder
*/
get commandDir(): string {
return this.wrapper.commands;
}
/**
* @readonly
* @returns {Client<boolean>} discord.js client
* @returns {Client<boolean>} the DiscordJS.Client();
*/
get client(): Client<boolean> {
return this.wrapper.client
}
/**
* @readonly
* @returns {{test: boolean, id: string}[]} private server id for testing or personal use
* @returns {{test: boolean, id: string}[]} Private server ID for testing or personal use
*/
get privateServers(): { test: boolean, id: string }[] {
return this.wrapper.privateServers;
}
}
/**
@@ -215,6 +253,7 @@ export interface Wrapper {
init?: (handler: Handler) => void,
readonly privateServers: { test: boolean, id: string }[],
}
/**
* An object to be passed into Sern.Handler constructor.
* @typedef {object} Module<T=string>
@@ -224,6 +263,7 @@ export interface Wrapper {
* @property {(eventParams : Context, args : Ok<T=string>) => Awaitable<Result<possibleOutput, string> | void>)} delegate
* @prop {(ctx: Context, args: Arg) => Utils.ArgType<T>} parse
*/
export interface Module<T = string> {
alias: string[],
desc: string,
@@ -232,9 +272,11 @@ export interface Module<T = string> {
delegate: (eventParams: Context, args: Ok<T>) => Awaitable<Result<possibleOutput, string> | void>
parse?: (ctx: Context, args: Arg) => Utils.ArgType<T>
}
/**
* @enum { number };
*/
export enum CommandType {
TEXT = 1,
SLASH = 2,

View File

@@ -1,14 +1,13 @@
import type { Message } from "discord.js";
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);
}

View File

@@ -1,22 +1,24 @@
import { Err, Ok, Result } from "ts-results";
import type { possibleOutput } from "../../../types/handler";
import { Err, Ok, Result } from 'ts-results';
import type { possibleOutput } from '../../../types/handler';
/**
* Wrapper type taking `Ok(T)` or `Err(possibleOutput)` e.g `Result<T, possibleOutput`
*/
export type ArgType<T> = Result<T, possibleOutput>
export type ArgType<T> = Result<T, possibleOutput>;
/**
*
* @param {string} arg - command arguments
* @param {possibleOutput} onFailure - if `Number.parseInt` returns NaN
* @returns {ArgType<number>} Attempts to use `Number.parseInt()` on `arg`
*/
export function parseInt(arg: string, onFailure: possibleOutput): ArgType<number> {
const val = Number.parseInt(arg);
return val === NaN ? Err(onFailure) : Ok(val);
}
/**
*
* @param {string} arg - command arguments
@@ -24,22 +26,25 @@ export function parseInt(arg: string, onFailure: possibleOutput): ArgType<number
* @param { {yesRegex: RegExp, noRegex: RegExp} } regexes - default regexes: yes : `/^(?:y(?:es)?|👍)$/i`, no : /^(?:n(?:o)?|👎)$/i
* @returns { ArgType<boolean> } attemps to parse `args` as a boolean
*/
export function parseBool(
arg: string,
onFailure: possibleOutput = `Cannot parse "${arg}" as a boolean`,
onFailure: possibleOutput = `Cannot parse '${arg}' as a boolean`,
regexes: { yesRegex: RegExp, noRegex: RegExp } = { yesRegex: /^(?:y(?:es)?|👍)$/i, noRegex: /^(?:n(?:o)?|👎)$/i }
): ArgType<boolean> {
if (arg.match(regexes.yesRegex)) return Ok(true);
if (arg.match(regexes.noRegex)) return Ok(false);
return Err(onFailure);
}
/**
*
* @param {string} arg - command arguments
* @param {string} sep - default separator = " "
* @param {string} sep - default separator = ' '
* @returns {Ok<string[]>}
*/
export function toArr(arg: string, sep = " "): ArgType<string[]> {
export function toArr(arg: string, sep = ' '): ArgType<string[]> {
return Ok(arg.split(sep));
}
@@ -49,8 +54,9 @@ export function toArr(arg: string, sep = " "): ArgType<string[]> {
* @param {possibleOutput} onFailure - delegates `Utils.parseInt`
* @returns {ArgType<number>}
*/
export function toPositiveInt(arg: string, onFailure: possibleOutput): ArgType<number> {
return parseInt(arg, onFailure).andThen(num => Ok(num > 0 ? num : -num))
return parseInt(arg, onFailure).andThen(num => Ok(num > 0 ? num : -num));
}
/**
@@ -62,8 +68,3 @@ export function toPositiveInt(arg: string, onFailure: possibleOutput): ArgType<n
export function toNegativeInt(arg: string, onFailure: possibleOutput): ArgType<number> {
return parseInt(arg, onFailure).andThen(num => Ok(num > 0 ? -num : num))
}

View File

@@ -1,40 +1,48 @@
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<unknown>, options: ApplicationCommandOptionData[], testOnly: boolean }
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<unknown>,
options: ApplicationCommandOptionData[],
testOnly: boolean
}
export const Commands = new Map<string, CommandVal>();
export const Alias = new Map<string, CommandVal>();
//courtesy of Townsy#0001 on Discord
// Courtesy of Townsy#0001 on Discord
async function readPath(dir: string, arrayOfFiles: string[] = []): Promise<string[]> {
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;
}
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;
return arrayOfFiles;
}
export const fmtFileName = (n: string) => {
const endsW = n.toLowerCase().endsWith("-test.js") || n.toLowerCase().endsWith("-test.ts");
const endsW = n.toLowerCase().endsWith('-test.js') || n.toLowerCase().endsWith('-test.ts');
return endsW
? { cmdName: n.substring(0, n.length - 8), testOnly: true }
: { cmdName: n.substring(0, n.length - 3), testOnly: false };
};
/**
*
* @param {Sern.Handler} handler an instance of Sern.Handler
* @returns {Promise<{ name: string; mod: Sern.Module<unknown>; absPath: string; }[]>} data from command files
*/
export async function buildData(handler: Sern.Handler)
: Promise<{
name: string;
@@ -50,4 +58,4 @@ export async function buildData(handler: Sern.Handler)
export async function getCommands(dir: string): Promise<string[]> {
return readPath(join(process.cwd(), dir))
}
}

View File

@@ -1,5 +1,6 @@
import * as Sern from "./handler/sern";
import * as Utils from "./handler/utils/preprocessors/args"
import * as Types from "./types/handler"
import * as Sern from './handler/sern';
import * as Utils from './handler/utils/preprocessors/args';
import * as Types from './types/handler';
module.exports = { Sern, Utils, Types };
export { Sern, Utils, Types };

View File

@@ -1,25 +1,33 @@
import type { Option } from 'ts-results'
import type { CommandInteraction, CommandInteractionOptionResolver, Message, MessagePayload, MessageOptions } from 'discord.js';
import type * as Sern from "../handler/sern"
import type { Option } from 'ts-results';
export type Visibility = "private" | "public"
import type {
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 `<TextChannel>#send` or `<CommandInteraction>#reply`
export type possibleOutput<T = string> = T | MessagePayload & MessageOptions;
export type Nullable<T> = T | null;
export type delegate = Sern.Module<unknown>["delegate"]
export type delegate = Sern.Module<unknown>['delegate'];
// Thanks @cursorsdottsx
export type ParseType<T> = {
[K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never;
}[keyof T];
} [keyof T];
// A Sern.Module["delegate"] will carry a Context Parameter
// A Sern.Module['delegate'] will carry a Context Parameter
export type Context = {
message: Option<Message>,
interaction: Option<CommandInteraction>
message: Option<Message>,
interaction: Option<CommandInteraction>
}
export type Arg = ParseType<{text : string, slash : SlashOptions}>
// TypeAlias for interaction.options
export type SlashOptions = Omit<CommandInteractionOptionResolver, "getMessage" | "getFocused">;
export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;