UNPKG

@grammyjs/commands

Version:
347 lines (346 loc) 13.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Command = void 0; const deps_node_js_1 = require("./deps.node.js"); const array_js_1 = require("./utils/array.js"); const checks_js_1 = require("./utils/checks.js"); const errors_js_1 = require("./utils/errors.js"); const NOCASE_COMMAND_NAME_REGEX = /^[0-9a-z_]+$/i; /** * Class that represents a single command and allows you to configure it. */ class Command { constructor(name, description, handlerOrOptions, options) { var _a; this._scopes = []; this._languages = new Map(); this._defaultScopeComposer = new deps_node_js_1.Composer(); this._options = { prefix: "/", matchOnlyAtStart: true, targetedCommands: "optional", ignoreCase: false, }; this._scopeHandlers = new Map(); this._cachedComposer = new deps_node_js_1.Composer(); this._cachedComposerInvalidated = false; let handler = (0, checks_js_1.isMiddleware)(handlerOrOptions) ? handlerOrOptions : undefined; options = !handler && (0, checks_js_1.isCommandOptions)(handlerOrOptions) ? handlerOrOptions : options; if (!handler) { handler = async (_ctx, next) => await next(); this._hasHandler = false; } else this._hasHandler = true; this._options = { ...this._options, ...options }; if (((_a = this._options.prefix) === null || _a === void 0 ? void 0 : _a.trim()) === "") this._options.prefix = "/"; this._languages.set("default", { name: name, description }); if (handler) { this.addToScope({ type: "default" }, handler); } return this; } /** * Whether the command has a custom prefix */ get hasCustomPrefix() { return this.prefix && this.prefix !== "/"; } /** * Gets the command name as string */ get stringName() { return typeof this.name === "string" ? this.name : this.name.source; } /** * Whether the command can be passed to a `setMyCommands` API call * and, if not, the reason. */ isApiCompliant(language) { const problems = []; if (this.hasCustomPrefix) { problems.push(`Command has custom prefix: ${this._options.prefix}`); } const name = language ? this.getLocalizedName(language) : this.name; if (typeof name !== "string") { problems.push("Command has a regular expression name"); } if (typeof name === "string") { if (name.toLowerCase() !== name) { problems.push("Command name has uppercase characters"); } if (name.length > 32) { problems.push(`Command name is too long (${name.length} characters). Maximum allowed is 32 characters`); } if (!NOCASE_COMMAND_NAME_REGEX.test(name)) { problems.push(`Command name has special characters (${name.replace(/[0-9a-z_]/ig, "")}). Only letters, digits and _ are allowed`); } } return problems.length ? [false, ...problems] : [true]; } /** * Get registered scopes for this command */ get scopes() { return this._scopes; } /** * Get registered languages for this command */ get languages() { return this._languages; } /** * Get registered names for this command */ get names() { return Array.from(this._languages.values()).map(({ name }) => name); } /** * Get the default name for this command */ get name() { return this._languages.get("default").name; } /** * Get the default description for this command */ get description() { return this._languages.get("default").description; } /** * Get the prefix for this command */ get prefix() { return this._options.prefix; } /** * Get if this command has a handler */ get hasHandler() { return this._hasHandler; } addToScope(scope, middleware, options = this._options) { const middlewareArray = middleware ? (0, array_js_1.ensureArray)(middleware) : undefined; const optionsObject = { ...this._options, ...options }; this._scopes.push(scope); if (middlewareArray && middlewareArray.length) { this._scopeHandlers.set(scope, [optionsObject, middlewareArray]); this._cachedComposerInvalidated = true; } 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"] } * ``` */ static findMatchingCommand(command, options, ctx) { var _a, _b; const { matchOnlyAtStart, prefix, targetedCommands } = options; if (!ctx.has([":text", ":caption"])) return null; const txt = (_a = ctx.msg.text) !== null && _a !== void 0 ? _a : ctx.msg.caption; if (matchOnlyAtStart && !txt.startsWith(prefix)) { return null; } const commandNames = (0, array_js_1.ensureArray)(command); const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const commandRegex = new RegExp(`${escapedPrefix}(?<command>[^@ ]+)(?:@(?<username>[^\\s]*))?(?<rest>.*)`, "g"); const firstCommand = (_b = commandRegex.exec(txt)) === null || _b === void 0 ? void 0 : _b.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 = (0, checks_js_1.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(txt), }; } 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 * * @example * ```ts * bot * .filter( * Command.hasCommand(/delete_(.*)/), * (ctx) => ctx.reply(`Deleting ${ctx.message?.text?.split("_")[1]}`) * ) * ``` * * @param command Command name or RegEx * @param options Options that should apply to the matching algorithm * @returns A predicate that matches the given command */ static hasCommand(command, options) { return (ctx) => { const matchingCommand = Command.findMatchingCommand(command, options, ctx); 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.commandMatch = matchingCommand; return true; }; } /** * Adds a new translation for the command * * @example * ```ts * myCommands * .command("start", "Starts the bot configuration") * .localize("pt", "iniciar", "Inicia a configuração do bot") * ``` * * @param languageCode Language this translation applies to. @see https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes * @param name Localized command name * @param description Localized command description */ localize(languageCode, name, description) { this._languages.set(languageCode, { name: name, description, }); this._cachedComposerInvalidated = true; return this; } /** * Gets the localized command name of an existing translation * @param languageCode Language to get the name for * @returns Localized command name */ getLocalizedName(languageCode) { var _a, _b; return (_b = (_a = this._languages.get(languageCode)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : this.name; } /** * Gets the localized command name of an existing translation * @param languageCode Language to get the name for * @returns Localized command name */ getLocalizedDescription(languageCode) { var _a, _b; return (_b = (_a = this._languages.get(languageCode)) === null || _a === void 0 ? void 0 : _a.description) !== null && _b !== void 0 ? _b : this.description; } /** * Converts command to an object representation. * Useful for JSON serialization. * * @param languageCode If specified, uses localized versions of the command name and description * @returns Object representation of this command */ toObject(languageCode = "default") { const localizedName = this.getLocalizedName(languageCode); return { command: localizedName instanceof RegExp ? localizedName.source : localizedName, description: this.getLocalizedDescription(languageCode), ...(this.hasHandler ? { hasHandler: true } : { hasHandler: false }), }; } registerScopeHandlers() { const entries = this._scopeHandlers.entries(); for (const [scope, [optionsObject, middlewareArray]] of entries) { if (middlewareArray) { switch (scope.type) { case "default": this._defaultScopeComposer .filter(Command.hasCommand(this.names, optionsObject)) .use(...middlewareArray); break; case "all_chat_administrators": this._cachedComposer .filter(Command.hasCommand(this.names, optionsObject)) .chatType(["group", "supergroup"]) .filter(checks_js_1.isAdmin) .use(...middlewareArray); break; case "all_private_chats": this._cachedComposer .filter(Command.hasCommand(this.names, optionsObject)) .chatType("private") .use(...middlewareArray); break; case "all_group_chats": this._cachedComposer .filter(Command.hasCommand(this.names, optionsObject)) .chatType(["group", "supergroup"]) .use(...middlewareArray); break; case "chat": if (scope.chat_id) { this._cachedComposer .filter(Command.hasCommand(this.names, optionsObject)) .chatType(["group", "supergroup", "private"]) .filter((ctx) => ctx.chatId === scope.chat_id) .use(...middlewareArray); } break; case "chat_administrators": if (scope.chat_id) { this._cachedComposer .filter(Command.hasCommand(this.names, optionsObject)) .chatType(["group", "supergroup"]) .filter((ctx) => ctx.chatId === scope.chat_id) .filter(checks_js_1.isAdmin) .use(...middlewareArray); } break; case "chat_member": if (scope.chat_id && scope.user_id) { this._cachedComposer .filter(Command.hasCommand(this.names, optionsObject)) .chatType(["group", "supergroup"]) .filter((ctx) => ctx.chatId === scope.chat_id) .filter((ctx) => { var _a; return ((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id) === scope.user_id; }) .use(...middlewareArray); } break; default: throw new errors_js_1.InvalidScopeError(scope); } } } this._cachedComposer.use(this._defaultScopeComposer); this._cachedComposerInvalidated = false; } middleware() { if (this._cachedComposerInvalidated) { this.registerScopeHandlers(); } return this._cachedComposer.middleware(); } } exports.Command = Command;