From ce105d1b0edc82d96a6d5961f26c98528ef5b2b3 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Sun, 19 Feb 2023 15:52:36 +0300 Subject: [PATCH 01/16] things --- README.md | 5 + docs/Commands.md | 47 +++++++- docs/PermissionsPlugin.md | 61 +++++++++++ plugins/discord.js | 36 +++++++ plugins/permissions.d.ts | 18 ++++ plugins/permissions.js | 78 ++++++++++++++ src/ArgumentParser.js | 24 +++-- src/CommandHandler.js | 5 +- src/index.d.ts | 218 +++++++++++--------------------------- 9 files changed, 322 insertions(+), 170 deletions(-) create mode 100644 docs/PermissionsPlugin.md create mode 100644 plugins/permissions.d.ts create mode 100644 plugins/permissions.js diff --git a/README.md b/README.md index cd3d2fd..e0613fb 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,11 @@ See these for docs: ## Changelog +**v1.2.0:** + +- More major changes! +- :warning: **BREAKING:** Command checks now use `ExecutorContext`! For compatability reasons, the runner args are still being kept in the function arguments, but you need to add a dummy argument at the start. Check the docs for more info. + **v1.1.0:** - :warning: **BREAKING:** In `ExecutorContext` (ctx in `failedChecksMessage(ctx)`/now `on("failedChecks", (ctx)=>{})`), the **`checks`** property is now `CommandCheckResult[]` instead of `string[]`. This allows Command Checks to supply additional information about the failed checks, such as diff --git a/docs/Commands.md b/docs/Commands.md index a8b2129..3a9efd8 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -54,23 +54,31 @@ let myCommand = { Your commands can also have custom checks. Its recommended to make the checks once and reuse them for commands. -Command Checks are just the same as the runner functions, but they must return an object with these props: +Command Checks are similar to runner functions, but there's a differience. + +The function's first argument is the [`ExecutorContext`](./CommandHandler.md#executorcontext) and the rest are [`Runner Args`](./CommandHandler.md#runnerargs), like this: `(execCtx, a, b, c) => {...}`. This is for other packages to be easily imported and used. + +**Migrating to 1.2.0:** just add an `_` argument to your checks, its that easy (`function(a,b){}` => `function(_,a,b){}`) + +The command checks need to return a `CommandCheckResult`, which is something like this: - `pass` (boolean) - set to true if check succeeded - `message` (string) - if it failed, the message explaining why +Example: `{ pass: false, message: "Thou shall not pass" }` + They can also be **asynchronous** ```js { checks: [ - (ctx, args) => { + (execCtx) => { // Im an useless check! Gonna make it run! return { pass: true }; }, - (ctx, args) => { - // just you wait until you hear that im after you + async (execCtx) => { + // haha No. return { pass: false, message: "Hardcoded Failiure", @@ -80,4 +88,35 @@ They can also be **asynchronous** } ``` +For catching when commands fail checks, see [here](./CommandHandler.md#failed-checks) + +You can also add other extra properties to the `CommandCheckResult` for more customization: + +```js +{ + checks: [ + ({ ctx }) => { + + if(ctx.cool < 5) { + return { + pass: false, + message: "Not cool enough", + code: "UNCOOL", + coolness: ctx.cool, + }; + } else { + return { pass: true } + }; + + }, + ] +} + +handler.on("failedChecks", ({ checks }) => { + if(checks[0].code == "UNCOOL") { + // do custom message stuff + } +}) +``` + [^ Back to top](#commands) diff --git a/docs/PermissionsPlugin.md b/docs/PermissionsPlugin.md new file mode 100644 index 0000000..2503473 --- /dev/null +++ b/docs/PermissionsPlugin.md @@ -0,0 +1,61 @@ +# Permissions Plugin + +## Permissions + +Permissions are defined are strings with dots for categorization. + +For example: + +```js +"dosomething" +"posts.create" +"posts.comments.reply" +``` + +Permissions can have a "*" to signify the user has every permission in a category: + +```js +"posts.*" + includes: + - "posts.comments" + - "posts.create" + - "posts.likes.remove" +``` + +## Using the middleware + +First, import the plugin. Then register it using `CommandHandler#use` + +```js +import { Permissions } from "string-commands/permissions"; + +// ... + +handler.use(Permissions()); +``` + +By default, the plugin fetches user permissions from CustomContext. If the permissions are in any of these paths: + +- + +Then you can skip the next part. + +### Get user permissions using CustomContext + +When creating the middleware, you can pass an object of options into `Permissions`. + +```js +handler.use(Permissions({ + getPermissions: async () => {}, +})); +``` + +The `getPermissions` function property should return the user/entity's posessed permissions. + +For example: + +```js +handler.use(Permissions({ + getPermissions: async () => {}, +})); +``` diff --git a/plugins/discord.js b/plugins/discord.js index fce79ab..f5e1cee 100644 --- a/plugins/discord.js +++ b/plugins/discord.js @@ -1,5 +1,6 @@ import { CommandHandler } from "../src/CommandHandler.js"; import { Client, channelMention, ChannelType } from "discord.js"; +import { Permissions } from "./permissions.js"; /** * @type {Object} @@ -160,4 +161,39 @@ class DiscordCommandHandler extends CommandHandler { } } +const DiscordPermissions = () => { + return [ + DiscordPermissions.userOnly(), + DiscordPermissions.botOnly(), + ]; +}; + +DiscordPermissions.userOnly = () => { + return Permissions({ + label: "user-permissions", + getPermissions: async (execCtx) => { + + }, + getPermissionsSource: async (execCtx) => { + + }, + }); +}; + +DiscordPermissions.botOnly = () => { + return Permissions({ + label: "bot-permissions", + getPermissions: async (execCtx) => { + + }, + getPermissionsSource: async (execCtx) => { + return execCtx.command.botpermissions || execCtx.command.botperms; + }, + }); +}; + +const GuildOnly = { + +}; + export { DiscordCommandHandler, DiscordUsages }; diff --git a/plugins/permissions.d.ts b/plugins/permissions.d.ts new file mode 100644 index 0000000..48cba54 --- /dev/null +++ b/plugins/permissions.d.ts @@ -0,0 +1,18 @@ +import { CommandHandlerMiddleware, BasicExecutorContext } from "../src/index"; + +type PermissionsOptions = { + getPermissions?: (ctx: BasicExecutorContext) => Promise, + getPermissionsSource?: (ctx: BasicExecutorContext) => Promise, +}; + +export type Permissions = (opts?: PermissionsOptions) => CommandHandlerMiddleware; + +export type Permissions = { + check: (required: string[] | string, posessed: string[] | string) => { + passed: true, + } | { + passed: false, + unmet: string[], + }; + compare: (target: string, source: string) => boolean; +}; \ No newline at end of file diff --git a/plugins/permissions.js b/plugins/permissions.js new file mode 100644 index 0000000..502e52c --- /dev/null +++ b/plugins/permissions.js @@ -0,0 +1,78 @@ +const Permissions = (opts = {}) => { + opts = Object.assign(opts, { + getPermissions: async (execCtx) => { + return execCtx.ctx?.user?.permissions || execCtx.ctx?.permissions || execCtx.ctx?.perms; + }, + getPermissionsSource: async (execCtx) => { + return execCtx.command.permissions || execCtx.command.perms; + }, + label: "", + }); + + return { + before: "checks", + id: "permissions", + run: (execCtx, next) => { + let required = opts.getPermissionsSource(execCtx) || []; + if(!required.length) return next(); + let posessed = opts.getPermissions(execCtx) || []; + let { passed, unmet } = Permissions.check(required, posessed); + + if(passed) { + return next(); + } else { + execCtx.handler.emit("insufficentPermissions", { + ...execCtx, + required, + posessed, + unmet, + label: opts.label, + }); + }; + }, + }; +}; + +/** + * Checks for permissions + * @param {string|string[]} required The required permissions to check for + * @param {string|string[]} posessed The permissions that the user has + * @returns {{ passed: true } | { unmet: string[], passed: false }} + */ +Permissions.check = (required, posessed) => { + if(typeof required == "string") required = [required]; + if(typeof posessed == "string") posessed = [posessed]; + + let unmet = []; + + for(let perm of required) { + if(posessed.some((v) => Permissions.compare(perm, v))) { + continue; + }; + + unmet.push(perm); + }; + + return { + passed: unmet.length, + unmet, + }; +}; + +Permissions.compare = (target, source) => { + let a = target.split("."); + let b = source.split("."); + + let wildcard = false; + return a.every((v, i) => { + if(wildcard) return true; + if(b[i] == v) return true; + if(b[i] == "*") { + wildcard = true; + return true; + }; + return false; + }); +}; + +export { Permissions }; \ No newline at end of file diff --git a/src/ArgumentParser.js b/src/ArgumentParser.js index 3afbb8e..594f110 100644 --- a/src/ArgumentParser.js +++ b/src/ArgumentParser.js @@ -13,7 +13,7 @@ import splitargs from "../utils/splitargs.js"; * @prop {ArgumentHandlerStylings} style */ -const fail = (m) => ({ fail: true, message: m }); +const fail = (m, code) => ({ fail: true, message: m, code }); /** * A collection of native/hardcoded argument parsers @@ -30,6 +30,7 @@ const NativeUsages = Object.entries({ )} cannot be longer than ${ctx.style.arg( opts.max, )} characters!`, + "TOO_LONG", ); } @@ -40,6 +41,7 @@ const NativeUsages = Object.entries({ )} cannot be shorter than ${ctx.style.arg( ctx.opts.min, )} characters!`, + "TOO_SHORT", ); } @@ -55,12 +57,13 @@ const NativeUsages = Object.entries({ let arg = Number(ctx.arg); if (isNaN(arg)) { - return fail(`${ctx.style.arg(ctx.name)} must be a number!`); + return fail(`${ctx.style.arg(ctx.name)} must be a number!`, "NAN"); } if (ctx.opts.isInt && arg % 1 !== 0) { return fail( `${ctx.style.arg(ctx.name)} must be a whole number!`, + "NOT_INT", ); } @@ -69,6 +72,7 @@ const NativeUsages = Object.entries({ `${ctx.style.arg( ctx.name, )} cannot be greater than ${ctx.style.arg(ctx.opts.max)}!`, + "TOO_BIG", ); } @@ -79,6 +83,7 @@ const NativeUsages = Object.entries({ )} cannot be smaller than ${ctx.style.arg( ctx.opts.min, )} characters!`, + "TOO_SMALL", ); } @@ -210,10 +215,11 @@ class ArgumentParser { rawArg = rawArgs.slice(i).join(" "); } - if (!rawArg.trim() && !currentUsage.optional) { + if (!(rawArg || "").trim() && !currentUsage.optional) { errors.push({ usage: currentUsage, - message: `${inlineCode(currentUsage.name)} is required!`, + message: `${(currentUsage.name)} is required!`, + code: "REQUIRED", }); continue; } @@ -222,7 +228,7 @@ class ArgumentParser { if (result.fail) { errors.push({ usage: currentUsage, - message: result.message, + ...result, }); } else { finalArgs.push(result.parsed); @@ -282,7 +288,10 @@ class ArgumentParser { parsed: defaultValue, }; } else { - return fail(`${this.styling.arg(usage.name)} is required!`); + return { + ...fail(`${this.styling.arg(usage.name)} is required!`), + code: "REQUIRED", + }; } } @@ -294,9 +303,10 @@ class ArgumentParser { name: usage.name, opts: usage, style: this.styling, - fail: (m) => ({ + fail: (m, extra = {}) => ({ fail: true, message: this.styling.arg(usage.name) + ": " + m, + ...extra, }), context, }); diff --git a/src/CommandHandler.js b/src/CommandHandler.js index 3a713c4..2b6a414 100644 --- a/src/CommandHandler.js +++ b/src/CommandHandler.js @@ -105,7 +105,7 @@ class CommandHandler extends EventEmitter { this.log.info("Registering folder: " + resolve(folderPath)); for (let entry of entries) { let fd = resolve(folderPath, entry.name); - if (entry.isDirectory()) registerCommands(fd); + if (entry.isDirectory()) await this.registerCommands(fd); else { let obj = {}; try { @@ -255,7 +255,7 @@ class CommandHandler extends EventEmitter { let failedChecks = []; for (let check of execCtx.command.checks) { /** @type {CommandCheckResult} */ - let result = await check(...execCtx.runArgs); + let result = await check(execCtx, execCtx.runArgs); if (!result.pass) { failedChecks.push(result); } @@ -297,6 +297,7 @@ class CommandHandler extends EventEmitter { execute({ input, ctx, + handler: this, }); return this; diff --git a/src/index.d.ts b/src/index.d.ts index 0946e77..8acca66 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -2,65 +2,31 @@ import { EventEmitter } from "node:events"; -type ValueOfMap = M extends Map ? V : never -type KeyOfMap = M extends Map ? K : never -type ConvertToMap = Map; -type AnyFunction = (...a: any) => any; -type ReturnTypePromise = ReturnType extends Promise ? U : ReturnType; -type KeyOfAsString = Extract; - export as namespace stringcommands; export as namespace strcmd; type BaseRunnerArgs = any[]; -type BaseUsageCollection = { [type: string]: UsageParser }; - -type ResolveUsageParser = V extends UsageParser ? V : - V extends Usage ? (V["type"] extends string ? TUsages[V["type"]] : never) : - V extends string ? - V extends `${"<" | "[" | ""}${infer NA}${"..." | ""}${">" | "]" | ""}` ? - NA extends `${infer N}:${infer RT}` ? TUsages[RT] : TUsages[NA] - : never - : never; -type _getUsageParserTOutput = TUsage extends UsageParser ? - TOutput : (TUsage extends { parse: (...any) => { parsed: infer T } } ? T : never); - -type StringToUsage = - { - rest: V extends `${string}...` ? true : false, - optional: V extends `[${string}]` ? true : false, - type: V extends `${"<" | "[" | ""}${infer Mid}${">" | "]" | ""}` ? - Mid extends `${infer Name}:${infer Type}` ? - Type - : Mid - : never, - name: V extends `${"<" | "[" | ""}${infer Name}:${infer Type}${">" | "]" | ""}` ? - Name - : "", - }; - -export type Command< - TName extends string, - TAliases extends string[] | null, +type BaseCustomContext = {}; + +type Command< TRunnerArgs extends BaseRunnerArgs, - TUsages extends BaseUsageCollection, > = { - name: TName, + name: string, run(...args: TRunnerArgs): Promise | void, - aliases?: TAliases, - args?: UsageResolvableList, - checks: CommandCheck[], + aliases?: string[], + args?: UsageResolvableList, + checks: CommandCheck[], }; -export type CommandCheck = (...args: TRunnerArgs[]) => Promise; +type CommandCheck = (execCtx: TExecutorContext, ...args: TRunnerArgs) => Promise; -export interface CommandCheckPass { pass: true } -export interface CommandCheckFail { pass: false, message: string } +interface CommandCheckPass { pass: true } +interface CommandCheckFail { pass: false, message: string } -export type CommandCheckResult = CommandCheckPass | CommandCheckFail; +type CommandCheckResult = CommandCheckPass | CommandCheckFail; -export interface CommandHandlerOptions { +interface CommandHandlerOptions { prefix: string, log: typeof console | { log: AnyFunction, info: AnyFunction, error: AnyFunction, } | false, } @@ -71,53 +37,24 @@ export interface CommandHandlerOptions { export class CommandHandler< Opts extends CommandHandlerOptions, - CustomContext, + CustomContext extends BaseCustomContext, RunnerArgs extends BaseRunnerArgs, - _commands extends { - [CName: string]: Command< - typeof CName, - any, - RunnerArgs, - _usages - > - }, - _aliases extends { [CAlias in _commands[keyof _commands]["aliases"][number]as string]: keyof _commands }, - _usages extends { - [P in {} as string]: UsageParser< - CustomContext, - _usages, - any, - any - > - }, - _middlewares extends CommandHandlerMiddleware<_middlewares>[], - - _execContext extends ExecutorContext, + _execContext extends ExecutorContext, > extends EventEmitter { constructor(opts?: Opts) prefix: Opts["prefix"] | string; - Commands: ConvertToMap<_commands>; - Aliases: ConvertToMap<_aliases>; - middlewares: _middlewares; - - argumentParser: ArgumentParser; - - registerCommand(cmd: Command< - string, - string[], - RunnerArgs, - _usages - >): this; + Commands: Map>; + Aliases: Map; + middlewares: CommandHandlerMiddleware[]; + + argumentParser: ArgumentParser; + + registerCommand(cmd: Command): this; registerCommands(path: string): Promise; - addCommand(cmd: Command< - string, - string[], - RunnerArgs, - _usages - >): this; + addCommand(cmd: Command): this; addCommands(path: string): Promise; on(event: "invalidUsage", handler: (ctx: _execContext) => void): this; @@ -125,11 +62,11 @@ export class CommandHandler< buildArguments(ctx: _execContext): RunnerArgs; - use(mw: CommandHandlerMiddleware<_middlewares>): this; + use(mw: CommandHandlerMiddleware): this; run(input: string, ctx: CustomContext): this; - prettyPrint(cmd: _commands[string]): string; + prettyPrint(cmd: Command): string; } /** @@ -137,22 +74,20 @@ export class CommandHandler< */ type ExecutorContext< CustomContext, - TCommand extends Command, TRunnerArgs extends BaseRunnerArgs, - TUsages extends BaseUsageCollection, > = { /** The raw input string */ input: string, /** The custom context */ ctx: CustomContext, /** Name of the executing command */ - name?: TCommand["name"], + name?: Command["name"], /** Unparsed arguments (raw) from the input string */ rawArgs?: string[], /** The command's information */ - command?: TCommand, + command?: Command, /** List of usage (argument parser) errors */ - errors?: UsageError[], + errors?: UsageError[], /** Parsed arguments/usages */ args?: any[], // TODO: typescript voodoo to make it relationship >:3 /** Built runner args */ @@ -163,109 +98,78 @@ type ExecutorContext< error?: Error, }; +type BasicExecutorContext = ExecutorContext; + // Usage System -class ArgumentParser< - CustomContext, - _usages extends BaseUsageCollection, -> { - ArgumentParsers: ConvertToMap<_usages>; +export class ArgumentParser { + ArgumentParsers: Map>; - parseUsages[]>(text: string, + parseUsages(text: string, commandUsages: T, ctx: CustomContext): { - args: { [I in keyof T]: _getUsageParserTOutput> }, - errors: [], + args: any[], + errors: UsageParserFail[], }; } -export type UsageResolvableList = UsageResolvable[]; +type UsageResolvableList = UsageResolvable[]; -export type UsageResolvable = - `${`${"<" | ""}${KeyOfAsString | `${string}:${KeyOfAsString}`}${">" | ""}` - | `[${KeyOfAsString | `${string}:${KeyOfAsString}`}]` - }${"..." | ""}` | { type: KeyOfAsString }; +type UsageResolvable = string | { type: UsageResolvable }; -export interface Usage< - CustomContext, - TName extends KeyOfAsString, - TUsages extends BaseUsageCollection -> extends UsageParser< - CustomContext, - TUsages, - any, - any -> { - name: TName, - type: UsageResolvable, - optional?: boolean, +interface Usage extends UsageParser { + name: string, } -export interface UsageParser< - CustomContext, - TUsages extends BaseUsageCollection, +interface UsageParser< + TInput, TOutput, - TOpts, > { // The second in this union is for Usage compat. - type: string | { type: string }, + type: UsageResolvable, parse: (ctx: UsageParserContext< - CustomContext, - ReturnTypePromise, - TOutput, - TOpts + TInput >) => Promise>; optional?: boolean, default?: TOutput | ((ctx: UsageParserContext< - CustomContext, - ReturnTypePromise, - TOutput, - TOpts - >) => TOutput), + TInput + >) => Promise), rest?: boolean, } -export type UsageParserContext< - CustomContext, +type UsageParserContext< TInput, - TOutput, - TOpts, > = { arg: TInput; name: string; - opts: TOpts; - fail(message: string): Extract, { fail: true }>; - context: CustomContext; + opts: {}; + fail(message: string): UsageParserFail; + context: BaseCustomContext; style: {}; // TODO: ArgumentHandlerStylings -} +}; -export interface UsageParserSuccess { fail: false, parsed: TOutput } -export interface UsageParserFail { fail: true, message: string } +interface UsageParserSuccess { fail: false, parsed: TOutput } +interface UsageParserFail { fail: true, message: string } -export type UsageParserResult = UsageParserSuccess | UsageParserFail; +type UsageParserResult = UsageParserSuccess | UsageParserFail; -export type UsageError< - TUsage extends UsageParser, -> = { - usage: TUsage, - message: UsageParserFail["message"], -}; +interface UsageError extends UsageParserFail { + usage: UsageParser, +} // Middlewares -export type ExecutorStage = "splitString" | "resolveCommand" | "parseUsages" | "checks" | "run"; -export type MiddlewareConstraint< +type ExecutorStage = "splitString" | "resolveCommand" | "parseUsages" | "checks" | "run"; +type MiddlewareConstraint< TMiddlewares extends CommandHandlerMiddleware[], > = TMiddlewares[number]["id"] | ExecutorStage; -export type CommandHandlerMiddleware< - TMiddlewares extends CommandHandlerMiddleware[], -> = { +export type CommandHandlerMiddleware = { id?: string, run: (ctx: ExecutorContext, next: (() => void)) => void; - requires?: (TMiddlewares[number]["id"])[], + requires?: ExecutorStage[], } & ({ - before?: MiddlewareConstraint, + before?: ExecutorStage, } | { - after?: MiddlewareConstraint, + after?: ExecutorStage, }); \ No newline at end of file From 7063fde958c5e886a0d755810785f4e12b4e2f08 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Fri, 12 May 2023 22:38:38 +0300 Subject: [PATCH 02/16] owo --- README.md | 2 + package.json | 3 +- src/ArgumentParser.ts | 79 +++++++++++++++++ src/CommandHandler.ts | 14 +++ src/classes/StringReader.ts | 50 +++++++++++ src/classes/UsageError.ts | 3 + src/index.ts | 3 + src/interfaces/Argument.ts | 5 ++ src/interfaces/Command.ts | 6 ++ src/interfaces/CommandHandlerOptions.ts | 3 + src/interfaces/ExecutorContext.ts | 18 ++++ src/interfaces/Middleware.ts | 3 + src/interfaces/Usage.ts | 14 +++ src/interfaces/UsageResult.ts | 8 ++ src/types.ts | 5 ++ src/usages/text.ts | 19 +++++ {src => srcold}/ArgumentParser.js | 0 {src => srcold}/CommandHandler.js | 4 +- {src => srcold}/index.d.ts | 2 +- {src => srcold}/index.js | 0 {src => srcold}/stageify.js | 0 tsconfig.json | 109 ++++++++++++++++++++++++ 22 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 src/ArgumentParser.ts create mode 100644 src/CommandHandler.ts create mode 100644 src/classes/StringReader.ts create mode 100644 src/classes/UsageError.ts create mode 100644 src/index.ts create mode 100644 src/interfaces/Argument.ts create mode 100644 src/interfaces/Command.ts create mode 100644 src/interfaces/CommandHandlerOptions.ts create mode 100644 src/interfaces/ExecutorContext.ts create mode 100644 src/interfaces/Middleware.ts create mode 100644 src/interfaces/Usage.ts create mode 100644 src/interfaces/UsageResult.ts create mode 100644 src/types.ts create mode 100644 src/usages/text.ts rename {src => srcold}/ArgumentParser.js (100%) rename {src => srcold}/CommandHandler.js (98%) rename {src => srcold}/index.d.ts (99%) rename {src => srcold}/index.js (100%) rename {src => srcold}/stageify.js (100%) create mode 100644 tsconfig.json diff --git a/README.md b/README.md index e0613fb..640d94b 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ See these for docs: - More major changes! - :warning: **BREAKING:** Command checks now use `ExecutorContext`! For compatability reasons, the runner args are still being kept in the function arguments, but you need to add a dummy argument at the start. Check the docs for more info. +- :warning: **BREAKING:** `ExecutorContext` got lots of renaming: + - `checks` => `failedChecks` **v1.1.0:** diff --git a/package.json b/package.json index 42ac3de..89bf04b 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ }, "homepage": "https://github.com/TheAlan404/string-commands#readme", "devDependencies": { - "prettier": "^2.6.2" + "prettier": "^2.6.2", + "typescript": "^5.0.4" }, "prettier": { "semi": true, diff --git a/src/ArgumentParser.ts b/src/ArgumentParser.ts new file mode 100644 index 0000000..10d8603 --- /dev/null +++ b/src/ArgumentParser.ts @@ -0,0 +1,79 @@ +import StringReader from "./classes/StringReader"; +import { Argument, ArgumentObject } from "./interfaces/Argument"; +import { ExecutorContext } from "./interfaces/ExecutorContext"; +import Usage from "./interfaces/Usage"; + +interface ArgumentParserPayload { + string: string, + args: Argument[], +}; + +class ArgumentParser { + usages: Map> = new Map(); + + constructor() { + + }; + + resolveArgument(arg: Argument): ArgumentObject { + if(typeof arg == "string") { + let rest = false; + let optional = false; + if (arg.endsWith("...")) { + rest = true; + arg = arg.slice(0, -3); + } + + let last = arg[arg.length - 1]; + if (arg[0] == "<") { + if (last !== ">") + throw new Error("Unclosed required argument identifier"); + arg = arg.slice(1).slice(0, -1); + } else if (arg[0] == "[") { + if (last !== "]") + throw new Error("Unclosed optional argument identifier"); + optional = true; + arg = arg.slice(1).slice(0, -1); + } + + let sp = arg.split(":"); + let type = sp.length === 2 ? sp[1] : sp[0]; + let name = sp.length === 2 ? sp[0] : null; + + if (!this.usages.has(type)) { + throw new Error( + `Can't resolve argument from string because Usage '${type}' doesn't exist!`, + ); + }; + + return { type, name, rest }; + }; + + return arg; + } + + getUsage(id: string) { + return this.usages.get(id); + }; + + parseArguments(input: string, args: Argument[], ctx: ExecutorContext) { + let reader: StringReader = new StringReader(input); + + for(let arg of args) { + let opts = this.resolveArgument(arg); + this.parseArgument(opts, ctx); + } + }; + + async parseArgument(arg: ArgumentObject, ctx: ExecutorContext) { + let usage = this.getUsage(arg.type!); + if(!usage) throw new Error("Can't find usage '" + arg.type + "'!"); + + return await usage.parse(ctx); + }; +}; + +export { + ArgumentParser, + ArgumentParserPayload, +}; \ No newline at end of file diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts new file mode 100644 index 0000000..5ca14b6 --- /dev/null +++ b/src/CommandHandler.ts @@ -0,0 +1,14 @@ +import { CommandHandlerOptions } from "./interfaces/CommandHandlerOptions"; +import Middleware from "./interfaces/Middleware"; + +class CommandHandler { + middlewares: Middleware[] = []; + + constructor(opts?: CommandHandlerOptions) { + + } +}; + +export { + CommandHandler, +}; \ No newline at end of file diff --git a/src/classes/StringReader.ts b/src/classes/StringReader.ts new file mode 100644 index 0000000..650181b --- /dev/null +++ b/src/classes/StringReader.ts @@ -0,0 +1,50 @@ +class StringReader { + input: string = ""; + index: number = 0; + + constructor(input: string) { + this.input = input; + } + + reset() { + this.index = 0; + } + + readChar() { + return this.input[this.index++]; + } + + peekChar() { + return this.input[this.index]; + } + + readUntil(predicate: ((char: string) => boolean) | string): string { + if(typeof predicate == "string") { + let str: string = predicate; + predicate = (x) => x == str; + }; + + let buf = ""; + + while(!this.end()) { + let char = this.readChar(); + if(predicate(char)) { + return buf; + } else { + buf += char; + }; + }; + + return ""; + } + + readLine() { + return this.readUntil('\n'); + } + + end() { + return this.index >= this.input.length; + } +} + +export default StringReader; \ No newline at end of file diff --git a/src/classes/UsageError.ts b/src/classes/UsageError.ts new file mode 100644 index 0000000..651e010 --- /dev/null +++ b/src/classes/UsageError.ts @@ -0,0 +1,3 @@ +export default class UsageError extends Error { + +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..fcaf09c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +import { CommandHandler } from "./CommandHandler"; + +export { CommandHandler }; \ No newline at end of file diff --git a/src/interfaces/Argument.ts b/src/interfaces/Argument.ts new file mode 100644 index 0000000..8db54e1 --- /dev/null +++ b/src/interfaces/Argument.ts @@ -0,0 +1,5 @@ +export type ArgumentObject = { + type?: string, + [others: string]: any; +}; +export type Argument = string | ArgumentObject; \ No newline at end of file diff --git a/src/interfaces/Command.ts b/src/interfaces/Command.ts new file mode 100644 index 0000000..2649485 --- /dev/null +++ b/src/interfaces/Command.ts @@ -0,0 +1,6 @@ +import { MaybePromise } from "../types"; + +export default interface Command { + name: string, + run(): MaybePromise, +}; \ No newline at end of file diff --git a/src/interfaces/CommandHandlerOptions.ts b/src/interfaces/CommandHandlerOptions.ts new file mode 100644 index 0000000..5151dca --- /dev/null +++ b/src/interfaces/CommandHandlerOptions.ts @@ -0,0 +1,3 @@ +export interface CommandHandlerOptions { + +} \ No newline at end of file diff --git a/src/interfaces/ExecutorContext.ts b/src/interfaces/ExecutorContext.ts new file mode 100644 index 0000000..6910593 --- /dev/null +++ b/src/interfaces/ExecutorContext.ts @@ -0,0 +1,18 @@ +import { CommandHandler } from "../CommandHandler"; +import StringReader from "../classes/StringReader"; +import Command from "./Command"; + +export type BaseExecutorContext = { + handler: CommandHandler, + rawInput: string, + input: string, + ctx: object, + [others: string]: any; +}; + +export type ExecutorContextCommand = BaseExecutorContext & { + commandName: string, + command: Command, +}; + +export type ExecutorContext = ExecutorContextCommand; \ No newline at end of file diff --git a/src/interfaces/Middleware.ts b/src/interfaces/Middleware.ts new file mode 100644 index 0000000..0f046bd --- /dev/null +++ b/src/interfaces/Middleware.ts @@ -0,0 +1,3 @@ +export default interface Middleware { + id: string, +}; \ No newline at end of file diff --git a/src/interfaces/Usage.ts b/src/interfaces/Usage.ts new file mode 100644 index 0000000..4dacf8e --- /dev/null +++ b/src/interfaces/Usage.ts @@ -0,0 +1,14 @@ +import StringReader from "../classes/StringReader"; +import { MaybePromise } from "../types"; +import { ExecutorContextCommand } from "./ExecutorContext"; + +interface UsageContext extends ExecutorContextCommand { + argumentName: string, + reader: StringReader, + options: O, +} + +export default interface Usage { + type?: string, + parse(ctx: UsageContext): MaybePromise, +} \ No newline at end of file diff --git a/src/interfaces/UsageResult.ts b/src/interfaces/UsageResult.ts new file mode 100644 index 0000000..b001953 --- /dev/null +++ b/src/interfaces/UsageResult.ts @@ -0,0 +1,8 @@ +export type UsageResult = { + fail: true, + message?: string, + [others: string]: any; +} | { + fail: false | undefined, + parsed: T, +} | T; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0ba7680 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,5 @@ +import Command from "./interfaces/Command"; + +export type MaybePromise = Promise | T; +export type Constructor = new (...args: any[]) => T; +export type MaybeCommand = Constructor | {default: Constructor} | {[k: string]: Constructor}; diff --git a/src/usages/text.ts b/src/usages/text.ts new file mode 100644 index 0000000..92c3147 --- /dev/null +++ b/src/usages/text.ts @@ -0,0 +1,19 @@ +import Usage from "../interfaces/Usage"; + +type TextUsageOptions = Partial<{ + max: number, + min: number, + regex: RegExp, +}>; + +const TextUsage: Usage = { + parse(ctx) { + let str = ctx.reader.readUntil(" "); + if(ctx.options.max && str.length > ctx.options.max) + }, +}; + +export { + TextUsage, + TextUsageOptions, +}; \ No newline at end of file diff --git a/src/ArgumentParser.js b/srcold/ArgumentParser.js similarity index 100% rename from src/ArgumentParser.js rename to srcold/ArgumentParser.js diff --git a/src/CommandHandler.js b/srcold/CommandHandler.js similarity index 98% rename from src/CommandHandler.js rename to srcold/CommandHandler.js index 2b6a414..b608554 100644 --- a/src/CommandHandler.js +++ b/srcold/CommandHandler.js @@ -8,7 +8,7 @@ import { stageify } from "./stageify.js"; * @typedef {Object} Command * @prop {string} name - Name of the command * @prop {string[]} [aliases] - aliases - * @prop {import("./usages").UsageResolvable[]} [args] - Arguments + * @prop {import("./usages.js").UsageResolvable[]} [args] - Arguments * @prop {CommandRun} run * @prop {CommandCheck[]} checks */ @@ -264,7 +264,7 @@ class CommandHandler extends EventEmitter { if (failedChecks.length) { this.emit("failedChecks", { ...execCtx, - checks: failedChecks, + failedChecks, }); return; } diff --git a/src/index.d.ts b/srcold/index.d.ts similarity index 99% rename from src/index.d.ts rename to srcold/index.d.ts index 8acca66..f4095e4 100644 --- a/src/index.d.ts +++ b/srcold/index.d.ts @@ -93,7 +93,7 @@ type ExecutorContext< /** Built runner args */ runArgs?: TRunnerArgs, /** List of failed checks */ - checks?: CommandCheckFail[], + failedChecks?: CommandCheckFail[], /** An error, if occured while executing the command */ error?: Error, }; diff --git a/src/index.js b/srcold/index.js similarity index 100% rename from src/index.js rename to srcold/index.js diff --git a/src/stageify.js b/srcold/stageify.js similarity index 100% rename from src/stageify.js rename to srcold/stageify.js diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e075f97 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} From 329292444111a72ebc9a887ddfdae1dfab069f5a Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Sun, 14 May 2023 20:31:32 +0300 Subject: [PATCH 03/16] idk what im doing --- jest.config.js | 5 + package.json | 3 + src/ArgumentParser.ts | 154 ++++++++++++++++++++---------- src/CommandHandler.ts | 31 ++++++ src/classes/InternalError.ts | 11 +++ src/classes/UsageError.ts | 12 +++ src/interfaces/Argument.ts | 5 +- src/interfaces/Command.ts | 4 + src/interfaces/ExecutorContext.ts | 8 +- src/interfaces/Middleware.ts | 7 ++ src/interfaces/Usage.ts | 20 +++- src/middlewares/ResolveCommand.ts | 12 +++ src/usages/index.ts | 5 + src/usages/text.ts | 28 +++++- 14 files changed, 240 insertions(+), 65 deletions(-) create mode 100644 jest.config.js create mode 100644 src/classes/InternalError.ts create mode 100644 src/middlewares/ResolveCommand.ts create mode 100644 src/usages/index.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..b413e10 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/package.json b/package.json index 89bf04b..1b04c13 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,10 @@ }, "homepage": "https://github.com/TheAlan404/string-commands#readme", "devDependencies": { + "@types/jest": "^29.5.1", + "jest": "^29.5.0", "prettier": "^2.6.2", + "ts-jest": "^29.1.0", "typescript": "^5.0.4" }, "prettier": { diff --git a/src/ArgumentParser.ts b/src/ArgumentParser.ts index 10d8603..a6358c8 100644 --- a/src/ArgumentParser.ts +++ b/src/ArgumentParser.ts @@ -1,79 +1,131 @@ +import InternalError from "./classes/InternalError"; import StringReader from "./classes/StringReader"; import { Argument, ArgumentObject } from "./interfaces/Argument"; +import Command, { CommandArgs } from "./interfaces/Command"; import { ExecutorContext } from "./interfaces/ExecutorContext"; -import Usage from "./interfaces/Usage"; +import { Usage, ReaderContext } from "./interfaces/Usage"; +import { TextUsage } from "./usages"; -interface ArgumentParserPayload { - string: string, - args: Argument[], +interface ArgumentParserOptions { + noBuiltins?: boolean, }; class ArgumentParser { - usages: Map> = new Map(); - - constructor() { + usages: Map> = new Map(); + constructor(opts?: ArgumentParserOptions & { usages?: Record> }) { + if(!opts) opts = {}; + if(!opts.noBuiltins) { + this.usages.set("text", TextUsage); + this.usages.set("string", TextUsage); + }; }; - resolveArgument(arg: Argument): ArgumentObject { - if(typeof arg == "string") { - let rest = false; - let optional = false; - if (arg.endsWith("...")) { - rest = true; - arg = arg.slice(0, -3); - } - - let last = arg[arg.length - 1]; - if (arg[0] == "<") { - if (last !== ">") - throw new Error("Unclosed required argument identifier"); - arg = arg.slice(1).slice(0, -1); - } else if (arg[0] == "[") { - if (last !== "]") - throw new Error("Unclosed optional argument identifier"); - optional = true; - arg = arg.slice(1).slice(0, -1); - } - - let sp = arg.split(":"); - let type = sp.length === 2 ? sp[1] : sp[0]; - let name = sp.length === 2 ? sp[0] : null; - - if (!this.usages.has(type)) { - throw new Error( - `Can't resolve argument from string because Usage '${type}' doesn't exist!`, - ); - }; - - return { type, name, rest }; + resolveArgumentFromString(arg: string): ArgumentObject { + let rest = false; + let optional = false; + if (arg.endsWith("...")) { + rest = true; + arg = arg.slice(0, -3); + } + + let last = arg[arg.length - 1]; + if (arg[0] == "<") { + if (last !== ">") + throw new Error("Unclosed required argument identifier"); + arg = arg.slice(1).slice(0, -1); + } else if (arg[0] == "[") { + if (last !== "]") + throw new Error("Unclosed optional argument identifier"); + optional = true; + arg = arg.slice(1).slice(0, -1); + } + + let sp = arg.split(":"); + let type = sp.length === 2 ? sp[1] : sp[0]; + let name = sp.length === 2 ? sp[0] : null; + + if (!this.usages.has(type)) { + throw new Error( + `Can't resolve argument from string because Usage '${type}' doesn't exist!`, + ); }; - return arg; + return { type, name, rest }; } getUsage(id: string) { return this.usages.get(id); }; - parseArguments(input: string, args: Argument[], ctx: ExecutorContext) { - let reader: StringReader = new StringReader(input); + async parseArguments(ctx: ExecutorContext & { + rawArgs: string, + command: Command & CommandArgs, + }) { + let reader: StringReader = new StringReader(ctx.rawArgs); - for(let arg of args) { - let opts = this.resolveArgument(arg); - this.parseArgument(opts, ctx); - } + ctx.reader = reader; + + let args = []; + + for (let arg of ctx.command.args) { + // cleanup + ctx.value = null; + ctx.options = {}; + + await this.parseArgument(ctx, arg); + + args.push(ctx.value); + }; + + }; - async parseArgument(arg: ArgumentObject, ctx: ExecutorContext) { - let usage = this.getUsage(arg.type!); - if(!usage) throw new Error("Can't find usage '" + arg.type + "'!"); + getOptions(arg: ArgumentObject) { + let { parse, read, type, ...options } = arg; + return options; + } + + async parseArgument(ctx: ExecutorContext, arg: Argument): Promise { + if (typeof arg == "string") { + return await this.parseArgument(ctx, this.resolveArgumentFromString(arg)); + } + + ctx.options = { + ...ctx.options, + ...this.getOptions(arg), + }; + + if(arg.type && arg.type !== "native") { + let child = this.getUsage(arg.type); + + if(!child) throw new InternalError(ctx, `Usage '${arg.type}' doesn't exist`); + + ctx.options = this.getOptions({ + ...ctx.options, + ...child, + }); + + await this.parseArgument(ctx, child); + }; + + if(ctx.value === null && arg.read) { + ctx.value = arg.read(ctx); + }; + + if(arg.parse) { + ctx.value = await arg.parse(ctx); + }; + + return ctx.value; + + //let usage = this.getUsage(arg.type!); + //if(!usage) throw new Error("Can't find usage '" + arg.type + "'!"); - return await usage.parse(ctx); + //return await usage.parse(ctx as ReaderContext); }; }; export { ArgumentParser, - ArgumentParserPayload, }; \ No newline at end of file diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts index 5ca14b6..ac3bfbe 100644 --- a/src/CommandHandler.ts +++ b/src/CommandHandler.ts @@ -1,4 +1,5 @@ import { CommandHandlerOptions } from "./interfaces/CommandHandlerOptions"; +import { ExecutorContext } from "./interfaces/ExecutorContext"; import Middleware from "./interfaces/Middleware"; class CommandHandler { @@ -6,6 +7,36 @@ class CommandHandler { constructor(opts?: CommandHandlerOptions) { + } + + use(mw: Middleware | Middleware["run"] | (Middleware | Middleware["run"])[]): CommandHandler { + if(Array.isArray(mw)) { + for (const m of mw) { + this.use(m); + } + return this; + }; + + if(typeof mw == "function") mw = { + id: "anonymous " + this.middlewares.length, + before: "run", + run: mw, + }; + + this.middlewares.push(mw); + + return this; + }; + + async run(input: string, customContext: object) { + let ctx: ExecutorContext = { + handler: this, + rawInput: input, + input, + ...customContext, + }; + + } }; diff --git a/src/classes/InternalError.ts b/src/classes/InternalError.ts new file mode 100644 index 0000000..e60ac90 --- /dev/null +++ b/src/classes/InternalError.ts @@ -0,0 +1,11 @@ +import { ExecutorContext } from "../interfaces/ExecutorContext"; + +export default class InternalError extends Error { + ctx: ExecutorContext; + + constructor(ctx: ExecutorContext, message: string) { + super(message); + + this.ctx = ctx; + } +}; \ No newline at end of file diff --git a/src/classes/UsageError.ts b/src/classes/UsageError.ts index 651e010..a5deaa5 100644 --- a/src/classes/UsageError.ts +++ b/src/classes/UsageError.ts @@ -1,3 +1,15 @@ +import { ExecutorContext } from "../interfaces/ExecutorContext"; + export default class UsageError extends Error { + ctx: ExecutorContext; + code: string; + meta: any; + + constructor(ctx: ExecutorContext, code: string, meta: any) { + super(); + this.ctx = ctx; + this.code = code; + this.meta = meta; + } }; \ No newline at end of file diff --git a/src/interfaces/Argument.ts b/src/interfaces/Argument.ts index 8db54e1..87c29ad 100644 --- a/src/interfaces/Argument.ts +++ b/src/interfaces/Argument.ts @@ -1,5 +1,8 @@ +import { Usage } from "./Usage"; + export type ArgumentObject = { type?: string, [others: string]: any; }; -export type Argument = string | ArgumentObject; \ No newline at end of file + +export type Argument = string | ArgumentObject | Usage; \ No newline at end of file diff --git a/src/interfaces/Command.ts b/src/interfaces/Command.ts index 2649485..cca8f16 100644 --- a/src/interfaces/Command.ts +++ b/src/interfaces/Command.ts @@ -3,4 +3,8 @@ import { MaybePromise } from "../types"; export default interface Command { name: string, run(): MaybePromise, +}; + +export interface CommandArgs { + args: any[], }; \ No newline at end of file diff --git a/src/interfaces/ExecutorContext.ts b/src/interfaces/ExecutorContext.ts index 6910593..35dc735 100644 --- a/src/interfaces/ExecutorContext.ts +++ b/src/interfaces/ExecutorContext.ts @@ -5,14 +5,14 @@ import Command from "./Command"; export type BaseExecutorContext = { handler: CommandHandler, rawInput: string, - input: string, - ctx: object, + //input: string, + //ctx: object, [others: string]: any; }; -export type ExecutorContextCommand = BaseExecutorContext & { +type ExecutorContextCommand = { commandName: string, command: Command, }; -export type ExecutorContext = ExecutorContextCommand; \ No newline at end of file +export type ExecutorContext = BaseExecutorContext & Partial; \ No newline at end of file diff --git a/src/interfaces/Middleware.ts b/src/interfaces/Middleware.ts index 0f046bd..4b309b9 100644 --- a/src/interfaces/Middleware.ts +++ b/src/interfaces/Middleware.ts @@ -1,3 +1,10 @@ +import { MaybePromise } from "../types"; +import { ExecutorContext } from "./ExecutorContext"; + export default interface Middleware { id: string, + run: (ctx: ExecutorContext, next: (() => void)) => MaybePromise, + before?: string, + after?: string, + requires?: string[], }; \ No newline at end of file diff --git a/src/interfaces/Usage.ts b/src/interfaces/Usage.ts index 4dacf8e..28a4b7b 100644 --- a/src/interfaces/Usage.ts +++ b/src/interfaces/Usage.ts @@ -1,14 +1,24 @@ import StringReader from "../classes/StringReader"; import { MaybePromise } from "../types"; -import { ExecutorContextCommand } from "./ExecutorContext"; +import { ExecutorContext } from "./ExecutorContext"; -interface UsageContext extends ExecutorContextCommand { - argumentName: string, +export interface ReaderContext extends ExecutorContext { reader: StringReader, options: O, + throw: (code: string) => never, } -export default interface Usage { +export interface ValueContext extends ReaderContext { + value: T, +} + +// ? ...?? wha +export type ParserUsage = { + parse?(ctx: ValueContext): MaybePromise, +} & Usage; + +export interface Usage { type?: string, - parse(ctx: UsageContext): MaybePromise, + read?(ctx: ReaderContext): MaybePromise, + parse?(ctx: ValueContext): MaybePromise, } \ No newline at end of file diff --git a/src/middlewares/ResolveCommand.ts b/src/middlewares/ResolveCommand.ts new file mode 100644 index 0000000..f21a3a5 --- /dev/null +++ b/src/middlewares/ResolveCommand.ts @@ -0,0 +1,12 @@ +import Middleware from "../interfaces/Middleware"; + +const ResolveCommand = (): Middleware => { + return { + id: "resolveCommand", + run(ctx, next) { + + }, + }; +}; + +export default ResolveCommand; \ No newline at end of file diff --git a/src/usages/index.ts b/src/usages/index.ts new file mode 100644 index 0000000..9f5af24 --- /dev/null +++ b/src/usages/index.ts @@ -0,0 +1,5 @@ +import { TextUsage } from "./text"; + +export { + TextUsage +}; \ No newline at end of file diff --git a/src/usages/text.ts b/src/usages/text.ts index 92c3147..ac5386b 100644 --- a/src/usages/text.ts +++ b/src/usages/text.ts @@ -1,4 +1,5 @@ -import Usage from "../interfaces/Usage"; +import { ExecutorContext } from "../interfaces/ExecutorContext"; +import { Usage, ValueContext } from "../interfaces/Usage"; type TextUsageOptions = Partial<{ max: number, @@ -6,10 +7,29 @@ type TextUsageOptions = Partial<{ regex: RegExp, }>; +let quotes = [`"`, `'`]; + const TextUsage: Usage = { - parse(ctx) { - let str = ctx.reader.readUntil(" "); - if(ctx.options.max && str.length > ctx.options.max) + read(ctx) { + let q = ctx.reader.peekChar(); + if(quotes.includes(q)) { + q = ctx.reader.readChar(); + let str = ctx.reader.readUntil(q); + ctx.reader.readChar(); + return str; + } else { + return ctx.reader.readUntil(" "); + }; + }, + // thanks typescript + parse(ctx: ValueContext) { + let str = ctx.value; + + if(ctx.options.max && str.length > ctx.options.max) ctx.throw("TOO_LONG"); + if(ctx.options.min && str.length < ctx.options.min) ctx.throw("TOO_SHORT"); + if(ctx.options.regex && !ctx.options.regex.test(str)) ctx.throw("REGEX_MISMATCH"); + + return str; }, }; From 1f1ddd9618c80a0fe86c367cf08b77bdad311a5e Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Tue, 23 Jan 2024 21:17:18 +0300 Subject: [PATCH 04/16] =?UTF-8?q?uhhh=20base=20stuff=20:+1:=F0=9F=91=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- .vscode/settings.json | 3 + examples/consoleExample.js | 117 ---------- examples/middlewareExample.js | 122 ---------- package.json | 100 ++++---- plugins/discord.js | 199 ---------------- plugins/permissions.d.ts | 18 -- plugins/permissions.js | 78 ------- pnpm-lock.yaml | 297 ++++++++++++++++++++++++ src/ArgumentParser.ts | 131 ----------- src/Arguments.ts | 5 + src/Command.ts | 7 + src/CommandHandler.ts | 53 ++--- src/Context.ts | 6 + src/Middleware.ts | 10 + src/classes/InternalError.ts | 11 - src/classes/StringReader.ts | 50 ---- src/classes/UsageError.ts | 15 -- src/index.ts | 19 +- src/interfaces/Argument.ts | 8 - src/interfaces/Command.ts | 10 - src/interfaces/CommandHandlerOptions.ts | 3 - src/interfaces/ExecutorContext.ts | 18 -- src/interfaces/Middleware.ts | 10 - src/interfaces/Usage.ts | 24 -- src/interfaces/UsageResult.ts | 8 - src/middlewares/CommandReplier.ts | 28 +++ src/middlewares/CommandResolver.ts | 20 ++ src/middlewares/ResolveCommand.ts | 12 - src/middlewares/StringSplit.ts | 21 ++ src/types.ts | 5 - src/usages/index.ts | 5 - src/usages/text.ts | 39 ---- tsconfig.json | 116 +-------- 34 files changed, 497 insertions(+), 1075 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 examples/consoleExample.js delete mode 100644 examples/middlewareExample.js delete mode 100644 plugins/discord.js delete mode 100644 plugins/permissions.d.ts delete mode 100644 plugins/permissions.js create mode 100644 pnpm-lock.yaml delete mode 100644 src/ArgumentParser.ts create mode 100644 src/Arguments.ts create mode 100644 src/Command.ts create mode 100644 src/Context.ts create mode 100644 src/Middleware.ts delete mode 100644 src/classes/InternalError.ts delete mode 100644 src/classes/StringReader.ts delete mode 100644 src/classes/UsageError.ts delete mode 100644 src/interfaces/Argument.ts delete mode 100644 src/interfaces/Command.ts delete mode 100644 src/interfaces/CommandHandlerOptions.ts delete mode 100644 src/interfaces/ExecutorContext.ts delete mode 100644 src/interfaces/Middleware.ts delete mode 100644 src/interfaces/Usage.ts delete mode 100644 src/interfaces/UsageResult.ts create mode 100644 src/middlewares/CommandReplier.ts create mode 100644 src/middlewares/CommandResolver.ts delete mode 100644 src/middlewares/ResolveCommand.ts create mode 100644 src/middlewares/StringSplit.ts delete mode 100644 src/types.ts delete mode 100644 src/usages/index.ts delete mode 100644 src/usages/text.ts diff --git a/.gitignore b/.gitignore index 25c8fdb..da3366d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -node_modules -package-lock.json \ No newline at end of file +**/node_modules/ +package-lock.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..00ad71f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/examples/consoleExample.js b/examples/consoleExample.js deleted file mode 100644 index 12f390a..0000000 --- a/examples/consoleExample.js +++ /dev/null @@ -1,117 +0,0 @@ -import { CommandHandler } from "../src/index.js"; - -/* -This example is a simple console commands handler -*/ - -let username = "guest"; - -let handler = new CommandHandler({ - prefix: "", - buildArguments: (build) => [build.args], -}); - -handler.on("invalidUsage", ({ command, errors }) => { - console.log("/!\\ Invalid Usage!"); - console.log("Usage: " + handler.prettyPrint(command)); - console.log(errors.map((x) => "- " + x.message).join("\n")); -}); - -handler.on("failedChecks", ({ checks }) => { - console.log("(x) Error: Failed Checks:"); - console.log(checks.map((x) => "- " + x.message).join("\n")); -}); - -// -- commands -- - -handler.registerCommand({ - name: "help", - desc: "Shows commands", - async run(args) { - handler.Commands.forEach((cmd) => { - console.log("> " + cmd.name + " : " + cmd.desc); - if (cmd.args && cmd.args.length) - console.log(" Usage: " + handler.prettyPrint(cmd)); - }); - }, -}); - -handler.registerCommand({ - name: "say", - desc: "Repeats your words", - args: [ - { - type: "text", - rest: true, - }, - ], - async run([text]) { - // Because rest: true, it all gets collected to the first element - console.log(text); - }, -}); - -handler.registerCommand({ - name: "add", - desc: "Add two numbers", - args: [ - { - type: "number", - name: "a", - }, - { - type: "number", - name: "b", - }, - ], - async run([a, b]) { - let sum = a + b; - console.log(a + " + " + b + " = " + sum); - }, -}); - -handler.registerCommand({ - name: "exit", - desc: "Exit this example", - async run() { - console.log("OK, bye!"); - process.exit(); - }, -}); - -handler.registerCommand({ - name: "su", - desc: "Switch user", - args: ["uname:string"], - async run([uname]) { - username = uname; - console.log("Welcome back, " + username + "!"); - }, -}); - -handler.registerCommand({ - name: "make_sandwich", - desc: "No (unless you're 'root')", - checks: [ - async () => { - if (username == "root") { - // Okay. - return { pass: true }; - } else { - return { pass: false, message: "What? Make it yourself." }; - } - }, - ], - async run() { - console.log("Ok, heres your virtual sandwich:"); - console.log(" 🥪 "); - }, -}); - -var stdin = process.openStdin(); -stdin.addListener("data", (d) => { - let input = d.toString().trim(); - handler.run(input); -}); - -handler.run("help"); diff --git a/examples/middlewareExample.js b/examples/middlewareExample.js deleted file mode 100644 index 83d9972..0000000 --- a/examples/middlewareExample.js +++ /dev/null @@ -1,122 +0,0 @@ -import { CommandHandler } from "../src/index.js"; - -/* -This example is like consoleExample.js but with middlewares -*/ - -let handler = new CommandHandler(); - -handler.buildArguments = ({ ctx, args }) => [ctx, args]; - -// Initialize a dummy database -let dummyDB = { - dennis: 12, - may: 20, - skyrina: 3, - voltrex: 5, - julia: 16, - guest: 5, -}; - -let currentUser = "guest"; - -// The middleware -handler.use({ - id: "dummydb", - before: "run", - run(execCtx, next) { - execCtx.ctx.balance = dummyDB[currentUser]; - execCtx.ctx.setBalance = (x) => (dummyDB[currentUser] = x); - execCtx.ctx.setBalanceOf = (u, x) => (dummyDB[u] = x); - execCtx.ctx.getBalances = () => dummyDB; - - next(); - }, -}); - -// Pre-defined checks: - -const Checks = { - /** - * Generate a command check - * @param {number} n Index of the argument to check for an user - * @returns {import("../src").CommandCheck} - */ - userMustExist: (n) => { - return async (ctx, args) => { - if (Object.keys(dummyDB).includes(args[n])) { - return { pass: true }; - } else { - return { pass: false, message: "User doesn't exist" }; - } - }; - }, -}; - -handler.addCommand({ - name: "baltop", - desc: "List top balances", - run(ctx, args) { - console.log("== Bal Top =="); - console.log( - Object.entries(ctx.getBalances()) - .map(([name, bal], i) => `${i + 1}. ${name}: ${bal}`) - .join("\n"), - ); - console.log("== Bal Top =="); - }, -}); - -handler.addCommand({ - name: "bal", - desc: "Shows your balance", - run(ctx, args) { - console.log("You have " + ctx.balance + " money."); - }, -}); - -handler.addCommand({ - name: "login", - args: ["user:string"], - checks: [Checks.userMustExist(0)], - desc: "Login as another user", - run(ctx, [user]) { - currentUser = user; - console.log(`Welcome back ${currentUser}`); - }, -}); - -handler.addCommand({ - name: "pay", - args: ["user:string", "amount:number"], - checks: [ - Checks.userMustExist(0), - async (ctx, [user, amount]) => { - // todo check if balance > amount - return { pass: true }; - }, - ], - run(ctx, [user, amount]) { - ctx.setBalanceOf(user, ctx.balance + amount); - ctx.setBalance(ctx.balance - amount); - }, -}); - -handler.registerCommand({ - name: "help", - desc: "this;", - async run() { - handler.Commands.forEach((cmd) => { - console.log("> " + cmd.name + " : " + cmd.desc); - if (cmd.args && cmd.args.length) - console.log(" Usage: " + handler.prettyPrint(cmd)); - }); - }, -}); - -var stdin = process.openStdin(); -stdin.addListener("data", (d) => { - let input = d.toString().trim(); - handler.run(input, {}); -}); -console.log("Type 'help' for a list of commands"); diff --git a/package.json b/package.json index 1b04c13..9c6983e 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,50 @@ { - "name": "string-commands", - "version": "1.1.1", - "description": "A powerful command handler and parser for all your needs. Includes checks, custom arguments, middlewares and more.", - "type": "module", - "main": "./src/index.js", - "exports": { - "discordjs": "./plugins/discord.js", - "default": "./src/index.js" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/TheAlan404/string-commands.git" - }, - "keywords": [ - "command-handler", - "command-handlers", - "command", - "handler", - "parser", - "commands", - "string-manipulation", - "command-pattern", - "command-line-parser", - "discord.js", - "discord" - ], - "author": "alan404", - "license": "GPL-3.0-or-later", - "bugs": { - "url": "https://github.com/TheAlan404/string-commands/issues" - }, - "homepage": "https://github.com/TheAlan404/string-commands#readme", - "devDependencies": { - "@types/jest": "^29.5.1", - "jest": "^29.5.0", - "prettier": "^2.6.2", - "ts-jest": "^29.1.0", - "typescript": "^5.0.4" - }, - "prettier": { - "semi": true, - "singleQuote": false, - "trailingComma": "all", - "htmlWhitespaceSensitivity": "ignore", - "useTabs": true, - "tabWidth": 4 - } -} + "name": "string-commands", + "version": "2.0.0", + "description": "A new way to handle commands", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "scripts": { + "tsx": "tsx" + }, + "homepage": "https://github.com/TheAlan404/string-commands#readme", + "devDependencies": { + "prettier": "^2.6.2", + "tsx": "^4.7.0", + "typescript": "^5.3.3" + }, + "prettier": { + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "htmlWhitespaceSensitivity": "ignore", + "useTabs": true, + "tabWidth": 4 + }, + "repository": { + "type": "git", + "url": "git+https://github.com/TheAlan404/string-commands.git" + }, + "keywords": [ + "command-handler", + "command-handlers", + "command", + "handler", + "parser", + "commands", + "string-manipulation", + "command-pattern", + "command-line-parser", + "discord.js", + "discord" + ], + "author": "alan404", + "license": "GPL-3.0-or-later", + "bugs": { + "url": "https://github.com/TheAlan404/string-commands/issues" + } +} \ No newline at end of file diff --git a/plugins/discord.js b/plugins/discord.js deleted file mode 100644 index f5e1cee..0000000 --- a/plugins/discord.js +++ /dev/null @@ -1,199 +0,0 @@ -import { CommandHandler } from "../src/CommandHandler.js"; -import { Client, channelMention, ChannelType } from "discord.js"; -import { Permissions } from "./permissions.js"; - -/** - * @type {Object} - */ -const DiscordUsages = { - _mentionable: { - type: "text", - async parse(ctx) { - return ctx.arg.replace(/[<@#!&>]/g, ""); - }, - }, - - user: { - type: "_mentionable", - async parse(ctx) { - let user = ctx.context.client.users - .fetch(ctx.arg) - .catch(() => null); - - if (!user) { - return ctx.fail(`${ctx.opts.bot ? "Bot" : "User"} not found!`); - } - - if (ctx.opts.bot && !user.bot) - return ctx.fail( - `${ctx.style.bold(user.username)} isn't a bot!`, - ); - else if (!opts.bot && user.bot) - return ctx.fail(`${ctx.style.bold(user.username)} is a bot!`); - - return { parsed: user }; - }, - }, - - member: { - type: "user", - async parse(ctx) { - const { guild } = ctx.context.message; - const member = await guild.members - .fetch(ctx.arg.id) - .catch(() => null); - - if (!member) - return ctx.fail( - `${ctx.style.bold( - ctx.arg.username, - )} isn't on ${ctx.style.bold(guild.name)}!`, - ); - - return { parsed: member }; - }, - }, - - role: { - type: "_mentionable", - async parse(ctx) { - const { guild } = ctx.context.message; - const role = await guild.roles.fetch(ctx.arg).catch(() => null); - - if (!role) return ctx.fail(`Role not found!`); - - return { parsed: role }; - }, - }, - - channel: { - type: "_mentionable", - async parse(ctx) { - const { guild } = ctx.context.message; - const channel = await guild.channels - .fetch(ctx.arg) - .catch(() => null); - - if (!channel) return ctx.fail(`Channel not found!`); - - if ( - ctx.opts.channelType !== undefined && - ctx.opts.channelType != channel.type - ) { - return ctx.fail( - `${channelMention(channel.id)} isn't ${ctx.style.bold( - ChannelType[ctx.opts.channelType], - )}`, - ); - } - - return { parsed: channel }; - }, - }, - - textChannel: { - type: "channel", - channelType: ChannelType.GuildText, - }, - - voiceChannel: { - type: "channel", - channelType: ChannelType.GuildVoice, - }, -}; - -const DiscordChecks = { - requirePermissions(permissions) {}, - - async requireGuildOwner(client, msg, args) { - return msg.guild.ownerId == msg.author.id - ? { - pass: true, - } - : { - pass: false, - message: "You must be the owner of this guild", - }; - }, -}; - -/** - * @typedef {strcmd.CommandHandlerOptions} DiscordCommandHandlerOptions - */ - -class DiscordCommandHandler extends CommandHandler { - /** - * - * @param {Client} client The discord client - * @param {DiscordCommandHandlerOptions} opts - Options for the command handler - */ - constructor(client, opts = {}) { - super( - Object.assign( - { - argumentParser: { - styling: { - arg: (x) => `\`${x}\``, - bold: (x) => `**${x}**`, - }, - }, - }, - opts, - ), - ); - this.client = client; - this.client.handler = this; - - for (let [id, usage] of Object.entries(DiscordUsages)) - this.registerUsage(id, usage); - } - - buildArguments({ args, ctx }) { - return [ctx.client, ctx.message, args]; - } - - // handles discord.js messages - handleMessage(msg) { - this.run(msg.content, { - message: msg, - client: this.client, - }); - } -} - -const DiscordPermissions = () => { - return [ - DiscordPermissions.userOnly(), - DiscordPermissions.botOnly(), - ]; -}; - -DiscordPermissions.userOnly = () => { - return Permissions({ - label: "user-permissions", - getPermissions: async (execCtx) => { - - }, - getPermissionsSource: async (execCtx) => { - - }, - }); -}; - -DiscordPermissions.botOnly = () => { - return Permissions({ - label: "bot-permissions", - getPermissions: async (execCtx) => { - - }, - getPermissionsSource: async (execCtx) => { - return execCtx.command.botpermissions || execCtx.command.botperms; - }, - }); -}; - -const GuildOnly = { - -}; - -export { DiscordCommandHandler, DiscordUsages }; diff --git a/plugins/permissions.d.ts b/plugins/permissions.d.ts deleted file mode 100644 index 48cba54..0000000 --- a/plugins/permissions.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CommandHandlerMiddleware, BasicExecutorContext } from "../src/index"; - -type PermissionsOptions = { - getPermissions?: (ctx: BasicExecutorContext) => Promise, - getPermissionsSource?: (ctx: BasicExecutorContext) => Promise, -}; - -export type Permissions = (opts?: PermissionsOptions) => CommandHandlerMiddleware; - -export type Permissions = { - check: (required: string[] | string, posessed: string[] | string) => { - passed: true, - } | { - passed: false, - unmet: string[], - }; - compare: (target: string, source: string) => boolean; -}; \ No newline at end of file diff --git a/plugins/permissions.js b/plugins/permissions.js deleted file mode 100644 index 502e52c..0000000 --- a/plugins/permissions.js +++ /dev/null @@ -1,78 +0,0 @@ -const Permissions = (opts = {}) => { - opts = Object.assign(opts, { - getPermissions: async (execCtx) => { - return execCtx.ctx?.user?.permissions || execCtx.ctx?.permissions || execCtx.ctx?.perms; - }, - getPermissionsSource: async (execCtx) => { - return execCtx.command.permissions || execCtx.command.perms; - }, - label: "", - }); - - return { - before: "checks", - id: "permissions", - run: (execCtx, next) => { - let required = opts.getPermissionsSource(execCtx) || []; - if(!required.length) return next(); - let posessed = opts.getPermissions(execCtx) || []; - let { passed, unmet } = Permissions.check(required, posessed); - - if(passed) { - return next(); - } else { - execCtx.handler.emit("insufficentPermissions", { - ...execCtx, - required, - posessed, - unmet, - label: opts.label, - }); - }; - }, - }; -}; - -/** - * Checks for permissions - * @param {string|string[]} required The required permissions to check for - * @param {string|string[]} posessed The permissions that the user has - * @returns {{ passed: true } | { unmet: string[], passed: false }} - */ -Permissions.check = (required, posessed) => { - if(typeof required == "string") required = [required]; - if(typeof posessed == "string") posessed = [posessed]; - - let unmet = []; - - for(let perm of required) { - if(posessed.some((v) => Permissions.compare(perm, v))) { - continue; - }; - - unmet.push(perm); - }; - - return { - passed: unmet.length, - unmet, - }; -}; - -Permissions.compare = (target, source) => { - let a = target.split("."); - let b = source.split("."); - - let wildcard = false; - return a.every((v, i) => { - if(wildcard) return true; - if(b[i] == v) return true; - if(b[i] == "*") { - wildcard = true; - return true; - }; - return false; - }); -}; - -export { Permissions }; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..91beb68 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,297 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + prettier: + specifier: ^2.6.2 + version: 2.8.8 + tsx: + specifier: ^4.7.0 + version: 4.7.0 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + +packages: + + /@esbuild/aix-ppc64@0.19.11: + resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.19.11: + resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.11: + resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.11: + resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.11: + resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.11: + resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.11: + resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.11: + resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.11: + resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.11: + resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.11: + resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.11: + resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.11: + resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.11: + resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.11: + resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.11: + resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.11: + resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.11: + resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.11: + resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.11: + resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.11: + resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.11: + resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.11: + resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild@0.19.11: + resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.11 + '@esbuild/android-arm': 0.19.11 + '@esbuild/android-arm64': 0.19.11 + '@esbuild/android-x64': 0.19.11 + '@esbuild/darwin-arm64': 0.19.11 + '@esbuild/darwin-x64': 0.19.11 + '@esbuild/freebsd-arm64': 0.19.11 + '@esbuild/freebsd-x64': 0.19.11 + '@esbuild/linux-arm': 0.19.11 + '@esbuild/linux-arm64': 0.19.11 + '@esbuild/linux-ia32': 0.19.11 + '@esbuild/linux-loong64': 0.19.11 + '@esbuild/linux-mips64el': 0.19.11 + '@esbuild/linux-ppc64': 0.19.11 + '@esbuild/linux-riscv64': 0.19.11 + '@esbuild/linux-s390x': 0.19.11 + '@esbuild/linux-x64': 0.19.11 + '@esbuild/netbsd-x64': 0.19.11 + '@esbuild/openbsd-x64': 0.19.11 + '@esbuild/sunos-x64': 0.19.11 + '@esbuild/win32-arm64': 0.19.11 + '@esbuild/win32-ia32': 0.19.11 + '@esbuild/win32-x64': 0.19.11 + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + + /tsx@4.7.0: + resolution: {integrity: sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.19.11 + get-tsconfig: 4.7.2 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true diff --git a/src/ArgumentParser.ts b/src/ArgumentParser.ts deleted file mode 100644 index a6358c8..0000000 --- a/src/ArgumentParser.ts +++ /dev/null @@ -1,131 +0,0 @@ -import InternalError from "./classes/InternalError"; -import StringReader from "./classes/StringReader"; -import { Argument, ArgumentObject } from "./interfaces/Argument"; -import Command, { CommandArgs } from "./interfaces/Command"; -import { ExecutorContext } from "./interfaces/ExecutorContext"; -import { Usage, ReaderContext } from "./interfaces/Usage"; -import { TextUsage } from "./usages"; - -interface ArgumentParserOptions { - noBuiltins?: boolean, -}; - -class ArgumentParser { - usages: Map> = new Map(); - - constructor(opts?: ArgumentParserOptions & { usages?: Record> }) { - if(!opts) opts = {}; - if(!opts.noBuiltins) { - this.usages.set("text", TextUsage); - this.usages.set("string", TextUsage); - }; - }; - - resolveArgumentFromString(arg: string): ArgumentObject { - let rest = false; - let optional = false; - if (arg.endsWith("...")) { - rest = true; - arg = arg.slice(0, -3); - } - - let last = arg[arg.length - 1]; - if (arg[0] == "<") { - if (last !== ">") - throw new Error("Unclosed required argument identifier"); - arg = arg.slice(1).slice(0, -1); - } else if (arg[0] == "[") { - if (last !== "]") - throw new Error("Unclosed optional argument identifier"); - optional = true; - arg = arg.slice(1).slice(0, -1); - } - - let sp = arg.split(":"); - let type = sp.length === 2 ? sp[1] : sp[0]; - let name = sp.length === 2 ? sp[0] : null; - - if (!this.usages.has(type)) { - throw new Error( - `Can't resolve argument from string because Usage '${type}' doesn't exist!`, - ); - }; - - return { type, name, rest }; - } - - getUsage(id: string) { - return this.usages.get(id); - }; - - async parseArguments(ctx: ExecutorContext & { - rawArgs: string, - command: Command & CommandArgs, - }) { - let reader: StringReader = new StringReader(ctx.rawArgs); - - ctx.reader = reader; - - let args = []; - - for (let arg of ctx.command.args) { - // cleanup - ctx.value = null; - ctx.options = {}; - - await this.parseArgument(ctx, arg); - - args.push(ctx.value); - }; - - - }; - - getOptions(arg: ArgumentObject) { - let { parse, read, type, ...options } = arg; - return options; - } - - async parseArgument(ctx: ExecutorContext, arg: Argument): Promise { - if (typeof arg == "string") { - return await this.parseArgument(ctx, this.resolveArgumentFromString(arg)); - } - - ctx.options = { - ...ctx.options, - ...this.getOptions(arg), - }; - - if(arg.type && arg.type !== "native") { - let child = this.getUsage(arg.type); - - if(!child) throw new InternalError(ctx, `Usage '${arg.type}' doesn't exist`); - - ctx.options = this.getOptions({ - ...ctx.options, - ...child, - }); - - await this.parseArgument(ctx, child); - }; - - if(ctx.value === null && arg.read) { - ctx.value = arg.read(ctx); - }; - - if(arg.parse) { - ctx.value = await arg.parse(ctx); - }; - - return ctx.value; - - //let usage = this.getUsage(arg.type!); - //if(!usage) throw new Error("Can't find usage '" + arg.type + "'!"); - - //return await usage.parse(ctx as ReaderContext); - }; -}; - -export { - ArgumentParser, -}; \ No newline at end of file diff --git a/src/Arguments.ts b/src/Arguments.ts new file mode 100644 index 0000000..1eec4f5 --- /dev/null +++ b/src/Arguments.ts @@ -0,0 +1,5 @@ +export interface Argument { + +} + + diff --git a/src/Command.ts b/src/Command.ts new file mode 100644 index 0000000..1eb1fbf --- /dev/null +++ b/src/Command.ts @@ -0,0 +1,7 @@ +export interface Command { + name: string, + description: string, + run: (ctx: Context, args: any[]) => PromiseLike, +} + + diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts index ac3bfbe..969a183 100644 --- a/src/CommandHandler.ts +++ b/src/CommandHandler.ts @@ -1,45 +1,36 @@ -import { CommandHandlerOptions } from "./interfaces/CommandHandlerOptions"; -import { ExecutorContext } from "./interfaces/ExecutorContext"; -import Middleware from "./interfaces/Middleware"; +import { Command } from "./Command"; +import { BaseContext } from "./Context"; +import { Middleware } from "./Middleware"; -class CommandHandler { - middlewares: Middleware[] = []; +export class CommandHandler { + commands: Map> = new Map(); + middlewares: Middleware[] = []; - constructor(opts?: CommandHandlerOptions) { + constructor() { } - use(mw: Middleware | Middleware["run"] | (Middleware | Middleware["run"])[]): CommandHandler { - if(Array.isArray(mw)) { - for (const m of mw) { - this.use(m); - } - return this; - }; - - if(typeof mw == "function") mw = { - id: "anonymous " + this.middlewares.length, - before: "run", - run: mw, - }; - + use(mw: Middleware) { this.middlewares.push(mw); + } - return this; - }; + add(cmd: Command) { + this.commands.set(cmd.name, cmd); + } - async run(input: string, customContext: object) { - let ctx: ExecutorContext = { + run(input: string, ctx: Context) { + let baseCtx: BaseContext = { handler: this, - rawInput: input, input, - ...customContext, }; + let context: Context = { + ...baseCtx, + ...ctx, + }; + for (let mw of this.middlewares) { + context = await mw.run(context); + } } -}; - -export { - CommandHandler, -}; \ No newline at end of file +} diff --git a/src/Context.ts b/src/Context.ts new file mode 100644 index 0000000..e1f5b16 --- /dev/null +++ b/src/Context.ts @@ -0,0 +1,6 @@ +import { CommandHandler } from "./CommandHandler"; + +export interface BaseContext { + handler: CommandHandler; + input: string; +} diff --git a/src/Middleware.ts b/src/Middleware.ts new file mode 100644 index 0000000..b368f69 --- /dev/null +++ b/src/Middleware.ts @@ -0,0 +1,10 @@ +import { BaseContext } from "./Context"; + +export interface Middleware { + id: string, + run: (ctx: Context) => PromiseLike, +} + +export type MiddlewareLike = Middleware; + +export type MiddlewareFactory = (options: Options) => Middleware; diff --git a/src/classes/InternalError.ts b/src/classes/InternalError.ts deleted file mode 100644 index e60ac90..0000000 --- a/src/classes/InternalError.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ExecutorContext } from "../interfaces/ExecutorContext"; - -export default class InternalError extends Error { - ctx: ExecutorContext; - - constructor(ctx: ExecutorContext, message: string) { - super(message); - - this.ctx = ctx; - } -}; \ No newline at end of file diff --git a/src/classes/StringReader.ts b/src/classes/StringReader.ts deleted file mode 100644 index 650181b..0000000 --- a/src/classes/StringReader.ts +++ /dev/null @@ -1,50 +0,0 @@ -class StringReader { - input: string = ""; - index: number = 0; - - constructor(input: string) { - this.input = input; - } - - reset() { - this.index = 0; - } - - readChar() { - return this.input[this.index++]; - } - - peekChar() { - return this.input[this.index]; - } - - readUntil(predicate: ((char: string) => boolean) | string): string { - if(typeof predicate == "string") { - let str: string = predicate; - predicate = (x) => x == str; - }; - - let buf = ""; - - while(!this.end()) { - let char = this.readChar(); - if(predicate(char)) { - return buf; - } else { - buf += char; - }; - }; - - return ""; - } - - readLine() { - return this.readUntil('\n'); - } - - end() { - return this.index >= this.input.length; - } -} - -export default StringReader; \ No newline at end of file diff --git a/src/classes/UsageError.ts b/src/classes/UsageError.ts deleted file mode 100644 index a5deaa5..0000000 --- a/src/classes/UsageError.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ExecutorContext } from "../interfaces/ExecutorContext"; - -export default class UsageError extends Error { - ctx: ExecutorContext; - code: string; - meta: any; - - constructor(ctx: ExecutorContext, code: string, meta: any) { - super(); - - this.ctx = ctx; - this.code = code; - this.meta = meta; - } -}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index fcaf09c..1061e96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,20 @@ import { CommandHandler } from "./CommandHandler"; +import { CommandReplier } from "./middlewares/CommandReplier"; +import { CommandResolver } from "./middlewares/CommandResolver"; -export { CommandHandler }; \ No newline at end of file +let handler = new CommandHandler(); +handler.use(CommandReplier({ + reply(data, ctx) { + console.log(data.type); + }, +})); +handler.use(CommandResolver()); + +handler.add({ + name: "owo", + run(ctx, args) { + console.log("uwu"); + }, +}) + +handler.run("owo", {}); diff --git a/src/interfaces/Argument.ts b/src/interfaces/Argument.ts deleted file mode 100644 index 87c29ad..0000000 --- a/src/interfaces/Argument.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Usage } from "./Usage"; - -export type ArgumentObject = { - type?: string, - [others: string]: any; -}; - -export type Argument = string | ArgumentObject | Usage; \ No newline at end of file diff --git a/src/interfaces/Command.ts b/src/interfaces/Command.ts deleted file mode 100644 index cca8f16..0000000 --- a/src/interfaces/Command.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { MaybePromise } from "../types"; - -export default interface Command { - name: string, - run(): MaybePromise, -}; - -export interface CommandArgs { - args: any[], -}; \ No newline at end of file diff --git a/src/interfaces/CommandHandlerOptions.ts b/src/interfaces/CommandHandlerOptions.ts deleted file mode 100644 index 5151dca..0000000 --- a/src/interfaces/CommandHandlerOptions.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface CommandHandlerOptions { - -} \ No newline at end of file diff --git a/src/interfaces/ExecutorContext.ts b/src/interfaces/ExecutorContext.ts deleted file mode 100644 index 35dc735..0000000 --- a/src/interfaces/ExecutorContext.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CommandHandler } from "../CommandHandler"; -import StringReader from "../classes/StringReader"; -import Command from "./Command"; - -export type BaseExecutorContext = { - handler: CommandHandler, - rawInput: string, - //input: string, - //ctx: object, - [others: string]: any; -}; - -type ExecutorContextCommand = { - commandName: string, - command: Command, -}; - -export type ExecutorContext = BaseExecutorContext & Partial; \ No newline at end of file diff --git a/src/interfaces/Middleware.ts b/src/interfaces/Middleware.ts deleted file mode 100644 index 4b309b9..0000000 --- a/src/interfaces/Middleware.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { MaybePromise } from "../types"; -import { ExecutorContext } from "./ExecutorContext"; - -export default interface Middleware { - id: string, - run: (ctx: ExecutorContext, next: (() => void)) => MaybePromise, - before?: string, - after?: string, - requires?: string[], -}; \ No newline at end of file diff --git a/src/interfaces/Usage.ts b/src/interfaces/Usage.ts deleted file mode 100644 index 28a4b7b..0000000 --- a/src/interfaces/Usage.ts +++ /dev/null @@ -1,24 +0,0 @@ -import StringReader from "../classes/StringReader"; -import { MaybePromise } from "../types"; -import { ExecutorContext } from "./ExecutorContext"; - -export interface ReaderContext extends ExecutorContext { - reader: StringReader, - options: O, - throw: (code: string) => never, -} - -export interface ValueContext extends ReaderContext { - value: T, -} - -// ? ...?? wha -export type ParserUsage = { - parse?(ctx: ValueContext): MaybePromise, -} & Usage; - -export interface Usage { - type?: string, - read?(ctx: ReaderContext): MaybePromise, - parse?(ctx: ValueContext): MaybePromise, -} \ No newline at end of file diff --git a/src/interfaces/UsageResult.ts b/src/interfaces/UsageResult.ts deleted file mode 100644 index b001953..0000000 --- a/src/interfaces/UsageResult.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type UsageResult = { - fail: true, - message?: string, - [others: string]: any; -} | { - fail: false | undefined, - parsed: T, -} | T; \ No newline at end of file diff --git a/src/middlewares/CommandReplier.ts b/src/middlewares/CommandReplier.ts new file mode 100644 index 0000000..824a529 --- /dev/null +++ b/src/middlewares/CommandReplier.ts @@ -0,0 +1,28 @@ +import { BaseContext } from "../Context"; +import { MiddlewareFactory } from "../Middleware"; + +export interface ReplyData extends Record { + type: string, +} + +export interface CommandReplierCtx extends BaseContext { + reply: (data: T, ctx: BaseContext & CommandReplierCtx) => PromiseLike, +} + +export interface CommandReplierOptions { + reply: (data: T, ctx: BaseContext & CommandReplierCtx) => PromiseLike, +} + +export const CommandReplier: MiddlewareFactory, CommandReplierCtx> = ({ + reply, +}) => { + return { + id: "command-replier", + run(ctx) { + return { + ...ctx, + reply, + }; + }, + } +} diff --git a/src/middlewares/CommandResolver.ts b/src/middlewares/CommandResolver.ts new file mode 100644 index 0000000..254ae89 --- /dev/null +++ b/src/middlewares/CommandResolver.ts @@ -0,0 +1,20 @@ +import { Command } from "../Command"; +import { BaseContext } from "../Context"; +import { Middleware, MiddlewareFactory } from "../Middleware"; + +export interface CommandResolverCtx extends BaseContext { + command: Command, +} + +export const CommandResolver: MiddlewareFactory<{}, CommandResolverCtx> = () => ({ + id: "command-resolver", + run(ctx) { + let { handler } = ctx; + + let command = handler.commands.get(); + + return { + ...ctx + }; + }, +}); diff --git a/src/middlewares/ResolveCommand.ts b/src/middlewares/ResolveCommand.ts deleted file mode 100644 index f21a3a5..0000000 --- a/src/middlewares/ResolveCommand.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Middleware from "../interfaces/Middleware"; - -const ResolveCommand = (): Middleware => { - return { - id: "resolveCommand", - run(ctx, next) { - - }, - }; -}; - -export default ResolveCommand; \ No newline at end of file diff --git a/src/middlewares/StringSplit.ts b/src/middlewares/StringSplit.ts new file mode 100644 index 0000000..0e2725d --- /dev/null +++ b/src/middlewares/StringSplit.ts @@ -0,0 +1,21 @@ +import { Command } from "../Command"; +import { BaseContext } from "../Context"; +import { Middleware, MiddlewareFactory } from "../Middleware"; + +export interface SplitStringCtx extends BaseContext { + commandName: string, + commandArguments: string, +} + +export const SplitString: MiddlewareFactory<{}, SplitStringCtx> = () => ({ + id: "split-string", + run(ctx) { + let { input } = ctx; + + let command = handler.commands.get(); + + return { + ...ctx + }; + }, +}); diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 0ba7680..0000000 --- a/src/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Command from "./interfaces/Command"; - -export type MaybePromise = Promise | T; -export type Constructor = new (...args: any[]) => T; -export type MaybeCommand = Constructor | {default: Constructor} | {[k: string]: Constructor}; diff --git a/src/usages/index.ts b/src/usages/index.ts deleted file mode 100644 index 9f5af24..0000000 --- a/src/usages/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { TextUsage } from "./text"; - -export { - TextUsage -}; \ No newline at end of file diff --git a/src/usages/text.ts b/src/usages/text.ts deleted file mode 100644 index ac5386b..0000000 --- a/src/usages/text.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ExecutorContext } from "../interfaces/ExecutorContext"; -import { Usage, ValueContext } from "../interfaces/Usage"; - -type TextUsageOptions = Partial<{ - max: number, - min: number, - regex: RegExp, -}>; - -let quotes = [`"`, `'`]; - -const TextUsage: Usage = { - read(ctx) { - let q = ctx.reader.peekChar(); - if(quotes.includes(q)) { - q = ctx.reader.readChar(); - let str = ctx.reader.readUntil(q); - ctx.reader.readChar(); - return str; - } else { - return ctx.reader.readUntil(" "); - }; - }, - // thanks typescript - parse(ctx: ValueContext) { - let str = ctx.value; - - if(ctx.options.max && str.length > ctx.options.max) ctx.throw("TOO_LONG"); - if(ctx.options.min && str.length < ctx.options.min) ctx.throw("TOO_SHORT"); - if(ctx.options.regex && !ctx.options.regex.test(str)) ctx.throw("REGEX_MISMATCH"); - - return str; - }, -}; - -export { - TextUsage, - TextUsageOptions, -}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e075f97..59c3ee1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,109 +1,11 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + "compilerOptions": { + "module": "ESNext", + "target": "ESNext", + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src/**/*" + ] } From 2a45c77ea1eec0b636534cf2972128373feaaa70 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Thu, 25 Jan 2024 19:52:42 +0300 Subject: [PATCH 05/16] a start? --- src/Command.ts | 2 +- src/CommandHandler.ts | 30 ++++++++++++------- src/Context.ts | 2 +- src/Middleware.ts | 11 ++++--- src/index.ts | 27 ++++++++++------- src/middlewares/CommandExecutor.ts | 18 +++++++++++ src/middlewares/CommandReplier.ts | 18 ----------- src/middlewares/CommandResolver.ts | 28 +++++++++++++---- src/middlewares/ContextStatic.ts | 15 ++++++++++ .../{StringSplit.ts => SplitString.ts} | 10 ++++--- 10 files changed, 107 insertions(+), 54 deletions(-) create mode 100644 src/middlewares/CommandExecutor.ts create mode 100644 src/middlewares/ContextStatic.ts rename src/middlewares/{StringSplit.ts => SplitString.ts} (57%) diff --git a/src/Command.ts b/src/Command.ts index 1eb1fbf..386d668 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -1,6 +1,6 @@ export interface Command { name: string, - description: string, + description?: string, run: (ctx: Context, args: any[]) => PromiseLike, } diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts index 969a183..913fd10 100644 --- a/src/CommandHandler.ts +++ b/src/CommandHandler.ts @@ -2,35 +2,45 @@ import { Command } from "./Command"; import { BaseContext } from "./Context"; import { Middleware } from "./Middleware"; -export class CommandHandler { +type LastMiddlewareReturnType[]> = T extends [...infer _, infer Last] ? Last extends Middleware ? R : BaseContext : BaseContext; +type InputOf> = T extends Middleware ? I : never; +type OutputOf> = T extends Middleware ? O : never; + +export class CommandHandler< + Context extends LastMiddlewareReturnType, + MiddlewareTypes extends Middleware[] = [], +> { commands: Map> = new Map(); - middlewares: Middleware[] = []; + middlewares: [...MiddlewareTypes] = [] as any; constructor() { } - use(mw: Middleware) { + use(mw: Middleware): + CommandHandler< + LastMiddlewareReturnType<[...MiddlewareTypes, Middleware]>, + [...MiddlewareTypes, Middleware] + > { this.middlewares.push(mw); + // TODO: i have no idea what i am doing but it kinda works? + // @ts-ignore + return this; } add(cmd: Command) { this.commands.set(cmd.name, cmd); } - run(input: string, ctx: Context) { - let baseCtx: BaseContext = { + async run(input: string) { + let context: BaseContext = { handler: this, input, }; - let context: Context = { - ...baseCtx, - ...ctx, - }; - for (let mw of this.middlewares) { context = await mw.run(context); + if(!context) return; } } } diff --git a/src/Context.ts b/src/Context.ts index e1f5b16..d49af1a 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -1,6 +1,6 @@ import { CommandHandler } from "./CommandHandler"; export interface BaseContext { - handler: CommandHandler; + handler: typeof this extends CommandHandler ? CommandHandler : never; input: string; } diff --git a/src/Middleware.ts b/src/Middleware.ts index b368f69..3bf762f 100644 --- a/src/Middleware.ts +++ b/src/Middleware.ts @@ -1,10 +1,13 @@ import { BaseContext } from "./Context"; -export interface Middleware { +export interface Middleware { id: string, - run: (ctx: Context) => PromiseLike, + run: (ctx: T) => PromiseLike, } -export type MiddlewareLike = Middleware; +export type MiddlewareLike = + Middleware + | MiddlewareLike[]; -export type MiddlewareFactory = (options: Options) => Middleware; +export type MiddlewareFactory = + (options: Options) => Middleware; diff --git a/src/index.ts b/src/index.ts index 1061e96..2a41cd8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,27 @@ import { CommandHandler } from "./CommandHandler"; -import { CommandReplier } from "./middlewares/CommandReplier"; +import { CommandExecutor } from "./middlewares/CommandExecutor"; import { CommandResolver } from "./middlewares/CommandResolver"; +import { ContextStatic } from "./middlewares/ContextStatic"; +import { SplitString } from "./middlewares/SplitString"; -let handler = new CommandHandler(); -handler.use(CommandReplier({ - reply(data, ctx) { - console.log(data.type); - }, -})); -handler.use(CommandResolver()); +let handler = new CommandHandler() + .use(SplitString()) + .use({ + id: "_", + async run(ctx) { + return { + ...ctx, + } + }, + }) + .use(CommandResolver()) + .use(CommandExecutor()) handler.add({ name: "owo", - run(ctx, args) { + async run(ctx, args) { console.log("uwu"); }, }) -handler.run("owo", {}); +handler.run("nya"); diff --git a/src/middlewares/CommandExecutor.ts b/src/middlewares/CommandExecutor.ts new file mode 100644 index 0000000..aaae20c --- /dev/null +++ b/src/middlewares/CommandExecutor.ts @@ -0,0 +1,18 @@ +import { Command } from "../Command"; +import { BaseContext } from "../Context"; +import { Middleware, MiddlewareFactory } from "../Middleware"; +import { CommandReplierCtx } from "./CommandReplier"; +import { CommandResolverCtx } from "./CommandResolver"; +import { SplitStringCtx } from "./SplitString"; + +export const CommandExecutor = () => ({ + id: "command-executor", + async run(ctx: T): Promise { + let { command } = ctx; + + // @ts-ignore + command.run(ctx, []); + + return ctx; + }, +}); diff --git a/src/middlewares/CommandReplier.ts b/src/middlewares/CommandReplier.ts index 824a529..8d7197c 100644 --- a/src/middlewares/CommandReplier.ts +++ b/src/middlewares/CommandReplier.ts @@ -8,21 +8,3 @@ export interface ReplyData extends Record { export interface CommandReplierCtx extends BaseContext { reply: (data: T, ctx: BaseContext & CommandReplierCtx) => PromiseLike, } - -export interface CommandReplierOptions { - reply: (data: T, ctx: BaseContext & CommandReplierCtx) => PromiseLike, -} - -export const CommandReplier: MiddlewareFactory, CommandReplierCtx> = ({ - reply, -}) => { - return { - id: "command-replier", - run(ctx) { - return { - ...ctx, - reply, - }; - }, - } -} diff --git a/src/middlewares/CommandResolver.ts b/src/middlewares/CommandResolver.ts index 254ae89..0fbec2e 100644 --- a/src/middlewares/CommandResolver.ts +++ b/src/middlewares/CommandResolver.ts @@ -1,20 +1,36 @@ import { Command } from "../Command"; import { BaseContext } from "../Context"; import { Middleware, MiddlewareFactory } from "../Middleware"; +import { CommandReplierCtx } from "./CommandReplier"; +import { SplitStringCtx } from "./SplitString"; -export interface CommandResolverCtx extends BaseContext { +export type ReplyCommandNotFound = { + type: "commandNotFound", + commandName: string, +}; + +export interface CommandResolverCtx { command: Command, } -export const CommandResolver: MiddlewareFactory<{}, CommandResolverCtx> = () => ({ +export const CommandResolver = () => ({ id: "command-resolver", - run(ctx) { - let { handler } = ctx; + async run)>(ctx: T): Promise { + let { handler, commandName, reply } = ctx; - let command = handler.commands.get(); + if(!handler.commands.has(commandName)) { + reply?.({ + type: "commandNotFound", + commandName, + }, ctx); + return; + } + + let command = handler.commands.get(commandName); return { - ...ctx + ...ctx, + command, }; }, }); diff --git a/src/middlewares/ContextStatic.ts b/src/middlewares/ContextStatic.ts new file mode 100644 index 0000000..d30bfd7 --- /dev/null +++ b/src/middlewares/ContextStatic.ts @@ -0,0 +1,15 @@ +import { BaseContext } from "../Context"; +import { MiddlewareFactory } from "../Middleware"; + +export const ContextStatic = < + T extends Record, + B extends BaseContext +>(obj: T) => ({ + id: "_", + run: async (ctx: B): Promise => { + return ({ + ...ctx, + ...obj, + }) + }, +}); diff --git a/src/middlewares/StringSplit.ts b/src/middlewares/SplitString.ts similarity index 57% rename from src/middlewares/StringSplit.ts rename to src/middlewares/SplitString.ts index 0e2725d..0db9360 100644 --- a/src/middlewares/StringSplit.ts +++ b/src/middlewares/SplitString.ts @@ -7,15 +7,17 @@ export interface SplitStringCtx extends BaseContext { commandArguments: string, } -export const SplitString: MiddlewareFactory<{}, SplitStringCtx> = () => ({ +export const SplitString: MiddlewareFactory = () => ({ id: "split-string", - run(ctx) { + async run(ctx) { let { input } = ctx; - let command = handler.commands.get(); + let [commandName, ...args] = input.split(" "); return { - ...ctx + ...ctx, + commandName, + commandArguments: args.join(" "), }; }, }); From e7ebfabc3068ef6d2225116115495a4b84c66134 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Thu, 25 Jan 2024 20:02:34 +0300 Subject: [PATCH 06/16] fix types :3 --- src/index.ts | 11 ++--------- src/middlewares/CommandReplier.ts | 2 +- src/middlewares/CommandResolver.ts | 2 +- src/middlewares/ContextStatic.ts | 11 ++++------- src/middlewares/SplitString.ts | 6 +++--- 5 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2a41cd8..d61fa90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,16 +6,9 @@ import { SplitString } from "./middlewares/SplitString"; let handler = new CommandHandler() .use(SplitString()) - .use({ - id: "_", - async run(ctx) { - return { - ...ctx, - } - }, - }) .use(CommandResolver()) - .use(CommandExecutor()) + .use(ContextStatic({ a: 1 })) + .use(CommandExecutor()); handler.add({ name: "owo", diff --git a/src/middlewares/CommandReplier.ts b/src/middlewares/CommandReplier.ts index 8d7197c..cdd1228 100644 --- a/src/middlewares/CommandReplier.ts +++ b/src/middlewares/CommandReplier.ts @@ -5,6 +5,6 @@ export interface ReplyData extends Record { type: string, } -export interface CommandReplierCtx extends BaseContext { +export interface CommandReplierCtx { reply: (data: T, ctx: BaseContext & CommandReplierCtx) => PromiseLike, } diff --git a/src/middlewares/CommandResolver.ts b/src/middlewares/CommandResolver.ts index 0fbec2e..4fec950 100644 --- a/src/middlewares/CommandResolver.ts +++ b/src/middlewares/CommandResolver.ts @@ -15,7 +15,7 @@ export interface CommandResolverCtx { export const CommandResolver = () => ({ id: "command-resolver", - async run)>(ctx: T): Promise { + async run & BaseContext)>(ctx: T): Promise { let { handler, commandName, reply } = ctx; if(!handler.commands.has(commandName)) { diff --git a/src/middlewares/ContextStatic.ts b/src/middlewares/ContextStatic.ts index d30bfd7..e95d04f 100644 --- a/src/middlewares/ContextStatic.ts +++ b/src/middlewares/ContextStatic.ts @@ -3,13 +3,10 @@ import { MiddlewareFactory } from "../Middleware"; export const ContextStatic = < T extends Record, - B extends BaseContext >(obj: T) => ({ id: "_", - run: async (ctx: B): Promise => { - return ({ - ...ctx, - ...obj, - }) - }, + run: async (ctx: B): Promise => ({ + ...ctx, + ...obj, + }), }); diff --git a/src/middlewares/SplitString.ts b/src/middlewares/SplitString.ts index 0db9360..1e6b669 100644 --- a/src/middlewares/SplitString.ts +++ b/src/middlewares/SplitString.ts @@ -2,14 +2,14 @@ import { Command } from "../Command"; import { BaseContext } from "../Context"; import { Middleware, MiddlewareFactory } from "../Middleware"; -export interface SplitStringCtx extends BaseContext { +export interface SplitStringCtx { commandName: string, commandArguments: string, } -export const SplitString: MiddlewareFactory = () => ({ +export const SplitString= () => ({ id: "split-string", - async run(ctx) { + async run(ctx: T): Promise { let { input } = ctx; let [commandName, ...args] = input.split(" "); From ab462dade25b05aa8fea81b8c4b68b313d37da85 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Thu, 25 Jan 2024 21:48:01 +0300 Subject: [PATCH 07/16] its working :333333 --- README.md | 72 +++++++++++++----------------- examples/stdin.ts | 45 +++++++++++++++++++ package.json | 7 +++ pnpm-lock.yaml | 43 ++++++++++++++++++ src/Command.ts | 2 +- src/CommandHandler.ts | 21 +++++---- src/Middleware.ts | 6 ++- src/extensions/lowdb.ts | 26 +++++++++++ src/index.ts | 24 ++-------- src/middlewares/CommandExecutor.ts | 9 ++-- src/middlewares/CommandReplier.ts | 2 +- src/middlewares/CommandResolver.ts | 2 +- src/middlewares/index.ts | 5 +++ src/utils/forEachFile.ts | 14 ++++++ src/utils/recursiveImport.ts | 8 ++++ src/utils/recursiveUnload.ts | 8 ++++ src/utils/unloadModule.ts | 7 +++ tsconfig.json | 3 +- 18 files changed, 226 insertions(+), 78 deletions(-) create mode 100644 examples/stdin.ts create mode 100644 src/extensions/lowdb.ts create mode 100644 src/middlewares/index.ts create mode 100644 src/utils/forEachFile.ts create mode 100644 src/utils/recursiveImport.ts create mode 100644 src/utils/recursiveUnload.ts create mode 100644 src/utils/unloadModule.ts diff --git a/README.md b/README.md index 640d94b..1cb6e90 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,41 @@ -# string-commands +# string-commands v2 rewrite -A powerful command handler and parser for all your needs. Includes checks, custom arguments, middlewares and more. +String Commands is a new experimental command handler with differient ideas. -## Features +This `v2` branch is a full rewrite, in typescript. -- Easy to use -- Recursive folder importing -- Compatability with older commands -- Configurable messages (no defaults) -- Command checks (requirements) -- Middlewares -- Argument handling with custom argument support +## Goals -## Examples +- Customizability +- Extensible +- Async by default -For the best example please see [consoleExample.js](/examples/consoleExample.js) - -## Usage - -### Installing - -You can install this package using - -```sh -npm i string-commands -``` - -And then import using - -```js -import { CommandHandler } from "string-commands"; -``` - -### Documentation - -See these for docs: +## TODO -- [Command Handler](./docs/CommandHandler.md) -- [Commands](./docs/Commands.md) -- [Usages](./docs/Usages.md) -- [Middlewares](./docs/Middlewares.md) +- [ ] CommandHandler + - [x] run + - [x] add + - [x] use + - [ ] addFolder + - [ ] remove + - [ ] removeFolder +- [ ] Core middlewares + - [x] Split string + - [x] Command resolver + - [ ] Aliases + - [x] Executor + - [ ] Command checks + - [ ] Argument system +- [ ] Adapters + - [ ] lowdb + - [ ] i18next + - [ ] discord.js +- [ ] Utilities + - [ ] Pretty printer +- [ ] Documentation + - [ ] Core middlewares -## TODO -- [x] Complete typings -- [x] Middleware -- [ ] Subcommands -- [ ] Database Middlewares -- [ ] Permissions Middleware ## Changelog diff --git a/examples/stdin.ts b/examples/stdin.ts new file mode 100644 index 0000000..f84c28d --- /dev/null +++ b/examples/stdin.ts @@ -0,0 +1,45 @@ +import { createInterface } from "readline"; +import { CommandHandler } from "../src"; +import { CommandExecutor, CommandResolver, SplitString } from "../src/middlewares"; + +let handler = new CommandHandler() + .use(SplitString()) + .use(CommandResolver()) + .use(CommandExecutor()); + +handler.add({ + name: "help", + description: "Show a list of commands", + run({ handler }, []) { + console.log(`Available commands:`); + for (let [name, cmd] of handler.commands.entries()) { + console.log(` - ${name} : ${cmd.description}`); + } + }, +}); + +handler.add({ + name: "hello", + description: "world", + run({}, []) { + console.log("world!"); + }, +}); + +const rl = createInterface({ + input: process.stdin, + output: process.stdout, + prompt: "> ", +}) + +rl.on("line", async (line) => { + await handler.run(line.trim()); + rl.prompt(); +}); + +rl.on("close", () => { + console.log('exiting...'); + process.exit(0); +}); + +rl.prompt(); diff --git a/package.json b/package.json index 9c6983e..29fc3f0 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,14 @@ }, "homepage": "https://github.com/TheAlan404/string-commands#readme", "devDependencies": { + "@types/node": "^20.11.6", "prettier": "^2.6.2", "tsx": "^4.7.0", "typescript": "^5.3.3" }, + "optionalDependencies": { + "lowdb": "^7.0.1" + }, "prettier": { "semi": true, "singleQuote": false, @@ -46,5 +50,8 @@ "license": "GPL-3.0-or-later", "bugs": { "url": "https://github.com/TheAlan404/string-commands/issues" + }, + "dependencies": { + "tiny-typed-emitter": "^2.1.0" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91beb68..e6479ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,7 +4,20 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +dependencies: + tiny-typed-emitter: + specifier: ^2.1.0 + version: 2.1.0 + +optionalDependencies: + lowdb: + specifier: ^7.0.1 + version: 7.0.1 + devDependencies: + '@types/node': + specifier: ^20.11.6 + version: 20.11.6 prettier: specifier: ^2.6.2 version: 2.8.8 @@ -224,6 +237,12 @@ packages: dev: true optional: true + /@types/node@20.11.6: + resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} + dependencies: + undici-types: 5.26.5 + dev: true + /esbuild@0.19.11: resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} engines: {node: '>=12'} @@ -269,6 +288,15 @@ packages: resolve-pkg-maps: 1.0.0 dev: true + /lowdb@7.0.1: + resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==} + engines: {node: '>=18'} + requiresBuild: true + dependencies: + steno: 4.0.2 + dev: false + optional: true + /prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -279,6 +307,17 @@ packages: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true + /steno@4.0.2: + resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} + engines: {node: '>=18'} + requiresBuild: true + dev: false + optional: true + + /tiny-typed-emitter@2.1.0: + resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} + dev: false + /tsx@4.7.0: resolution: {integrity: sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==} engines: {node: '>=18.0.0'} @@ -295,3 +334,7 @@ packages: engines: {node: '>=14.17'} hasBin: true dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true diff --git a/src/Command.ts b/src/Command.ts index 386d668..b3f292a 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -1,7 +1,7 @@ export interface Command { name: string, description?: string, - run: (ctx: Context, args: any[]) => PromiseLike, + run: (ctx: Context, args: any[]) => Promise | void, } diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts index 913fd10..ec89f7f 100644 --- a/src/CommandHandler.ts +++ b/src/CommandHandler.ts @@ -1,20 +1,21 @@ import { Command } from "./Command"; import { BaseContext } from "./Context"; -import { Middleware } from "./Middleware"; +import { Middleware, LastMiddlewareReturnType } from "./Middleware"; +import { TypedEmitter } from "tiny-typed-emitter"; -type LastMiddlewareReturnType[]> = T extends [...infer _, infer Last] ? Last extends Middleware ? R : BaseContext : BaseContext; -type InputOf> = T extends Middleware ? I : never; -type OutputOf> = T extends Middleware ? O : never; +export interface CommandHandlerEvents { + earlyReturn: (ctx: Context) => void, +} export class CommandHandler< Context extends LastMiddlewareReturnType, MiddlewareTypes extends Middleware[] = [], -> { +> extends TypedEmitter> { commands: Map> = new Map(); middlewares: [...MiddlewareTypes] = [] as any; constructor() { - + super(); } use(mw: Middleware): @@ -30,6 +31,7 @@ export class CommandHandler< add(cmd: Command) { this.commands.set(cmd.name, cmd); + return this; } async run(input: string) { @@ -39,8 +41,11 @@ export class CommandHandler< }; for (let mw of this.middlewares) { - context = await mw.run(context); - if(!context) return; + let next = await mw.run(context); + if(!next) { + return; + } + context = next; } } } diff --git a/src/Middleware.ts b/src/Middleware.ts index 3bf762f..303702b 100644 --- a/src/Middleware.ts +++ b/src/Middleware.ts @@ -2,7 +2,7 @@ import { BaseContext } from "./Context"; export interface Middleware { id: string, - run: (ctx: T) => PromiseLike, + run: (ctx: T) => Promise | U, } export type MiddlewareLike = @@ -11,3 +11,7 @@ export type MiddlewareLike = export type MiddlewareFactory = (options: Options) => Middleware; +export type LastMiddlewareReturnType[]> = T extends [...infer _, infer Last] ? Last extends Middleware ? R : BaseContext : BaseContext; + +export type InputOf> = T extends Middleware ? I : never; +export type OutputOf> = T extends Middleware ? O : never; diff --git a/src/extensions/lowdb.ts b/src/extensions/lowdb.ts new file mode 100644 index 0000000..91e86a0 --- /dev/null +++ b/src/extensions/lowdb.ts @@ -0,0 +1,26 @@ +import { Low } from "lowdb"; +import { BaseContext } from "../Context"; + +export interface LowDBCtx { + db: Low, +} + +export const LowDBExtension = (low: Low) => { + low.read(); + + return { + id: "lowdb", + run: async (ctx: C): Promise> => ({ + ...ctx, + db: low, + }), + }; +}; + +export const LowDBSave = () => ({ + id: "lowdb-save", + run: async >(ctx: C): Promise => { + await ctx.db.write(); + return ctx; + } +}) diff --git a/src/index.ts b/src/index.ts index d61fa90..89c09f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,4 @@ -import { CommandHandler } from "./CommandHandler"; -import { CommandExecutor } from "./middlewares/CommandExecutor"; -import { CommandResolver } from "./middlewares/CommandResolver"; -import { ContextStatic } from "./middlewares/ContextStatic"; -import { SplitString } from "./middlewares/SplitString"; - -let handler = new CommandHandler() - .use(SplitString()) - .use(CommandResolver()) - .use(ContextStatic({ a: 1 })) - .use(CommandExecutor()); - -handler.add({ - name: "owo", - async run(ctx, args) { - console.log("uwu"); - }, -}) - -handler.run("nya"); +export * from "./CommandHandler"; +export * from "./Middleware"; +export * from "./Context"; +export * from "./Command"; diff --git a/src/middlewares/CommandExecutor.ts b/src/middlewares/CommandExecutor.ts index aaae20c..b1fd93b 100644 --- a/src/middlewares/CommandExecutor.ts +++ b/src/middlewares/CommandExecutor.ts @@ -7,11 +7,14 @@ import { SplitStringCtx } from "./SplitString"; export const CommandExecutor = () => ({ id: "command-executor", - async run(ctx: T): Promise { + async run(ctx: C): Promise { let { command } = ctx; - // @ts-ignore - command.run(ctx, []); + try { + await command.run(ctx, []); + } catch(e) { + // TODO + } return ctx; }, diff --git a/src/middlewares/CommandReplier.ts b/src/middlewares/CommandReplier.ts index cdd1228..26af15f 100644 --- a/src/middlewares/CommandReplier.ts +++ b/src/middlewares/CommandReplier.ts @@ -6,5 +6,5 @@ export interface ReplyData extends Record { } export interface CommandReplierCtx { - reply: (data: T, ctx: BaseContext & CommandReplierCtx) => PromiseLike, + reply?: (data: T, ctx: BaseContext & CommandReplierCtx) => PromiseLike, } diff --git a/src/middlewares/CommandResolver.ts b/src/middlewares/CommandResolver.ts index 4fec950..e3905ff 100644 --- a/src/middlewares/CommandResolver.ts +++ b/src/middlewares/CommandResolver.ts @@ -10,7 +10,7 @@ export type ReplyCommandNotFound = { }; export interface CommandResolverCtx { - command: Command, + command: Command, } export const CommandResolver = () => ({ diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 0000000..e99fbcc --- /dev/null +++ b/src/middlewares/index.ts @@ -0,0 +1,5 @@ +export * from "./CommandExecutor"; +export * from "./CommandReplier"; +export * from "./CommandResolver"; +export * from "./ContextStatic"; +export * from "./SplitString"; diff --git a/src/utils/forEachFile.ts b/src/utils/forEachFile.ts new file mode 100644 index 0000000..70890a0 --- /dev/null +++ b/src/utils/forEachFile.ts @@ -0,0 +1,14 @@ +import { Dirent } from "fs"; +import { readdir } from "fs/promises"; + +export const forEachFile = async (path: string, exts: string[], fn: (f: Dirent) => PromiseLike) => { + let files = await readdir(path, { withFileTypes: true }); + + for(let file of files) { + if(file.isDirectory()) { + await forEachFile(file.path, exts, fn); + } else if(exts.some(x => file.name.endsWith(x))) { + await fn(file); + } + } +} diff --git a/src/utils/recursiveImport.ts b/src/utils/recursiveImport.ts new file mode 100644 index 0000000..a310d70 --- /dev/null +++ b/src/utils/recursiveImport.ts @@ -0,0 +1,8 @@ +import { unloadModule } from "./unloadModule"; +import { forEachFile } from "./forEachFile"; + +export const recursiveImport = (path: string, exts: string[] = [".js"]) => forEachFile( + path, + exts, + async (file) => await import(file.path), +); diff --git a/src/utils/recursiveUnload.ts b/src/utils/recursiveUnload.ts new file mode 100644 index 0000000..26b4118 --- /dev/null +++ b/src/utils/recursiveUnload.ts @@ -0,0 +1,8 @@ +import { unloadModule } from "./unloadModule"; +import { forEachFile } from "./forEachFile"; + +export const recursiveUnload = (path: string, exts: string[] = [".js"]) => forEachFile( + path, + exts, + async (file) => unloadModule(file.path), +); diff --git a/src/utils/unloadModule.ts b/src/utils/unloadModule.ts new file mode 100644 index 0000000..a535294 --- /dev/null +++ b/src/utils/unloadModule.ts @@ -0,0 +1,7 @@ +export const unloadModule = (path: string) => { + let module = require.cache[path]; + if(module) { + for(let child of module.children) unloadModule(child.id); + } + delete require.cache[path]; +}; diff --git a/tsconfig.json b/tsconfig.json index 59c3ee1..9b41776 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "module": "ESNext", "target": "ESNext", "declaration": true, - "outDir": "./dist" + "outDir": "./dist", + "moduleResolution": "Bundler" }, "include": [ "src/**/*" From 8c4d42c41fb4fa4ea773ddb14f36047c491b31a0 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Fri, 26 Jan 2024 10:24:39 +0300 Subject: [PATCH 08/16] other stuff --- .env | 3 + examples/discordjs-slash.ts | 49 +++++ examples/stdin.ts | 7 +- package.json | 15 +- pnpm-lock.yaml | 280 ++++++++++++++++++++++++++++- src/Arguments.ts | 60 ++++++- src/Command.ts | 10 +- src/CommandHandler.ts | 24 ++- src/extensions/discordjs-slash.ts | 129 +++++++++++++ src/extensions/index.ts | 2 + src/index.ts | 2 + src/middlewares/ArgumentParser.ts | 15 ++ src/middlewares/CommandExecutor.ts | 7 +- src/middlewares/MultiPrefix.ts | 19 ++ src/middlewares/Prefix.ts | 19 ++ src/middlewares/SplitString.ts | 2 +- src/utils/StringReader.ts | 44 +++++ 17 files changed, 663 insertions(+), 24 deletions(-) create mode 100644 .env create mode 100644 examples/discordjs-slash.ts create mode 100644 src/extensions/discordjs-slash.ts create mode 100644 src/extensions/index.ts create mode 100644 src/middlewares/ArgumentParser.ts create mode 100644 src/middlewares/MultiPrefix.ts create mode 100644 src/middlewares/Prefix.ts create mode 100644 src/utils/StringReader.ts diff --git a/.env b/.env new file mode 100644 index 0000000..6b37fb2 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +TOKEN=NzYwNDI0ODQ0OTE0ODUxODQw.GRF83r.TIFEGw-xVbclSj7eX9Rw_QGVdRXCSPOGNz5ZjE +CLIENT_ID=760424844914851840 +GUILD_ID=1197520507617153064 diff --git a/examples/discordjs-slash.ts b/examples/discordjs-slash.ts new file mode 100644 index 0000000..5c823d6 --- /dev/null +++ b/examples/discordjs-slash.ts @@ -0,0 +1,49 @@ +import { Client } from "discord.js"; +import { DiscordSlashCommandHandler } from "../src/extensions/discordjs-slash"; +import { config } from "dotenv"; + +config(); + +const handler = new DiscordSlashCommandHandler({ + client: new Client({ + intents: [ + "Guilds" + ] + }), +}); + +handler.registerEvents(); + +handler.add({ + name: "test", + description: "tests stuff", + run({ interaction }) { + interaction.reply({ + content: "hello world", + ephemeral: true, + }); + }, +}) + + +handler.add({ + name: "list", + description: "manage lists", + subcommands: { + create: { + description: "creat", + run({ interaction }) { + interaction.reply({ content: "no" }) + }, + } + } +}) + +handler.publishCommandsGuild(process.env.GUILD_ID as string); + +handler.client.on("ready", () => { + console.log("Bot is ready!"); +}); + +handler.login(); + diff --git a/examples/stdin.ts b/examples/stdin.ts index f84c28d..2c73537 100644 --- a/examples/stdin.ts +++ b/examples/stdin.ts @@ -1,10 +1,11 @@ import { createInterface } from "readline"; import { CommandHandler } from "../src"; -import { CommandExecutor, CommandResolver, SplitString } from "../src/middlewares"; +import { CommandExecutor, CommandResolver, ContextStatic, SplitString } from "../src/middlewares"; let handler = new CommandHandler() .use(SplitString()) .use(CommandResolver()) + .use(ContextStatic({ appName: "exampleApp" })) .use(CommandExecutor()); handler.add({ @@ -33,7 +34,9 @@ const rl = createInterface({ }) rl.on("line", async (line) => { - await handler.run(line.trim()); + await handler.run({ + input: line.trim(), + }); rl.prompt(); }); diff --git a/package.json b/package.json index 29fc3f0..aa8fcc8 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,21 @@ "tsx": "tsx" }, "homepage": "https://github.com/TheAlan404/string-commands#readme", + "dependencies": { + "tiny-typed-emitter": "^2.1.0" + }, + "optionalDependencies": { + "discord.js": "^14.14.1", + "dotenv": "^16.4.1", + "lowdb": "^7.0.1", + "react-reconciler": "^0.29.0" + }, "devDependencies": { "@types/node": "^20.11.6", "prettier": "^2.6.2", "tsx": "^4.7.0", "typescript": "^5.3.3" }, - "optionalDependencies": { - "lowdb": "^7.0.1" - }, "prettier": { "semi": true, "singleQuote": false, @@ -50,8 +56,5 @@ "license": "GPL-3.0-or-later", "bugs": { "url": "https://github.com/TheAlan404/string-commands/issues" - }, - "dependencies": { - "tiny-typed-emitter": "^2.1.0" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6479ae..5680f3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,9 +10,18 @@ dependencies: version: 2.1.0 optionalDependencies: + discord.js: + specifier: ^14.14.1 + version: 14.14.1 + dotenv: + specifier: ^16.4.1 + version: 16.4.1 lowdb: specifier: ^7.0.1 version: 7.0.1 + react-reconciler: + specifier: ^0.29.0 + version: 0.29.0(react@18.2.0) devDependencies: '@types/node': @@ -30,6 +39,88 @@ devDependencies: packages: + /@discordjs/builders@1.7.0: + resolution: {integrity: sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==} + engines: {node: '>=16.11.0'} + requiresBuild: true + dependencies: + '@discordjs/formatters': 0.3.3 + '@discordjs/util': 1.0.2 + '@sapphire/shapeshift': 3.9.6 + discord-api-types: 0.37.61 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.3 + tslib: 2.6.2 + dev: false + optional: true + + /@discordjs/collection@1.5.3: + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + requiresBuild: true + dev: false + optional: true + + /@discordjs/collection@2.0.0: + resolution: {integrity: sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==} + engines: {node: '>=18'} + requiresBuild: true + dev: false + optional: true + + /@discordjs/formatters@0.3.3: + resolution: {integrity: sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==} + engines: {node: '>=16.11.0'} + requiresBuild: true + dependencies: + discord-api-types: 0.37.61 + dev: false + optional: true + + /@discordjs/rest@2.2.0: + resolution: {integrity: sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==} + engines: {node: '>=16.11.0'} + requiresBuild: true + dependencies: + '@discordjs/collection': 2.0.0 + '@discordjs/util': 1.0.2 + '@sapphire/async-queue': 1.5.2 + '@sapphire/snowflake': 3.5.1 + '@vladfrangu/async_event_emitter': 2.2.4 + discord-api-types: 0.37.61 + magic-bytes.js: 1.8.0 + tslib: 2.6.2 + undici: 5.27.2 + dev: false + optional: true + + /@discordjs/util@1.0.2: + resolution: {integrity: sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==} + engines: {node: '>=16.11.0'} + requiresBuild: true + dev: false + optional: true + + /@discordjs/ws@1.0.2: + resolution: {integrity: sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==} + engines: {node: '>=16.11.0'} + requiresBuild: true + dependencies: + '@discordjs/collection': 2.0.0 + '@discordjs/rest': 2.2.0 + '@discordjs/util': 1.0.2 + '@sapphire/async-queue': 1.5.2 + '@types/ws': 8.5.9 + '@vladfrangu/async_event_emitter': 2.2.4 + discord-api-types: 0.37.61 + tslib: 2.6.2 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + optional: true + /@esbuild/aix-ppc64@0.19.11: resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} engines: {node: '>=12'} @@ -237,11 +328,94 @@ packages: dev: true optional: true + /@fastify/busboy@2.1.0: + resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} + engines: {node: '>=14'} + requiresBuild: true + dev: false + optional: true + + /@sapphire/async-queue@1.5.2: + resolution: {integrity: sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + requiresBuild: true + dev: false + optional: true + + /@sapphire/shapeshift@3.9.6: + resolution: {integrity: sha512-4+Na/fxu2SEepZRb9z0dbsVh59QtwPuBg/UVaDib3av7ZY14b14+z09z6QVn0P6Dv6eOU2NDTsjIi0mbtgP56g==} + engines: {node: '>=v18'} + requiresBuild: true + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.17.21 + dev: false + optional: true + + /@sapphire/snowflake@3.5.1: + resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + requiresBuild: true + dev: false + optional: true + /@types/node@20.11.6: resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} dependencies: undici-types: 5.26.5 - dev: true + + /@types/ws@8.5.9: + resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} + requiresBuild: true + dependencies: + '@types/node': 20.11.6 + dev: false + optional: true + + /@vladfrangu/async_event_emitter@2.2.4: + resolution: {integrity: sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + requiresBuild: true + dev: false + optional: true + + /discord-api-types@0.37.61: + resolution: {integrity: sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==} + requiresBuild: true + dev: false + optional: true + + /discord.js@14.14.1: + resolution: {integrity: sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==} + engines: {node: '>=16.11.0'} + requiresBuild: true + dependencies: + '@discordjs/builders': 1.7.0 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.3.3 + '@discordjs/rest': 2.2.0 + '@discordjs/util': 1.0.2 + '@discordjs/ws': 1.0.2 + '@sapphire/snowflake': 3.5.1 + '@types/ws': 8.5.9 + discord-api-types: 0.37.61 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + tslib: 2.6.2 + undici: 5.27.2 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + optional: true + + /dotenv@16.4.1: + resolution: {integrity: sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==} + engines: {node: '>=12'} + requiresBuild: true + dev: false + optional: true /esbuild@0.19.11: resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} @@ -274,6 +448,12 @@ packages: '@esbuild/win32-x64': 0.19.11 dev: true + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + requiresBuild: true + dev: false + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -288,6 +468,32 @@ packages: resolve-pkg-maps: 1.0.0 dev: true + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + requiresBuild: true + dev: false + optional: true + + /lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + requiresBuild: true + dev: false + optional: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + requiresBuild: true + dev: false + optional: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + optional: true + /lowdb@7.0.1: resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==} engines: {node: '>=18'} @@ -297,16 +503,51 @@ packages: dev: false optional: true + /magic-bytes.js@1.8.0: + resolution: {integrity: sha512-lyWpfvNGVb5lu8YUAbER0+UMBTdR63w2mcSUlhhBTyVbxJvjgqwyAf3AZD6MprgK0uHuBoWXSDAMWLupX83o3Q==} + requiresBuild: true + dev: false + optional: true + /prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true dev: true + /react-reconciler@0.29.0(react@18.2.0): + resolution: {integrity: sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==} + engines: {node: '>=0.10.0'} + requiresBuild: true + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + optional: true + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + optional: true + /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + requiresBuild: true + dependencies: + loose-envify: 1.4.0 + dev: false + optional: true + /steno@4.0.2: resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} engines: {node: '>=18'} @@ -318,6 +559,18 @@ packages: resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} dev: false + /ts-mixer@6.0.3: + resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==} + requiresBuild: true + dev: false + optional: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + requiresBuild: true + dev: false + optional: true + /tsx@4.7.0: resolution: {integrity: sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==} engines: {node: '>=18.0.0'} @@ -337,4 +590,27 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true + + /undici@5.27.2: + resolution: {integrity: sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==} + engines: {node: '>=14.0'} + requiresBuild: true + dependencies: + '@fastify/busboy': 2.1.0 + dev: false + optional: true + + /ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + requiresBuild: true + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + optional: true diff --git a/src/Arguments.ts b/src/Arguments.ts index 1eec4f5..b32308d 100644 --- a/src/Arguments.ts +++ b/src/Arguments.ts @@ -1,5 +1,61 @@ -export interface Argument { +import { BaseContext } from "."; +import { StringReader } from "./utils/StringReader"; -} +export type ArgumentLike = Argument; +export type Argument< + S extends ParserStore, + T extends keyof S = "string", +> = { + type: T, +}; +export interface ParserStore { + [key: string]: Parser; +}; + +export type Parser< + S extends ParserStore, + Id extends string, + T extends (keyof S | void), + U, + Ctx, +> = ExtendingArgumentParser | ReaderArgumentParser; + +export type ExtendingArgumentParser< + S extends ParserStore, + Id extends string, + T extends (keyof S | void), + U, + Ctx, +> = { + id: Id, + extends?: T, + parse: (value: (ValueOf & Ctx)) => Promise | U, +}; + +export type ReaderArgumentParser< + S extends ParserStore, + Id extends string, + U, + Ctx, +> = { + id: Id, + read: (ctx: Ctx & { reader: StringReader }) => Promise | U, +}; + +type ValueOf = + I extends keyof S ? ( + S[I] extends Parser ? U : never + ) : ( + string + ); + +export const DefaultParserTypes: ParserStore = { + string: { + id: "string", + read({ reader }) { + return reader.rest(); + }, + } +}; diff --git a/src/Command.ts b/src/Command.ts index b3f292a..b9e79ff 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -1,7 +1,11 @@ -export interface Command { - name: string, +export interface Command< + Context, + Args = [], +> { + name?: string, description?: string, - run: (ctx: Context, args: any[]) => Promise | void, + run?: (ctx: Context, args: any[]) => Promise | void, + subcommands?: Record>, } diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts index ec89f7f..fbe3e45 100644 --- a/src/CommandHandler.ts +++ b/src/CommandHandler.ts @@ -4,11 +4,13 @@ import { Middleware, LastMiddlewareReturnType } from "./Middleware"; import { TypedEmitter } from "tiny-typed-emitter"; export interface CommandHandlerEvents { - earlyReturn: (ctx: Context) => void, + earlyReturn: (ctx: Context, id: string) => void, + commandError: (err: Error, ctx: Context) => void, + middlewareError: (err: Error, ctx: Context, id: string) => void, } export class CommandHandler< - Context extends LastMiddlewareReturnType, + Context extends BaseContext & LastMiddlewareReturnType, MiddlewareTypes extends Middleware[] = [], > extends TypedEmitter> { commands: Map> = new Map(); @@ -29,20 +31,32 @@ export class CommandHandler< return this; } + getMiddleware(id: Id): MiddlewareTypes[number] { + // @ts-ignore + return this.middlewares.find(mw => mw.id == id); + } + add(cmd: Command) { this.commands.set(cmd.name, cmd); return this; } - async run(input: string) { + async run(ctx: T) { let context: BaseContext = { handler: this, - input, + ...ctx, }; for (let mw of this.middlewares) { - let next = await mw.run(context); + let next; + try { + next = await mw.run(context); + } catch(err) { + this.emit("middlewareError", err, context as Context, mw.id); + return; + } if(!next) { + this.emit("earlyReturn", context as Context, mw.id); return; } context = next; diff --git a/src/extensions/discordjs-slash.ts b/src/extensions/discordjs-slash.ts new file mode 100644 index 0000000..4e43226 --- /dev/null +++ b/src/extensions/discordjs-slash.ts @@ -0,0 +1,129 @@ +import { CacheType, ChatInputCommandInteraction, Client, Events, Interaction, REST, RESTPostAPIChatInputApplicationCommandsJSONBody, Routes, SlashCommandBuilder } from "discord.js"; +import { BaseContext, Command, CommandHandler } from ".."; +import { CommandExecutor, CommandResolverCtx, ContextStatic } from "../middlewares"; + +export interface DiscordClientCtx { + client: Client, +}; + +export interface InteractionCtx { + interaction: ChatInputCommandInteraction, +} + +export class DiscordSlashCommandHandler< + Context extends BaseContext & DiscordClientCtx & InteractionCtx +> extends CommandHandler { + client: Client; + clientId: string; + rest: REST; + + constructor({ + client, + token, + clientId, + }: { + client: Client, + token?: string, + clientId?: string, + }) { + super(); + + this.client = client; + this.client.token = token || process.env.TOKEN; + this.clientId = clientId || process.env.CLIENT_ID; + this.use(ContextStatic({ client })) + .use({ + id: "discord-command-resolver", + run: async (ctx: T): Promise => { + return { + ...ctx, + command: ctx.handler.commands.get(ctx.interaction.commandName), + }; + }, + }) + .use(CommandExecutor()); + + this.rest = new REST() + .setToken(this.client.token); + } + + getSlashCommandData() { + const toData = (cmd: Command) => { + let builder = new SlashCommandBuilder(); + builder.setName(cmd.name); + builder.setDescription(cmd.description || ""); + + if (cmd.subcommands) { + for (let [name, subcommand] of Object.entries(cmd.subcommands)) { + let isGroup = !!subcommand.subcommands; + if (isGroup) { + builder.addSubcommandGroup( + (group) => { + group + .setName(name) + .setDescription(subcommand.description || ""); + + for (let [name, sub] of Object.entries(subcommand.subcommands)) { + group.addSubcommand( + b => b.setName(name).setDescription(sub.description || "") + ) + } + + return group; + } + ); + } else { + builder.addSubcommand( + b => b.setName(name).setDescription(subcommand.description || "") + ) + } + } + } + + return builder.toJSON(); + } + + let arr: RESTPostAPIChatInputApplicationCommandsJSONBody[] = []; + + for(let [name, cmd] of this.commands.entries()) { + arr.push(toData(cmd)); + } + + return arr; + } + + async publishCommandsGuild(guildId: string) { + let data = this.getSlashCommandData(); + + return await this.rest.put( + Routes.applicationGuildCommands(this.clientId, guildId), + { body: data } + ); + } + + async publishCommandsGlobal() { + let data = this.getSlashCommandData(); + + return await this.rest.put( + Routes.applicationCommands(this.clientId), + { body: data } + ); + } + + registerEvents() { + this.client.on(Events.InteractionCreate, this.onInteractionCreate.bind(this)); + } + + async onInteractionCreate(interaction: Interaction) { + if(interaction.isChatInputCommand()) { + this.run({ + input: interaction.commandName, + interaction, + }) + } + } + + async login() { + return await this.client.login(); + } +} diff --git a/src/extensions/index.ts b/src/extensions/index.ts new file mode 100644 index 0000000..e5a6e86 --- /dev/null +++ b/src/extensions/index.ts @@ -0,0 +1,2 @@ +export * from "./discordjs-slash"; +export * from "./lowdb"; diff --git a/src/index.ts b/src/index.ts index 89c09f4..eccdf91 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,5 @@ export * from "./CommandHandler"; export * from "./Middleware"; export * from "./Context"; export * from "./Command"; +export * as middlewares from "./middlewares"; +export * as extensions from "./extensions"; diff --git a/src/middlewares/ArgumentParser.ts b/src/middlewares/ArgumentParser.ts new file mode 100644 index 0000000..89bf0a7 --- /dev/null +++ b/src/middlewares/ArgumentParser.ts @@ -0,0 +1,15 @@ +import { CommandReplierCtx, CommandResolverCtx, SplitStringCtx } from "."; +import { BaseContext } from ".."; +import { ParserStore } from "../Arguments"; + +export type ReplyInvalidUsage = { + type: "invalidUsage", + commandName: string, +}; + +export const ArgumentParser = (usages: S) => ({ + id: "argument-parser", + run: async & SplitStringCtx>() => { + + } +}); diff --git a/src/middlewares/CommandExecutor.ts b/src/middlewares/CommandExecutor.ts index b1fd93b..2a0cea9 100644 --- a/src/middlewares/CommandExecutor.ts +++ b/src/middlewares/CommandExecutor.ts @@ -7,13 +7,14 @@ import { SplitStringCtx } from "./SplitString"; export const CommandExecutor = () => ({ id: "command-executor", - async run(ctx: C): Promise { - let { command } = ctx; + async run(ctx: C): Promise { + let { command, handler } = ctx; try { await command.run(ctx, []); } catch(e) { - // TODO + handler.emit("commandError", e, ctx); + return; } return ctx; diff --git a/src/middlewares/MultiPrefix.ts b/src/middlewares/MultiPrefix.ts new file mode 100644 index 0000000..1eae123 --- /dev/null +++ b/src/middlewares/MultiPrefix.ts @@ -0,0 +1,19 @@ +import { BaseContext } from "../Context"; + +export const MultiPrefix = ({ + prefixes = ["!"], +}: { + prefixes: string[], +}) => ({ + id: "multi-prefix", + async run(ctx: T): Promise { + let { input } = ctx; + + if(!prefixes.some(p => input.startsWith(p))) return; + + return { + ...ctx, + input: input.slice(prefixes.find(p => input.startsWith(p)).length), + }; + }, +}); diff --git a/src/middlewares/Prefix.ts b/src/middlewares/Prefix.ts new file mode 100644 index 0000000..44c3fbb --- /dev/null +++ b/src/middlewares/Prefix.ts @@ -0,0 +1,19 @@ +import { BaseContext } from "../Context"; + +export const Prefix = ({ + prefix = "!", +}: { + prefix: string, +}) => ({ + id: "prefix", + async run(ctx: T): Promise { + let { input } = ctx; + + if(!input.startsWith(prefix)) return; + + return { + ...ctx, + input: input.slice(prefix.length), + }; + }, +}); diff --git a/src/middlewares/SplitString.ts b/src/middlewares/SplitString.ts index 1e6b669..5406835 100644 --- a/src/middlewares/SplitString.ts +++ b/src/middlewares/SplitString.ts @@ -7,7 +7,7 @@ export interface SplitStringCtx { commandArguments: string, } -export const SplitString= () => ({ +export const SplitString = () => ({ id: "split-string", async run(ctx: T): Promise { let { input } = ctx; diff --git a/src/utils/StringReader.ts b/src/utils/StringReader.ts new file mode 100644 index 0000000..d708990 --- /dev/null +++ b/src/utils/StringReader.ts @@ -0,0 +1,44 @@ +export class StringReader { + string: string = ""; + index: number = 0; + + constructor(str: string) { + this.string = str; + this.reset(); + } + + reset() { + this.index = 0; + } + + eof() { + return this.string.length < this.index; + } + + readOne() { + return this.string[this.index++]; + } + + read(count = 1) { + let buf = ""; + + while(count--) { + if(this.eof()) break; + buf += this.readOne(); + } + + return buf; + } + + skipOne() { + this.readOne(); + } + + peekOne() { + return this.string[this.index]; + } + + rest() { + return this.string.slice(this.index); + } +} From 635980f1c512e772097aabe758f9a28df597be83 Mon Sep 17 00:00:00 2001 From: dennis <43997085+TheAlan404@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:27:33 +0300 Subject: [PATCH 09/16] dont leak tokens --- .env | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 6b37fb2..0000000 --- a/.env +++ /dev/null @@ -1,3 +0,0 @@ -TOKEN=NzYwNDI0ODQ0OTE0ODUxODQw.GRF83r.TIFEGw-xVbclSj7eX9Rw_QGVdRXCSPOGNz5ZjE -CLIENT_ID=760424844914851840 -GUILD_ID=1197520507617153064 From b06f1824b0f127c53a3aacf66394acf850cd43e5 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Fri, 26 Jan 2024 10:28:10 +0300 Subject: [PATCH 10/16] dont leak tokens fr... --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index da3366d..4d68ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/node_modules/ package-lock.json +.env From d298337be54a7231b8e09952fec3d40d5e841f9e Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Fri, 26 Jan 2024 11:07:50 +0300 Subject: [PATCH 11/16] add some docs too --- README.md | 153 +++++++++++++++++++++++++---- src/extensions/discordjs-slash.ts | 15 ++- src/middlewares/CommandExecutor.ts | 4 +- src/middlewares/CommandResolver.ts | 9 +- src/middlewares/Inspect.ts | 7 ++ src/middlewares/index.ts | 1 + src/utils/decorators.ts | 0 7 files changed, 164 insertions(+), 25 deletions(-) create mode 100644 src/middlewares/Inspect.ts create mode 100644 src/utils/decorators.ts diff --git a/README.md b/README.md index 1cb6e90..476a524 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,47 @@ This `v2` branch is a full rewrite, in typescript. - Extensible - Async by default +## Example + +See tested and working examples: +- [Console REPL](./examples/stdin.ts) +- [discord.js with Slash Commands](./examples/discordjs-slash.ts) + +```js +let handler = new CommandHandler(); + +// Set up middlewares depending on what you need: + +handler + .use(MultiPrefix({ prefixes: ["!", ".", "/"] })) + .use(SplitString()) + .use(CommandResolver()) + .use(CommandExecutor()) + +// You can also define your own middlewares + +let globalNumber = 0; +handler.use({ + id: "command-number", + run: (ctx) => ({ ..ctx, number: globalNumber++ }), +}) + +// Add your commands + +handler.add({ + name: "hello", + run: ({ number }) => { + console.log(`Hi! This is execution #${number}, provided by the custom middleware.`); + } +}) + +// and run them + +handler.run({ + input: "hello", +}) +``` + ## TODO - [ ] CommandHandler @@ -25,7 +66,9 @@ This `v2` branch is a full rewrite, in typescript. - [ ] Aliases - [x] Executor - [ ] Command checks - - [ ] Argument system +- [ ] Argument system + - [ ] reader impl + - [ ] extensible parsers - [ ] Adapters - [ ] lowdb - [ ] i18next @@ -35,30 +78,102 @@ This `v2` branch is a full rewrite, in typescript. - [ ] Documentation - [ ] Core middlewares +## Concepts + +### Context + +Command resolving, execution etc are all made possible using Contexts. + +The `BaseContext` contains `{ handler, input }` + +Every middleware gets the last context and adds/removes/modifies properties. + +For example, the `CommandResolver` middleware requires `{ commandName, handler }` in the context and adds `{ rootCommand, targetCommand }` to the context. + +## Docs + +### Middleware: Inspect + +**Options** `fn: (ctx: T) => void` + +Inspects the current context, useful for debugging + +```js +handler + // Logs { handler: CommandHandler, ... } + .use(Inspect()) + + // Custom function + .use(Inspect((ctx) => { ... })) +``` + +### Middleware: Prefix + +`input: string` => `input: string` + +**Options:** `{ prefix: string }` + +Ignore runs where the input does not start with `prefix` and strip it when it does. + +```js +handler + .use(Inspect()) // { input: "!help", ... } + .use(Prefix({ prefix: "!" })) + .use(Inspect()) // { input: "help", ... } + +handler.run({ input: "!help" }) +``` + +### Middleware: MultiPrefix + +`input: string` => `input: string` + +**Options:** `{ prefixes: string[] }` + +Same as `Prefix` middleware, but supports multiple. + +### Middleware: SplitString + +**Requires:** `{ input: string }` + +**Outputs:** `{ commandName: string, commandArgs: string }` + +Splits the first word of the input to be able to pass it into a command resolver + +```js +handler + .use(SplitString()) + .use(Inspect()) // { commandName: "roll", commandArgs: "1d6", ... } + +handler.run({ input: "roll 1d6" }) +``` + +### Middleware: ContextStatic + +**Options:** any object + +This utility middleware adds the properties given via options to the context. + +```js +handler + .use(ContextStatic({ a: 1 })) + .use(Inspect()) // { a: 1, ... } +``` +### Middleware: CommandExecutor -## Changelog +**Requires:** `{ targetCommand, handler }` -**v1.2.0:** +This middleware executes aka calls the `run` method of the command. -- More major changes! -- :warning: **BREAKING:** Command checks now use `ExecutorContext`! For compatability reasons, the runner args are still being kept in the function arguments, but you need to add a dummy argument at the start. Check the docs for more info. -- :warning: **BREAKING:** `ExecutorContext` got lots of renaming: - - `checks` => `failedChecks` +### Middleware: CommandResolver -**v1.1.0:** +**Requires:** `{ commandName, handler }` -- :warning: **BREAKING:** In `ExecutorContext` (ctx in `failedChecksMessage(ctx)`/now `on("failedChecks", (ctx)=>{})`), the **`checks`** property is now `CommandCheckResult[]` instead of `string[]`. This allows Command Checks to supply additional information about the failed checks, such as - - Codes for custom error messages - - Additional context information (for example, you could supply the user's score or something so your failed checks handler doesnt have to fetch it from a database again, or maybe supply the needed threshold etc) -- :warning: The `invalidUsageMessage` and `failedChecksMessage` functions have been removed. Please use the `invalidUsage` and `failedChecks` events instead. -- Default prefix is now `""` (empty string) +**Outputs:** `{ rootCommand, targetCommand }` -- Added [Middlewares](./docs/Middlewares.md) -- Added `index.d.ts` declarations file that's needlessly complex (and also incomplete) -- Added more documentation +This middleware resolves the command based on the `commandName` property. -**v1.0.0:** +If a command is not found, the `commandNotFound` reply is invoked. -- Created project -- Added documentation +`targetCommand` is usually equal to `rootCommand` unless there are subcommands, in which case the resolved subcommand is assigned to `targetCommand`. diff --git a/src/extensions/discordjs-slash.ts b/src/extensions/discordjs-slash.ts index 4e43226..0aa3746 100644 --- a/src/extensions/discordjs-slash.ts +++ b/src/extensions/discordjs-slash.ts @@ -35,9 +35,22 @@ export class DiscordSlashCommandHandler< .use({ id: "discord-command-resolver", run: async (ctx: T): Promise => { + let cmd = ctx.handler.commands.get(ctx.interaction.commandName); + + let targetCommand = cmd; + + let subcommandGroup = ctx.interaction.options.getSubcommandGroup(); + let subcommand = ctx.interaction.options.getSubcommand(); + if(subcommandGroup) { + targetCommand = cmd.subcommands[subcommandGroup].subcommands[subcommand]; + } else if(subcommand) { + targetCommand = cmd.subcommands[subcommand]; + } + return { ...ctx, - command: ctx.handler.commands.get(ctx.interaction.commandName), + rootCommand: cmd, + targetCommand, }; }, }) diff --git a/src/middlewares/CommandExecutor.ts b/src/middlewares/CommandExecutor.ts index 2a0cea9..11449ee 100644 --- a/src/middlewares/CommandExecutor.ts +++ b/src/middlewares/CommandExecutor.ts @@ -8,10 +8,10 @@ import { SplitStringCtx } from "./SplitString"; export const CommandExecutor = () => ({ id: "command-executor", async run(ctx: C): Promise { - let { command, handler } = ctx; + let { targetCommand, handler } = ctx; try { - await command.run(ctx, []); + await targetCommand.run(ctx, []); } catch(e) { handler.emit("commandError", e, ctx); return; diff --git a/src/middlewares/CommandResolver.ts b/src/middlewares/CommandResolver.ts index e3905ff..d7a0f8a 100644 --- a/src/middlewares/CommandResolver.ts +++ b/src/middlewares/CommandResolver.ts @@ -10,7 +10,8 @@ export type ReplyCommandNotFound = { }; export interface CommandResolverCtx { - command: Command, + rootCommand: Command, + targetCommand: Command, } export const CommandResolver = () => ({ @@ -26,11 +27,13 @@ export const CommandResolver = () => ({ return; } - let command = handler.commands.get(commandName); + let rootCommand = handler.commands.get(commandName); return { ...ctx, - command, + rootCommand, + // TODO: resolve subcommands + targetCommand: rootCommand, }; }, }); diff --git a/src/middlewares/Inspect.ts b/src/middlewares/Inspect.ts new file mode 100644 index 0000000..c123192 --- /dev/null +++ b/src/middlewares/Inspect.ts @@ -0,0 +1,7 @@ +export const Inspect = (fn: (ctx: T) => void = console.log) => ({ + id: "inspect_" + Math.floor(Math.random() * 1000), + run: (ctx) => { + fn(ctx); + return ctx; + } +}) diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index e99fbcc..0375f05 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -3,3 +3,4 @@ export * from "./CommandReplier"; export * from "./CommandResolver"; export * from "./ContextStatic"; export * from "./SplitString"; +export * from "./Inspect"; diff --git a/src/utils/decorators.ts b/src/utils/decorators.ts new file mode 100644 index 0000000..e69de29 From fc2fa3ce2fad8d4d3f2444d101e7437b5b3ec93e Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Sat, 23 Mar 2024 12:10:41 +0300 Subject: [PATCH 12/16] stuff --- README.md | 8 ++++++++ examples/discordbot/bot.js | 0 src/Command.ts | 4 ++-- src/CommandHandler.ts | 7 ++++--- src/extensions/discordjs-react.ts | 0 src/extensions/discordjs-slash.ts | 4 ++-- src/middlewares/CommandExecutor.ts | 2 +- src/middlewares/CommandResolver.ts | 6 +++--- src/middlewares/SplitString.ts | 2 +- 9 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 examples/discordbot/bot.js create mode 100644 src/extensions/discordjs-react.ts diff --git a/README.md b/README.md index 476a524..d57517b 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,14 @@ For example, the `CommandResolver` middleware requires `{ commandName, handler } ## Docs +### CommandHandler + +```js +// Create a new Command Handler +let handler = new CommandHandler(); + +``` + ### Middleware: Inspect **Options** `fn: (ctx: T) => void` diff --git a/examples/discordbot/bot.js b/examples/discordbot/bot.js new file mode 100644 index 0000000..e69de29 diff --git a/src/Command.ts b/src/Command.ts index b9e79ff..d9038f2 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -1,11 +1,11 @@ -export interface Command< +export interface BaseCommand< Context, Args = [], > { name?: string, description?: string, run?: (ctx: Context, args: any[]) => Promise | void, - subcommands?: Record>, + subcommands?: Record>, } diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts index fbe3e45..0699885 100644 --- a/src/CommandHandler.ts +++ b/src/CommandHandler.ts @@ -1,4 +1,4 @@ -import { Command } from "./Command"; +import { BaseCommand } from "./Command"; import { BaseContext } from "./Context"; import { Middleware, LastMiddlewareReturnType } from "./Middleware"; import { TypedEmitter } from "tiny-typed-emitter"; @@ -12,8 +12,9 @@ export interface CommandHandlerEvents { export class CommandHandler< Context extends BaseContext & LastMiddlewareReturnType, MiddlewareTypes extends Middleware[] = [], + Command extends BaseCommand = BaseCommand, > extends TypedEmitter> { - commands: Map> = new Map(); + commands: Map = new Map(); middlewares: [...MiddlewareTypes] = [] as any; constructor() { @@ -36,7 +37,7 @@ export class CommandHandler< return this.middlewares.find(mw => mw.id == id); } - add(cmd: Command) { + add(cmd: Command) { this.commands.set(cmd.name, cmd); return this; } diff --git a/src/extensions/discordjs-react.ts b/src/extensions/discordjs-react.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/extensions/discordjs-slash.ts b/src/extensions/discordjs-slash.ts index 0aa3746..8568324 100644 --- a/src/extensions/discordjs-slash.ts +++ b/src/extensions/discordjs-slash.ts @@ -1,5 +1,5 @@ import { CacheType, ChatInputCommandInteraction, Client, Events, Interaction, REST, RESTPostAPIChatInputApplicationCommandsJSONBody, Routes, SlashCommandBuilder } from "discord.js"; -import { BaseContext, Command, CommandHandler } from ".."; +import { BaseContext, BaseCommand, CommandHandler } from ".."; import { CommandExecutor, CommandResolverCtx, ContextStatic } from "../middlewares"; export interface DiscordClientCtx { @@ -61,7 +61,7 @@ export class DiscordSlashCommandHandler< } getSlashCommandData() { - const toData = (cmd: Command) => { + const toData = (cmd: BaseCommand) => { let builder = new SlashCommandBuilder(); builder.setName(cmd.name); builder.setDescription(cmd.description || ""); diff --git a/src/middlewares/CommandExecutor.ts b/src/middlewares/CommandExecutor.ts index 11449ee..7c6d0ca 100644 --- a/src/middlewares/CommandExecutor.ts +++ b/src/middlewares/CommandExecutor.ts @@ -1,4 +1,4 @@ -import { Command } from "../Command"; +import { BaseCommand } from "../Command"; import { BaseContext } from "../Context"; import { Middleware, MiddlewareFactory } from "../Middleware"; import { CommandReplierCtx } from "./CommandReplier"; diff --git a/src/middlewares/CommandResolver.ts b/src/middlewares/CommandResolver.ts index d7a0f8a..c864984 100644 --- a/src/middlewares/CommandResolver.ts +++ b/src/middlewares/CommandResolver.ts @@ -1,4 +1,4 @@ -import { Command } from "../Command"; +import { BaseCommand } from "../Command"; import { BaseContext } from "../Context"; import { Middleware, MiddlewareFactory } from "../Middleware"; import { CommandReplierCtx } from "./CommandReplier"; @@ -10,8 +10,8 @@ export type ReplyCommandNotFound = { }; export interface CommandResolverCtx { - rootCommand: Command, - targetCommand: Command, + rootCommand: BaseCommand, + targetCommand: BaseCommand, } export const CommandResolver = () => ({ diff --git a/src/middlewares/SplitString.ts b/src/middlewares/SplitString.ts index 5406835..9b21a47 100644 --- a/src/middlewares/SplitString.ts +++ b/src/middlewares/SplitString.ts @@ -1,4 +1,4 @@ -import { Command } from "../Command"; +import { BaseCommand } from "../Command"; import { BaseContext } from "../Context"; import { Middleware, MiddlewareFactory } from "../Middleware"; From afd6b60f027cc393f916a4089a439246f64d8954 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Mon, 1 Apr 2024 16:36:07 +0300 Subject: [PATCH 13/16] =?UTF-8?q?uhhh=20stuff=20=F0=9F=91=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/discordjs-slash.ts | 17 +++-- src/CommandHandler.ts | 25 ++------ src/Middleware.ts | 6 +- src/extensions/discordjs-slash.ts | 100 +++++++++++++++++++---------- src/extensions/lowdb.ts | 20 ++---- src/middlewares/ArgumentParser.ts | 7 +- src/middlewares/CommandExecutor.ts | 23 +++---- src/middlewares/CommandResolver.ts | 37 +++++------ src/middlewares/ContextStatic.ts | 11 ++-- src/middlewares/Inspect.ts | 9 +-- src/middlewares/MultiPrefix.ts | 17 ++--- src/middlewares/Prefix.ts | 17 ++--- src/middlewares/SplitString.ts | 19 +++--- src/utils.ts | 3 + 14 files changed, 149 insertions(+), 162 deletions(-) create mode 100644 src/utils.ts diff --git a/examples/discordjs-slash.ts b/examples/discordjs-slash.ts index 5c823d6..a80dc97 100644 --- a/examples/discordjs-slash.ts +++ b/examples/discordjs-slash.ts @@ -1,10 +1,10 @@ import { Client } from "discord.js"; -import { DiscordSlashCommandHandler } from "../src/extensions/discordjs-slash"; +import { DiscordFramework } from "../src/extensions/discordjs-slash"; import { config } from "dotenv"; config(); -const handler = new DiscordSlashCommandHandler({ +let fw = new DiscordFramework({ client: new Client({ intents: [ "Guilds" @@ -12,9 +12,9 @@ const handler = new DiscordSlashCommandHandler({ }), }); -handler.registerEvents(); +fw.registerEvents(); -handler.add({ +fw.slashCommands.add({ name: "test", description: "tests stuff", run({ interaction }) { @@ -25,8 +25,7 @@ handler.add({ }, }) - -handler.add({ +fw.slashCommands.add({ name: "list", description: "manage lists", subcommands: { @@ -39,11 +38,11 @@ handler.add({ } }) -handler.publishCommandsGuild(process.env.GUILD_ID as string); +fw.publishSlashCommandsGuild(process.env.GUILD_ID as string); -handler.client.on("ready", () => { +fw.client.on("ready", () => { console.log("Bot is ready!"); }); -handler.login(); +fw.login(); diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts index 0699885..e8adb3a 100644 --- a/src/CommandHandler.ts +++ b/src/CommandHandler.ts @@ -4,39 +4,27 @@ import { Middleware, LastMiddlewareReturnType } from "./Middleware"; import { TypedEmitter } from "tiny-typed-emitter"; export interface CommandHandlerEvents { - earlyReturn: (ctx: Context, id: string) => void, commandError: (err: Error, ctx: Context) => void, - middlewareError: (err: Error, ctx: Context, id: string) => void, + middlewareError: (err: Error, ctx: Context) => void, } export class CommandHandler< - Context extends BaseContext & LastMiddlewareReturnType, - MiddlewareTypes extends Middleware[] = [], + Context extends BaseContext = BaseContext, Command extends BaseCommand = BaseCommand, > extends TypedEmitter> { commands: Map = new Map(); - middlewares: [...MiddlewareTypes] = [] as any; + middlewares: Middleware[] = []; constructor() { super(); } - use(mw: Middleware): - CommandHandler< - LastMiddlewareReturnType<[...MiddlewareTypes, Middleware]>, - [...MiddlewareTypes, Middleware] - > { + use(mw: Middleware): CommandHandler { this.middlewares.push(mw); - // TODO: i have no idea what i am doing but it kinda works? // @ts-ignore return this; } - getMiddleware(id: Id): MiddlewareTypes[number] { - // @ts-ignore - return this.middlewares.find(mw => mw.id == id); - } - add(cmd: Command) { this.commands.set(cmd.name, cmd); return this; @@ -51,13 +39,12 @@ export class CommandHandler< for (let mw of this.middlewares) { let next; try { - next = await mw.run(context); + next = await mw(context); } catch(err) { - this.emit("middlewareError", err, context as Context, mw.id); + this.emit("middlewareError", err, context as Context); return; } if(!next) { - this.emit("earlyReturn", context as Context, mw.id); return; } context = next; diff --git a/src/Middleware.ts b/src/Middleware.ts index 303702b..c8cd71b 100644 --- a/src/Middleware.ts +++ b/src/Middleware.ts @@ -1,9 +1,7 @@ import { BaseContext } from "./Context"; +import { MaybePromise } from "./utils"; -export interface Middleware { - id: string, - run: (ctx: T) => Promise | U, -} +export type Middleware = (ctx: T) => MaybePromise | MaybePromise; export type MiddlewareLike = Middleware diff --git a/src/extensions/discordjs-slash.ts b/src/extensions/discordjs-slash.ts index 8568324..5ea8a2b 100644 --- a/src/extensions/discordjs-slash.ts +++ b/src/extensions/discordjs-slash.ts @@ -1,22 +1,31 @@ -import { CacheType, ChatInputCommandInteraction, Client, Events, Interaction, REST, RESTPostAPIChatInputApplicationCommandsJSONBody, Routes, SlashCommandBuilder } from "discord.js"; -import { BaseContext, BaseCommand, CommandHandler } from ".."; +import { ButtonInteraction, CacheType, ChatInputCommandInteraction, Client, Events, Interaction, MessageComponentInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, REST, RESTPostAPIChatInputApplicationCommandsJSONBody, Routes, SlashCommandBuilder, UserContextMenuCommandInteraction } from "discord.js"; +import { BaseContext, BaseCommand, CommandHandler, Middleware } from ".."; import { CommandExecutor, CommandResolverCtx, ContextStatic } from "../middlewares"; +import { TypedEmitter } from "tiny-typed-emitter"; export interface DiscordClientCtx { client: Client, }; -export interface InteractionCtx { - interaction: ChatInputCommandInteraction, +export interface InteractionCtx { + interaction: T, } -export class DiscordSlashCommandHandler< - Context extends BaseContext & DiscordClientCtx & InteractionCtx -> extends CommandHandler { +export type CommonContext = BaseContext & DiscordClientCtx; + +export class DiscordFramework extends TypedEmitter<{}> { client: Client; clientId: string; rest: REST; + slashCommands: CommandHandler>; + modals: CommandHandler>; + buttons: CommandHandler>; + userContextMenuCommands: CommandHandler>; + messageContextMenuCommands: CommandHandler>; + messageComponents: CommandHandler>; + messageCommands: CommandHandler; + constructor({ client, token, @@ -29,35 +38,53 @@ export class DiscordSlashCommandHandler< super(); this.client = client; - this.client.token = token || process.env.TOKEN; + this.client.token = token || process.env.DISCORD_TOKEN || process.env.TOKEN; this.clientId = clientId || process.env.CLIENT_ID; - this.use(ContextStatic({ client })) - .use({ - id: "discord-command-resolver", - run: async (ctx: T): Promise => { - let cmd = ctx.handler.commands.get(ctx.interaction.commandName); - - let targetCommand = cmd; - - let subcommandGroup = ctx.interaction.options.getSubcommandGroup(); - let subcommand = ctx.interaction.options.getSubcommand(); - if(subcommandGroup) { - targetCommand = cmd.subcommands[subcommandGroup].subcommands[subcommand]; - } else if(subcommand) { - targetCommand = cmd.subcommands[subcommand]; - } - - return { - ...ctx, - rootCommand: cmd, - targetCommand, - }; - }, - }) - .use(CommandExecutor()); this.rest = new REST() .setToken(this.client.token); + + this.slashCommands = new CommandHandler() + .use(ContextStatic({ client })) + .use(async & DiscordClientCtx>(ctx: T): Promise => { + let cmd = ctx.handler.commands.get(ctx.interaction.commandName); + + let targetCommand = cmd; + + let subcommandGroup = ctx.interaction.options.getSubcommandGroup(); + let subcommand = ctx.interaction.options.getSubcommand(); + if(subcommandGroup) { + targetCommand = cmd.subcommands[subcommandGroup].subcommands[subcommand]; + } else if(subcommand) { + targetCommand = cmd.subcommands[subcommand]; + } + + return { + ...ctx, + rootCommand: cmd, + targetCommand, + }; + }); + + this.messageCommands = new CommandHandler() + .use(ContextStatic({ client })); + + } + + use(mw: Middleware): typeof this { + // @ts-ignore + this.slashCommands.use(mw); + // @ts-ignore + this.messageCommands.use(mw); + // @ts-ignore + return this; + } + + useExecutor() { + // @ts-ignore + this.slashCommands = this.slashCommands.use(CommandExecutor()); + // @ts-ignore + this.messageCommands = this.messageCommands.use(CommandExecutor()); } getSlashCommandData() { @@ -98,14 +125,15 @@ export class DiscordSlashCommandHandler< let arr: RESTPostAPIChatInputApplicationCommandsJSONBody[] = []; - for(let [name, cmd] of this.commands.entries()) { + for(let [name, cmd] of this.slashCommands.commands.entries()) { + // @ts-ignore arr.push(toData(cmd)); } return arr; } - async publishCommandsGuild(guildId: string) { + async publishSlashCommandsGuild(guildId: string) { let data = this.getSlashCommandData(); return await this.rest.put( @@ -129,10 +157,12 @@ export class DiscordSlashCommandHandler< async onInteractionCreate(interaction: Interaction) { if(interaction.isChatInputCommand()) { - this.run({ + this.slashCommands.run({ input: interaction.commandName, interaction, }) + } else if (interaction.isMessageComponent()) { + } } diff --git a/src/extensions/lowdb.ts b/src/extensions/lowdb.ts index 91e86a0..838d13e 100644 --- a/src/extensions/lowdb.ts +++ b/src/extensions/lowdb.ts @@ -8,19 +8,13 @@ export interface LowDBCtx { export const LowDBExtension = (low: Low) => { low.read(); - return { - id: "lowdb", - run: async (ctx: C): Promise> => ({ - ...ctx, - db: low, - }), - }; + return async (ctx: C): Promise> => ({ + ...ctx, + db: low, + }); }; -export const LowDBSave = () => ({ - id: "lowdb-save", - run: async >(ctx: C): Promise => { - await ctx.db.write(); - return ctx; - } +export const LowDBSave = () => (async >(ctx: C): Promise => { + await ctx.db.write(); + return ctx; }) diff --git a/src/middlewares/ArgumentParser.ts b/src/middlewares/ArgumentParser.ts index 89bf0a7..e703eba 100644 --- a/src/middlewares/ArgumentParser.ts +++ b/src/middlewares/ArgumentParser.ts @@ -7,9 +7,6 @@ export type ReplyInvalidUsage = { commandName: string, }; -export const ArgumentParser = (usages: S) => ({ - id: "argument-parser", - run: async & SplitStringCtx>() => { +export const ArgumentParser = (usages: S) => async & SplitStringCtx>() => { - } -}); +}; diff --git a/src/middlewares/CommandExecutor.ts b/src/middlewares/CommandExecutor.ts index 7c6d0ca..09670c6 100644 --- a/src/middlewares/CommandExecutor.ts +++ b/src/middlewares/CommandExecutor.ts @@ -5,18 +5,15 @@ import { CommandReplierCtx } from "./CommandReplier"; import { CommandResolverCtx } from "./CommandResolver"; import { SplitStringCtx } from "./SplitString"; -export const CommandExecutor = () => ({ - id: "command-executor", - async run(ctx: C): Promise { - let { targetCommand, handler } = ctx; - - try { - await targetCommand.run(ctx, []); - } catch(e) { - handler.emit("commandError", e, ctx); - return; - } +export const CommandExecutor = () => (async (ctx: C): Promise => { + let { targetCommand, handler } = ctx; + + try { + await targetCommand.run(ctx, []); + } catch(e) { + handler.emit("commandError", e, ctx); + return; + } - return ctx; - }, + return ctx; }); diff --git a/src/middlewares/CommandResolver.ts b/src/middlewares/CommandResolver.ts index c864984..f7db3f0 100644 --- a/src/middlewares/CommandResolver.ts +++ b/src/middlewares/CommandResolver.ts @@ -14,26 +14,23 @@ export interface CommandResolverCtx { targetCommand: BaseCommand, } -export const CommandResolver = () => ({ - id: "command-resolver", - async run & BaseContext)>(ctx: T): Promise { - let { handler, commandName, reply } = ctx; +export const CommandResolver = () => (async & BaseContext)>(ctx: T): Promise => { + let { handler, commandName, reply } = ctx; - if(!handler.commands.has(commandName)) { - reply?.({ - type: "commandNotFound", - commandName, - }, ctx); - return; - } - - let rootCommand = handler.commands.get(commandName); + if (!handler.commands.has(commandName)) { + reply?.({ + type: "commandNotFound", + commandName, + }, ctx); + return; + } - return { - ...ctx, - rootCommand, - // TODO: resolve subcommands - targetCommand: rootCommand, - }; - }, + let rootCommand = handler.commands.get(commandName); + + return { + ...ctx, + rootCommand, + // TODO: resolve subcommands + targetCommand: rootCommand, + }; }); diff --git a/src/middlewares/ContextStatic.ts b/src/middlewares/ContextStatic.ts index e95d04f..e60002a 100644 --- a/src/middlewares/ContextStatic.ts +++ b/src/middlewares/ContextStatic.ts @@ -3,10 +3,7 @@ import { MiddlewareFactory } from "../Middleware"; export const ContextStatic = < T extends Record, ->(obj: T) => ({ - id: "_", - run: async (ctx: B): Promise => ({ - ...ctx, - ...obj, - }), -}); +>(obj: T) => (async (ctx: B): Promise => ({ + ...ctx, + ...obj, +})); diff --git a/src/middlewares/Inspect.ts b/src/middlewares/Inspect.ts index c123192..d0a5dfc 100644 --- a/src/middlewares/Inspect.ts +++ b/src/middlewares/Inspect.ts @@ -1,7 +1,4 @@ -export const Inspect = (fn: (ctx: T) => void = console.log) => ({ - id: "inspect_" + Math.floor(Math.random() * 1000), - run: (ctx) => { - fn(ctx); - return ctx; - } +export const Inspect = (fn: (ctx: T) => void = console.log) => ((ctx) => { + fn(ctx); + return ctx; }) diff --git a/src/middlewares/MultiPrefix.ts b/src/middlewares/MultiPrefix.ts index 1eae123..dd3fdd8 100644 --- a/src/middlewares/MultiPrefix.ts +++ b/src/middlewares/MultiPrefix.ts @@ -4,16 +4,13 @@ export const MultiPrefix = ({ prefixes = ["!"], }: { prefixes: string[], -}) => ({ - id: "multi-prefix", - async run(ctx: T): Promise { - let { input } = ctx; +}) => (async (ctx: T): Promise => { + let { input } = ctx; - if(!prefixes.some(p => input.startsWith(p))) return; + if(!prefixes.some(p => input.startsWith(p))) return; - return { - ...ctx, - input: input.slice(prefixes.find(p => input.startsWith(p)).length), - }; - }, + return { + ...ctx, + input: input.slice(prefixes.find(p => input.startsWith(p)).length), + }; }); diff --git a/src/middlewares/Prefix.ts b/src/middlewares/Prefix.ts index 44c3fbb..ab38a7d 100644 --- a/src/middlewares/Prefix.ts +++ b/src/middlewares/Prefix.ts @@ -4,16 +4,13 @@ export const Prefix = ({ prefix = "!", }: { prefix: string, -}) => ({ - id: "prefix", - async run(ctx: T): Promise { - let { input } = ctx; +}) => (async (ctx: T): Promise => { + let { input } = ctx; - if(!input.startsWith(prefix)) return; + if(!input.startsWith(prefix)) return; - return { - ...ctx, - input: input.slice(prefix.length), - }; - }, + return { + ...ctx, + input: input.slice(prefix.length), + }; }); diff --git a/src/middlewares/SplitString.ts b/src/middlewares/SplitString.ts index 9b21a47..dd44128 100644 --- a/src/middlewares/SplitString.ts +++ b/src/middlewares/SplitString.ts @@ -7,17 +7,14 @@ export interface SplitStringCtx { commandArguments: string, } -export const SplitString = () => ({ - id: "split-string", - async run(ctx: T): Promise { - let { input } = ctx; +export const SplitString = () => (async (ctx: T): Promise => { + let { input } = ctx; - let [commandName, ...args] = input.split(" "); + let [commandName, ...args] = input.split(" "); - return { - ...ctx, - commandName, - commandArguments: args.join(" "), - }; - }, + return { + ...ctx, + commandName, + commandArguments: args.join(" "), + }; }); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..6e4656b --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,3 @@ +export type ArrayLast = T extends [...infer _, ...infer Last] ? Last : never; + +export type MaybePromise = Promise | T; From 2ac7fb9fd7404dc02c7744d582f3553dc7db21cb Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Mon, 1 Apr 2024 16:39:24 +0300 Subject: [PATCH 14/16] aaa --- README.md | 320 +++++++++++++++++++++++++++++++++++++ examples/consoleExample.js | 75 +++++++++ src/CommandHandler.js | 316 ++++++++++++++++++++++++++++++++++++ srcold/ArgumentParser.js | 64 +++++++- 4 files changed, 772 insertions(+), 3 deletions(-) create mode 100644 examples/consoleExample.js create mode 100644 src/CommandHandler.js diff --git a/README.md b/README.md index d57517b..19b73ad 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ handler.run({ }) ``` +<<<<<<< Updated upstream ## TODO - [ ] CommandHandler @@ -77,10 +78,329 @@ handler.run({ - [ ] Pretty printer - [ ] Documentation - [ ] Core middlewares +======= +### Creating a Command Handler + +You can pass an object of options into the CommandHandler. + +```js +let handler = new CommandHandler({ + // inputs must also begin with prefix + // you can set this to an empty string + prefix: "!", + + // by default, log uses console + // set to false to disable logging + log: false, + // or put in your own logger + log: myLogger, + + // you can also put the functions here to overwrite them, + // instead of overriding them after initialization + // note that these are still optional + transformCommand: () => {}, + buildArguments: () => {}, +}); +``` + +### Writing Commands + +Commands are just objects that must have two properties: + +- `name` (string) The name of the command +- `run` (function) The 'runner function' + +**Example Command:** + +```js +let myCommand = { + name: "ping", + run: (ctx, args) => { + console.log("Pong!"); + }, +}; +``` + +#### Runner Functions + +By default, the arguments of the runner functions are as follows: + +```js +run: (ctx, args) => {} +``` + +The arguments of the Runner Functions are defined using `CommandHandler#buildArguments` + +You can change the order, remove, or add new params to the runner functions by overwriting `CommandHandler#buildArguments` like so: + +```js +handler.buildArguments = (b) => { + return [b.args, b.ctx.username]; +} + +// which would make you be able to define the runner function as: +let run = (args, username) => {}; +``` + +### Registering Commands + +There are two ways to register commands: + +**1. using an object:** + +```js +handler.registerCommand(cmd); +handler.registerCommand({ ... }); +``` + +**2. from a folder (recursively):** + +```js +// path is "./commands" by default +handler.registerCommands(); +handler.registerCommands(path); +handler.registerCommands("./src/cmds"); +``` + +#### Registering old commands + +If your project has old commands from another command handler that has differient command objects, you can still import them. + +You can set the `CommandHandler#transformCommand` to a helper function that would 'transform' the exported object into a valid command object. + +**Example:** + +```js +let oldCommand = { + help: { + name: "ping" + }, + execute: async () => {}, +}; + +handler.transformCommand = (obj) => { + if(!obj.name) obj.name = obj.help.name; + if(!obj.run) obj.run = obj.execute; + return obj; +} +``` + +### Checks + +Your commands can also have custom checks. Its recommended to make the checks once and reuse them for commands. + +Command Checks are just the same as runner functions, but they must return an object with these props: + +- `pass` (boolean) - set to true if check succeeded +- `message` (string) - if it failed, the message explaining why + +```js +{ + checks: [ + (ctx, args) => { + // Im an useless check! Gonna make it run! + return { pass: true }; + }, + + (ctx, args) => { + // just you wait until you hear that im after you + return { + pass: false, + message: "Hardcoded Failiure", + }; + }, + ] +} +``` + +### ArgumentParser + +The argument parser is a complex system that parses and validates given arguments for you. This means no more if-else checks for arguments in every command :D + +Argument parser will look into `args` of your command objects. + +**Examples:** + +Arguments are also just objects. They must have a `type`, the rest are **parser options** + +Arguments can have custom names using `name` + +For example, in the code below, the `rest: true` field is a parser option + +```js +{ + name: "say", + args: [{ + type: "text", + name: "yourMessage", + rest: true, + }], + run: () => {}, +} +``` + +**String Resolving:** + +You can also put strings instead of objects, but the side effect is that you cant define other parser options. + +```js +args: ["text"] + +// a ":" can be used to give it a name +args: ["yourMessage:text"] + +// it can also be marked optional and required +args: ["[text]", ""] + +// you can use three dots in the end to set `rest: true` +args: ["text..."] + +// you can also combine it like so: +args: ["..."] + +// you can also also turn the whole array into a string +args: " [comment:string]..." +``` + +#### Special Parser Options + +**`rest` (boolean)** - if set to true, consumes the rest of the input with it +**`optional` (boolean)** - if set to true, this argument is considered optional +**`name` (string)** - define the name of the argument + +#### Native Parsers + +##### ArgumentParser: text + +Options: + +- `min` (number) - Minimum characters +- `max` (number) - Maximum characters + +##### ArgumentParser: number + +Options: + +- `min` (number) +- `max` (number) +- `isInt` (boolean = false) - If the number should be an integer + +##### ArgumentParser: bool + +Options: + +- `acceptNull` (boolean|string) - Will accept values not true or false as `null`. Set to `"strict"` to strictly check if value is `"null"` or not. + +#### Writing Custom Argument Parsers + +Arguments parsers are internally called **Usage Parser**s. + +A usage parser is an object, like an argument, but with a `parse` function. + +```js +// usage parser: "that" +{ + // underlying type + type: "text", + + // options to pass into underlying type + max: 1024, + + // the parse function + async parse(ctx) { + // the user input or parsed value + ctx.arg; + + // the options + ctx.opts.foo; + + // "custom name" + ctx.name; + } +} + +args: [{ type: "that", name: "custom name", foo: 1 }] +``` + +**What should I return?** + +If the parsing etc was successful, return an object with `parsed` as your value. + +If there were any errors etc, return an object with `fail: true` and `message` set to the error message. + +```js +// it doesnt have to be a variable btw +return { parsed: myValue }; + +return { + fail: true, + message: "Your argument failed the vibe check.", +} +``` + +Usage parsers can easily inherit other parsers using `type`. + +ArgumentParser automatically parses the lowest type and builds up from there. + +This means if you have an usage parser with type set to `"number"`, `ctx.arg` will be a number instead of a string. + +**Inheritance Example:** + +```js +const specialChars = "!'^+%&/()=?_-*>£#$½{[]}\\".split(""); + +handler.registerUsages({ + char: { + type: "text", + min: 1, + max: 1, + }, + + specialChar: { + type: "char", + async parse(ctx) { + if(specialChars.includes(ctx.arg)) { + return { parsed: ctx.arg }; + } else { + return { + fail: true, + message: "Your char isnt special.", + } + }; + }, + }, +}) +``` + +#### Registering Custom Argument Parsers + +```js +handler.registerUsage(usage); +handler.registerUsage({ ... }); + +// obj: Object +handler.registerUsages(obj); +handler.registerUsages({ + name1: usage1, + ... +}); +``` + +## TODO + +- [ ] Discord.js Plugin +- [ ] Discord.js Slash commands plugin +- [ ] Subcommands +- [ ] Permissions +>>>>>>> Stashed changes ## Concepts +<<<<<<< Updated upstream ### Context +======= +**v1.0.0:** +>>>>>>> Stashed changes Command resolving, execution etc are all made possible using Contexts. diff --git a/examples/consoleExample.js b/examples/consoleExample.js new file mode 100644 index 0000000..9e8edc1 --- /dev/null +++ b/examples/consoleExample.js @@ -0,0 +1,75 @@ +import { CommandHandler } from "../src/index.js"; + +/* +This example is a simple console commands handler +*/ + +let handler = new CommandHandler({ + prefix: "", + buildArguments: (build) => [build.args], +}); + +handler.invalidUsageMessage = ({ command, errors }) => { + console.log("/!\\ Invalid Usage!"); + console.log("Usage: " + handler.prettyPrint(command)); + console.log(errors.map(x => "- " + x.message).join("\n")); +}; + +handler.failedChecksMessage = ({ command, errors }) => { + console.log("(x) Error: Failed Checks:"); + console.log(errors.map(x => "- " + x.message).join("\n")); +}; + +// -- commands -- + +handler.registerCommand({ + name: "help", + desc: "Shows commands", + async run(args) { + handler.Commands.forEach((cmd) => { + console.log("> " + cmd.name); + console.log(" " + cmd.desc); + console.log(" Usage: " + handler.prettyPrint(cmd)); + }) + }, +}); + +handler.registerCommand({ + name: "say", + desc: "Repeats your words", + args: [{ + type: "text", + rest: true, + }], + async run([text]) { + // Because rest: true, it all gets collected to the first element + console.log(text); + }, +}); + +handler.registerCommand({ + name: "add", + desc: "Add two numbers", + args: " ", + async run([a, b]) { + let sum = a + b; + console.log(a + " + " + b + " = " + sum); + }, +}); + +handler.registerCommand({ + name: "exit", + desc: "Exit this example", + async run() { + console.log("OK, bye!"); + process.exit(); + }, +}); + +var stdin = process.openStdin(); +stdin.addListener("data", (d) => { + let input = d.toString().trim(); + handler.run(input); +}); + +handler.run("help"); \ No newline at end of file diff --git a/src/CommandHandler.js b/src/CommandHandler.js new file mode 100644 index 0000000..68548e4 --- /dev/null +++ b/src/CommandHandler.js @@ -0,0 +1,316 @@ +import { readdirSync } from "node:fs"; +import { resolve } from "node:path"; +import { EventEmitter } from "node:events"; +import { ArgumentParser } from "./ArgumentParser.js"; + +/** + * @typedef {Object} Command + * @prop {string} name - Name of the command + * @prop {string[]} [aliases] - aliases + * @prop {import("./usages").UsageResolvable[]} [args] - Arguments + * @prop {CommandRun} run + * @prop {CommandCheck[]} checks + */ + +/** + * Execute function of Command + * @callback CommandRun + * @param {Message} msg + * @param {any[]} args + * @param {Object} ctx + */ + +/** + * Check callback + * @callback CommandCheck + * @async + * @param {Message} msg + * @param {any[]} args + * @param {Object} ctx + * @returns {CommandCheckResult} + */ + +/** + * @typedef {Object} CommandCheckResult + * @prop {boolean} pass - true if pass + * @prop {string} message - message of not passing + */ + +const noLogger = { + log: () => {}, + info: () => {}, + debug: () => {}, + error: () => {}, +}; + +class CommandHandler extends EventEmitter { + /** + * + * @param {Object} opts - Options for the command handler + * @param {string} opts.prefix - Prefix of the command handler. Defaults to `"!"` + * @param {Object} opts.argumentParser - ArgumentParser options + * @param {Object|false} opts.log - The logger to use. + * - Set to `false` to disable logging. + * - Default is `console` + * @param {function(Object):Command} opts.transformCommand - @see transformCommand + * @param {function(Object):any[]} opts.buildArguments - @see buildArguments + */ + constructor(opts = {}) { + super(); + this.prefix = typeof(opts.prefix) == "string" ? opts.prefix : "!"; + this.log = opts.log === false ? noLogger : (opts.log || console); + + if(typeof opts.transformCommand == "function") this.transformCommand = opts.transformCommand; + if(typeof opts.buildArguments == "function") this.buildArguments = opts.buildArguments; + + /** @type {Map} */ + this.Commands = new Map(); + /** @type {Map} */ + this.Aliases = new Map(); + + this.argumentParser = new ArgumentParser(opts.argumentParser); + } + + /** + * Register a command + * @param {Command} cmd + */ + registerCommand(cmd = {}) { + cmd = this.transformCommand(cmd); + if(typeof cmd !== "object") throw new Error("registerCommand: Command must be an object"); + + if(!cmd.name) throw new Error("registerCommand: Command does not have a name"); + if(!cmd.run) throw new Error("registerCommand: Command does not have a runner function"); + + if(!Array.isArray(cmd.aliases)) + cmd.aliases = []; + if(typeof cmd.args === "string") + cmd.args = cmd.args.split(" "); + if(!Array.isArray(cmd.args)) + cmd.args = []; + if(!Array.isArray(cmd.checks)) + cmd.checks = []; + + this.Commands.set(cmd.name, cmd); + cmd.aliases.forEach(alias => Aliases.set(alias, cmd.name)); + + this.log.info("Registered command: " + cmd.name); + } + + /** + * Register commands from a local folder + * @param {string} [folderPath] - Defaults to "./commands" + * @remarks The path begins at node's pwd + */ + async registerCommands(folderPath = "./commands") { + let entries = readdirSync(resolve(folderPath), { withFileTypes: true }); + this.log.info("Registering folder: " + resolve(folderPath)); + for(let entry of entries) { + let fd = resolve(dir, entry.name); + if(entry.isDirectory()) + registerCommands(fd); + else { + let obj = await import(fd); + registerCommand(obj); + }; + }; + } + + /** + * Transform an object into a valid Command. + * This is intended for backwards compatability with other command handlers. + * @param {Object} obj - the supposed command object + * @returns {Command} + */ + transformCommand(obj) { + return obj; + } + + /** + * This function returns an array that will be supplied to {@link Command#run}. + * Modify this using the configuration or overwrite it yourself. + * Intended for backwards compatability and ease of use. + * @param {Object} context - Argument builder context + * @param {any[]} context.args - The parsed arguments array from {@link ArgumentParser} + * @param {Object} context.ctx - Other contextual values supplied in {@link CommandHandler#run} + * @returns {any[]} + * + * @example + * myHandler.buildArguments((build) => [build.ctx.message, build.args, build.ctx.db]); + * + * myHandler.run("!hi", { message: msg, db: getDB() }); + * + * let cmd = { + * name: "hi", + * run: async (message, args, db) => { + * //look ^^^^^^^^^^^^^^^^^ + * }, + * }; + */ + buildArguments({ + args, + ctx, + }) { + return [ctx, args]; + }; + + /** + * + * @param {Object} context + */ + invalidUsageMessage({ + ctx, + input, + name, + command, + errors, + }) { + + }; + + failedChecksMessage({ + ctx, + input, + name, + command, + errors, + }) { + + }; + + async run(input, ctx) { + if(!input.startsWith(this.prefix)) return; + + // parse text + + let split = input.slice(this.prefix.length).split(" "); + let cmdName = split[0]; + let cmdArgs = split.slice(1).join(" "); + + // resolve + + if(!cmdName) return; + cmdName = cmdName.toLowerCase(); + // todo replacers locales + + let cmd; + + if(this.Commands.has(cmdName)) { + cmd = this.Commands.get(cmdName); + } else if(this.Aliases.has(cmdName)) { + let alias = this.Aliases.get(cmdName); + if(!this.Commands.has(alias)) throw new Error("run: Alias points to nothing"); + cmd = this.Commands.get(alias); + } else { + this.emit("unknownCommand", { + input, + name: cmdName, + ctx, + }); + return; + }; + + // parse args + + let { + args, + errors, + } = await this.argumentParser.parseUsages(cmdArgs, cmd.args); + + if(errors.length) { + this.invalidUsageMessage({ + ctx, + input, + name: cmdName, + command: cmd, + errors, + }); + return; + } + + // build arguments + + let fnArgs = this.buildArguments({ + input, + ctx, + args, + }); + + // checks + + let failedChecks = []; + for(let check of cmd.checks) { + /** @type {CommandCheckResult} */ + let result = await check(...fnArgs); + if(!result.pass) { + failedChecks.push(result.message); + } + } + + if(failedChecks.length) { + this.failedChecksMessage({ + ctx, + input, + name: cmdName, + command: cmd, + errors: failedChecks, + }); + return; + } + + // run + + try { + let fn = cmd.run.bind(cmd); + fn(...fnArgs); + this.emit("commandRun", { + command: cmd, + name: cmdName, + input, + ctx, + args, + runArgs: fnArgs, + }); + } catch(e) { + console.log(e); + this.emit("commandError", { + error: e, + command: cmd, + name: cmdName, + input, + ctx, + args, + runArgs: fnArgs, + }); + } + }; + + /** + * Pretty print the usage of a command + * @param {Command} cmd command + */ + prettyPrint(cmd) { + return this.prefix + cmd.name + (cmd.args?.length ? (" " + this.argumentParser.usagesToString(cmd.args)) : ""); + } + + /** + * Registers a usage parser + * @param {string} id Usage Name + * @param {UsageParser} usage The usage to register + */ + registerUsage(id, usage) { + this.argumentParser.registerUsage(id, usage); + } + + /** + * Registers multiple usage parsers at once + * @param {Object} obj + */ + registerUsages(obj) { + this.argumentParser.registerUsages(obj); + } +} + +export { + CommandHandler, +}; \ No newline at end of file diff --git a/srcold/ArgumentParser.js b/srcold/ArgumentParser.js index 594f110..5064f70 100644 --- a/srcold/ArgumentParser.js +++ b/srcold/ArgumentParser.js @@ -1,4 +1,8 @@ +<<<<<<< Updated upstream:srcold/ArgumentParser.js import splitargs from "../utils/splitargs.js"; +======= +import splitargs from '../utils/splitargs.js'; +>>>>>>> Stashed changes:src/ArgumentParser.js // The Usage System @@ -47,6 +51,11 @@ const NativeUsages = Object.entries({ return { parsed: ctx.arg }; }, +<<<<<<< Updated upstream:srcold/ArgumentParser.js +======= + + _hideType: true, +>>>>>>> Stashed changes:src/ArgumentParser.js }, string: { type: "text" }, @@ -90,7 +99,33 @@ const NativeUsages = Object.entries({ return { parsed: arg }; }, }, +<<<<<<< Updated upstream:srcold/ArgumentParser.js }); +======= + + bool: { + type: "native", + async parse(ctx) { + let arg = ctx.arg.toLowerCase(); + if(["0", "false", "f"].includes(arg)) { + return { parsed: false }; + } else if(["1", "true", "t"].includes(arg)) { + return { parsed: true }; + } else { + if(ctx.opts.acceptNull) { + if(ctx.opts.acceptNull == "strict") { + return fail(`${ctx.style.arg(ctx.name)} must be a boolean! (true, false or null)`); + } else { + return { parsed: null }; + }; + } else { + return fail(`${ctx.style.arg(ctx.name)} must be a boolean! (true or false)`); + }; + }; + }, + }, +}; +>>>>>>> Stashed changes:src/ArgumentParser.js /** * The stylings object for ArgumentHandler. @@ -110,7 +145,7 @@ class ArgumentParser { } /** - * Registers an usage + * Registers a usage parser * @param {string} id Usage Name * @param {UsageParser} usage The usage to register */ @@ -118,6 +153,15 @@ class ArgumentParser { this.ArgumentParsers.set(id, usage); } + /** + * Registers multiple usage parsers at once + * @param {Object} obj + */ + registerUsages(obj) { + for(let [k, v] of Object.entries(obj)) + this.registerUsage(k, v); + } + /** * Resolves an Usage Parser * @param {UsageResolvable} parser @@ -144,7 +188,7 @@ class ArgumentParser { parser = parser.slice(1).slice(0, -1); } - let sp = parser.split(":"); + let sp = parser.split(":").map(s => s.trim()); let type = sp.length === 2 ? sp[1] : sp[0]; let name = sp.length === 2 ? sp[0] : null; parser = this.ArgumentParsers.get(type); @@ -180,14 +224,19 @@ class ArgumentParser { let braceOpen = usage.optional ? "[" : "<"; let braceClose = usage.optional ? "]" : ">"; - let usageTypeName = usage.desc; + let usageTypeName = usage.type; + let typeStr = usage._hideType ? "" : (": " + usageTypeName); +<<<<<<< Updated upstream:srcold/ArgumentParser.js return ( braceOpen + usage.name + (usageTypeName ? ": " + usageTypeName : "") + braceClose ); +======= + return braceOpen + usage.name + typeStr + braceClose; +>>>>>>> Stashed changes:src/ArgumentParser.js } /** @@ -207,8 +256,13 @@ class ArgumentParser { // iterates over usages and parses them // adds to errors if it fails // adds to finalArgs if succeeds +<<<<<<< Updated upstream:srcold/ArgumentParser.js for (let i = 0; i < usages.length; i++) { let rawArg = rawArgs[i]; +======= + for(let i = 0; i < usages.length; i++) { + let rawArg = rawArgs[i] || ""; +>>>>>>> Stashed changes:src/ArgumentParser.js let currentUsage = usages[i]; if (currentUsage.rest) { @@ -218,8 +272,12 @@ class ArgumentParser { if (!(rawArg || "").trim() && !currentUsage.optional) { errors.push({ usage: currentUsage, +<<<<<<< Updated upstream:srcold/ArgumentParser.js message: `${(currentUsage.name)} is required!`, code: "REQUIRED", +======= + message: `${this.styling.arg(currentUsage.name)} is required!`, +>>>>>>> Stashed changes:src/ArgumentParser.js }); continue; } From 3eae4ec124eb550954e50572ca1265a9a07a4271 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Tue, 17 Sep 2024 20:33:25 +0300 Subject: [PATCH 15/16] wtf am i doing --- package.json | 2 + pnpm-lock.yaml | 715 ++++++++++++++++--------------- src/CommandHandler.ts | 20 +- src/Middleware.ts | 75 +++- src/Namespace.ts | 13 + src/Pipeline.ts | 80 ++++ src/index.ts | 5 + src/middlewares/ContextStatic.ts | 9 +- src/middlewares/Inspect.ts | 4 +- src/test.ts | 14 + 10 files changed, 574 insertions(+), 363 deletions(-) create mode 100644 src/Namespace.ts create mode 100644 src/Pipeline.ts create mode 100644 src/test.ts diff --git a/package.json b/package.json index aa8fcc8..63f7fa2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "homepage": "https://github.com/TheAlan404/string-commands#readme", "dependencies": { + "@alan404/enum": "^0.2.1", "tiny-typed-emitter": "^2.1.0" }, "optionalDependencies": { @@ -24,6 +25,7 @@ "devDependencies": { "@types/node": "^20.11.6", "prettier": "^2.6.2", + "simplytyped": "^3.3.0", "tsx": "^4.7.0", "typescript": "^5.3.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5680f3f..6f157af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,394 +1,519 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - tiny-typed-emitter: - specifier: ^2.1.0 - version: 2.1.0 - -optionalDependencies: - discord.js: - specifier: ^14.14.1 - version: 14.14.1 - dotenv: - specifier: ^16.4.1 - version: 16.4.1 - lowdb: - specifier: ^7.0.1 - version: 7.0.1 - react-reconciler: - specifier: ^0.29.0 - version: 0.29.0(react@18.2.0) - -devDependencies: - '@types/node': - specifier: ^20.11.6 - version: 20.11.6 - prettier: - specifier: ^2.6.2 - version: 2.8.8 - tsx: - specifier: ^4.7.0 - version: 4.7.0 - typescript: - specifier: ^5.3.3 - version: 5.3.3 +importers: + + .: + dependencies: + '@alan404/enum': + specifier: ^0.2.1 + version: 0.2.1 + tiny-typed-emitter: + specifier: ^2.1.0 + version: 2.1.0 + optionalDependencies: + discord.js: + specifier: ^14.14.1 + version: 14.14.1 + dotenv: + specifier: ^16.4.1 + version: 16.4.1 + lowdb: + specifier: ^7.0.1 + version: 7.0.1 + react-reconciler: + specifier: ^0.29.0 + version: 0.29.0(react@18.2.0) + devDependencies: + '@types/node': + specifier: ^20.11.6 + version: 20.11.6 + prettier: + specifier: ^2.6.2 + version: 2.8.8 + simplytyped: + specifier: ^3.3.0 + version: 3.3.0(typescript@5.3.3) + tsx: + specifier: ^4.7.0 + version: 4.7.0 + typescript: + specifier: ^5.3.3 + version: 5.3.3 packages: - /@discordjs/builders@1.7.0: + '@alan404/enum@0.2.1': + resolution: {integrity: sha512-cZcDbVcBrvv1xWnjA8UjqPVh2dfKThli3QeiACMBUBOi3Jbhh8K64KrKmHBKzwEn9tAasyESXLeN8D+sjWxeEg==} + + '@discordjs/builders@1.7.0': resolution: {integrity: sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==} engines: {node: '>=16.11.0'} - requiresBuild: true - dependencies: - '@discordjs/formatters': 0.3.3 - '@discordjs/util': 1.0.2 - '@sapphire/shapeshift': 3.9.6 - discord-api-types: 0.37.61 - fast-deep-equal: 3.1.3 - ts-mixer: 6.0.3 - tslib: 2.6.2 - dev: false - optional: true - /@discordjs/collection@1.5.3: + '@discordjs/collection@1.5.3': resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} engines: {node: '>=16.11.0'} - requiresBuild: true - dev: false - optional: true - /@discordjs/collection@2.0.0: + '@discordjs/collection@2.0.0': resolution: {integrity: sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==} engines: {node: '>=18'} - requiresBuild: true - dev: false - optional: true - /@discordjs/formatters@0.3.3: + '@discordjs/formatters@0.3.3': resolution: {integrity: sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==} engines: {node: '>=16.11.0'} - requiresBuild: true - dependencies: - discord-api-types: 0.37.61 - dev: false - optional: true - /@discordjs/rest@2.2.0: + '@discordjs/rest@2.2.0': resolution: {integrity: sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==} engines: {node: '>=16.11.0'} - requiresBuild: true - dependencies: - '@discordjs/collection': 2.0.0 - '@discordjs/util': 1.0.2 - '@sapphire/async-queue': 1.5.2 - '@sapphire/snowflake': 3.5.1 - '@vladfrangu/async_event_emitter': 2.2.4 - discord-api-types: 0.37.61 - magic-bytes.js: 1.8.0 - tslib: 2.6.2 - undici: 5.27.2 - dev: false - optional: true - /@discordjs/util@1.0.2: + '@discordjs/util@1.0.2': resolution: {integrity: sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==} engines: {node: '>=16.11.0'} - requiresBuild: true - dev: false - optional: true - /@discordjs/ws@1.0.2: + '@discordjs/ws@1.0.2': resolution: {integrity: sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==} engines: {node: '>=16.11.0'} - requiresBuild: true - dependencies: - '@discordjs/collection': 2.0.0 - '@discordjs/rest': 2.2.0 - '@discordjs/util': 1.0.2 - '@sapphire/async-queue': 1.5.2 - '@types/ws': 8.5.9 - '@vladfrangu/async_event_emitter': 2.2.4 - discord-api-types: 0.37.61 - tslib: 2.6.2 - ws: 8.14.2 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - optional: true - /@esbuild/aix-ppc64@0.19.11: + '@esbuild/aix-ppc64@0.19.11': resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm64@0.19.11: + '@esbuild/android-arm64@0.19.11': resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} engines: {node: '>=12'} cpu: [arm64] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm@0.19.11: + '@esbuild/android-arm@0.19.11': resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} engines: {node: '>=12'} cpu: [arm] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-x64@0.19.11: + '@esbuild/android-x64@0.19.11': resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} engines: {node: '>=12'} cpu: [x64] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-arm64@0.19.11: + '@esbuild/darwin-arm64@0.19.11': resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-x64@0.19.11: + '@esbuild/darwin-x64@0.19.11': resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} engines: {node: '>=12'} cpu: [x64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-arm64@0.19.11: + '@esbuild/freebsd-arm64@0.19.11': resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-x64@0.19.11: + '@esbuild/freebsd-x64@0.19.11': resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm64@0.19.11: + '@esbuild/linux-arm64@0.19.11': resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} engines: {node: '>=12'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm@0.19.11: + '@esbuild/linux-arm@0.19.11': resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} engines: {node: '>=12'} cpu: [arm] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ia32@0.19.11: + '@esbuild/linux-ia32@0.19.11': resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-loong64@0.19.11: + '@esbuild/linux-loong64@0.19.11': resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-mips64el@0.19.11: + '@esbuild/linux-mips64el@0.19.11': resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ppc64@0.19.11: + '@esbuild/linux-ppc64@0.19.11': resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-riscv64@0.19.11: + '@esbuild/linux-riscv64@0.19.11': resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-s390x@0.19.11: + '@esbuild/linux-s390x@0.19.11': resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} engines: {node: '>=12'} cpu: [s390x] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-x64@0.19.11: + '@esbuild/linux-x64@0.19.11': resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} engines: {node: '>=12'} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/netbsd-x64@0.19.11: + '@esbuild/netbsd-x64@0.19.11': resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/openbsd-x64@0.19.11: + '@esbuild/openbsd-x64@0.19.11': resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/sunos-x64@0.19.11: + '@esbuild/sunos-x64@0.19.11': resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-arm64@0.19.11: + '@esbuild/win32-arm64@0.19.11': resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} engines: {node: '>=12'} cpu: [arm64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-ia32@0.19.11: + '@esbuild/win32-ia32@0.19.11': resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} engines: {node: '>=12'} cpu: [ia32] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-x64@0.19.11: + '@esbuild/win32-x64@0.19.11': resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} engines: {node: '>=12'} cpu: [x64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@fastify/busboy@2.1.0: + '@fastify/busboy@2.1.0': resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} engines: {node: '>=14'} - requiresBuild: true - dev: false - optional: true - /@sapphire/async-queue@1.5.2: + '@sapphire/async-queue@1.5.2': resolution: {integrity: sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - requiresBuild: true - dev: false - optional: true - /@sapphire/shapeshift@3.9.6: + '@sapphire/shapeshift@3.9.6': resolution: {integrity: sha512-4+Na/fxu2SEepZRb9z0dbsVh59QtwPuBg/UVaDib3av7ZY14b14+z09z6QVn0P6Dv6eOU2NDTsjIi0mbtgP56g==} engines: {node: '>=v18'} - requiresBuild: true + + '@sapphire/snowflake@3.5.1': + resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@types/node@20.11.6': + resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} + + '@types/ws@8.5.9': + resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} + + '@vladfrangu/async_event_emitter@2.2.4': + resolution: {integrity: sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + discord-api-types@0.37.61: + resolution: {integrity: sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==} + + discord.js@14.14.1: + resolution: {integrity: sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==} + engines: {node: '>=16.11.0'} + + dotenv@16.4.1: + resolution: {integrity: sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==} + engines: {node: '>=12'} + + esbuild@0.19.11: + resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + engines: {node: '>=12'} + hasBin: true + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lowdb@7.0.1: + resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==} + engines: {node: '>=18'} + + magic-bytes.js@1.8.0: + resolution: {integrity: sha512-lyWpfvNGVb5lu8YUAbER0+UMBTdR63w2mcSUlhhBTyVbxJvjgqwyAf3AZD6MprgK0uHuBoWXSDAMWLupX83o3Q==} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + react-reconciler@0.29.0: + resolution: {integrity: sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^18.2.0 + + react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + + simplytyped@3.3.0: + resolution: {integrity: sha512-mz4RaNdKTZiaKXgi6P1k/cdsxV3gz+y1Wh2NXHWD40dExktLh4Xx/h6MFakmQWODZHj/2rKe59acacpL74ZhQA==} + peerDependencies: + typescript: '>=2.8.0' + + steno@4.0.2: + resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} + engines: {node: '>=18'} + + tiny-typed-emitter@2.1.0: + resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} + + ts-mixer@6.0.3: + resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==} + + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + tsx@4.7.0: + resolution: {integrity: sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici@5.27.2: + resolution: {integrity: sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==} + engines: {node: '>=14.0'} + + ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + +snapshots: + + '@alan404/enum@0.2.1': {} + + '@discordjs/builders@1.7.0': + dependencies: + '@discordjs/formatters': 0.3.3 + '@discordjs/util': 1.0.2 + '@sapphire/shapeshift': 3.9.6 + discord-api-types: 0.37.61 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.3 + tslib: 2.6.2 + optional: true + + '@discordjs/collection@1.5.3': + optional: true + + '@discordjs/collection@2.0.0': + optional: true + + '@discordjs/formatters@0.3.3': + dependencies: + discord-api-types: 0.37.61 + optional: true + + '@discordjs/rest@2.2.0': + dependencies: + '@discordjs/collection': 2.0.0 + '@discordjs/util': 1.0.2 + '@sapphire/async-queue': 1.5.2 + '@sapphire/snowflake': 3.5.1 + '@vladfrangu/async_event_emitter': 2.2.4 + discord-api-types: 0.37.61 + magic-bytes.js: 1.8.0 + tslib: 2.6.2 + undici: 5.27.2 + optional: true + + '@discordjs/util@1.0.2': + optional: true + + '@discordjs/ws@1.0.2': + dependencies: + '@discordjs/collection': 2.0.0 + '@discordjs/rest': 2.2.0 + '@discordjs/util': 1.0.2 + '@sapphire/async-queue': 1.5.2 + '@types/ws': 8.5.9 + '@vladfrangu/async_event_emitter': 2.2.4 + discord-api-types: 0.37.61 + tslib: 2.6.2 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + optional: true + + '@esbuild/aix-ppc64@0.19.11': + optional: true + + '@esbuild/android-arm64@0.19.11': + optional: true + + '@esbuild/android-arm@0.19.11': + optional: true + + '@esbuild/android-x64@0.19.11': + optional: true + + '@esbuild/darwin-arm64@0.19.11': + optional: true + + '@esbuild/darwin-x64@0.19.11': + optional: true + + '@esbuild/freebsd-arm64@0.19.11': + optional: true + + '@esbuild/freebsd-x64@0.19.11': + optional: true + + '@esbuild/linux-arm64@0.19.11': + optional: true + + '@esbuild/linux-arm@0.19.11': + optional: true + + '@esbuild/linux-ia32@0.19.11': + optional: true + + '@esbuild/linux-loong64@0.19.11': + optional: true + + '@esbuild/linux-mips64el@0.19.11': + optional: true + + '@esbuild/linux-ppc64@0.19.11': + optional: true + + '@esbuild/linux-riscv64@0.19.11': + optional: true + + '@esbuild/linux-s390x@0.19.11': + optional: true + + '@esbuild/linux-x64@0.19.11': + optional: true + + '@esbuild/netbsd-x64@0.19.11': + optional: true + + '@esbuild/openbsd-x64@0.19.11': + optional: true + + '@esbuild/sunos-x64@0.19.11': + optional: true + + '@esbuild/win32-arm64@0.19.11': + optional: true + + '@esbuild/win32-ia32@0.19.11': + optional: true + + '@esbuild/win32-x64@0.19.11': + optional: true + + '@fastify/busboy@2.1.0': + optional: true + + '@sapphire/async-queue@1.5.2': + optional: true + + '@sapphire/shapeshift@3.9.6': dependencies: fast-deep-equal: 3.1.3 lodash: 4.17.21 - dev: false optional: true - /@sapphire/snowflake@3.5.1: - resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - requiresBuild: true - dev: false + '@sapphire/snowflake@3.5.1': optional: true - /@types/node@20.11.6: - resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} + '@types/node@20.11.6': dependencies: undici-types: 5.26.5 - /@types/ws@8.5.9: - resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} - requiresBuild: true + '@types/ws@8.5.9': dependencies: '@types/node': 20.11.6 - dev: false optional: true - /@vladfrangu/async_event_emitter@2.2.4: - resolution: {integrity: sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - requiresBuild: true - dev: false + '@vladfrangu/async_event_emitter@2.2.4': optional: true - /discord-api-types@0.37.61: - resolution: {integrity: sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==} - requiresBuild: true - dev: false + discord-api-types@0.37.61: optional: true - /discord.js@14.14.1: - resolution: {integrity: sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==} - engines: {node: '>=16.11.0'} - requiresBuild: true + discord.js@14.14.1: dependencies: '@discordjs/builders': 1.7.0 '@discordjs/collection': 1.5.3 @@ -407,21 +532,12 @@ packages: transitivePeerDependencies: - bufferutil - utf-8-validate - dev: false optional: true - /dotenv@16.4.1: - resolution: {integrity: sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==} - engines: {node: '>=12'} - requiresBuild: true - dev: false + dotenv@16.4.1: optional: true - /esbuild@0.19.11: - resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true + esbuild@0.19.11: optionalDependencies: '@esbuild/aix-ppc64': 0.19.11 '@esbuild/android-arm': 0.19.11 @@ -446,171 +562,90 @@ packages: '@esbuild/win32-arm64': 0.19.11 '@esbuild/win32-ia32': 0.19.11 '@esbuild/win32-x64': 0.19.11 - dev: true - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - requiresBuild: true - dev: false + fast-deep-equal@3.1.3: optional: true - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true + fsevents@2.3.3: optional: true - /get-tsconfig@4.7.2: - resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + get-tsconfig@4.7.2: dependencies: resolve-pkg-maps: 1.0.0 - dev: true - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - requiresBuild: true - dev: false + js-tokens@4.0.0: optional: true - /lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - requiresBuild: true - dev: false + lodash.snakecase@4.1.1: optional: true - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - requiresBuild: true - dev: false + lodash@4.17.21: optional: true - /loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - dev: false optional: true - /lowdb@7.0.1: - resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==} - engines: {node: '>=18'} - requiresBuild: true + lowdb@7.0.1: dependencies: steno: 4.0.2 - dev: false optional: true - /magic-bytes.js@1.8.0: - resolution: {integrity: sha512-lyWpfvNGVb5lu8YUAbER0+UMBTdR63w2mcSUlhhBTyVbxJvjgqwyAf3AZD6MprgK0uHuBoWXSDAMWLupX83o3Q==} - requiresBuild: true - dev: false + magic-bytes.js@1.8.0: optional: true - /prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - dev: true + prettier@2.8.8: {} - /react-reconciler@0.29.0(react@18.2.0): - resolution: {integrity: sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==} - engines: {node: '>=0.10.0'} - requiresBuild: true - peerDependencies: - react: ^18.2.0 + react-reconciler@0.29.0(react@18.2.0): dependencies: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: false optional: true - /react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} + react@18.2.0: dependencies: loose-envify: 1.4.0 - dev: false optional: true - /resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - dev: true + resolve-pkg-maps@1.0.0: {} - /scheduler@0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} - requiresBuild: true + scheduler@0.23.0: dependencies: loose-envify: 1.4.0 - dev: false optional: true - /steno@4.0.2: - resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} - engines: {node: '>=18'} - requiresBuild: true - dev: false + simplytyped@3.3.0(typescript@5.3.3): + dependencies: + typescript: 5.3.3 + + steno@4.0.2: optional: true - /tiny-typed-emitter@2.1.0: - resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} - dev: false + tiny-typed-emitter@2.1.0: {} - /ts-mixer@6.0.3: - resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==} - requiresBuild: true - dev: false + ts-mixer@6.0.3: optional: true - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - requiresBuild: true - dev: false + tslib@2.6.2: optional: true - /tsx@4.7.0: - resolution: {integrity: sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==} - engines: {node: '>=18.0.0'} - hasBin: true + tsx@4.7.0: dependencies: esbuild: 0.19.11 get-tsconfig: 4.7.2 optionalDependencies: fsevents: 2.3.3 - dev: true - /typescript@5.3.3: - resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} - engines: {node: '>=14.17'} - hasBin: true - dev: true + typescript@5.3.3: {} - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@5.26.5: {} - /undici@5.27.2: - resolution: {integrity: sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==} - engines: {node: '>=14.0'} - requiresBuild: true + undici@5.27.2: dependencies: '@fastify/busboy': 2.1.0 - dev: false optional: true - /ws@8.14.2: - resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} - engines: {node: '>=10.0.0'} - requiresBuild: true - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: false + ws@8.14.2: optional: true diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts index e8adb3a..82c1c7d 100644 --- a/src/CommandHandler.ts +++ b/src/CommandHandler.ts @@ -1,7 +1,9 @@ +import { middlewares } from "."; import { BaseCommand } from "./Command"; import { BaseContext } from "./Context"; -import { Middleware, LastMiddlewareReturnType } from "./Middleware"; +import { Middleware, LastMiddlewareReturnType, MiddlewareList } from "./Middleware"; import { TypedEmitter } from "tiny-typed-emitter"; +import { TNamespace } from "./Namespace"; export interface CommandHandlerEvents { commandError: (err: Error, ctx: Context) => void, @@ -9,23 +11,25 @@ export interface CommandHandlerEvents { } export class CommandHandler< - Context extends BaseContext = BaseContext, - Command extends BaseCommand = BaseCommand, -> extends TypedEmitter> { - commands: Map = new Map(); - middlewares: Middleware[] = []; + Namespace extends TNamespace = TNamespace, +> extends TypedEmitter> { + commands: Map = new Map(); + middlewares: TNamespace["middlewares"] = [(x) => x]; constructor() { super(); } - use(mw: Middleware): CommandHandler { + use(mw: Middleware): + CommandHandler> + { + // @ts-ignore this.middlewares.push(mw); // @ts-ignore return this; } - add(cmd: Command) { + add(cmd: Namespace["command"]) { this.commands.set(cmd.name, cmd); return this; } diff --git a/src/Middleware.ts b/src/Middleware.ts index c8cd71b..6f9ff21 100644 --- a/src/Middleware.ts +++ b/src/Middleware.ts @@ -1,15 +1,70 @@ -import { BaseContext } from "./Context"; -import { MaybePromise } from "./utils"; +import { Prev, PromiseOr } from "simplytyped" -export type Middleware = (ctx: T) => MaybePromise | MaybePromise; +export const NOOPMiddleware = ((x: T) => x); -export type MiddlewareLike = - Middleware - | MiddlewareLike[]; +export type Middleware = ((ctx: T) => PromiseOr) & { displayName?: string }; -export type MiddlewareFactory = +export type MiddlewareFactory = (options: Options) => Middleware; -export type LastMiddlewareReturnType[]> = T extends [...infer _, infer Last] ? Last extends Middleware ? R : BaseContext : BaseContext; -export type InputOf> = T extends Middleware ? I : never; -export type OutputOf> = T extends Middleware ? O : never; +export type AnyMiddleware = Middleware; + +type ParseInt = + T extends any + ? (T extends `${infer Digit extends number}` + ? Digit + : never) + : never + +export type MiddlewareList = { + [Index in keyof List]: ( + Middleware< + Index extends "0" ? ( + Input + ) : ( + List[Prev>] + ), + List[Index] + > + ) +} + +type Concat = T extends [infer A] ? A : ( + T extends [infer A, ...infer Rest] ? A & Concat : never +); + +export type MiddlewareOutput = M extends Middleware ? O : never; +export type MiddlewareInput = M extends Middleware ? I : never; + +export type MiddlewareOutputs = { + [Index in keyof Types]: MiddlewareOutput +}; + +export type MiddlewareInputs = { + [Index in keyof Types]: MiddlewareInput +}; + +export type MiddlewareResolvable = AnyMiddleware | MiddlewareFactory; +export type ResolveMiddleware = T extends MiddlewareFactory ? Middleware : ( + T extends Middleware ? Middleware : never +); +export type ResolveMiddlewares = { + [Index in keyof T]: ResolveMiddleware; +}; + +export type Requires = Concat>> + + + + + +type A = { a: number } +type B = { b: number } +type C = { c: string } & B + + +type _A = Requires<[ + Middleware, +]> + + diff --git a/src/Namespace.ts b/src/Namespace.ts new file mode 100644 index 0000000..8aac258 --- /dev/null +++ b/src/Namespace.ts @@ -0,0 +1,13 @@ +import { BaseCommand } from "./Command"; +import { BaseContext } from "./Context"; +import { ArrayLast } from "./utils"; + +export interface TNamespace< + MiddlewareTypes extends any[] = any[], + TCommand extends BaseCommand> = BaseCommand>, +> { + middlewareTypes: MiddlewareTypes, + middlewares: MiddlewareList, + context: ArrayLast, + command: TCommand, +} diff --git a/src/Pipeline.ts b/src/Pipeline.ts new file mode 100644 index 0000000..e636c7c --- /dev/null +++ b/src/Pipeline.ts @@ -0,0 +1,80 @@ +import { AnyMiddleware, Middleware, MiddlewareList, NOOPMiddleware } from "./Middleware"; +import { Enum, variant } from "@alan404/enum"; + +type ArrayLast = T extends [...infer _, infer Last] ? Last : never; +type ArrayFirst = T extends [infer First, ...infer _] ? First : never; +type ArraySliceFirst = T extends [infer _, ...infer Tail] ? Tail : never; + +export type Pipeline = { + middlwares: MiddlewareList, ArraySliceFirst>; + pipe: >(mw: Middleware, Next>) => + Pipeline<[...Types, Next]>; + execute: (initial: ArrayFirst) => Promise>; +} + +export type ExecutionResult = Enum<{ + success: T; + cancelled: { + middleware: AnyMiddleware; + }; + error: { + middleware: AnyMiddleware; + error: Error; + }; +}>; + +export const createPipeline = < + Input, + Output extends Input +>(mw: Middleware): Pipeline<[Input, Output]> => { + let pipeline = { + middlwares: [mw], + pipe: (next) => { + pipeline.middlwares.push(next); + return pipeline; + }, + execute: async (initial: Input) => { + let ctx = initial as any; + for(let middleware of pipeline.middlwares) { + let next: any; + try { + next = await middleware(ctx); + } catch(error) { + return ({ + type: "error", + data: { + middleware, + error, + }, + } as ExecutionResult); + } + + if(!next) return ({ + type: "cancelled", + data: { + middleware, + }, + } as ExecutionResult); + + ctx = next; + } + + return ({ + type: "success", + data: ctx, + } as ExecutionResult); + }, + } + + return pipeline as Pipeline<[Input, Output]>; +}; + + + + + + + + + + diff --git a/src/index.ts b/src/index.ts index eccdf91..688ad1f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,8 @@ export * from "./Context"; export * from "./Command"; export * as middlewares from "./middlewares"; export * as extensions from "./extensions"; + + + + + diff --git a/src/middlewares/ContextStatic.ts b/src/middlewares/ContextStatic.ts index e60002a..ec5c7d7 100644 --- a/src/middlewares/ContextStatic.ts +++ b/src/middlewares/ContextStatic.ts @@ -1,9 +1,10 @@ -import { BaseContext } from "../Context"; -import { MiddlewareFactory } from "../Middleware"; +import { CombineObjects, PlainObject } from "simplytyped"; +import { Middleware } from "../Middleware"; export const ContextStatic = < - T extends Record, ->(obj: T) => (async (ctx: B): Promise => ({ + T extends PlainObject, + Input extends object, +>(obj: T): Middleware> => ((ctx) => ({ ...ctx, ...obj, })); diff --git a/src/middlewares/Inspect.ts b/src/middlewares/Inspect.ts index d0a5dfc..b5e326b 100644 --- a/src/middlewares/Inspect.ts +++ b/src/middlewares/Inspect.ts @@ -1,4 +1,6 @@ -export const Inspect = (fn: (ctx: T) => void = console.log) => ((ctx) => { +import { Middleware } from "../Middleware"; + +export const Inspect = (fn: (ctx: T) => void = console.log): Middleware => ((ctx) => { fn(ctx); return ctx; }) diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..20030d0 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,14 @@ +import { PlainObject } from "simplytyped"; +import { ContextStatic, Inspect } from "./middlewares"; +import { createPipeline } from "./Pipeline"; +import { Middleware } from "./Middleware"; + + +createPipeline(() => ({ a: 1 })) + .pipe(ctx => ({ ...ctx, b: 2 })) + .pipe(ContextStatic({ k: 1 })) + .pipe((ctx) => ctx) + .pipe(Inspect((ctx) => { ctx })) + .execute + + From 1f15a7ffbf8cf3ef93a7611bd12004b89c7d9ad8 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Wed, 18 Sep 2024 14:57:53 +0300 Subject: [PATCH 16/16] pipeline --- src/Arguments.ts | 61 ---- src/Command.ts | 11 - src/CommandHandler.js | 316 ------------------ src/CommandHandler.ts | 57 ---- src/Context.ts | 6 - src/Middleware.ts | 70 ---- src/Namespace.ts | 13 - src/_/Command.ts | 6 + src/builtin/index.ts | 1 + .../middlewares/_}/CommandExecutor.ts | 6 +- .../middlewares/_}/CommandReplier.ts | 4 +- .../middlewares/_}/CommandResolver.ts | 6 +- .../middlewares/_}/MultiPrefix.ts | 2 +- .../middlewares/_}/Prefix.ts | 2 +- .../middlewares/_}/SplitString.ts | 6 +- src/builtin/middlewares/_/index.ts | 6 + src/builtin/middlewares/base/Inspect.ts | 8 + src/builtin/middlewares/base/Static.ts | 10 + src/builtin/middlewares/base/index.ts | 2 + src/builtin/middlewares/index.ts | 1 + src/core/index.ts | 2 + src/core/middleware/IO.ts | 28 ++ src/core/middleware/Middleware.ts | 12 + src/core/middleware/MiddlewareFactory.ts | 9 + src/core/middleware/MiddlewareList.ts | 22 ++ src/core/middleware/MiddlewareMixin.ts | 10 + src/core/middleware/index.ts | 5 + src/{ => core/pipeline}/Pipeline.ts | 25 +- src/core/pipeline/index.ts | 1 + src/extensions/discordjs-slash.ts | 2 +- src/extensions/lowdb.ts | 2 +- src/index.ts | 7 +- src/middlewares/ArgumentParser.ts | 12 - src/middlewares/ContextStatic.ts | 10 - src/middlewares/Inspect.ts | 6 - src/middlewares/index.ts | 6 - src/test.ts | 28 +- src/utils.ts | 3 - 38 files changed, 164 insertions(+), 620 deletions(-) delete mode 100644 src/Arguments.ts delete mode 100644 src/Command.ts delete mode 100644 src/CommandHandler.js delete mode 100644 src/CommandHandler.ts delete mode 100644 src/Context.ts delete mode 100644 src/Middleware.ts delete mode 100644 src/Namespace.ts create mode 100644 src/_/Command.ts create mode 100644 src/builtin/index.ts rename src/{middlewares => builtin/middlewares/_}/CommandExecutor.ts (72%) rename src/{middlewares => builtin/middlewares/_}/CommandReplier.ts (65%) rename src/{middlewares => builtin/middlewares/_}/CommandResolver.ts (83%) rename src/{middlewares => builtin/middlewares/_}/MultiPrefix.ts (87%) rename src/{middlewares => builtin/middlewares/_}/Prefix.ts (85%) rename src/{middlewares => builtin/middlewares/_}/SplitString.ts (67%) create mode 100644 src/builtin/middlewares/_/index.ts create mode 100644 src/builtin/middlewares/base/Inspect.ts create mode 100644 src/builtin/middlewares/base/Static.ts create mode 100644 src/builtin/middlewares/base/index.ts create mode 100644 src/builtin/middlewares/index.ts create mode 100644 src/core/index.ts create mode 100644 src/core/middleware/IO.ts create mode 100644 src/core/middleware/Middleware.ts create mode 100644 src/core/middleware/MiddlewareFactory.ts create mode 100644 src/core/middleware/MiddlewareList.ts create mode 100644 src/core/middleware/MiddlewareMixin.ts create mode 100644 src/core/middleware/index.ts rename src/{ => core/pipeline}/Pipeline.ts (79%) create mode 100644 src/core/pipeline/index.ts delete mode 100644 src/middlewares/ArgumentParser.ts delete mode 100644 src/middlewares/ContextStatic.ts delete mode 100644 src/middlewares/Inspect.ts delete mode 100644 src/middlewares/index.ts delete mode 100644 src/utils.ts diff --git a/src/Arguments.ts b/src/Arguments.ts deleted file mode 100644 index b32308d..0000000 --- a/src/Arguments.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { BaseContext } from "."; -import { StringReader } from "./utils/StringReader"; - -export type ArgumentLike = Argument; - -export type Argument< - S extends ParserStore, - T extends keyof S = "string", -> = { - type: T, -}; - -export interface ParserStore { - [key: string]: Parser; -}; - -export type Parser< - S extends ParserStore, - Id extends string, - T extends (keyof S | void), - U, - Ctx, -> = ExtendingArgumentParser | ReaderArgumentParser; - -export type ExtendingArgumentParser< - S extends ParserStore, - Id extends string, - T extends (keyof S | void), - U, - Ctx, -> = { - id: Id, - extends?: T, - parse: (value: (ValueOf & Ctx)) => Promise | U, -}; - -export type ReaderArgumentParser< - S extends ParserStore, - Id extends string, - U, - Ctx, -> = { - id: Id, - read: (ctx: Ctx & { reader: StringReader }) => Promise | U, -}; - -type ValueOf = - I extends keyof S ? ( - S[I] extends Parser ? U : never - ) : ( - string - ); - -export const DefaultParserTypes: ParserStore = { - string: { - id: "string", - read({ reader }) { - return reader.rest(); - }, - } -}; diff --git a/src/Command.ts b/src/Command.ts deleted file mode 100644 index d9038f2..0000000 --- a/src/Command.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface BaseCommand< - Context, - Args = [], -> { - name?: string, - description?: string, - run?: (ctx: Context, args: any[]) => Promise | void, - subcommands?: Record>, -} - - diff --git a/src/CommandHandler.js b/src/CommandHandler.js deleted file mode 100644 index 68548e4..0000000 --- a/src/CommandHandler.js +++ /dev/null @@ -1,316 +0,0 @@ -import { readdirSync } from "node:fs"; -import { resolve } from "node:path"; -import { EventEmitter } from "node:events"; -import { ArgumentParser } from "./ArgumentParser.js"; - -/** - * @typedef {Object} Command - * @prop {string} name - Name of the command - * @prop {string[]} [aliases] - aliases - * @prop {import("./usages").UsageResolvable[]} [args] - Arguments - * @prop {CommandRun} run - * @prop {CommandCheck[]} checks - */ - -/** - * Execute function of Command - * @callback CommandRun - * @param {Message} msg - * @param {any[]} args - * @param {Object} ctx - */ - -/** - * Check callback - * @callback CommandCheck - * @async - * @param {Message} msg - * @param {any[]} args - * @param {Object} ctx - * @returns {CommandCheckResult} - */ - -/** - * @typedef {Object} CommandCheckResult - * @prop {boolean} pass - true if pass - * @prop {string} message - message of not passing - */ - -const noLogger = { - log: () => {}, - info: () => {}, - debug: () => {}, - error: () => {}, -}; - -class CommandHandler extends EventEmitter { - /** - * - * @param {Object} opts - Options for the command handler - * @param {string} opts.prefix - Prefix of the command handler. Defaults to `"!"` - * @param {Object} opts.argumentParser - ArgumentParser options - * @param {Object|false} opts.log - The logger to use. - * - Set to `false` to disable logging. - * - Default is `console` - * @param {function(Object):Command} opts.transformCommand - @see transformCommand - * @param {function(Object):any[]} opts.buildArguments - @see buildArguments - */ - constructor(opts = {}) { - super(); - this.prefix = typeof(opts.prefix) == "string" ? opts.prefix : "!"; - this.log = opts.log === false ? noLogger : (opts.log || console); - - if(typeof opts.transformCommand == "function") this.transformCommand = opts.transformCommand; - if(typeof opts.buildArguments == "function") this.buildArguments = opts.buildArguments; - - /** @type {Map} */ - this.Commands = new Map(); - /** @type {Map} */ - this.Aliases = new Map(); - - this.argumentParser = new ArgumentParser(opts.argumentParser); - } - - /** - * Register a command - * @param {Command} cmd - */ - registerCommand(cmd = {}) { - cmd = this.transformCommand(cmd); - if(typeof cmd !== "object") throw new Error("registerCommand: Command must be an object"); - - if(!cmd.name) throw new Error("registerCommand: Command does not have a name"); - if(!cmd.run) throw new Error("registerCommand: Command does not have a runner function"); - - if(!Array.isArray(cmd.aliases)) - cmd.aliases = []; - if(typeof cmd.args === "string") - cmd.args = cmd.args.split(" "); - if(!Array.isArray(cmd.args)) - cmd.args = []; - if(!Array.isArray(cmd.checks)) - cmd.checks = []; - - this.Commands.set(cmd.name, cmd); - cmd.aliases.forEach(alias => Aliases.set(alias, cmd.name)); - - this.log.info("Registered command: " + cmd.name); - } - - /** - * Register commands from a local folder - * @param {string} [folderPath] - Defaults to "./commands" - * @remarks The path begins at node's pwd - */ - async registerCommands(folderPath = "./commands") { - let entries = readdirSync(resolve(folderPath), { withFileTypes: true }); - this.log.info("Registering folder: " + resolve(folderPath)); - for(let entry of entries) { - let fd = resolve(dir, entry.name); - if(entry.isDirectory()) - registerCommands(fd); - else { - let obj = await import(fd); - registerCommand(obj); - }; - }; - } - - /** - * Transform an object into a valid Command. - * This is intended for backwards compatability with other command handlers. - * @param {Object} obj - the supposed command object - * @returns {Command} - */ - transformCommand(obj) { - return obj; - } - - /** - * This function returns an array that will be supplied to {@link Command#run}. - * Modify this using the configuration or overwrite it yourself. - * Intended for backwards compatability and ease of use. - * @param {Object} context - Argument builder context - * @param {any[]} context.args - The parsed arguments array from {@link ArgumentParser} - * @param {Object} context.ctx - Other contextual values supplied in {@link CommandHandler#run} - * @returns {any[]} - * - * @example - * myHandler.buildArguments((build) => [build.ctx.message, build.args, build.ctx.db]); - * - * myHandler.run("!hi", { message: msg, db: getDB() }); - * - * let cmd = { - * name: "hi", - * run: async (message, args, db) => { - * //look ^^^^^^^^^^^^^^^^^ - * }, - * }; - */ - buildArguments({ - args, - ctx, - }) { - return [ctx, args]; - }; - - /** - * - * @param {Object} context - */ - invalidUsageMessage({ - ctx, - input, - name, - command, - errors, - }) { - - }; - - failedChecksMessage({ - ctx, - input, - name, - command, - errors, - }) { - - }; - - async run(input, ctx) { - if(!input.startsWith(this.prefix)) return; - - // parse text - - let split = input.slice(this.prefix.length).split(" "); - let cmdName = split[0]; - let cmdArgs = split.slice(1).join(" "); - - // resolve - - if(!cmdName) return; - cmdName = cmdName.toLowerCase(); - // todo replacers locales - - let cmd; - - if(this.Commands.has(cmdName)) { - cmd = this.Commands.get(cmdName); - } else if(this.Aliases.has(cmdName)) { - let alias = this.Aliases.get(cmdName); - if(!this.Commands.has(alias)) throw new Error("run: Alias points to nothing"); - cmd = this.Commands.get(alias); - } else { - this.emit("unknownCommand", { - input, - name: cmdName, - ctx, - }); - return; - }; - - // parse args - - let { - args, - errors, - } = await this.argumentParser.parseUsages(cmdArgs, cmd.args); - - if(errors.length) { - this.invalidUsageMessage({ - ctx, - input, - name: cmdName, - command: cmd, - errors, - }); - return; - } - - // build arguments - - let fnArgs = this.buildArguments({ - input, - ctx, - args, - }); - - // checks - - let failedChecks = []; - for(let check of cmd.checks) { - /** @type {CommandCheckResult} */ - let result = await check(...fnArgs); - if(!result.pass) { - failedChecks.push(result.message); - } - } - - if(failedChecks.length) { - this.failedChecksMessage({ - ctx, - input, - name: cmdName, - command: cmd, - errors: failedChecks, - }); - return; - } - - // run - - try { - let fn = cmd.run.bind(cmd); - fn(...fnArgs); - this.emit("commandRun", { - command: cmd, - name: cmdName, - input, - ctx, - args, - runArgs: fnArgs, - }); - } catch(e) { - console.log(e); - this.emit("commandError", { - error: e, - command: cmd, - name: cmdName, - input, - ctx, - args, - runArgs: fnArgs, - }); - } - }; - - /** - * Pretty print the usage of a command - * @param {Command} cmd command - */ - prettyPrint(cmd) { - return this.prefix + cmd.name + (cmd.args?.length ? (" " + this.argumentParser.usagesToString(cmd.args)) : ""); - } - - /** - * Registers a usage parser - * @param {string} id Usage Name - * @param {UsageParser} usage The usage to register - */ - registerUsage(id, usage) { - this.argumentParser.registerUsage(id, usage); - } - - /** - * Registers multiple usage parsers at once - * @param {Object} obj - */ - registerUsages(obj) { - this.argumentParser.registerUsages(obj); - } -} - -export { - CommandHandler, -}; \ No newline at end of file diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts deleted file mode 100644 index 82c1c7d..0000000 --- a/src/CommandHandler.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { middlewares } from "."; -import { BaseCommand } from "./Command"; -import { BaseContext } from "./Context"; -import { Middleware, LastMiddlewareReturnType, MiddlewareList } from "./Middleware"; -import { TypedEmitter } from "tiny-typed-emitter"; -import { TNamespace } from "./Namespace"; - -export interface CommandHandlerEvents { - commandError: (err: Error, ctx: Context) => void, - middlewareError: (err: Error, ctx: Context) => void, -} - -export class CommandHandler< - Namespace extends TNamespace = TNamespace, -> extends TypedEmitter> { - commands: Map = new Map(); - middlewares: TNamespace["middlewares"] = [(x) => x]; - - constructor() { - super(); - } - - use(mw: Middleware): - CommandHandler> - { - // @ts-ignore - this.middlewares.push(mw); - // @ts-ignore - return this; - } - - add(cmd: Namespace["command"]) { - this.commands.set(cmd.name, cmd); - return this; - } - - async run(ctx: T) { - let context: BaseContext = { - handler: this, - ...ctx, - }; - - for (let mw of this.middlewares) { - let next; - try { - next = await mw(context); - } catch(err) { - this.emit("middlewareError", err, context as Context); - return; - } - if(!next) { - return; - } - context = next; - } - } -} diff --git a/src/Context.ts b/src/Context.ts deleted file mode 100644 index d49af1a..0000000 --- a/src/Context.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CommandHandler } from "./CommandHandler"; - -export interface BaseContext { - handler: typeof this extends CommandHandler ? CommandHandler : never; - input: string; -} diff --git a/src/Middleware.ts b/src/Middleware.ts deleted file mode 100644 index 6f9ff21..0000000 --- a/src/Middleware.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Prev, PromiseOr } from "simplytyped" - -export const NOOPMiddleware = ((x: T) => x); - -export type Middleware = ((ctx: T) => PromiseOr) & { displayName?: string }; - -export type MiddlewareFactory = - (options: Options) => Middleware; - -export type AnyMiddleware = Middleware; - -type ParseInt = - T extends any - ? (T extends `${infer Digit extends number}` - ? Digit - : never) - : never - -export type MiddlewareList = { - [Index in keyof List]: ( - Middleware< - Index extends "0" ? ( - Input - ) : ( - List[Prev>] - ), - List[Index] - > - ) -} - -type Concat = T extends [infer A] ? A : ( - T extends [infer A, ...infer Rest] ? A & Concat : never -); - -export type MiddlewareOutput = M extends Middleware ? O : never; -export type MiddlewareInput = M extends Middleware ? I : never; - -export type MiddlewareOutputs = { - [Index in keyof Types]: MiddlewareOutput -}; - -export type MiddlewareInputs = { - [Index in keyof Types]: MiddlewareInput -}; - -export type MiddlewareResolvable = AnyMiddleware | MiddlewareFactory; -export type ResolveMiddleware = T extends MiddlewareFactory ? Middleware : ( - T extends Middleware ? Middleware : never -); -export type ResolveMiddlewares = { - [Index in keyof T]: ResolveMiddleware; -}; - -export type Requires = Concat>> - - - - - -type A = { a: number } -type B = { b: number } -type C = { c: string } & B - - -type _A = Requires<[ - Middleware, -]> - - diff --git a/src/Namespace.ts b/src/Namespace.ts deleted file mode 100644 index 8aac258..0000000 --- a/src/Namespace.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { BaseCommand } from "./Command"; -import { BaseContext } from "./Context"; -import { ArrayLast } from "./utils"; - -export interface TNamespace< - MiddlewareTypes extends any[] = any[], - TCommand extends BaseCommand> = BaseCommand>, -> { - middlewareTypes: MiddlewareTypes, - middlewares: MiddlewareList, - context: ArrayLast, - command: TCommand, -} diff --git a/src/_/Command.ts b/src/_/Command.ts new file mode 100644 index 0000000..bf9eef3 --- /dev/null +++ b/src/_/Command.ts @@ -0,0 +1,6 @@ +export type CommandMetadata = { + name: string, + description?: string, +}; + + diff --git a/src/builtin/index.ts b/src/builtin/index.ts new file mode 100644 index 0000000..03f1840 --- /dev/null +++ b/src/builtin/index.ts @@ -0,0 +1 @@ +export * from "./middlewares" diff --git a/src/middlewares/CommandExecutor.ts b/src/builtin/middlewares/_/CommandExecutor.ts similarity index 72% rename from src/middlewares/CommandExecutor.ts rename to src/builtin/middlewares/_/CommandExecutor.ts index 09670c6..8fba0cc 100644 --- a/src/middlewares/CommandExecutor.ts +++ b/src/builtin/middlewares/_/CommandExecutor.ts @@ -1,6 +1,6 @@ -import { BaseCommand } from "../Command"; -import { BaseContext } from "../Context"; -import { Middleware, MiddlewareFactory } from "../Middleware"; +import { BaseCommand } from "../../../_/Command"; +import { BaseContext } from "../../_/Context"; +import { Middleware, MiddlewareFactory } from "../../../core/middleware/Middleware"; import { CommandReplierCtx } from "./CommandReplier"; import { CommandResolverCtx } from "./CommandResolver"; import { SplitStringCtx } from "./SplitString"; diff --git a/src/middlewares/CommandReplier.ts b/src/builtin/middlewares/_/CommandReplier.ts similarity index 65% rename from src/middlewares/CommandReplier.ts rename to src/builtin/middlewares/_/CommandReplier.ts index 26af15f..3884d21 100644 --- a/src/middlewares/CommandReplier.ts +++ b/src/builtin/middlewares/_/CommandReplier.ts @@ -1,5 +1,5 @@ -import { BaseContext } from "../Context"; -import { MiddlewareFactory } from "../Middleware"; +import { BaseContext } from "../../_/Context"; +import { MiddlewareFactory } from "../../../core/middleware/Middleware"; export interface ReplyData extends Record { type: string, diff --git a/src/middlewares/CommandResolver.ts b/src/builtin/middlewares/_/CommandResolver.ts similarity index 83% rename from src/middlewares/CommandResolver.ts rename to src/builtin/middlewares/_/CommandResolver.ts index f7db3f0..03e5389 100644 --- a/src/middlewares/CommandResolver.ts +++ b/src/builtin/middlewares/_/CommandResolver.ts @@ -1,6 +1,6 @@ -import { BaseCommand } from "../Command"; -import { BaseContext } from "../Context"; -import { Middleware, MiddlewareFactory } from "../Middleware"; +import { BaseCommand } from "../../../_/Command"; +import { BaseContext } from "../../_/Context"; +import { Middleware, MiddlewareFactory } from "../../../core/middleware/Middleware"; import { CommandReplierCtx } from "./CommandReplier"; import { SplitStringCtx } from "./SplitString"; diff --git a/src/middlewares/MultiPrefix.ts b/src/builtin/middlewares/_/MultiPrefix.ts similarity index 87% rename from src/middlewares/MultiPrefix.ts rename to src/builtin/middlewares/_/MultiPrefix.ts index dd3fdd8..89ce44a 100644 --- a/src/middlewares/MultiPrefix.ts +++ b/src/builtin/middlewares/_/MultiPrefix.ts @@ -1,4 +1,4 @@ -import { BaseContext } from "../Context"; +import { BaseContext } from "../../_/Context"; export const MultiPrefix = ({ prefixes = ["!"], diff --git a/src/middlewares/Prefix.ts b/src/builtin/middlewares/_/Prefix.ts similarity index 85% rename from src/middlewares/Prefix.ts rename to src/builtin/middlewares/_/Prefix.ts index ab38a7d..6d22340 100644 --- a/src/middlewares/Prefix.ts +++ b/src/builtin/middlewares/_/Prefix.ts @@ -1,4 +1,4 @@ -import { BaseContext } from "../Context"; +import { BaseContext } from "../../_/Context"; export const Prefix = ({ prefix = "!", diff --git a/src/middlewares/SplitString.ts b/src/builtin/middlewares/_/SplitString.ts similarity index 67% rename from src/middlewares/SplitString.ts rename to src/builtin/middlewares/_/SplitString.ts index dd44128..60d1509 100644 --- a/src/middlewares/SplitString.ts +++ b/src/builtin/middlewares/_/SplitString.ts @@ -1,6 +1,6 @@ -import { BaseCommand } from "../Command"; -import { BaseContext } from "../Context"; -import { Middleware, MiddlewareFactory } from "../Middleware"; +import { BaseCommand } from "../../../_/Command"; +import { BaseContext } from "../../_/Context"; +import { Middleware, MiddlewareFactory } from "../../../core/middleware/Middleware"; export interface SplitStringCtx { commandName: string, diff --git a/src/builtin/middlewares/_/index.ts b/src/builtin/middlewares/_/index.ts new file mode 100644 index 0000000..6b88061 --- /dev/null +++ b/src/builtin/middlewares/_/index.ts @@ -0,0 +1,6 @@ +export * from "./_/CommandExecutor"; +export * from "./_/CommandReplier"; +export * from "./_/CommandResolver"; +export * from "./base/ContextStatic"; +export * from "./_/SplitString"; +export * from "./base/Inspect"; diff --git a/src/builtin/middlewares/base/Inspect.ts b/src/builtin/middlewares/base/Inspect.ts new file mode 100644 index 0000000..f175b83 --- /dev/null +++ b/src/builtin/middlewares/base/Inspect.ts @@ -0,0 +1,8 @@ +import { NOOPMiddleware } from "../../../core"; + +export type InspectCallback = (ctx: T) => any; + +export const Inspect = (fn: InspectCallback = console.log): NOOPMiddleware => ((ctx) => { + fn(ctx); + return ctx; +}) diff --git a/src/builtin/middlewares/base/Static.ts b/src/builtin/middlewares/base/Static.ts new file mode 100644 index 0000000..ed55be8 --- /dev/null +++ b/src/builtin/middlewares/base/Static.ts @@ -0,0 +1,10 @@ +import { PlainObject } from "simplytyped"; +import { Middleware } from "../../../core"; + +export const Static = < + T extends PlainObject, + Input extends object, +>(obj: T): Middleware => ((ctx) => ({ + ...ctx, + ...obj, +})); diff --git a/src/builtin/middlewares/base/index.ts b/src/builtin/middlewares/base/index.ts new file mode 100644 index 0000000..7d9a5f1 --- /dev/null +++ b/src/builtin/middlewares/base/index.ts @@ -0,0 +1,2 @@ +export * from "./Static"; +export * from "./Inspect"; diff --git a/src/builtin/middlewares/index.ts b/src/builtin/middlewares/index.ts new file mode 100644 index 0000000..955fdd1 --- /dev/null +++ b/src/builtin/middlewares/index.ts @@ -0,0 +1 @@ +export * from "./base"; diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..4e2bde6 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,2 @@ +export * from "./middleware" +export * from "./pipeline" diff --git a/src/core/middleware/IO.ts b/src/core/middleware/IO.ts new file mode 100644 index 0000000..0516e3e --- /dev/null +++ b/src/core/middleware/IO.ts @@ -0,0 +1,28 @@ +import { AnyMiddleware, Middleware } from "./Middleware"; +import { MiddlewareFactory } from "./MiddlewareFactory"; + +type Concat = T extends [infer A] ? A : ( + T extends [infer A, ...infer Rest] ? A & Concat : never +); + +export type MiddlewareOutput = M extends Middleware ? O : never; +export type MiddlewareInput = M extends Middleware ? I : never; + +export type MiddlewareOutputs = { + [Index in keyof Types]: MiddlewareOutput +}; + +export type MiddlewareInputs = { + [Index in keyof Types]: MiddlewareInput +}; + +export type MiddlewareResolvable = AnyMiddleware | MiddlewareFactory; +export type ResolveMiddleware = T extends MiddlewareFactory ? Middleware : ( + T extends Middleware ? Middleware : T +); +export type ResolveMiddlewares = { + [Index in keyof T]: ResolveMiddleware; +}; + +export type ExtractOutputs = Concat>> +export type ExtractInputs = Concat>> diff --git a/src/core/middleware/Middleware.ts b/src/core/middleware/Middleware.ts new file mode 100644 index 0000000..1c7c9fb --- /dev/null +++ b/src/core/middleware/Middleware.ts @@ -0,0 +1,12 @@ +import { PromiseOr } from "simplytyped" + +export type Middleware = ((ctx: T) => PromiseOr) & { displayName?: string }; +export type AnyMiddleware = Middleware; + +export const createMiddleware = (mw: Middleware, displayName?: string) => { + mw.displayName = displayName; + return mw; +}; + +export type NOOPMiddleware = Middleware; +export const NOOPMiddleware: NOOPMiddleware = createMiddleware((x: T) => x, "NOOPMiddleware"); diff --git a/src/core/middleware/MiddlewareFactory.ts b/src/core/middleware/MiddlewareFactory.ts new file mode 100644 index 0000000..dc13717 --- /dev/null +++ b/src/core/middleware/MiddlewareFactory.ts @@ -0,0 +1,9 @@ +import { Middleware } from "./Middleware"; + +export type MiddlewareFactory = + (options: Options) => Middleware; + + + + + diff --git a/src/core/middleware/MiddlewareList.ts b/src/core/middleware/MiddlewareList.ts new file mode 100644 index 0000000..2b561fd --- /dev/null +++ b/src/core/middleware/MiddlewareList.ts @@ -0,0 +1,22 @@ +import { Prev } from "simplytyped" +import { Middleware } from "./Middleware" + +type ParseInt = + T extends any + ? (T extends `${infer Digit extends number}` + ? Digit + : never) + : never + +export type MiddlewareList = { + [Index in keyof List]: ( + Middleware< + Index extends "0" ? ( + Input + ) : ( + List[Prev>] + ), + List[Index] + > + ) +} diff --git a/src/core/middleware/MiddlewareMixin.ts b/src/core/middleware/MiddlewareMixin.ts new file mode 100644 index 0000000..1d1bf82 --- /dev/null +++ b/src/core/middleware/MiddlewareMixin.ts @@ -0,0 +1,10 @@ +import { ExtractOutputs, MiddlewareResolvable } from "./IO"; +import { Middleware } from "./Middleware"; + +export type MiddlewareMixin = Middleware< + ExtractOutputs, + ExtractOutputs & Provides +>; + +export type Provider = Middleware; +export type Consumer = Middleware; diff --git a/src/core/middleware/index.ts b/src/core/middleware/index.ts new file mode 100644 index 0000000..62080d6 --- /dev/null +++ b/src/core/middleware/index.ts @@ -0,0 +1,5 @@ +export * from "./Middleware" +export * from "./MiddlewareList" +export * from "./MiddlewareFactory" +export * from "./IO" +export * from "./MiddlewareMixin" diff --git a/src/Pipeline.ts b/src/core/pipeline/Pipeline.ts similarity index 79% rename from src/Pipeline.ts rename to src/core/pipeline/Pipeline.ts index e636c7c..9eb1b6e 100644 --- a/src/Pipeline.ts +++ b/src/core/pipeline/Pipeline.ts @@ -1,5 +1,5 @@ -import { AnyMiddleware, Middleware, MiddlewareList, NOOPMiddleware } from "./Middleware"; -import { Enum, variant } from "@alan404/enum"; +import { Enum } from "@alan404/enum"; +import { AnyMiddleware, Middleware, MiddlewareList } from "../middleware"; type ArrayLast = T extends [...infer _, infer Last] ? Last : never; type ArrayFirst = T extends [infer First, ...infer _] ? First : never; @@ -7,9 +7,10 @@ type ArraySliceFirst = T extends [infer _, ...infer Tail] ? Tail : never; export type Pipeline = { middlwares: MiddlewareList, ArraySliceFirst>; - pipe: >(mw: Middleware, Next>) => + pipe: (mw: Middleware, Next>) => Pipeline<[...Types, Next]>; - execute: (initial: ArrayFirst) => Promise>; + execute: (initial: ArrayFirst) => Promise>>; + fire: (initial: ArrayFirst) => Pipeline; } export type ExecutionResult = Enum<{ @@ -25,7 +26,7 @@ export type ExecutionResult = Enum<{ export const createPipeline = < Input, - Output extends Input + Output extends Input = Input, >(mw: Middleware): Pipeline<[Input, Output]> => { let pipeline = { middlwares: [mw], @@ -64,17 +65,11 @@ export const createPipeline = < data: ctx, } as ExecutionResult); }, + fire: (initial: Input) => { + pipeline.execute(initial); + return pipeline; + }, } return pipeline as Pipeline<[Input, Output]>; }; - - - - - - - - - - diff --git a/src/core/pipeline/index.ts b/src/core/pipeline/index.ts new file mode 100644 index 0000000..3c19322 --- /dev/null +++ b/src/core/pipeline/index.ts @@ -0,0 +1 @@ +export * from "./Pipeline" diff --git a/src/extensions/discordjs-slash.ts b/src/extensions/discordjs-slash.ts index 5ea8a2b..cfc4cea 100644 --- a/src/extensions/discordjs-slash.ts +++ b/src/extensions/discordjs-slash.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, CacheType, ChatInputCommandInteraction, Client, Events, Interaction, MessageComponentInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, REST, RESTPostAPIChatInputApplicationCommandsJSONBody, Routes, SlashCommandBuilder, UserContextMenuCommandInteraction } from "discord.js"; import { BaseContext, BaseCommand, CommandHandler, Middleware } from ".."; -import { CommandExecutor, CommandResolverCtx, ContextStatic } from "../middlewares"; +import { CommandExecutor, CommandResolverCtx, ContextStatic } from "../builtin/middlewares"; import { TypedEmitter } from "tiny-typed-emitter"; export interface DiscordClientCtx { diff --git a/src/extensions/lowdb.ts b/src/extensions/lowdb.ts index 838d13e..4b3fe64 100644 --- a/src/extensions/lowdb.ts +++ b/src/extensions/lowdb.ts @@ -1,5 +1,5 @@ import { Low } from "lowdb"; -import { BaseContext } from "../Context"; +import { BaseContext } from "../_/Context"; export interface LowDBCtx { db: Low, diff --git a/src/index.ts b/src/index.ts index 688ad1f..0a79d0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,4 @@ -export * from "./CommandHandler"; -export * from "./Middleware"; -export * from "./Context"; -export * from "./Command"; -export * as middlewares from "./middlewares"; -export * as extensions from "./extensions"; +export * from "./core"; diff --git a/src/middlewares/ArgumentParser.ts b/src/middlewares/ArgumentParser.ts deleted file mode 100644 index e703eba..0000000 --- a/src/middlewares/ArgumentParser.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CommandReplierCtx, CommandResolverCtx, SplitStringCtx } from "."; -import { BaseContext } from ".."; -import { ParserStore } from "../Arguments"; - -export type ReplyInvalidUsage = { - type: "invalidUsage", - commandName: string, -}; - -export const ArgumentParser = (usages: S) => async & SplitStringCtx>() => { - -}; diff --git a/src/middlewares/ContextStatic.ts b/src/middlewares/ContextStatic.ts deleted file mode 100644 index ec5c7d7..0000000 --- a/src/middlewares/ContextStatic.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { CombineObjects, PlainObject } from "simplytyped"; -import { Middleware } from "../Middleware"; - -export const ContextStatic = < - T extends PlainObject, - Input extends object, ->(obj: T): Middleware> => ((ctx) => ({ - ...ctx, - ...obj, -})); diff --git a/src/middlewares/Inspect.ts b/src/middlewares/Inspect.ts deleted file mode 100644 index b5e326b..0000000 --- a/src/middlewares/Inspect.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Middleware } from "../Middleware"; - -export const Inspect = (fn: (ctx: T) => void = console.log): Middleware => ((ctx) => { - fn(ctx); - return ctx; -}) diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts deleted file mode 100644 index 0375f05..0000000 --- a/src/middlewares/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./CommandExecutor"; -export * from "./CommandReplier"; -export * from "./CommandResolver"; -export * from "./ContextStatic"; -export * from "./SplitString"; -export * from "./Inspect"; diff --git a/src/test.ts b/src/test.ts index 20030d0..010240d 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,14 +1,16 @@ -import { PlainObject } from "simplytyped"; -import { ContextStatic, Inspect } from "./middlewares"; -import { createPipeline } from "./Pipeline"; -import { Middleware } from "./Middleware"; - - -createPipeline(() => ({ a: 1 })) - .pipe(ctx => ({ ...ctx, b: 2 })) - .pipe(ContextStatic({ k: 1 })) - .pipe((ctx) => ctx) - .pipe(Inspect((ctx) => { ctx })) - .execute - +import { Inspect } from "./builtin/middlewares"; +import { createPipeline } from "./core"; + +let piper = createPipeline(x => x * 2) + .pipe(x => x + 1) + .pipe(x => x.toString()) + .pipe(x => `Output: ${x}`) + .pipe(Inspect()); + + +Promise.all([ + piper.execute(2), + piper.execute(3), +]).then(r => console.log(r)); + diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 6e4656b..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type ArrayLast = T extends [...infer _, ...infer Last] ? Last : never; - -export type MaybePromise = Promise | T;