@grammyjs/commands
Version:
grammY Commands Plugin
273 lines (272 loc) • 10 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommandGroup = void 0;
exports.commandNotFound = commandNotFound;
const mod_js_1 = require("./mod.js");
const deps_node_js_1 = require("./deps.node.js");
const array_js_1 = require("./utils/array.js");
const set_bot_commands_js_1 = require("./utils/set-bot-commands.js");
const checks_js_1 = require("./utils/checks.js");
/**
* Central class that manages all registered commands.
* This is the starting point for the plugin, and this is what you should pass to `bot.use` so your commands get properly registered.
*
* @example
* ```typescript
* const myCommands = new CommandGroup()
* commands.command("start", "start the bot configuration", (ctx) => ctx.reply("Hello there!"))
*
* // Registers the commands with the bot instance.
* bot.use(myCommands)
* ```
*/
class CommandGroup {
constructor(options = {}) {
var _a;
this._languages = new Set();
this._scopes = new Map();
this._commands = [];
this._cachedComposer = new deps_node_js_1.Composer();
this._cachedComposerInvalidated = false;
this._commandOptions = {};
this._commandOptions = options;
if (((_a = this._commandOptions.prefix) === null || _a === void 0 ? void 0 : _a.trim()) === "") {
this._commandOptions.prefix = "/";
}
}
_addCommandToScope(scope, command) {
var _a;
const commands = (_a = this._scopes.get(JSON.stringify(scope))) !== null && _a !== void 0 ? _a : [];
this._scopes.set(JSON.stringify(scope), commands.concat([command]));
}
_populateMetadata() {
this._languages.clear();
this._scopes.clear();
this._commands.forEach((command) => {
for (const scope of command.scopes) {
this._addCommandToScope(scope, command);
}
for (const language of command.languages.keys()) {
this._languages.add(language);
}
});
}
command(name, description, handlerOrOptions, _options) {
const handler = (0, checks_js_1.isMiddleware)(handlerOrOptions)
? handlerOrOptions
: undefined;
const options = !handler && (0, checks_js_1.isCommandOptions)(handlerOrOptions)
? { ...this._commandOptions, ...handlerOrOptions }
: { ...this._commandOptions, ..._options };
const command = new mod_js_1.Command(name, description, handler, options);
this._commands.push(command);
this._cachedComposerInvalidated = true;
return command;
}
/**
* Registers a Command that was created by it's own.
*
* @param command the command or list of commands being added to the group
*/
add(command) {
this._commands.push(...(0, array_js_1.ensureArray)(command));
this._cachedComposerInvalidated = true;
return this;
}
/**
* Serializes the commands into multiple objects that can each be passed to a `setMyCommands` call.
*
* @returns One item for each combination of command + scope + language
*/
toArgs(chat_id) {
this._populateMetadata();
const scopes = [];
const uncompliantCommands = [];
for (const [scope, commands] of this._scopes.entries()) {
for (const language of this._languages) {
const compliantScopedCommands = [];
commands.forEach((command) => {
const [isApiCompliant, ...reasons] = command.isApiCompliant(language);
if (isApiCompliant) {
return compliantScopedCommands.push(command);
}
uncompliantCommands.push({
name: command.stringName,
reasons: reasons,
language,
});
});
if (compliantScopedCommands.length) {
scopes.push({
scope: chat_id
? { ...JSON.parse(scope), chat_id }
: { ...JSON.parse(scope) },
language_code: language === "default" ? undefined : language,
commands: compliantScopedCommands.map((command) => command.toObject(language)),
});
}
}
}
return {
scopes,
uncompliantCommands,
};
}
/**
* Serializes the commands of a single scope into objects that can each be passed to a `setMyCommands` call.
*
* @param scope Selected scope to be serialized
* @returns One item per command per language
*/
toSingleScopeArgs(scope) {
this._populateMetadata();
const commandParams = [];
const uncompliantCommands = [];
for (const language of this._languages) {
const compliantCommands = [];
this._commands.forEach((command) => {
const [isApiCompliant, ...reasons] = command.isApiCompliant(language);
if (!isApiCompliant) {
return uncompliantCommands.push({
name: command.stringName,
reasons: reasons,
language,
});
}
if (command.scopes.length)
compliantCommands.push(command);
});
commandParams.push({
scope,
language_code: language === "default" ? undefined : language,
commands: compliantCommands.map((command) => command.toObject(language)),
});
}
return { commandParams, uncompliantCommands };
}
/**
* Registers all commands to be displayed by clients according to their scopes and languages
* Calls `setMyCommands` for each language of each scope of each command.
*
* [!IMPORTANT]
* Calling this method with upperCased command names registered, will throw
* @see https://core.telegram.org/bots/api#botcommand
* @see https://core.telegram.org/method/bots.setBotCommands
*
* @param Instance of `bot` or { api: bot.api }
*/
async setCommands({ api }, options) {
const { scopes, uncompliantCommands } = this.toArgs();
await (0, set_bot_commands_js_1.setBotCommands)(api, scopes, uncompliantCommands, options);
}
/**
* 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 BotCommandX}
*
* Note: mainly used to serialize for {@link FuzzyMatch}
*/
toElementals(filterLanguage) {
this._populateMetadata();
return Array.from(this._scopes.values())
.flat()
.flatMap((command) => {
const elements = [];
for (const [language, local] of command.languages.entries()) {
elements.push({
command: local.name instanceof RegExp
? local.name.source
: local.name,
language,
prefix: command.prefix,
scopes: command.scopes,
description: command.getLocalizedDescription(language),
...(command.hasHandler
? { hasHandler: true }
: { hasHandler: false }),
});
}
if (filterLanguage) {
const filtered = elements.filter((command) => command.language === filterLanguage);
const defaulted = elements.filter((command) => command.language === "default");
return filtered.length ? filtered[0] : defaulted[0];
}
else
return elements;
});
}
/**
* @returns A JSON serialized version of all the currently registered commands
*/
toString() {
return JSON.stringify(this);
}
middleware() {
if (this._cachedComposerInvalidated) {
this._cachedComposer = new deps_node_js_1.Composer(...this._commands);
this._cachedComposerInvalidated = false;
}
return this._cachedComposer.middleware();
}
/**
* @returns all {@link Command}s contained in the instance
*/
get commands() {
return this._commands;
}
/**
* @returns all prefixes registered in this instance
*/
get prefixes() {
return [
...new Set(this._commands.flatMap((command) => command.prefix)),
];
}
/**
* Replaces the `toString` method on Deno
*
* @see toString
*/
[Symbol.for("Deno.customInspect")]() {
return this.toString();
}
/**
* Replaces the `toString` method on Node.js
*
* @see toString
*/
[Symbol.for("nodejs.util.inspect.custom")]() {
return this.toString();
}
}
exports.CommandGroup = CommandGroup;
function commandNotFound(commands, opts = {}) {
return function (ctx) {
if (containsCommands(ctx, commands)) {
ctx
.commandSuggestion = ctx
.getNearestCommand(commands, opts);
return true;
}
return false;
};
}
function containsCommands(ctx, commands) {
let allPrefixes = [
...new Set((0, array_js_1.ensureArray)(commands).flatMap((cmds) => cmds.prefixes)),
];
if (allPrefixes.length < 1) {
allPrefixes = ["/"];
}
for (const prefix of allPrefixes) {
const regex = (0, array_js_1.getCommandsRegex)(prefix);
if (ctx.hasText(regex))
return true;
}
return false;
}