diff --git a/src/command-group.ts b/src/command-group.ts index cb7b8eb..bb16252 100644 --- a/src/command-group.ts +++ b/src/command-group.ts @@ -9,7 +9,7 @@ import { type LanguageCode, Middleware, } from "./deps.deno.ts"; -import type { CommandElementals, CommandOptions } from "./types.ts"; +import type { BotCommandX, CommandOptions } from "./types.ts"; import { ensureArray, getCommandsRegex, @@ -34,7 +34,7 @@ export interface SetMyCommandsParams { */ language_code?: LanguageCode; /** Commands that can be each one passed to a SetMyCommands Call */ - commands: BotCommand[]; + commands: (BotCommand & { noHandler?: boolean })[]; } /** @@ -275,20 +275,21 @@ export class CommandGroup { } /** - * Serialize all register commands into it's name, prefix and language + * Serialize all register commands into a more detailed object + * including it's name, prefix and language, and more data * * @param filterLanguage if undefined, it returns all names * else get only the locales for the given filterLanguage * fallbacks to "default" * - * @returns an array of {@link CommandElementals} + * @returns an array of {@link BotCommandX} * * Note: mainly used to serialize for {@link FuzzyMatch} */ public toElementals( filterLanguage?: LanguageCode | "default", - ): CommandElementals[] { + ): BotCommandX[] { this._populateMetadata(); return Array.from(this._scopes.values()) @@ -300,7 +301,7 @@ export class CommandGroup { const [language, local] of command.languages.entries() ) { elements.push({ - name: local.name instanceof RegExp + command: local.name instanceof RegExp ? local.name.source : local.name, language, @@ -309,6 +310,7 @@ export class CommandGroup { description: command.getLocalizedDescription( language, ), + ...(command.noHandler ? { noHandler: true } : {}), }); } if (filterLanguage) { diff --git a/src/command.ts b/src/command.ts index 19dee50..5272c22 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,6 +1,5 @@ import { CommandsFlavor } from "./context.ts"; import { - type BotCommand, type BotCommandScope, type BotCommandScopeAllChatAdministrators, type BotCommandScopeAllGroupChats, @@ -12,8 +11,9 @@ import { type LanguageCode, type Middleware, type MiddlewareObj, + type NextFunction, } from "./deps.deno.ts"; -import type { CommandOptions } from "./types.ts"; +import type { BotCommandX, CommandOptions } from "./types.ts"; import { ensureArray, type MaybeArray } from "./utils/array.ts"; import { isAdmin, @@ -66,6 +66,7 @@ export class Command implements MiddlewareObj { targetedCommands: "optional", ignoreCase: false, }; + private _noHandler: boolean; /** * Initialize a new command with a default handler. @@ -130,14 +131,17 @@ export class Command implements MiddlewareObj { | Partial, options?: Partial, ) { - const handler = isMiddleware(handlerOrOptions) - ? handlerOrOptions - : undefined; + let handler = isMiddleware(handlerOrOptions) ? handlerOrOptions : undefined; options = !handler && isCommandOptions(handlerOrOptions) ? handlerOrOptions : options; + if (!handler) { + handler = async (_ctx: Context, next: NextFunction) => await next(); + this._noHandler = true; + } else this._noHandler = false; + this._options = { ...this._options, ...options }; if (this._options.prefix?.trim() === "") this._options.prefix = "/"; this._languages.set("default", { name: name, description }); @@ -248,6 +252,14 @@ export class Command implements MiddlewareObj { return this._options.prefix; } + /** + * Get if this command has a handler + */ + + get noHandler(): boolean { + return this._noHandler; + } + /** * Registers the command to a scope to allow it to be handled and used with `setMyCommands`. * This will automatically apply filtering middlewares for you, so the handler only runs on the specified scope. @@ -507,13 +519,14 @@ export class Command implements MiddlewareObj { */ public toObject( languageCode: LanguageCode | "default" = "default", - ): BotCommand { + ): Pick { const localizedName = this.getLocalizedName(languageCode); return { command: localizedName instanceof RegExp ? localizedName.source : localizedName, description: this.getLocalizedDescription(languageCode), + ...(this.noHandler ? { noHandler: true } : {}), }; } diff --git a/src/context.ts b/src/context.ts index b15e72e..e94b6b3 100644 --- a/src/context.ts +++ b/src/context.ts @@ -123,7 +123,7 @@ export function commands() { if (!result || !result.command) return null; - return result.command.prefix + result.command.name; + return result.command.prefix + result.command.command; }; ctx.getCommandEntities = ( diff --git a/src/types.ts b/src/types.ts index f23e664..77c4f49 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import type { + BotCommand, BotCommandScope, LanguageCode, MessageEntity, @@ -34,12 +35,24 @@ export interface CommandOptions { ignoreCase: boolean; } -export interface CommandElementals { - name: string; +/** + * BotCommand representation with more information about it. + * Specially in regards to the plugin manipulation of it + */ +export interface BotCommandX extends BotCommand { prefix: string; + /** + * Language in which this command is localize + */ language: LanguageCode | "default"; + /** + * Scopes in which this command is registered + */ scopes: BotCommandScope[]; - description: string; + /** + * True if this command has no middleware attach to it. False if it has. + */ + noHandler?: boolean; } /** represents a bot__command entity inside a text message */ diff --git a/src/utils/jaro-winkler.ts b/src/utils/jaro-winkler.ts index d1b73e0..8f7e26c 100644 --- a/src/utils/jaro-winkler.ts +++ b/src/utils/jaro-winkler.ts @@ -1,6 +1,6 @@ import { CommandGroup } from "../command-group.ts"; import { Context, LanguageCode } from "../deps.deno.ts"; -import type { CommandElementals } from "../types.ts"; +import type { BotCommandX } from "../types.ts"; import { LanguageCodes } from "../language-codes.ts"; export function distance(s1: string, s2: string) { @@ -79,7 +79,7 @@ export type JaroWinklerOptions = { }; type CommandSimilarity = { - command: CommandElementals | null; + command: BotCommandX | null; similarity: number; }; @@ -142,7 +142,7 @@ export function fuzzyMatch( const bestMatch = cmds.reduce( (best: CommandSimilarity, command) => { - const similarity = JaroWinklerDistance(userInput, command.name, { + const similarity = JaroWinklerDistance(userInput, command.command, { ...options, }); return similarity > best.similarity ? { command, similarity } : best; diff --git a/test/command-group.test.ts b/test/command-group.test.ts index b3c12d5..c66a2c8 100644 --- a/test/command-group.test.ts +++ b/test/command-group.test.ts @@ -4,6 +4,7 @@ import { dummyCtx } from "./context.test.ts"; import { assert, assertEquals, + assertObjectMatch, assertRejects, assertThrows, describe, @@ -16,7 +17,15 @@ describe("CommandGroup", () => { const commands = new CommandGroup(); commands.command("test", "no handler"); - assertEquals(commands.toArgs().scopes, []); + assertObjectMatch(commands.toArgs().scopes[0], { + commands: [ + { + command: "test", + description: "no handler", + noHandler: true, + }, + ], + }); }); it("should create a command with a default handler", () => { @@ -125,10 +134,10 @@ describe("CommandGroup", () => { }, ]); }); - it("should omit commands with no handler", () => { + it("should mark commands with no handler", () => { const commands = new CommandGroup(); commands.command("test", "handler", (_) => _); - commands.command("omitme", "nohandler"); + commands.command("markme", "nohandler"); const params = commands.toSingleScopeArgs({ type: "chat", chat_id: 10, @@ -139,6 +148,11 @@ describe("CommandGroup", () => { language_code: undefined, commands: [ { command: "test", description: "handler" }, + { + command: "markme", + description: "nohandler", + noHandler: true, + }, ], }, ]); diff --git a/test/integration.test.ts b/test/integration.test.ts index 3a006e0..185f273 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -96,6 +96,20 @@ describe("Integration", () => { assertSpyCalls(setMyCommandsSpy, 0); }); + it("should be able to set commands with no handler", async () => { + const myCommands = new CommandGroup(); + myCommands.command("command", "super description"); + + const setMyCommandsSpy = spy(resolvesNext([true] as const)); + + await myCommands.setCommands({ + api: { + raw: { setMyCommands: setMyCommandsSpy }, + } as unknown as Api, + }); + + assertSpyCalls(setMyCommandsSpy, 1); + }); }); describe("ctx.setMyCommands", () => { diff --git a/test/jaroWrinkler.test.ts b/test/jaroWrinkler.test.ts index c282724..8c067c6 100644 --- a/test/jaroWrinkler.test.ts +++ b/test/jaroWrinkler.test.ts @@ -43,7 +43,7 @@ describe("Jaro-Wrinkler Algorithm", () => { () => {}, ); assertEquals( - fuzzyMatch("strt", cmds, { language: "fr" })?.command?.name, + fuzzyMatch("strt", cmds, { language: "fr" })?.command?.command, "start", ); }); @@ -73,7 +73,7 @@ describe("Jaro-Wrinkler Algorithm", () => { (ctx) => ctx.reply(`Hello, ${ctx.chat.first_name}!`), ); assertEquals( - fuzzyMatch("magcal", cmds, { language: "fr" })?.command?.name, + fuzzyMatch("magcal", cmds, { language: "fr" })?.command?.command, "magical_\\d", ); }); @@ -88,11 +88,11 @@ describe("Jaro-Wrinkler Algorithm", () => { ).localize("es", /magico_(c|d)/, "Comando Mágico"); assertEquals( - fuzzyMatch("magici_c", cmds, { language: "es" })?.command?.name, + fuzzyMatch("magici_c", cmds, { language: "es" })?.command?.command, "magico_(c|d)", ); assertEquals( - fuzzyMatch("magici_a", cmds, { language: "fr" })?.command?.name, + fuzzyMatch("magici_a", cmds, { language: "fr" })?.command?.command, "magical_(a|b)", ); }); @@ -114,56 +114,56 @@ describe("Jaro-Wrinkler Algorithm", () => { const json = cmds.toElementals(); assertEquals(json, [ { - name: "butcher", + command: "butcher", language: "default", prefix: "?", scopes: [{ type: "default" }], description: "_", }, { - name: "carnicero", + command: "carnicero", language: "es", prefix: "?", scopes: [{ type: "default" }], description: "a", }, { - name: "macellaio", + command: "macellaio", language: "it", prefix: "?", scopes: [{ type: "default" }], description: "b", }, { - name: "duke", + command: "duke", language: "default", prefix: "/", scopes: [{ type: "default" }], description: "_", }, { - name: "duque", + command: "duque", language: "es", prefix: "/", scopes: [{ type: "default" }], description: "c", }, { - name: "duc", + command: "duc", language: "fr", prefix: "/", scopes: [{ type: "default" }], description: "d", }, { - name: "dad_(.*)", + command: "dad_(.*)", language: "default", prefix: "/", scopes: [{ type: "default" }], description: "dad", }, { - name: "papa_(.*)", + command: "papa_(.*)", language: "es", prefix: "/", scopes: [{ type: "default" }], @@ -188,36 +188,36 @@ describe("Jaro-Wrinkler Algorithm", () => { it("sv", () => { assertEquals( fuzzyMatch("hertog", cmds, { language: "sv" })?.command - ?.name, + ?.command, "hertig", ); }); it("da", () => { assertEquals( fuzzyMatch("hertog", cmds, { language: "da" })?.command - ?.name, + ?.command, "hertug", ); }); describe("default", () => { it("duke", () => assertEquals( - fuzzyMatch("duk", cmds, {})?.command?.name, + fuzzyMatch("duk", cmds, {})?.command?.command, "duke", )); it("duke", () => assertEquals( - fuzzyMatch("due", cmds, {})?.command?.name, + fuzzyMatch("due", cmds, {})?.command?.command, "duke", )); it("duke", () => assertEquals( - fuzzyMatch("dule", cmds, {})?.command?.name, + fuzzyMatch("dule", cmds, {})?.command?.command, "duke", )); it("duke", () => assertEquals( - fuzzyMatch("duje", cmds, {})?.command?.name, + fuzzyMatch("duje", cmds, {})?.command?.command, "duke", )); }); @@ -225,19 +225,19 @@ describe("Jaro-Wrinkler Algorithm", () => { it("duque", () => assertEquals( fuzzyMatch("duquw", cmds, { language: "es" })?.command - ?.name, + ?.command, "duque", )); it("duque", () => assertEquals( fuzzyMatch("duqe", cmds, { language: "es" })?.command - ?.name, + ?.command, "duque", )); it("duque", () => assertEquals( fuzzyMatch("duwue", cmds, { language: "es" })?.command - ?.name, + ?.command, "duque", )); }); @@ -245,19 +245,19 @@ describe("Jaro-Wrinkler Algorithm", () => { it("duc", () => assertEquals( fuzzyMatch("duk", cmds, { language: "fr" })?.command - ?.name, + ?.command, "duc", )); it("duc", () => assertEquals( fuzzyMatch("duce", cmds, { language: "fr" })?.command - ?.name, + ?.command, "duc", )); it("duc", () => assertEquals( fuzzyMatch("ducñ", cmds, { language: "fr" })?.command - ?.name, + ?.command, "duc", )); }); @@ -276,25 +276,25 @@ describe("Jaro-Wrinkler Algorithm", () => { it("poussar", () => assertEquals( fuzzyMatch("pousssr", cmds, { language: "pt" })?.command - ?.name, + ?.command, "poussar", )); it("poussar", () => assertEquals( fuzzyMatch("pousar", cmds, { language: "pt" })?.command - ?.name, + ?.command, "poussar", )); it("poussar", () => assertEquals( fuzzyMatch("poussqr", cmds, { language: "pt" })?.command - ?.name, + ?.command, "poussar", )); it("poussar", () => assertEquals( fuzzyMatch("poussrr", cmds, { language: "pt" })?.command - ?.name, + ?.command, "poussar", )); }); @@ -302,25 +302,25 @@ describe("Jaro-Wrinkler Algorithm", () => { it("pousser", () => assertEquals( fuzzyMatch("pousssr", cmds, { language: "fr" })?.command - ?.name, + ?.command, "pousser", )); it("pousser", () => assertEquals( fuzzyMatch("pouser", cmds, { language: "fr" })?.command - ?.name, + ?.command, "pousser", )); it("pousser", () => assertEquals( fuzzyMatch("pousrr", cmds, { language: "fr" })?.command - ?.name, + ?.command, "pousser", )); it("pousser", () => assertEquals( fuzzyMatch("poussrr", cmds, { language: "fr" })?.command - ?.name, + ?.command, "pousser", )); });