instal eslint and prettierconfic, applied to files

This commit is contained in:
jacoobes
2022-02-07 00:48:04 -06:00
parent 43d169062c
commit 38336590f1
10 changed files with 2541 additions and 271 deletions

6
.eslintrc Normal file
View File

@@ -0,0 +1,6 @@
{
"parser": "@typescript-eslint/parser",
"extends": ["plugin:@typescript-eslint/recommended"],
"parserOptions": { "ecmaVersion": "esnext", "sourceType": "" },
"rules": {}
}

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2
}

2267
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,18 +4,23 @@
"description": "",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"compile": "tsc",
"watch" : "tsc -w"
"watch": "tsc -w",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"discord.js": "^13.6.0",
"eslint": "^8.8.0",
"prettier": "^2.5.1",
"ts-results": "^3.3.0"
},
"devDependencies": {
"typescript": "^4.5.5"
}
}
}

View File

@@ -1,159 +1,159 @@
import type { Arg, Context, ParseType, Visibility } from "../types/handler/handler";
import { Files } from "./utils/readFile"
import type { ApplicationCommandOptionData, Awaitable, Client, CommandInteraction, CommandInteractionOptionResolver, Message} from "discord.js";
import * as Files from "./utils/readFile"
import type { ApplicationCommandOptionData, Awaitable, Client, CommandInteraction, Message } from "discord.js";
import type { possibleOutput } from "../types/handler/handler"
import { Ok, Result, None, Some } from "ts-results";
import type { Utils } from "./utils/preprocessors/args";
import type * as Utils from "./utils/preprocessors/args";
import { CtxHandler } from "./utils/ctxHandler";
export namespace Sern {
/**
* @class
*/
export class Handler {
private wrapper: Wrapper;
/**
* @class
* @constructor
* @param {Wrapper} wrapper Some data that is required to run sern handler
*/
export class Handler {
private wrapper: Sern.Wrapper;
/**
* @constructor
* @param {Sern.Wrapper} wrapper Some data that is required to run sern handler
*/
constructor(
wrapper : Sern.Wrapper,
) {
this.wrapper = wrapper;
this.wrapper.client
.on("ready", async () => {
if (this.wrapper.init !== undefined) this.wrapper.init(this);
await Files.registerModules(this);
})
constructor(
wrapper: Wrapper,
) {
this.wrapper = wrapper;
this.wrapper.client
.on("ready", async () => {
if (this.wrapper.init !== undefined) this.wrapper.init(this);
await Files.registerModules(this);
})
.on("messageCreate", async message => {
if (CtxHandler.isBot(message) || !CtxHandler.hasPrefix(message,this.prefix)) return;
let tryFmt = CtxHandler.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;
}
let cmdResult = (await this.commandResult(module?.mod, message, tryFmt.join(" ")))
if (cmdResult === undefined) return;
message.channel.send(cmdResult)
})
.on("interactionCreate", async interaction => {
if(!interaction.isCommand()) return;
const module = Files.Commands.get(interaction.commandName);
let res = await this.interactionResult(module, interaction);
if (res === undefined) return;
await interaction.reply(res);
})
}
private async interactionResult(
module: { mod: Sern.Module<unknown>, options: ApplicationCommandOptionData[]} | undefined,
interaction: CommandInteraction) : Promise<possibleOutput | undefined> {
if (module === undefined) return "Unknown slash command!";
const name = Array.from(Files.Commands.keys()).find(it => it === interaction.commandName)!;
(await this.client.guilds.fetch(this.privateServerId))
.commands
.create({
name,
description : module.mod.desc,
options: module.options
});
if(module.mod.type < CommandType.SLASH) return "This is not a slash command";
const context = {text: None, slash: Some(interaction)}
const parsedArgs = module.mod.parse?.(context, ["slash", interaction.options ] ) ?? Ok("");
if(parsedArgs.err) return parsedArgs.val;
const fn = await module.mod.delegate(context, parsedArgs);
return fn?.val;
}
private async commandResult(module: Sern.Module<unknown> | undefined, message: Message, args : string) : Promise<possibleOutput| undefined> {
if (module === undefined) return "Unknown legacy command";
if (module.visibility === "private" && message.guildId !== this.privateServerId) {
return "This command is not availible in this guild!"
.on("messageCreate", async message => {
if (CtxHandler.isBot(message) || !CtxHandler.hasPrefix(message, this.prefix)) return;
const tryFmt = CtxHandler.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;
}
if (module.type === CommandType.SLASH) return `This may be a slash command and not a legacy command`
const context = {text: Some(message), slash: None}
const parsedArgs = module.parse?.(context, ["text", args] ) ?? Ok("");
if(parsedArgs.err) return parsedArgs.val;
let fn = await module.delegate(context, parsedArgs)
return fn?.val
}
/**
* @readonly
* @returns {string} prefix used for legacy commands
*/
get prefix() : string {
return this.wrapper.prefix;
}
/**
* @readonly
* @returns {string} directory of your commands folder
*/
get commandDir() : string {
return this.wrapper.commands;
}
/**
* @readonly
* @returns {Client<boolean>} discord.js client
*/
get client() : Client<boolean> {
return this.wrapper.client
}
/**
* @readonly
* @returns {string} private server id for testing or personal use
*/
get privateServerId(): string {
return this.wrapper.privateServerId;
}
const cmdResult = (await this.commandResult(module?.mod, message, tryFmt.join(" ")))
if (cmdResult === undefined) return;
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);
})
}
private async interactionResult(
module: { mod: Module<unknown>, options: ApplicationCommandOptionData[] } | undefined,
interaction: CommandInteraction): Promise<possibleOutput | undefined> {
if (module === undefined) return "Unknown slash command!";
const name = Array.from(Files.Commands.keys()).find(it => it === interaction.commandName)!;
(await this.client.guilds.fetch(this.privateServerId))
.commands
.create({
name,
description: module.mod.desc,
options: module.options
});
if (module.mod.type < CommandType.SLASH) return "This is not a slash command";
const context = { text: None, slash: Some(interaction) }
const parsedArgs = module.mod.parse?.(context, ["slash", interaction.options]) ?? Ok("");
if (parsedArgs.err) return parsedArgs.val;
const fn = await module.mod.delegate(context, parsedArgs);
return fn?.val;
}
private async commandResult(module: Module<unknown> | undefined, message: Message, args: string): Promise<possibleOutput | undefined> {
if (module === undefined) return "Unknown legacy command";
if (module.visibility === "private" && message.guildId !== this.privateServerId) {
return "This command is not availible in this guild!"
}
if (module.type === CommandType.SLASH) return `This may be a slash command and not a legacy command`
const context = { text: Some(message), slash: None }
const parsedArgs = module.parse?.(context, ["text", args]) ?? Ok("");
if (parsedArgs.err) return parsedArgs.val;
const fn = await module.delegate(context, parsedArgs)
return fn?.val
}
/**
* An object to be passed into Sern.Handler constructor.
* ```ts
* new Sern.Handler({
* client, // Discord.js client instance
* prefix : "!", // an example prefix
* commands: "", //commands directory
* init : () => console.log("Bot is ready") // function called on ready
* privateServerId : "" // a server id that can be used for private or test server
* })
* ```
* @readonly
* @returns {string} prefix used for legacy commands
*/
export interface Wrapper {
readonly client : Client,
readonly prefix: string,
readonly commands : string
init? : (handler : Sern.Handler) => void,
readonly privateServerId : string
get prefix(): string {
return this.wrapper.prefix;
}
/**
* @interface - Modules that are used in command files
*/
export interface Module<T = string> {
alias: string[],
desc : string,
visibility : Visibility,
type: CommandType,
delegate : ( eventParams : Context , args: Ok<T> ) => Awaitable<Result<possibleOutput, string > | void>
parse? : (ctx: Context, args: ParseType<Arg> ) => Utils.ArgType<T>
* @readonly
* @returns {string} directory of your commands folder
*/
get commandDir(): string {
return this.wrapper.commands;
}
/**
* @enum { number };
* @readonly
* @returns {Client<boolean>} discord.js client
*/
export enum CommandType {
TEXT = 1,
SLASH = 2,
get client(): Client<boolean> {
return this.wrapper.client
}
/**
* @readonly
* @returns {string} private server id for testing or personal use
*/
get privateServerId(): string {
return this.wrapper.privateServerId;
}
}
/**
* An object to be passed into Sern.Handler constructor.
* ```ts
* new Sern.Handler({
* client, // Discord.js client instance
* prefix : "!", // an example prefix
* commands: "", //commands directory
* init : () => console.log("Bot is ready") // function called on ready
* privateServerId : "" // a server id that can be used for private or test server
* })
* ```
*/
export interface Wrapper {
readonly client: Client,
readonly prefix: string,
readonly commands: string
init?: (handler: Handler) => void,
readonly privateServerId: string
}
/**
* @interface - Modules that are used in command files
*/
export interface Module<T = string> {
alias: string[],
desc: string,
visibility: Visibility,
type: CommandType,
delegate: (eventParams: Context, args: Ok<T>) => Awaitable<Result<possibleOutput, string> | void>
parse?: (ctx: Context, args: ParseType<Arg>) => Utils.ArgType<T>
}
/**
* @enum { number };
*/
export enum CommandType {
TEXT = 1,
SLASH = 2,
}

View File

@@ -8,9 +8,9 @@ export class CtxHandler {
static hasPrefix(message: Message, prefix: string) {
return (message.content.slice(0, prefix.length).toLowerCase().trim()) === prefix;
}
}
static fmt(msg: Message, prefix: string) : string[] {
static fmt(msg: Message, prefix: string): string[] {
return msg.content.slice(prefix.length).trim().split(/\s+/g)
}
}

View File

@@ -2,68 +2,68 @@ import { Err, Ok, Result } from "ts-results";
import type { possibleOutput } from "../../../types/handler/handler";
export namespace Utils {
/**
* Wrapper type taking `Ok(T)` or `Err(possibleOutput)` e.g `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
* @param {possibleOutput} onFailure - If cannot parse `arg` into boolean.
* @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`,
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 = " "
* @returns {Ok<string[]>}
*/
export function toArr(arg: string, sep: string = " ") : ArgType<string[]> {
return Ok(arg.split(sep));
}
/**
*
* @param {string} arg - command arguments
* @param {possibleOutput} onFailure - delegates `Utils.parseInt`
* @returns {ArgType<number>}
*/
export function toPositiveInt(arg: string, onFailure: possibleOutput) : ArgType<number> {
return Utils.parseInt(arg, onFailure).andThen( num => Ok(num > 0 ? num :-num))
}
/**
*
* @param {string} arg - command arguments
* @param {possibleOutput} onFailure - delegates `Utils.parseInt`
* @returns {ArgType<number>}
*/
export function toNegativeInt(arg: string, onFailure: possibleOutput) : ArgType<number> {
return Utils.parseInt(arg, onFailure).andThen( num => Ok(num > 0 ? -num : num))
}
/**
* Wrapper type taking `Ok(T)` or `Err(possibleOutput)` e.g `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
* @param {possibleOutput} onFailure - If cannot parse `arg` into boolean.
* @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`,
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 = " "
* @returns {Ok<string[]>}
*/
export function toArr(arg: string, sep = " "): ArgType<string[]> {
return Ok(arg.split(sep));
}
/**
*
* @param {string} arg - command arguments
* @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))
}
/**
*
* @param {string} arg - command arguments
* @param {possibleOutput} onFailure - delegates `parseInt`
* @returns {ArgType<number>}
*/
export function toNegativeInt(arg: string, onFailure: possibleOutput): ArgType<number> {
return parseInt(arg, onFailure).andThen(num => Ok(num > 0 ? -num : num))
}

View File

@@ -1,62 +1,57 @@
import type { ApplicationCommandOptionData, CommandInteractionOption } from "discord.js";
import type { ApplicationCommandOptionData } from "discord.js";
import { readdirSync, statSync } from "fs";
import { basename, join } from "path";
import type { Sern } from "../sern";
import type * as Sern from "../sern";
export namespace Files {
export const Commands = new Map<string, { mod: Sern.Module<unknown>, options: ApplicationCommandOptionData[] }>();
export const Alias = new Map<string, { mod: Sern.Module<unknown>, options: ApplicationCommandOptionData[] }>();
export const Commands = new Map<string, { mod: Sern.Module<unknown>, options: ApplicationCommandOptionData[] }>();
export const Alias = new Map<string, { mod: Sern.Module<unknown>, options: ApplicationCommandOptionData[] }>();
//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()) {
//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;
} else {
arrayOfFiles.push(join(dir, "/", file));
}
}
return arrayOfFiles;
} catch (err) {
throw err;
}
export async function registerModules(handler : Sern.Handler) : Promise<void> {
const commandDir = handler.commandDir;
Promise.all((await getCommands(commandDir)).map(async absPath => {
return { name : basename(absPath), mod: ( await import(absPath)).default as Sern.Module<unknown>, absPath }
})).then( async modArr => {
for ( const { name, mod, absPath } of modArr) {
switch (mod.type) {
case 1 : Commands.set(name.substring(0, name.length-3), { mod, options: [] }); break;
case 2 : {
const options = ((await import(absPath)).options as ApplicationCommandOptionData[])
Commands.set(name.substring(0, name.length - 3), { mod, options : options ?? [] });
} break;
case 1 | 2 : {
const options = ((await import(absPath)).options as ApplicationCommandOptionData[])
Commands.set(name.substring(0, name.length-3),{mod, options : options ?? [] } );
} break;
default : throw Error(`${name}.js is not a valid module type.`);
}
if(mod.alias.length > 0) {
for ( const alias of mod.alias) {
Alias.set(alias, {mod, options : []})
}
}
return arrayOfFiles;
}
export async function registerModules(handler: Sern.Handler): Promise<void> {
const commandDir = handler.commandDir;
Promise.all((await getCommands(commandDir)).map(async absPath => {
return { name: basename(absPath), mod: (await import(absPath)).default as Sern.Module<unknown>, absPath }
})).then(async modArr => {
for (const { name, mod, absPath } of modArr) {
switch (mod.type) {
case 1: Commands.set(name.substring(0, name.length - 3), { mod, options: [] }); break;
case 2:
case 1 | 2: {
const options = ((await import(absPath)).options as ApplicationCommandOptionData[])
Commands.set(name.substring(0, name.length - 3), { mod, options: options ?? [] });
} break;
default: throw Error(`${name}.js is not a valid module type.`);
}
})
}
export async function getCommands(dir: string) : Promise<string[]> {
return readPath(join(process.cwd(), dir ))
}
if (mod.alias.length > 0) {
for (const alias of mod.alias) {
Alias.set(alias, { mod, options: [] })
}
}
}
})
}
export async function getCommands(dir: string): Promise<string[]> {
return readPath(join(process.cwd(), dir))
}

View File

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

View File

@@ -1,5 +1,5 @@
import type { Option } from 'ts-results';
import type { CommandInteraction, CommandInteractionOptionResolver, Message, MessagePayload, MessageOptions} from 'discord.js';
import type { Option } from 'ts-results'
import type { CommandInteraction, CommandInteractionOptionResolver, Message, MessagePayload, MessageOptions } from 'discord.js';
import type { Sern } from '../../handler/sern';
export type Visibility = "private" | "public"
@@ -11,17 +11,17 @@ 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;
[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 = {
text : Option<Message>,
slash : Option<CommandInteraction>
text: Option<Message>,
slash: Option<CommandInteraction>
}
export interface Arg {
text : string;
slash : SlashOptions
};
text: string;
slash: SlashOptions
}
// TypeAlias for interaction.options
export type SlashOptions = Omit<CommandInteractionOptionResolver, "getMessage" | "getFocused">;
export type SlashOptions = Omit<CommandInteractionOptionResolver, "getMessage" | "getFocused">;