@grammyjs/commands
Version:
grammY Commands Plugin
347 lines (346 loc) • 13.7 kB
JavaScript
"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;