diff --git a/src/composer.ts b/src/composer.ts index c383aa96..af653b33 100644 --- a/src/composer.ts +++ b/src/composer.ts @@ -329,6 +329,9 @@ export class Composer implements MiddlewareObj { * bot.command('start', ctx => { ... }) * // Reacts to /help commands * bot.command('help', ctx => { ... }) + * // Reacts to any command + * bot.command(undefined, ctx => { ... }) + * bot.command().use(ctx => { ... }) * ``` * * The rest of the message (excluding the command, and trimmed) is provided @@ -347,13 +350,11 @@ export class Composer implements MiddlewareObj { * > }) * > ``` * - * Note that commands are not matched in captions or in the middle of the - * text. + * Note that commands are not matched in the middle of the text. * ```ts * bot.command('start', ctx => { ... }) * // ... does not match: * // A message saying: “some text /start some more text” - * // A photo message with the caption “/start” * ``` * * By default, commands are detected in channel posts, too. This means that @@ -377,7 +378,7 @@ export class Composer implements MiddlewareObj { * @param middleware The middleware to register */ command( - command: MaybeArray, + command?: MaybeArray, ...middleware: Array> ): Composer> { return this.filter(Context.has.command(command), ...middleware); diff --git a/src/context.ts b/src/context.ts index e67ada4b..fadbf9d1 100644 --- a/src/context.ts +++ b/src/context.ts @@ -82,7 +82,7 @@ interface StaticHas { * @param command The command to match */ command( - command: MaybeArray, + command?: MaybeArray, ): (ctx: C) => ctx is CommandContext; /** * Generates a predicate function that can test context objects for @@ -185,10 +185,9 @@ const checker: StaticHas = { }; }, command(command) { - const hasEntities = checker.filterQuery(":entities:bot_command"); - const atCommands = new Set(); - const noAtCommands = new Set(); - toArray(command).forEach((cmd) => { + const hasEntities = checker.filterQuery("::bot_command"); + const noAtCommands = new Set(toArray(command ?? [])); + for (const cmd of noAtCommands) { if (cmd.startsWith("/")) { throw new Error( `Do not include '/' when registering command handlers (use '${ @@ -196,28 +195,26 @@ const checker: StaticHas = { }' not '${cmd}')`, ); } - const set = cmd.includes("@") ? atCommands : noAtCommands; - set.add(cmd); - }); + } return (ctx: C): ctx is CommandContext => { if (!hasEntities(ctx)) return false; const msg = ctx.message ?? ctx.channelPost; - const txt = msg.text ?? msg.caption; - return msg.entities.some((e) => { + const txt = msg.text ?? msg.caption ?? ""; + const entities = msg.entities ?? msg.caption_entities; + return entities.some((e) => { if (e.type !== "bot_command") return false; if (e.offset !== 0) return false; const cmd = txt.substring(1, e.length); - if (noAtCommands.has(cmd) || atCommands.has(cmd)) { - ctx.match = txt.substring(cmd.length + 1).trimStart(); - return true; + let index = cmd.indexOf("@"); + if (index === -1) { + index = Infinity; + } else { + const atTarget = cmd.substring(index + 1).toLowerCase(); + const username = ctx.me.username.toLowerCase(); + if (atTarget !== username) return false; } - const index = cmd.indexOf("@"); - if (index === -1) return false; - const atTarget = cmd.substring(index + 1).toLowerCase(); - const username = ctx.me.username.toLowerCase(); - if (atTarget !== username) return false; const atCommand = cmd.substring(0, index); - if (noAtCommands.has(atCommand)) { + if (command === undefined || noAtCommands.has(atCommand)) { ctx.match = txt.substring(cmd.length + 1).trimStart(); return true; } @@ -853,7 +850,7 @@ export class Context implements RenamedUpdate { * @param command The command to match */ hasCommand( - command: MaybeArray, + command?: MaybeArray, ): this is CommandContextCore { return Context.has.command(command)(this); } @@ -3174,7 +3171,7 @@ type CommandContextCore = */ export type CommandContext = FilterQueryContext< NarrowMatch, - ":entities:bot_command" + "::bot_command" >; type NarrowMatchCore = { match: T }; type NarrowMatch = { diff --git a/test/composer.test.ts b/test/composer.test.ts index af102cc7..3bd38eb3 100644 --- a/test/composer.test.ts +++ b/test/composer.test.ts @@ -49,7 +49,7 @@ describe("Composer", () => { beforeEach(() => { composer = new Composer(); - middleware = spy((_ctx) => {}); + middleware = spy((_ctx, next) => next()); }); it("should call handlers", async () => { @@ -160,9 +160,10 @@ describe("Composer", () => { 0 as any, ); it("should check for commands", async () => { + composer.command(undefined, middleware); composer.command("start", middleware); await exec(c); - assertEquals(middleware.calls.length, 1); + assertEquals(middleware.calls.length, 2); assertEquals(middleware.calls[0].args[0], c); }); it("should allow chaining commands", async () => { @@ -540,9 +541,7 @@ describe("Composer", () => { composer.use(() => { throw err; }); - await assertRejects(async () => { - await exec(); - }, "yay"); + await assertRejects(async () => await exec(), Error, "yay"); assertEquals(handler.calls.length, 0); }); it("should support passing on the control flow via next", async () => { diff --git a/test/composer.type.test.ts b/test/composer.type.test.ts index 52b3d334..73fa34c8 100644 --- a/test/composer.type.test.ts +++ b/test/composer.type.test.ts @@ -110,7 +110,7 @@ describe("Composer types", () => { const channelPostCaption = ctx.channelPost?.caption; const channelPostText = ctx.channelPost?.text; const match = ctx.match; - assertType>(true); + assertType>(true); assertType>( true, ); diff --git a/test/context.test.ts b/test/context.test.ts index 69e9e508..5948c75c 100644 --- a/test/context.test.ts +++ b/test/context.test.ts @@ -381,23 +381,30 @@ describe("Context", () => { assertFalse(ctx.hasChatType("group")); }); - it("should be able to check for commands", () => { + it("should be able to check for commands in text", () => + testCommands("text")); + it("should be able to check for commands in captions", () => + testCommands("caption")); + + function testCommands(text: "text" | "caption") { + const entities = text === "text" ? "entities" : "caption_entities"; let up = { message: { - text: "/start args", - entities: [{ + [text]: "/start args", + [entities]: [{ type: "bot_command", offset: 0, length: "/start".length, }], }, - } as Update; + } as unknown as Update; let ctx = new Context(up, api, me); assert(Context.has.command("start")(ctx)); assert(ctx.hasCommand("start")); assert(Context.has.command(["help", "start"])(ctx)); assert(ctx.hasCommand(["help", "start"])); + assert(ctx.hasCommand()); assertEquals(ctx.match, "args"); assertFalse(Context.has.command("help")(ctx)); assertFalse(ctx.hasCommand("help")); @@ -409,51 +416,55 @@ describe("Context", () => { up = { message: { - text: "Test with /start args", - entities: [{ + [text]: "Test with /start args", + [entities]: [{ type: "bot_command", offset: "Test with ".length, length: "/start".length, }], }, - } as Update; + } as unknown as Update; ctx = new Context(up, api, me); assertFalse(Context.has.command("start")(ctx)); assertFalse(ctx.hasCommand("start")); + assertFalse(ctx.hasCommand()); up = { message: { - text: "/start@BoT args", - entities: [{ + [text]: "/start@BoT args", + [entities]: [{ type: "bot_command", offset: 0, length: "/start@BoT".length, }], }, - } as Update; + } as unknown as Update; ctx = new Context(up, api, me); assert(Context.has.command("start")(ctx)); assert(ctx.hasCommand("start")); + assert(ctx.hasCommand()); up = { message: { - text: "/start@not args", - entities: [{ + [text]: "/start@not args", + [entities]: [{ type: "bot_command", offset: 0, length: "/start@not".length, }], }, - } as Update; + } as unknown as Update; ctx = new Context(up, api, me); assertFalse(Context.has.command("start")(ctx)); assertFalse(ctx.hasCommand("start")); + assertFalse(ctx.hasCommand()); up = { message: { text: "/start" } } as Update; ctx = new Context(up, api, me); assertFalse(Context.has.command("start")(ctx)); assertFalse(ctx.hasCommand("start")); - }); + assertFalse(ctx.hasCommand()); + } it("should be able to check for game queries", () => { const ctx = new Context(update, api, me);