Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fill ctx.match and add ctx.commandMatch and node chores #57

Merged
merged 4 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"lock": false,
"tasks": {
"backport": "rm -rf out && deno run --no-prompt --allow-read=. --allow-write=. https://deno.land/x/deno2node@v1.9.0/src/cli.ts",
"backport": "deno run --no-prompt --allow-read=. --allow-write=. https://deno.land/x/deno2node@v1.14.0/src/cli.ts",
"check": "deno lint && deno fmt --check && deno check --allow-import src/mod.ts",
"fix": "deno lint --fix && deno fmt",
"test": "deno test --allow-import --seed=123456 --parallel ./test/",
Expand Down
126 changes: 109 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"description": "grammY Commands Plugin",
"main": "out/mod.js",
"scripts": {
"backport": "deno task backport",
"prepare": "deno task backport"
"backport": "deno2node tsconfig.json",
"prepare": "npm run backport"
},
"keywords": [
"grammY",
Expand All @@ -27,7 +27,7 @@
"grammy": "^1.17.1"
},
"devDependencies": {
"deno-bin": "^2.0.6",
"deno2node": "^1.14.0",
"typescript": "^5.6.3"
},
"files": [
Expand Down
132 changes: 102 additions & 30 deletions src/command.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CommandsFlavor } from "./context.ts";
import {
type BotCommand,
type BotCommandScope,
Expand All @@ -12,7 +13,6 @@ import {
type Middleware,
type MiddlewareObj,
} from "./deps.deno.ts";
import { InvalidScopeError } from "./utils/errors.ts";
import type { CommandOptions } from "./types.ts";
import { ensureArray, type MaybeArray } from "./utils/array.ts";
import {
Expand All @@ -21,11 +21,32 @@ import {
isMiddleware,
matchesPattern,
} from "./utils/checks.ts";
import { InvalidScopeError } from "./utils/errors.ts";

type BotCommandGroupsScope =
| BotCommandScopeAllGroupChats
| BotCommandScopeAllChatAdministrators;

/**
* Represents a matched command, the result of the RegExp match, and the rest of the input.
*/
export interface CommandMatch {
/**
* The matched command.
*/
command: string | RegExp;
/**
* The rest of the input after the command.
*/
rest: string;
/**
* The result of the RegExp match.
*
* Only defined if the command is a RegExp.
*/
match?: RegExpExecArray | null;
}

const NOCASE_COMMAND_NAME_REGEX = /^[0-9a-z_]+$/i;

/**
Expand Down Expand Up @@ -326,6 +347,75 @@ export class Command<C extends Context = Context> implements MiddlewareObj<C> {
return this;
}

/**
* Finds the matching command in the given context
*
* @example
* ```ts
* // ctx.msg.text = "/delete_123 something"
* const match = Command.findMatchingCommand(/delete_(.*)/, { prefix: "/", ignoreCase: true }, ctx)
* // match is { command: /delete_(.*)/, rest: ["something"], match: ["delete_123"] }
* ```
*/
public static findMatchingCommand(
command: MaybeArray<string | RegExp>,
options: CommandOptions,
ctx: Context,
): CommandMatch | null {
const { matchOnlyAtStart, prefix, targetedCommands } = options;

if (!ctx.has(":text")) return null;

if (matchOnlyAtStart && !ctx.msg.text.startsWith(prefix)) {
return null;
}

const commandNames = ensureArray(command);
const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const commandRegex = new RegExp(
`${escapedPrefix}(?<command>[^@ ]+)(?:@(?<username>[^\\s]*))?(?<rest>.*)`,
"g",
);

const firstCommand = commandRegex.exec(ctx.msg.text)?.groups;

if (!firstCommand) return null;

if (!firstCommand.username && targetedCommands === "required") return null;
if (firstCommand.username && firstCommand.username !== ctx.me.username) {
return null;
}
if (firstCommand.username && targetedCommands === "ignored") return null;

const matchingCommand = commandNames.find((name) => {
const matches = matchesPattern(
name instanceof RegExp
? firstCommand.command + firstCommand.rest
: firstCommand.command,
name,
options.ignoreCase,
);
return matches;
});

if (matchingCommand instanceof RegExp) {
return {
command: matchingCommand,
rest: firstCommand.rest.trim(),
match: matchingCommand.exec(ctx.msg.text),
};
}

if (matchingCommand) {
return {
command: matchingCommand,
rest: firstCommand.rest.trim(),
};
}

return null;
}

/**
* Creates a matcher for the given command that can be used in filtering operations
*
Expand All @@ -346,38 +436,20 @@ export class Command<C extends Context = Context> implements MiddlewareObj<C> {
command: MaybeArray<string | RegExp>,
options: CommandOptions,
) {
const { matchOnlyAtStart, prefix, targetedCommands } = options;

return (ctx: Context) => {
if (!ctx.has(":text")) return false;
if (matchOnlyAtStart && !ctx.msg.text.startsWith(prefix)) {
return false;
}
const matchingCommand = Command.findMatchingCommand(
command,
options,
ctx,
);

const commandNames = ensureArray(command);
const commands = prefix === "/"
? ctx.entities("bot_command")
: ctx.msg.text.split(prefix).map((text) => ({ text }));

for (const { text } of commands) {
const [command, username] = text.split("@");
if (targetedCommands === "ignored" && username) continue;
if (targetedCommands === "required" && !username) continue;
if (username && username !== ctx.me.username) continue;
if (
commandNames.some((name) =>
matchesPattern(
command.replace(prefix, "").split(" ")[0],
name,
options.ignoreCase,
)
)
) {
return true;
}
}
if (!matchingCommand) return false;

ctx.match = matchingCommand.rest;
// TODO: Clean this up. But how to do it without requiring the user to install the commands flavor?
(ctx as Context & CommandsFlavor).commandMatch = matchingCommand;

return false;
return true;
};
}

Expand Down
Loading
Loading