diff --git a/site/docs/.vitepress/configs/locales/en.ts b/site/docs/.vitepress/configs/locales/en.ts index b6c92ee9c..bf05d4c98 100644 --- a/site/docs/.vitepress/configs/locales/en.ts +++ b/site/docs/.vitepress/configs/locales/en.ts @@ -243,6 +243,12 @@ const pluginOfficial = { // do not add the following line to translations: activeMatch: "^(/plugins/chat-members|/ref/chat-members/)$", }, + { + text: "Commands (commands)", + link: "/plugins/commands", + // do not add the following line to translations: + activeMatch: "^(/plugins/commands|/ref/commands/)$", + }, ], }; diff --git a/site/docs/.vitepress/configs/locales/es.ts b/site/docs/.vitepress/configs/locales/es.ts index 5117e04c2..202bc3abc 100644 --- a/site/docs/.vitepress/configs/locales/es.ts +++ b/site/docs/.vitepress/configs/locales/es.ts @@ -208,6 +208,14 @@ const pluginOfficial = { text: "Modo de parsear (parse-mode)", link: "/es/plugins/parse-mode", }, + { + text: "Miembros del chat (chat-members)", + link: "/es/plugins/chat-members", + }, + { + text: "Comandos (commands)", + link: "/es/plugins/commands", + }, ], }; diff --git a/site/docs/.vitepress/configs/locales/id.ts b/site/docs/.vitepress/configs/locales/id.ts index f62a4d062..98064e1f9 100644 --- a/site/docs/.vitepress/configs/locales/id.ts +++ b/site/docs/.vitepress/configs/locales/id.ts @@ -209,6 +209,10 @@ const pluginOfficial = { text: "Chat Members (chat-members)", link: "/id/plugins/chat-members", }, + { + text: "Perintah (commands)", + link: "/id/plugins/commands", + }, ], }; const pluginThirdparty = { diff --git a/site/docs/.vitepress/configs/locales/ru.ts b/site/docs/.vitepress/configs/locales/ru.ts index 6781822b2..5808491f1 100644 --- a/site/docs/.vitepress/configs/locales/ru.ts +++ b/site/docs/.vitepress/configs/locales/ru.ts @@ -212,6 +212,10 @@ const pluginOfficial = { text: "Пользователи чата (chat-members)", link: "/ru/plugins/chat-members", }, + { + text: "Команды (commands)", + link: "/ru/plugins/commands", + }, ], }; diff --git a/site/docs/.vitepress/configs/locales/uk.ts b/site/docs/.vitepress/configs/locales/uk.ts index cdc1007f3..0c9aebfdc 100644 --- a/site/docs/.vitepress/configs/locales/uk.ts +++ b/site/docs/.vitepress/configs/locales/uk.ts @@ -212,6 +212,10 @@ const pluginOfficial = { text: "Учасники чату (chat-members)", link: "/uk/plugins/chat-members", }, + { + text: "Команди (commands)", + link: "/uk/plugins/commands", + }, ], }; diff --git a/site/docs/.vitepress/plugins/current-versions/modules.json b/site/docs/.vitepress/plugins/current-versions/modules.json index 31c076fa9..e065fa19c 100644 --- a/site/docs/.vitepress/plugins/current-versions/modules.json +++ b/site/docs/.vitepress/plugins/current-versions/modules.json @@ -14,6 +14,7 @@ "grammy_storages", "grammy_conversations", "grammy_autoquote", - "grammy_i18n" + "grammy_i18n", + "grammy_commands" ] } diff --git a/site/docs/es/guide/introduction.md b/site/docs/es/guide/introduction.md index a65bff628..ba895ceec 100644 --- a/site/docs/es/guide/introduction.md +++ b/site/docs/es/guide/introduction.md @@ -184,7 +184,7 @@ Para poder ejecutar el archivo `bot.js`, tienes que tener instalado [Node.js](ht En resumen, esto es lo que tienes que hacer para Node.js: -1. Crea un archivo fuente `bot.ts` con código TypeScript, por ejemplo usando [VS Code](https://code.visualstudio.com/) (o cualquier otro editor de código). +1. Crea un archivo fuente `bot.ts` con código TypeScript, por ejemplo usando [VS Code](https://code.visualstudio.com) (o cualquier otro editor de código). 2. Compila el código ejecutando un comando en tu terminal. Esto genera un archivo llamado `bot.js`. 3. Ejecuta `bot.js` usando Node.js, de nuevo desde tu terminal. diff --git a/site/docs/es/hosting/heroku.md b/site/docs/es/hosting/heroku.md index 1453f12a3..fca44f525 100644 --- a/site/docs/es/hosting/heroku.md +++ b/site/docs/es/hosting/heroku.md @@ -273,7 +273,7 @@ Si se ejecuta con éxito y no imprime ningún error, nuestros archivos compilado ### Configurar el `Procfile` -Por el momento, `Heroku` tiene varios [tipos de dynos](https://devcenter.heroku.com/articles/dyno-types). +Por el momento, `Heroku` tiene varios [tipos de dynos](https://devcenter.heroku.com/articles/dynos#use-cases). Dos de ellos son: - **Web dynos**: diff --git a/site/docs/es/plugins/commands.md b/site/docs/es/plugins/commands.md index d98f10de9..8b8f275f3 100644 --- a/site/docs/es/plugins/commands.md +++ b/site/docs/es/plugins/commands.md @@ -5,10 +5,632 @@ next: false # Comandos (`commands`) -Próximamente, por favor vuelva más tarde. +Manejo de comandos con esteroides. -## Resumen del plugin +Este plugin proporciona varias características relacionadas con el manejo de comandos que no están contenidas en [el manejo de comandos realizado por la librería central](../guide/commands). +He aquí un rápido resumen de lo que obtienes con este plugin: -- Nombre del plugin: `commands` -- [Fuente](https://github.com/grammyjs/commands) -- Referencia +- Mejor legibilidad del código encapsulando el middleware con definiciones de comandos +- Sincronización del menú de comandos de usuario mediante `setMyCommands`. +- Mejor agrupación y organización de comandos +- Posibilidad de ampliar el alcance de los comandos, por ejemplo: sólo accesibles para administradores de grupo o + canales, etc. +- Definición de traducciones de comandos +- Función «¿Quería decir...?», que encuentra el comando existente más cercano a una determinada + error de entrada del usuario +- Coincidencia de comandos sin distinción entre mayúsculas y minúsculas +- Establecimiento de un comportamiento personalizado para los comandos que mencionan explícitamente al usuario de tu bot, + como: `/start@your_bot`. +- Prefijos de comando personalizados, por ejemplo: `+`, `?` o cualquier símbolo en lugar de `/`. +- Soporte para comandos que no están al principio del mensaje +- Comandos RegExp + +Todas estas características son posibles porque definirás una o más estructuras de comandos centrales que definan los comandos de tu bot. + +## Uso básico + +Antes de entrar en materia, echa un vistazo a cómo puedes registrar y manejar un comando con el plugin: + +```ts +const myCommands = new CommandGroup(); + +myCommands.command("hello", "Di hola", (ctx) => ctx.reply(`¡Hola, mundo!`)); + +bot.use(myCommands); +``` + +Esto registra un nuevo comando `/start` a tu bot que será manejado por el middleware dado. + +Ahora, vamos a entrar en algunas de las herramientas adicionales que este plugin tiene para ofrecer. + +## Importación + +En primer lugar, así es como puedes importar todos los tipos y clases necesarios que proporciona el plugin. + +::: code-group + +```ts [TypeScript] +import { + CommandGroup, + commandNotFound, + commands, + type CommandsFlavor, +} from "@grammyjs/commands"; +``` + +```js [JavaScript] +const { CommandGroup, commands, commandNotFound } = require( + "@grammyjs/commands", +); +``` + +```ts [Deno] +import { + CommandGroup, + commandNotFound, + commands, + type CommandsFlavor, +} from "https://deno.land/x/grammy_commands/mod.ts"; +``` + +::: + +Ahora que las importaciones están asentadas, veamos cómo podemos hacer que nuestros comandos sean visibles para nuestros usuarios. + +## Configuración del menú de comandos de usuario + +Una vez que hayas definido tus comandos con una instancia de la clase `CommandGroup`, puedes llamar al método `setCommands`, que registrará todos los comandos definidos en tu bot. + +```ts +const myCommands = new CommandGroup(); + +myCommands.command("hello", "Di hola", (ctx) => ctx.reply("¡Hola!")); +myCommands.command( + "start", + "Iniciar el bot", + (ctx) => ctx.reply("Empezando..."), +); + +bot.use(myCommands); + +await myCommands.setCommands(bot); +``` + +Esto hará que todos los comandos que registres se muestren en el menú de un chat privado con tu bot, o siempre que los usuarios escriban `/` en un chat del que tu bot sea miembro. + +### Acceso directo contextual + +¿Qué pasa si quieres que algunos comandos sólo se muestren a determinados usuarios? Por ejemplo, imagina que tienes un comando `login` y otro `logout`. +El comando `login` sólo debería aparecer para los usuarios que han cerrado sesión, y viceversa. +Así es como puedes hacerlo con el plugin de comandos: + +::: code-group + +```ts [TypeScript] +// Utilice el flavor para crear un contexto personalizado +type MyContext = Context & CommandsFlavor; + +// Utiliza el nuevo contexto para instanciar tu bot +const bot = new Bot("token"); + +// Registrar el acceso directo de contexto +bot.use(commands()); + +const loggedOutCommands = new CommandGroup(); +const loggedInCommands = new CommandGroup(); + +loggedOutCommands.command( + "login", + "Inicie su sesión con el bot", + async (ctx) => { + await ctx.setMyCommands(loggedInCommands); + await ctx.reply("¡Bienvenidos! ¡Sesión iniciada!"); + }, +); + +loggedInCommands.command( + "logout", + "Termina tu sesión con el bot", + async (ctx) => { + await ctx.setMyCommands(loggedOutCommands); + await ctx.reply("Hasta luego :)"); + }, +); + +bot.use(loggedInCommands); +bot.use(loggedOutCommands); + +// Por defecto, los usuarios no están logueados, +// por lo que puedes establecer los comandos de desconexión para todos +await loggedOutCommands.setCommands(bot); +``` + +```js [JavaScript] +// Registrar el acceso directo de contexto +bot.use(commands()); + +const loggedOutCommands = new CommandGroup(); +const loggedInCommands = new CommandGroup(); + +loggedOutCommands.command( + "login", + "Inicie su sesión con el bot", + async (ctx) => { + await ctx.setMyCommands(loggedInCommands); + await ctx.reply("¡Bienvenidos! ¡Sesión iniciada!"); + }, +); + +loggedInCommands.command( + "logout", + "Termina tu sesión con el bot", + async (ctx) => { + await ctx.setMyCommands(loggedOutCommands); + await ctx.reply("Hasta luego :)"); + }, +); + +bot.use(loggedInCommands); +bot.use(loggedOutCommands); + +// Por defecto, los usuarios no están logueados, +// por lo que puedes establecer los comandos de desconexión para todos +await loggedOutCommands.setCommands(bot); +``` + +::: + +De esta forma, cuando un usuario llame a `/login`, su lista de comandos cambiará para contener sólo el comando `logout`. +Genial, ¿verdad? + +::: danger Restricciones en los Nombres de Comandos +Como se indica en la [Telegram Bot API documentation](https://core.telegram.org/bots/api#botcommand), los nombres de comando sólo pueden estar formados por: + +> 1-32 caracteres. +> Sólo puede contener letras minúsculas inglesas, dígitos y guiones bajos. + +Por lo tanto, llamar a `setCommands` o `setMyCommands` con algo que no sea lower_c4s3_commands lanzará una excepción. +Los comandos que no sigan estas reglas aún podrán ser registrados, utilizados y manejados, pero nunca se mostrarán en el menú de usuario como tales. +::: + +**Ten en cuenta** que `setCommands` y `setMyCommands` sólo afectan a los comandos mostrados en el menú de comandos del usuario, y no al acceso real a los mismos. +Aprenderás cómo implementar el acceso restringido a comandos en la sección [Comandos restringidos](#comandos-de-ambito). + +### Agrupando comandos + +Dado que podemos dividir y agrupar nuestros comandos en diferentes instancias, permite una organización de archivos de comandos mucho más idiomática. + +Digamos que queremos tener comandos sólo para desarrolladores. +Podemos lograrlo con la siguiente estructura de código: + +```ascii +src/ +├─ commands/ +│ ├─ admin.ts +│ ├─ users/ +│ │ ├─ group.ts +│ │ ├─ say-hi.ts +│ │ ├─ say-bye.ts +│ │ ├─ ... +├─ bot.ts +├─ types.ts +tsconfig.json +``` + +El siguiente grupo de código ejemplifica cómo podríamos implementar un grupo de comandos sólo para desarrolladores, y actualizar el menú de comandos del cliente de Telegram en consecuencia. +Asegúrate de fijarte en los diferentes patrones utilizados en los archivos `admin.ts` y `group.ts`. + +::: code-group + +```ts [types.ts] +export type MyContext = Context & CommandsFlavor; +``` + +```ts [bot.ts] +import { devCommands } from "./commands/admin.ts"; +import { userCommands } from "./commands/users/group.ts"; +import type { MyContext } from "./types.ts"; + +export const bot = new Bot("MyBotToken"); + +bot.use(commands()); + +bot.use(userCommands); +bot.use(devCommands); +``` + +```ts [admin.ts] +import { userCommands } from './users/group.ts' +import type { MyContext } from '../types.ts' + +export const devCommands = new CommandGroup() + +devCommands.command('devlogin', 'Saludos', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply('Hola a mí') + await ctx.setMyCommands(userCommands, devCommands) + } else { + await next() + } +}) + +devCommands.command('usercount', 'Saludos', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply( + `Usuarios activos: ${/** Your business logic */}` + ) + } else { + await next() + } +}) + +devCommands.command('devlogout', 'Saludos', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply('Adiós') + await ctx.setMyCommands(userCommands) + } else { + await next() + } + }) +``` + +```ts [group.ts] +import sayHi from "./say-hi.ts"; +import sayBye from "./say-bye.ts"; +import etc from "./another-command.ts"; +import type { MyContext } from "../../types.ts"; + +export const userCommands = new CommandGroup() + .add([sayHi, sayBye]); +``` + +```ts [say-hi.ts] +import type { MyContext } from "../../types.ts"; + +export default new Command("sayhi", "Saludos", async (ctx) => { + await ctx.reply("¡Hola pequeño usuario!"); +}); +``` + +::: + +¿Te has dado cuenta de que es posible registrar comandos individuales inicializados a través del método `.add` en la instancia `CommandGroup` o también directamente a través del método `.command(...)`? +Esto permite una estructura de un solo archivo, como en el archivo `admin.ts`, o una estructura de archivos más distribuida como en el archivo `group.ts`. + +::: tip Utiliza siempre grupos de comandos + +Al crear y exportar comandos utilizando el constructor `Command`, es obligatorio registrarlos en una instancia `CommandGroup` mediante el método `.add`. +Por sí solos son inútiles, así que asegúrate de hacerlo en algún momento. + +::: + +El plugin también te obliga a tener el mismo tipo de Contexto para un determinado `CommandGroup` y sus respectivos `Commands` ¡así evitarás a primera vista ese tipo de errores tontos! + +Combinando este conocimiento con la siguiente sección llevarás tu juego de comandos al siguiente nivel. + +## Comandos de ámbito + +¿Sabías que puedes permitir que se muestren diferentes comandos en diferentes chats dependiendo del tipo de chat, el idioma, e incluso el estado del usuario en un grupo de chat? Eso es lo que Telegram llama [**Ámbitos de Comandos**](https://core.telegram.org/bots/features#command-scopes). + +Ahora, los Ámbitos de Comandos son una característica genial, pero usarlos a mano puede ser realmente complicado, ya que es difícil hacer un seguimiento de todos los ámbitos y qué comandos presentan. +Además, al usar los Ámbitos de Comandos por sí solos, tienes que hacer un filtrado manual dentro de cada comando para asegurarte de que sólo se ejecutarán para los ámbitos correctos. +Sincronizar esas dos cosas puede ser una pesadilla, y por eso existe este plugin. +Comprueba cómo se hace. + +La clase `Command` devuelta por el método `command` expone un método llamado `addToScope`. +Este método toma un [BotCommandScope](/ref/types/botcommandscope) junto con uno o más handlers, y registra esos handlers para ser ejecutados en ese scope específico. + +Ni siquiera tienes que preocuparte de llamar a `filter`, el método `addToScope` garantizará que tu handler sólo sea llamado si el contexto es el correcto. + +Este es un ejemplo de un comando con ámbito: + +```ts +const myCommands = new CommandGroup(); + +myCommands + .command("start", "Inicializa la configuración del bot") + .addToScope( + { type: "all_private_chats" }, + (ctx) => ctx.reply(`Hola, ${ctx.chat.first_name}!`), + ) + .addToScope( + { type: "all_group_chats" }, + (ctx) => ctx.reply(`Hola, miembros de ${ctx.chat.title}!`), + ); +``` + +El comando `start` ahora puede ser llamado tanto desde chats privados como de grupo, y dará una respuesta diferente dependiendo desde donde sea llamado. +Ahora, si llamas a `myCommands.setCommands`, el comando `start` se registrará tanto en los chats privados como en los de grupo. + +Aquí tienes un ejemplo de un comando al que sólo pueden acceder los administradores de grupo. + +```js +adminCommands + .command("secret", "Sólo para administradores") + .addToScope( + { type: "all_chat_administrators" }, + (ctx) => ctx.reply("¡Pastel gratis!"), + ); +``` + +Y aquí hay un ejemplo de un comando que sólo es accesible en grupos + +```js +myCommands + .command("fun", "Risa") + .addToScope( + { type: "all_group_chats" }, + (ctx) => ctx.reply("Jaja"), + ); +``` + +Observa que cuando llamas al método `command`, se abre un nuevo comando. +Si le das un manejador, ese manejador se aplicará al ámbito `default` de ese comando. +Al llamar a `addToScope` en ese comando se añadirá un nuevo manejador, que se filtrará a ese ámbito. +Echa un vistazo a este ejemplo. + +```ts +myCommands + .command( + "default", + "Default command", + // Se ejecutará cuando no se esté en un chat de grupo o cuando el usuario no sea un administrador. + (ctx) => ctx.reply("Hello from default scope"), + ) + .addToScope( + { type: "all_group_chats" }, + // Esto sólo se llamará para los usuarios no administradores de un grupo + (ctx) => ctx.reply("Hello, group chat!"), + ) + .addToScope( + { type: "all_chat_administrators" }, + // Esto será llamado para los administradores de grupo, cuando estén dentro de ese grupo + (ctx) => ctx.reply("Hello, admin!"), + ); +``` + +## Traducciones de comandos + +Otra potente característica es la capacidad de establecer diferentes nombres para el mismo comando, y sus respectivas descripciones basadas en el idioma del usuario. +El plugin de comandos lo hace fácil proporcionando el método `localize`. +Compruébalo: + +```js +myCommands + // Debe establecer un nombre y una descripción por defecto + .command("hello", "Di hola") + // Y luego puede establecer los localizados + .localize("pt", "ola", "Dizer olá"); +``` + +¡Añade tantos como quieras! El plugin se encargará de registrarlos por ti cuando llames a `myCommands.setCommands`. + +Por conveniencia, grammY exporta un objeto tipo enum `LanguageCodes` que puedes usar para una aproximación más idiomática: + +::: code-group + +```ts [TypeScript] +import { LanguageCodes } from "grammy/types"; + +myCommands.command( + "chef", + "Entrega de filetes", + (ctx) => ctx.reply("¡Filete al plato!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Filete a domicilio", + ); +``` + +```js [JavaScript] +const { LanguageCodes } = require("grammy/types"); + +myCommands.command( + "chef", + "Entrega de filetes", + (ctx) => ctx.reply("¡Filete al plato!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Filete a domicilio", + ); +``` + +```ts [Deno] +import { LanguageCodes } from "https://deno.land/x/grammy/types.ts"; + +myCommands.command( + "chef", + "Entrega de filetes", + (ctx) => ctx.reply("¡Filete al plato!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Filete a domicilioo", + ); +``` + +::: + +### Localización de comandos con el plugin de internacionalización + +Si desea que los nombres y descripciones de los comandos localizados se agrupen dentro de los archivos `.ftl`, puede utilizar la siguiente idea: + +```ts +function addLocalizations(command: Command) { + i18n.locales.forEach((locale) => { + command.localize( + locale, + i18n.t(locale, `${command.name}.command`), + i18n.t(locale, `${command.name}.description`), + ); + }); + return command; +} + +myCommands.commands.forEach(addLocalizations); +``` + +## Encontrar el comando más cercano + +Aunque Telegram es capaz de autocompletar los comandos registrados, a veces los usuarios los escriben manualmente y, en algunos casos, cometen errores. +El plugin de comandos te ayuda a lidiar con eso permitiéndote sugerir un comando que podría ser lo que el usuario quería en primer lugar. +Es compatible con prefijos personalizados, así que no tienes que preocuparte por eso, y su uso es bastante sencillo: + +::: code-group + +```ts [TypeScript] +// Utilice el flavor para crear un contexto personalizado +type MyContext = Context & CommandsFlavor; + +// Utiliza el nuevo contexto para instanciar tu bot +const bot = new Bot("token"); +const myCommands = new CommandGroup(); + +// ... Registre los comandos + +bot + // Comprobar si existe un comando + .filter(commandNotFound(myCommands)) + // Si es así, significa que no fue manejado por ninguno de nuestros comandos. + .use(async (ctx) => { + // Encontramos una coincidencia potencial + if (ctx.commandSuggestion) { + await ctx.reply( + `Hmm... No conozco ese comando. ¿Te refieres a ${ctx.commandSuggestion}?`, + ); + } + + // Nada parece acercarse a lo que el usuario escribió + await ctx.reply("Uy... No conozco ese comando :/"); + }); +``` + +```js [JavaScript] +// Utiliza el nuevo contexto para instanciar tu bot +const bot = new Bot("token"); +const myCommands = new CommandGroup(); + +// ... Registre los comandos + +bot + // Comprobar si existe un comando + .filter(commandNotFound(myCommands)) + // Si es así, significa que no fue manejado por ninguno de nuestros comandos. + .use(async (ctx) => { + // Encontramos una coincidencia potencial + if (ctx.commandSuggestion) { + await ctx.reply( + `Hmm... No conozco ese comando. ¿Te refieres a ${ctx.commandSuggestion}?`, + ); + } + + // Nada parece acercarse a lo que el usuario escribió + await ctx.reply("Uy... No conozco ese comando :/"); + }); +``` + +::: + +Entre bastidores, `commandNotFound` utilizará el método contextual `getNearestCommand` que, por defecto, dará prioridad a los comandos que correspondan al idioma del usuario. +Si no se desea este comportamiento, se puede pasar el parámetro `ignoreLocalization` a true. +Es posible buscar entre múltiples instancias de CommandGroup, y `ctx.commandSuggestion` será el comando más similar, si lo hay, entre todos ellos. +También permite establecer la bandera `ignoreCase`, que ignorará las mayúsculas y minúsculas mientras se busca un comando similar y la bandera `similarityThreshold`, que controla lo similar que tiene que ser el nombre de un comando a la entrada del usuario para que sea recomendado. + +La función `commandNotFound` sólo se activará para actualizaciones que contengan texto similar a comandos registrados. +Por ejemplo, si sólo ha registrado [comandos con un prefijo personalizado](#prefijo) como `?`, se activará el controlador para cualquier cosa que se parezca a sus comandos, por ejemplo: `?sayhi` pero no `/definitely_a_command`. +Lo mismo ocurre a la inversa, si sólo tienes comandos con el prefijo por defecto, sólo se activará en las actualizaciones que se parezcan a `/regular` `/commands`. + +Los comandos recomendados sólo provendrán de las instancias de `CommandGroup` que pases a la función. Así que podrías diferir las comprobaciones en múltiples filtros separados. + +Utilicemos los conocimientos anteriores para inspeccionar el siguiente ejemplo: + +```ts +const myCommands = new CommandGroup(); +myCommands.command("dad", "calls dad", () => {}, { prefix: "?" }) + .localize("es", "papa", "llama a papa") + .localize("fr", "pere", "appelle papa"); + +const otherCommands = new CommandGroup(); +otherCommands.command("bread", "eat a toast", () => {}) + .localize("es", "pan", "come un pan") + .localize("fr", "pain", "manger du pain"); + +// Registrar cada grupo de comandos específico del idioma + +// Supongamos que el usuario es francés y ha escrito /Papi +bot + // este filtro se activará para cualquier comando como '/regular' o '?custom' + .filter(commandNotFound([myCommands, otherCommands], { + ignoreLocalization: true, + ignoreCase: true, + })) + .use(async (ctx) => { + ctx.commandSuggestion === "?papa"; // se evalúa como verdadero + }); +``` + +Si el `ignoreLocalization` fuera falso habríamos obtenido «`ctx.commandSuggestion` igual a `/pain`». +Podríamos añadir más filtros como el anterior, con diferentes parámetros o `CommandGroups` para comprobar. +Hay muchas posibilidades. + +## Opciones de comandos + +Hay algunas opciones que se pueden especificar por comando, por ámbito, o globalmente para una instancia `CommandGroup`. +Estas opciones te permiten personalizar aún más cómo tu bot maneja los comandos, dándote más flexibilidad. + +### ignoreCase + +Por defecto, los comandos harán coincidir la entrada del usuario distinguiendo entre mayúsculas y minúsculas. +Si se activa esta opción, por ejemplo, en un comando llamado `/dandy`, coincidirá con `/DANDY` del mismo modo que con `/dandY` o cualquier otra variación que distinga entre mayúsculas y minúsculas. + +### targetedCommands + +Cuando los usuarios invocan un comando, pueden etiquetar opcionalmente su bot, de la siguiente manera: `/comando@nombre_usuario_bot`. Puedes decidir qué hacer con estos comandos utilizando la opción de configuración `targetedCommands`. +Con ella puedes elegir entre tres comportamientos diferentes: + +- `ignored`: Ignora los comandos que mencionan al usuario de tu bot. +- `optional`: Maneja tanto los comandos que mencionan como los que no mencionan al usuario del bot +- `required`: Sólo maneja comandos que mencionan el usuario del bot + +### prefix + +Actualmente, sólo los comandos que empiezan por `/` son reconocidos por Telegram y, por tanto, por el [manejo de comandos realizado por la librería central de grammY](../guide/commands). +En algunas ocasiones, puede que quieras cambiar eso y usar un prefijo personalizado para tu bot. +Esto es posible gracias a la opción `prefix`, que le dirá al plugin de comandos que busque ese prefijo cuando intente identificar un comando. + +Si alguna vez necesitas recuperar entidades `botCommand` de una actualización y necesitas que se hidraten con el prefijo personalizado que has registrado, existe un método específicamente adaptado para ello, llamado `ctx.getCommandEntities(yourCommands)`, que devuelve la misma interfaz que `ctx.entities('bot_command')`. + +::: tip +Los comandos con prefijos personalizados no pueden mostrarse en el Menú Comandos. +::: + +### matchOnlyAtStart + +Cuando [maneja comandos](../guide/commands), la biblioteca central de grammY sólo reconocerá comandos que empiecen en el primer carácter de un mensaje. +El plugin de comandos, sin embargo, te permite escuchar comandos en medio del texto del mensaje, o al final, ¡no importa! +Todo lo que tienes que hacer es establecer la opción `matchOnlyAtStart` a `false`, y el resto lo hará el plugin. + +## Comandos RegExp + +Esta característica es para aquellos que realmente buscan ir salvaje, que le permite crear manejadores de comandos basados en expresiones regulares en lugar de cadenas estáticas, un ejemplo básico se vería así: + +```ts +myCommands + .command( + /delete_([a-zA-Z]+)/, + (ctx) => ctx.reply(`Deleting ${ctx.msg?.text?.split("_")[1]}`), + ); +``` + +Este gestor de órdenes se disparará en `/delete_me` igual que en `/delete_you`, y responderá «Borrarme» en el primer caso y «Borrarte» en el segundo, pero no se disparará en `/delete_` ni en `/delete_123xyz`, pasando como si no estuviera. + +## Plugin Summary + +- Name: `commands` +- [Source](https://github.com/grammyjs/commands) +- [Reference](/ref/commands/) diff --git a/site/docs/guide/introduction.md b/site/docs/guide/introduction.md index 997b96b5d..49f58acb2 100644 --- a/site/docs/guide/introduction.md +++ b/site/docs/guide/introduction.md @@ -184,7 +184,7 @@ In order to run the `bot.js` file, you have to have [Node.js](https://nodejs.org In summary, this is what you have to do for Node.js: -1. Create a source file `bot.ts` with TypeScript code, e.g. using [VS Code](https://code.visualstudio.com/) (or any other code editor). +1. Create a source file `bot.ts` with TypeScript code, e.g. using [VS Code](https://code.visualstudio.com) (or any other code editor). 2. Compile the code by running a command in your terminal. This generates a file called `bot.js`. 3. Run `bot.js` using Node.js, again from your terminal. diff --git a/site/docs/hosting/heroku.md b/site/docs/hosting/heroku.md index 42baf476e..d99a2c6ad 100644 --- a/site/docs/hosting/heroku.md +++ b/site/docs/hosting/heroku.md @@ -274,7 +274,7 @@ If it runs successfully and does not print any errors, our compiled files should ### Set up `Procfile` -For the time being, `Heroku` has several [types of dynos](https://devcenter.heroku.com/articles/dyno-types). +For the time being, `Heroku` has several [types of dynos](https://devcenter.heroku.com/articles/dynos#use-cases). Two of them are: - **Web dynos**: diff --git a/site/docs/id/guide/introduction.md b/site/docs/id/guide/introduction.md index 70842611f..78332a73c 100644 --- a/site/docs/id/guide/introduction.md +++ b/site/docs/id/guide/introduction.md @@ -146,7 +146,7 @@ Pertama-tama, [instal Deno](https://docs.deno.com/runtime/getting_started/instal Siapkan juga text editor yang sesuai untuk coding. Salah satu yang sesuai untuk Deno adalah Visual Studio Code, atau biasa disebut dengan VS Code. -Silahkan [diinstal](https://code.visualstudio.com/) juga. +Silahkan [diinstal](https://code.visualstudio.com) juga. Selanjutnya, kamu perlu menghubungkan Deno dan VS Code. Caranya sangat mudah: VS Code punya extension yang bisa melakukan semua hal tersebut secara otomatis. @@ -195,7 +195,7 @@ Untuk menjalankan file `bot.js`, kamu harus meng-install [Node.js](https://nodej Berikut tahap-tahap yang perlu dilakukan di Node.js: -1. Buat source file `bot.ts` menggunakan TypeScript, misalnya dengan menggunakan [VS Code](https://code.visualstudio.com/) (atau kode editor lainnya). +1. Buat source file `bot.ts` menggunakan TypeScript, misalnya dengan menggunakan [VS Code](https://code.visualstudio.com) (atau kode editor lainnya). 2. Compile kode dengan menjalankan perintah di terminal. Langkah ini akan menghasilkan file bernama `bot.js`. 3. Jalankan `bot.js` menggunakan Node.js, sekali lagi dari terminal. diff --git a/site/docs/id/hosting/heroku.md b/site/docs/id/hosting/heroku.md index 12db32abd..f61a80ebb 100644 --- a/site/docs/id/hosting/heroku.md +++ b/site/docs/id/hosting/heroku.md @@ -275,7 +275,7 @@ Jika berhasil dijalankan dan tidak ada pesan error yang muncul, file-file yang t ### Siapkan File `Procfile` -`Heroku` memiliki beberapa [jenis dynos](https://devcenter.heroku.com/articles/dyno-types). +`Heroku` memiliki beberapa [jenis dynos](https://devcenter.heroku.com/articles/dynos#use-cases). Dua diantaranya adalah: - **Web dynos**: diff --git a/site/docs/id/plugins/commands.md b/site/docs/id/plugins/commands.md index 6ff56bab2..70dcc3123 100644 --- a/site/docs/id/plugins/commands.md +++ b/site/docs/id/plugins/commands.md @@ -5,10 +5,663 @@ next: false # Perintah (`commands`) -Segera hadir, silahkan datang lagi di lain waktu. +Paket lengkap penanganan perintah (command). + +Plugin ini menyediakan berbagai fitur tambahan yang tidak tersedia di [library inti](../guide/commands). +Berikut manfaat yang dapat kamu peroleh: + +- Kode jadi lebih mudah dibaca. +- Sinkronisasi menu perintah user melalui `setMyCommands`. +- Perintah lebih tertata karena dapat dikelompokkan. +- Penerapan perintah untuk lingkup tertentu saja, misalnya diatur hanya tersedia untuk admin grup atau channel, dsb. +- Penerjemahan perintah. +- Fitur `Mungkin maksud Anda ...?` yang akan membantu menemukan perintah yang dimaksud ketika user salah ketik. +- Pencocokan perintah yang tidak peka huruf kapital (case-insensitive). +- Perilaku tersuai untuk perintah yang secara eksplisit terkandung di mention bot, seperti `/start@bot_kamu`. +- Awalan perintah bisa diubah, misalnya diganti menjadi `+`, `?`, `!`, dsb (semua simbol selain `/`). +- Mampu mendeteksi perintah yang tidak terletak di awal pesan. +- RegExp didukung! + +Semua fitur di atas dapat dicapai karena nantinya kamu diharuskan membuat struktur perintah yang sedemikian rupa untuk bot kamu. + +## Penggunaan Dasar + +Sebelum memulai, mari kita lihat cara mendaftarkan sebuah perintah menggunakan plugin ini: + +```ts +const myCommands = new CommandGroup(); + +myCommands.command("halo", "Ucapkan salam", (ctx) => ctx.reply(`Halo, dunia!`)); + +bot.use(myCommands); +``` + +Kode tersebut akan mendaftarkan perintah `/halo` ke bot kamu, yang kemudian akan diteruskan ke middleware terkait. + +Sekarang, mari kita pelajari alat-alat tambahan yang tersedia di plugin ini. + +## Melakukan Import + +Berikut cara meng-import semua class beserta type yang diperlukan: + +::: code-group + +```ts [TypeScript] +import { + CommandGroup, + commandNotFound, + commands, + type CommandsFlavor, +} from "@grammyjs/commands"; +``` + +```js [JavaScript] +const { CommandGroup, commands, commandNotFound } = require( + "@grammyjs/commands", +); +``` + +```ts [Deno] +import { + CommandGroup, + commandNotFound, + commands, + type CommandsFlavor, +} from "https://deno.land/x/grammy_commands/mod.ts"; +``` + +::: + +Semua import yang diperlukan sudah disusun. +Sekarang, mari kita cari tahu cara menampilkan perintah ke user. + +## Mengatur Menu Perintah User + +Setelah perintah dibuat menggunakan sebuah instansiasi class `CommandGroup`, sekarang kamu bisa memanggil method `setCommands`. +Method tersebut bertugas untuk mendaftarkan seluruh perintah tadi ke bot kamu. + +```ts +const myCommands = new CommandGroup(); + +myCommands.command( + "halo", + "Ucapkan salam", + (ctx) => ctx.reply("Halo, semuanya!"), +); +myCommands.command("start", "Mulai bot", (ctx) => ctx.reply("Memulai...")); + +bot.use(myCommands); + +await myCommands.setCommands(bot); +``` + +Kode di atas akan menampilkan setiap perintah ke menu yang berada di chat pribadi bot kamu, atau setiap kali user mengetik `/` di sebuah chat yang di dalamnya terdapat bot kamu juga. + +### Pintasan Context + +Bagaimana jika kamu ingin menampilkan perintah ke user tertentu saja? +Misalnya, bayangkan kamu memiliki sebuah perintah `login` dan `logout`. +Perintah `login` seharusnya hanya ditampilkan ketika user logout, dan sebaliknya. +Berikut cara mengatasinya menggunakan plugin commands: + +::: code-group + +```ts [TypeScript] +// Gunakan flavor untuk context tersuai. +type MyContext = Context & CommandsFlavor; + +// Gunakan context yang telah dibuat untuk menginisiasi bot kamu. +const bot = new Bot("token"); + +// Daftarkan pintasan context-nya. +bot.use(commands()); + +// Perintah untuk logout. +const loggedOutCommands = new CommandGroup(); +// Perintah untuk login. +const loggedInCommands = new CommandGroup(); + +// Tampilkan perintah masuk (login) ketika user keluar (logout). +loggedOutCommands.command( + "login", + "Mulai sesi baru", + async (ctx) => { + await ctx.setMyCommands(loggedInCommands); + await ctx.reply("Selamat datang! Sesi telah dimulai!"); + }, +); + +// Tampilkan perintah keluar (logout) ketika user masuk (login). +loggedInCommands.command( + "logout", + "Akhiri sesi", + async (ctx) => { + await ctx.setMyCommands(loggedOutCommands); + await ctx.reply("Sampai jumpa :)"); + }, +); + +bot.use(loggedInCommands); +bot.use(loggedOutCommands); + +// Secara bawaan, user tidak login. +// Oleh karena itu, kamu bisa menyetel perintah logout ke semua orang. +await loggedOutCommands.setCommands(bot); +``` + +```js [JavaScript] +// Daftarkan pintasan context-nya. +bot.use(commands()); + +// Perintah untuk logout. +const loggedOutCommands = new CommandGroup(); +// Perintah untuk login. +const loggedInCommands = new CommandGroup(); + +// Tampilkan perintah masuk (login) ketika user keluar (logout). +loggedOutCommands.command( + "login", + "Mulai sesi baru", + async (ctx) => { + await ctx.setMyCommands(loggedInCommands); + await ctx.reply("Selamat datang! Sesi telah dimulai!"); + }, +); + +// Tampilkan perintah keluar (logout) ketika user masuk (login). +loggedInCommands.command( + "logout", + "Akhiri sesi", + async (ctx) => { + await ctx.setMyCommands(loggedOutCommands); + await ctx.reply("Sampai jumpa :)"); + }, +); + +bot.use(loggedInCommands); +bot.use(loggedOutCommands); + +// Secara bawaan, user tidak login. +// Oleh karena itu, kamu bisa menyetel perintah logout ke semua orang. +await loggedOutCommands.setCommands(bot); +``` + +::: + +Dengan demikian, ketika user memanggil `/login`, daftar perintah mereka akan berubah menjadi perintah `logout`. +Keren, bukan? + +::: danger Ketentuan untuk Nama Perintah +Seperti yang telah diutarakan di [dokumentasi API Bot Telegram](https://core.telegram.org/bots/api#botcommand), nama perintah hanya boleh terdiri atas: + +> 1-32 karakter. +> Karakter yang diperbolehkan hanya huruf abjad non-kapital (a-z), angka (0-9), dan garis bawah (_). + +Itulah kenapa, pemanggilan `setCommands` atau `setMyCommands` di luar ketentuan di atas menyebabkan sebuah galat atau error. +Perintah yang tidak mengikuti ketentuan tersebut masih bisa ditambahkan, digunakan, dan ditangani, tetapi tidak akan pernah bisa ditampilkan di menu user. +::: + +**Perlu diketahui** bahwa `setCommands` dan `setMyCommands` hanya akan berdampak ke tampilan visual di menu perintah user, mereka tidak berdampak ke hak aksesnya. +Kamu akan mempelajari cara menerapkan akses terbatas untuk perintah tertentu di bagian [Lingkup Command](#lingkup-perintah). + +### Pengelompokan Perintah + +Karena kita bisa membagi dan mengelompokkan perintah menjadi beberapa bagian, maka penataan perintah ke bebeberapa file terpisah juga bisa dilakukan. + +Sebagai contoh, kita ingin perintah tertentu hanya tersedia untuk para developer saja. +Kita bisa melakukannya menggunakan struktur kode berikut: + +```ascii +src/ +├─ commands/ +│ ├─ admin.ts +│ ├─ users/ +│ │ ├─ grup.ts +│ │ ├─ salam-sambutan.ts +│ │ ├─ salam-perpisahan.ts +│ │ ├─ ... +├─ bot.ts +├─ types.ts +tsconfig.json +``` + +Berikut contoh kode untuk membuat grup perintah yang hanya tersedia untuk developer, serta memperbarui menu perintah di aplikasi Telegram user. +Perhatikan perbedaan pola yang digunakan di tab `admin.ts` dan `group.ts`. + +::: code-group + +```ts [types.ts] +export type MyContext = Context & CommandsFlavor; +``` + +```ts [bot.ts] +import { commandDev } from "./commands/admin.ts"; +import { commandUser } from "./commands/users/grup.ts"; +import type { MyContext } from "./types.ts"; + +export const bot = new Bot("TokenBot"); + +bot.use(commands()); + +bot.use(commandDev); +bot.use(commandUser); +``` + +```ts [admin.ts] +import { commandUser } from './users/grup.ts' +import type { MyContext } from '../types.ts' + +export const commandDev = new CommandGroup() + +commandDev.command('dev_login', 'Ucapkan salam', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply('Halo, diriku sendiri!') + await ctx.setMyCommands(commandUser, commandDev) + } else { + await next() + } +}) + +commandDev.command('jumlah_user', 'Ucapkan salam', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply( + `User aktif: ${/** Tulis alur kodemu di sini */}` + ) + } else { + await next() + } +}) + +commandDev.command('dev_logout', 'Ucapkan salam', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply('Sampai jumpa, diriku!') + await ctx.setMyCommands(commandUser) + } else { + await next() + } + }) +``` + +```ts [grup.ts] +import salamSambutan from "./salam-sambutan.ts"; +import salamPerpisahan from "./salam-perpisahan.ts"; +import dsb from "./command-lainnya.ts"; +import type { MyContext } from "../../types.ts"; + +export const userCommands = new CommandGroup() + .add([salamSambutan, salamPerpisahan]); +``` + +```ts [salamSambutan.ts] +import type { MyContext } from "../../types.ts"; + +export default new Command("halo", "Ucapkan salam", async (ctx) => { + await ctx.reply("Halo, user keren!"); +}); +``` + +::: + +Dari kode di atas, apakah kamu menyadari selain melalui method `.command(...)`, kita juga bisa menambahkan instansiasi `Commands` ke dalam `CommandGroup` menggunakan method `.add`? +Dengan begitu, baik struktur satu-file-tunggal, seperti yang telah kita terapkan di file `admin.ts`, maupun struktur file terdistribusi, seperti di file `group.ts`, keduanya bisa diterapkan dengan baik. + +::: tip Selalu Gunakan Pengelompokan Perintah + +Ketika membuat dan meng-export perintah menggunakan constructor `Command`, ia tidak bisa melakukan apa-apa hingga ia didaftarkan ke penangan terkait. +Oleh karena itu, pastikan untuk mendaftarkannya ke pengelompokan perintah `CommandGroup` melalui method `.add`. + +::: + +Plugin ini juga mengharuskan kamu untuk menggunakan type `Context` yang sama untuk `CommandGroup` dan `Commands` terkait agar kamu terhindar dari kesalahan-kesalahan remeh yang mungkin ditimbulkan. + +Dengan menggabungan pengetahuan di bagian ini dan bagian selanjutnya, kamu akan lebih jago untuk mengotak-atik perintah sesuai keinginan. + +## Lingkup Perintah + +Tahukah kamu bahwa kita bisa menampilkan command yang berbeda berdasarkan tipe obrolan, bahasa, atau bahkan status user di suatu grup? +Itulah yang Telegram sebut sebagai [**Command Scopes**](https://core.telegram.org/bots/features#command-scopes) atau lingkup perintah. + +Oke, lingkup perintah merupakan fitur yang keren. +Akan tetapi, menggunakannya secara manual dapat menimbulkan masalah baru karena untuk memantau semua lingkup serta perintah yang menyertainya bukanlah perkara mudah. +Selain itu, dengan menggunakan lingkup perintah secara langsung, kamu perlu melakukan filter secara manual di setiap perintah untuk memastikan mereka dijalankan hanya untuk lingkup tertentu saja. +Menyinkronkan kedua hal tersebut akan sangat merepotkan, itulah kenapa plugin ini dibuat. +Mari kita lihat bagaimana prosesnya. + +Class `Command` yang dikembalikan oleh method `command` mengekspos sebuah method bernama `addToScope`. +Method tersebut menerima sebuah [BotCommandScope](/ref/types/botcommandscope) serta satu atau lebih penangan lainnya. +Penangan tersebut kemudian akan didaftarkan dan dijalankan untuk lingkup-lingkup yang telah ditentukan. + +Kamu bahkan tidak perlu memikirkan `filter`, karena method `addToScope` akan memastikan penangan kamu dipanggil hanya jika context-nya sesuai. + +Berikut contoh penggunaan lingkup perintah: + +```ts +const myCommands = new CommandGroup(); + +myCommands + .command("start", "Mulai konfigurasi bot") + .addToScope( + { type: "all_private_chats" }, + (ctx) => ctx.reply(`Halo, ${ctx.chat.first_name}!`), + ) + .addToScope( + { type: "all_group_chats" }, + (ctx) => ctx.reply(`Halo, anggota ${ctx.chat.title}!`), + ); +``` + +Perintah `start` sekarang bisa dipanggil baik dari obrolan privat maupun grup. +Responnya pun akan berbeda berdasarkan sumber obrolannya. +Selain itu, jika kamu memanggil `myCommands.setCommands`, perintah `start` akan ditambahkan baik untuk obrolan privat maupun grup. + +Berikut contoh sebuah perintah yang hanya bisa diakses melalui grup admin. + +```js +adminCommands + .command("rahasia", "Khusus admin") + .addToScope( + { type: "all_chat_administrators" }, + (ctx) => ctx.reply("Ayam geprek gratis!"), + ); +``` + +Contoh yang ini untuk perintah yang hanya bisa diakses melalui grup. + +```js +myCommands + .command("lucu", "Ketawa") + .addToScope( + { type: "all_group_chats" }, + (ctx) => ctx.reply("Wkwk"), + ); +``` + +Dari contoh di atas, ketika kamu memanggil method `command`, ia menerima sebuah perintah. +Selain perintah, jika kamu memberinya sebuah penangan, penangan tersebut akan diterapkan untuk lingkup `default` atau bawaan. +Pemanggilan `addToScope` untuk command tersebut akan menambah sebuah penangan baru untuk lingkup yang telah ditentukan. +Coba lihat contoh berikut: + +```ts +myCommands + .command( + "default", + "Perintah Bawaan", + // Berikut akan dipanggil ketika berasal selain dari obrolan grup. + (ctx) => ctx.reply("Halo! Respon ini berasal dari lingkup bawaan."), + ) + .addToScope( + { type: "all_group_chats" }, + // Berikut akan dipanggil untuk anggota grup yang bukan admin. + (ctx) => ctx.reply("Halo, grup!"), + ) + .addToScope( + { type: "all_chat_administrators" }, + // Berikut akan dipanggil khusus untuk admin grup yang bersangkutan. + (ctx) => ctx.reply("Halo, admin!"), + ); +``` + +## Terjemahan Perintah + +Fitur keren lainnya adalah kita bisa mengatur beberapa nama dan deskripsi yang berbeda untuk satu perintah yang sama berdasarkan bahasa yang digunakan oleh user. +Plugin commands menyediakan method `localize` yang bisa kamu gunakan untuk melakukan pekerjaan tersebut dengan mudah. +Coba lihat contoh berikut: + +```js +myCommands + // Kamu diharuskan membuat satu nama bawaan beserta deskripsinya. + .command("hello", "Say hello") + // Setelah itu, kamu bisa membuat versi terjemahannya. + .localize("id", "halo", "Bilang Halo"); +``` + +Kamu bisa menambahkan beberapa terjemahan sebanyak yang kamu mau! +Plugin akan mendaftarkan semua terjemahan tersebut ketika `myCommands.setCommands` dipanggil. + +Untuk mempermudah pekerjaan, grammY menyediakan sebuah object enum `LanguageCodes` yang bisa kamu gunakan seperti pada contoh berikut: + +::: code-group + +```ts [TypeScript] +import { LanguageCodes } from "grammy/types"; + +myCommands.command( + "chef", + "Fried chicken delivery", + (ctx) => ctx.reply("Fried chicken on the plate!"), +) + .localize( + LanguageCodes.Indonesian, + "Sajikan ayam goreng", + "Ayam goreng siap disajikan!", + ); +``` + +```js [JavaScript] +const { LanguageCodes } = require("grammy/types"); + +myCommands.command( + "chef", + "Fried chicken delivery", + (ctx) => ctx.reply("Fried chicken on the plate!"), +) + .localize( + LanguageCodes.Indonesian, + "Sajikan ayam goreng", + "Ayam goreng siap disajikan!", + ); +``` + +```ts [Deno] +import { LanguageCodes } from "https://deno.land/x/grammy/types.ts"; + +myCommands.command( + "chef", + "Fried chicken delivery", + (ctx) => ctx.reply("Fried chicken on the plate!"), +) + .localize( + LanguageCodes.Indonesian, + "Sajikan ayam goreng", + "Ayam goreng siap disajikan!", + ); +``` + +::: + +### Melokalkan Perintah Menggunakan Plugin Internasionalisasi + +Jika kamu mencari cara untuk melokalkan nama beserta deskripsi untuk file `.ftl`, kamu bisa menerapkan konsep berikut: + +```ts +function addLocalizations(command: Command) { + i18n.locales.forEach((locale) => { + command.localize( + locale, + i18n.t(locale, `${command.name}.command`), + i18n.t(locale, `${command.name}.description`), + ); + }); + return command; +} + +myCommands.commands.forEach(addLocalizations); +``` + +## Menemukan Perintah yang Paling Mirip + +Meski Telegram menyediakan fitur lengkapi-otomatis (auto-complete) untuk perintah-perintah yang terdaftar, namun user terkadang mengetiknya secara manual, yang mana kesalahan ketik sangat mungkin terjadi. +Plugin ini dapat membantumu mengatasi skenario tersebut dengan cara menyediakan saran perintah yang mungkin dimaksud oleh user. +Fitur tersebut juga kompatibel dengan awalan tersuai. +Jadi, kamu tidak perlu khawatir akan hal tersebut. +Cara penggunaanya pun tidak rumit: + +::: code-group + +```ts [TypeScript] +// Gunakan flavor untuk membuat context tersuai. +type MyContext = Context & CommandsFlavor; + +// Gunakan context yang telah dibuat untuk menginisiasi bot kamu. +const bot = new Bot("token"); +const myCommands = new CommandGroup(); + +// ... Daftarkan perintah-perintahnya. + +bot + // Periksa apakah mengandung sebuah perintah. + .filter(commandNotFound(myCommands)) + // Jika mengandung perintah, berarti ia tidak ditangani oleh penangan perintah sebelumnya. + .use(async (ctx) => { + // Sarankan perintah yang mirip. + if (ctx.commandSuggestion) { + await ctx.reply( + `Maaf, saya tidak tahu perintah tersebut. Apakah maksud Anda ${ctx.commandSuggestion}?`, + ); + } + + // Perintah yang mirip tidak ditemukan. + await ctx.reply("Maaf, saya tidak tahu perintah tersebut."); + }); +``` + +```js [JavaScript] +const bot = new Bot("token"); +const myCommands = new CommandGroup(); + +// ... Daftarkan perintah-perintahnya. + +bot + // Periksa apakah mengandung sebuah perintah. + .filter(commandNotFound(myCommands)) + // Jika mengandung perintah, berarti ia tidak ditangani oleh penangan perintah sebelumnya. + .use(async (ctx) => { + // Sarankan perintah yang mirip. + if (ctx.commandSuggestion) { + await ctx.reply( + `Maaf, saya tidak tahu perintah tersebut. Apakah maksud Anda ${ctx.commandSuggestion}?`, + ); + } + + // Perintah yang mirip tidak ditemukan. + await ctx.reply("Maaf, saya tidak tahu perintah tersebut."); + }); +``` + +::: + +Di balik layar, `commandNotFound` menggunakan method context `getNearestCommand` yang secara bawaan memprioritaskan perintah berdasarkan bahasa user. +Jika kamu tidak menghendakinya, atur nilai flag `ignoreLocalization` menjadi `true`. +Pencarian di beberapa instansiasi `CommandGroup` juga dimungkinkan karena `ctx.commandSuggestion` juga akan mencari perintah yang paling mirip di semua instansiasi tersebut. +Selain itu, kamu bisa mengatur flag `ignoreCase` untuk mengabaikan peka huruf kapital (case-sensitive) ketika mencari perintah yang serupa, dan flag `similarityThreshold` untuk mengatur tingkat kemiripan suatu perintah hingga layak dijadikan sebuah saran. + +Function `commandNotFound` akan terpicu hanya untuk update yang mengandung teks perintah yang mirip dengan perintah-perintah yang kamu telah kamu daftarkan. +Misalnya, jika kamu mendaftarkan [perintah yang menggunakan awalan tersuai](#prefix) seperti `?`, ia akan memicu penangan terkait untuk semua entitas yang mirip dengan perintah tersebut. +Sebagai contoh, teks `?halo` akan memicu penangan terkait, tapi tidak dengan `/halo`. +Hal yang sama juga berlaku sebaliknya, jika kamu memiliki perintah yang menggunakan awalan bawaan, ia hanya akan terpicu untuk update seperti `/halo`, `/mulai`, dsb. + +Perintah yang disarankan berasal dari instansiasi `CommandGroup` yang kamu terapkan ke function terkait saja. +Artinya, pengecekan bisa dialihkan menjadi beberapa filter terpisah. + +Mari kita terapkan pengetahuan yang sudah kita dapat sejauh ini: + +```ts +const myCommands = new CommandGroup(); +myCommands.command("dad", "calls dad", () => {}, { prefix: "?" }) + // Indonesia + .localize("id", "ayah", "panggil ayah") + // Spanyol + .localize("es", "papa", "llama a papa") + // Perancis + .localize("fr", "pere", "appelle papa"); + +const otherCommands = new CommandGroup(); +otherCommands.command("bread", "eat a toast", () => {}) + // Indonesia + .localize("id", "roti", "makan roti goreng") + // Spanyol + .localize("es", "pan", "come un pan") + // Perancis + .localize("fr", "pain", "manger du pain"); + +// Daftarkan grup perintah untuk setiap bahasa. + +// Mari kita asumsikan user adalah orang Perancis dan mengetik /Papi. +bot + // Filter ini akan terpicu untuk semua perintah, baik '/reguler' ataupun '?tersuai'. + .filter(commandNotFound([myCommands, otherCommands], { + ignoreLocalization: true, + ignoreCase: true, + })) + .use(async (ctx) => { + ctx.commandSuggestion === "?papa"; // Menghasilkan nilai true. + }); +``` + +Jika misalkan `ignoreLocalization` bernilai `false`, `ctx.commandSuggestion` akan bernilai `/pain`. +Kita bisa menambahkan lebih banyak filter dengan parameter yang berbeda ataupun menggunakan `CommandGroups` untuk melakukan pengecekan. +Dan cara-cara lain yang bisa kita eksplorasi! + +## Opsi Perintah + +Instansiasi `CommandGroup` memiliki beberapa opsi yang bisa kita terapkan, baik untuk setiap perintah, setiap lingkup, ataupun secara global. +Opsi-opsi berikut bisa kamu manfaatkan untuk mengatur perilaku bot dalam menangani perintah secara fleksibel. + +### ignoreCase + +Secara bawaan, perintah akan dicocokkan dengan memperhatikan huruf kapital (case-sensitive). +Ketika flag ini diterapkan, semua huruf baik kapital maupun tidak, akan dianggap sama. +Perintah bernama `/budi` akan cocok dengan `/BUDI`, `/budI`, atau variasi huruf kapital lainnya. + +### targetedCommands + +Ketika user memanggil sebuah perintah, mereka bisa menyebut bot kamu seperti ini: `/perintah@username_bot`. +Kamu bisa memutuskan apa yang bot harus lakukan terhadap jenis perintah tersebut menggunakan opsi pengaturan `targetedCommands`. +Melalui opsi tersebut, kamu bisa memilih tiga perilaku yang berbeda: + +- `ignored`: Abaikan perintah yang menyertakan username bot kamu. +- `optional`: Tangani kedua jenis perintah, baik yang menyertakan username bot kamu, maupun yang tidak. +- `required`: Hanya tangani perintah yang menyertakan username bot kamu. + +### prefix + +Ketika tulisan ini dibuat, Telegram hanya mengenali perintah yang dimulai dengan awalan `/`, yang mana bisa ditangani dengan baik oleh [library inti grammY](../guide/commands). +Di beberapa skenario, kamu mungkin ingin mengubah perilaku tersebut dan menggantinya dengan awalan tersuai. +Kamu bisa memanfaatkan opsi `prefix` supaya plugin mencari awalan yang dimaksud ketika menganalisa sebuah perintah. + +Jika kamu perlu mengambil entity `botCommand` dari suatu update untuk kemudian dihidrasi menggunakan awalan tersuai yang telah didaftarkan, kamu bisa menggunakan method bernama `ctx.getCommandEntities(perintahKamu)`. +Ia mengembalikan interface yang sama dengan `ctx.entities('bot_command')`. + +:::tip +Perintah dengan awalan tersuai tidak dapat ditampilkan di menu perintah user. +::: + +### matchOnlyAtStart + +Ketika [menangani perintah](../guide/commands), library inti grammY hanya akan mengenali perintah yang terletak di awal kalimat. +Berbeda dengan plugin commands, Ia mampu menyimak perintah baik yang terletak di pertengahan, maupun di akhir kalimat. +Cukup atur nilai opsi `matchOnlyAtStart` menjadi `false` untuk mengaktifkannya. + +## Perintah RegExp + +Fitur ini ditujukan untuk kamu yang suka petualangan. +Kamu bisa menggunakan regular expression (RegExp) alih-alih string statis. +Berikut contoh sederhananya: + +```ts +myCommands + .command( + /hapus_([a-zA-Z]+)/, + (ctx) => ctx.reply(`Menghapus ${ctx.msg?.text?.split("_")[1]}`), + ); +``` + +Penangan perintah di atas akan terpicu untuk `/hapus_kenangan`, ataupun `/hapus_dosa`. +Ia akan membalas pesan dengan "Menghapus kenangan" untuk contoh pertama dan "Menghapus dosa" untuk contoh kedua. +Tetapi, ia tidak akan terpicu untuk perintah `/hapus_` ataupun `/hapus_123xyz`. ## Ringkasan Plugin - Nama: `commands` - [Sumber](https://github.com/grammyjs/commands) -- Referensi +- [Reference](/ref/commands/) diff --git a/site/docs/plugins/README.md b/site/docs/plugins/README.md index 9559a70a9..4739a11da 100644 --- a/site/docs/plugins/README.md +++ b/site/docs/plugins/README.md @@ -36,6 +36,7 @@ Installing plugins is fun and easy, and we want you to know what we have in stor | [Media Groups](./media-group) | _built-in_ | Simplify sending media groups and editing media | | [Inline Queries](./inline-query) | _built-in_ | Easily build results for inline queries | | [Auto-retry](./auto-retry) | [`auto-retry`](./auto-retry) | Automatically handle rate limiting | +| [Commands](./commands) | [`commands`](./commands) | Manage commands in a powerful way | | [Conversations](./conversations) | [`conversations`](./conversations) | Build powerful conversational interfaces and dialogs | | [Chat Members](./chat-members) | [`chat-members`](./chat-members) | Track which user joined which chat | | [Emoji](./emoji) | [`emoji`](./emoji) | Simplify using emoji in code | diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index e8525a963..82b87dbc8 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -5,10 +5,653 @@ next: false # Commands (`commands`) -Coming soon, please come back later. +Command handling on steroids. + +This plugin offers advanced command-handling features beyond the core library's [command handling](../guide/commands). +Here is a quick overview of what you get with this plugin: + +- Better code readability by encapsulating middleware with command definitions. +- User command menu synchronization via `setMyCommands`. +- Improved command grouping and organization. +- Command reach scoping, e.g. limiting access to group admins or specific channels. +- Support for command translations. +- `Did you mean ...?` feature to suggest the closest command when a user makes a typo. +- Case-insensitive command matching. +- Setting custom behavior for commands that explicitly mention your bot's username, such as `/start@your_bot`. +- Custom command prefixes, e.g. `+`, `?`, or any symbol instead of `/`. +- Support for commands not located at the start of a message. +- RegExp commands! + +All of these features are powered by central command structures that you define for your bot. + +## Basic Usage + +Before we dive in, take a look at how you can register and handle a command with the plugin: + +```ts +const myCommands = new CommandGroup(); + +myCommands.command("hello", "Say hello", (ctx) => ctx.reply(`Hello, world!`)); + +bot.use(myCommands); +``` + +This registers a new `/hello` command to your bot, which will be handled by the given middleware. + +Now, let's get into some of the extra tools this plugin has to offer. + +## Importing + +First of all, here's how you can import all the necessary types and classes provided by the plugin. + +::: code-group + +```ts [TypeScript] +import { + CommandGroup, + commandNotFound, + commands, + type CommandsFlavor, +} from "@grammyjs/commands"; +``` + +```js [JavaScript] +const { CommandGroup, commandNotFound, commands } = require( + "@grammyjs/commands", +); +``` + +```ts [Deno] +import { + CommandGroup, + commandNotFound, + commands, + type CommandsFlavor, +} from "https://deno.land/x/grammy_commands/mod.ts"; +``` + +::: + +Now that the imports are settled, let's see how we can make our commands visible to our users. + +## User Command Menu Setting + +Once you have defined your commands using the `CommandGroup` class, you can call the `setCommands` method to add all the defined commands to the user command menu. + +```ts +const myCommands = new CommandGroup(); + +myCommands.command("hello", "Say hello", (ctx) => ctx.reply("Hello there!")); +myCommands.command("start", "Start the bot", (ctx) => ctx.reply("Starting...")); + +bot.use(myCommands); + +// Update the user command menu +await myCommands.setCommands(bot); // [!code highlight] +``` + +This ensures that each registered command appears in the menu of a private chat with your bot or when users type `/` in a chat where your bot is a member. + +### Context Shortcut + +What if you want some commands displayed only to certain users? +For example, imagine you have a `login` and a `logout` command. +The `login` command should only appear for logged-out users, and vice versa. +Here's how to do that with the commands plugin: + +::: code-group + +```ts [TypeScript] +// Use the flavor to create a custom context +type MyContext = CommandsFlavor; + +// Use the new context to instantiate your bot +const bot = new Bot(""); // <-- put your bot token between the "" (https://t.me/BotFather) + +// Register the context shortcut +bot.use(commands()); + +const loggedOutCommands = new CommandGroup(); +const loggedInCommands = new CommandGroup(); + +loggedOutCommands.command( + "login", + "Start your session with the bot", + async (ctx) => { + await ctx.setMyCommands(loggedInCommands); + await ctx.reply("Welcome! Session started!"); + }, +); + +loggedInCommands.command( + "logout", + "End your session with the bot", + async (ctx) => { + await ctx.setMyCommands(loggedOutCommands); + await ctx.reply("Goodbye :)"); + }, +); + +bot.use(loggedInCommands); +bot.use(loggedOutCommands); + +// By default, users are not logged in, +// so you can set the logged-out commands for everyone +await loggedOutCommands.setCommands(bot); +``` + +```js [JavaScript] +const bot = new Bot(""); // <-- put your bot token between the "" (https://t.me/BotFather) + +// Register the context shortcut +bot.use(commands()); + +const loggedOutCommands = new CommandGroup(); +const loggedInCommands = new CommandGroup(); + +loggedOutCommands.command( + "login", + "Start your session with the bot", + async (ctx) => { + await ctx.setMyCommands(loggedInCommands); + await ctx.reply("Welcome! Session started!"); + }, +); + +loggedInCommands.command( + "logout", + "End your session with the bot", + async (ctx) => { + await ctx.setMyCommands(loggedOutCommands); + await ctx.reply("Goodbye :)"); + }, +); + +bot.use(loggedInCommands); +bot.use(loggedOutCommands); + +// By default, users are not logged in, +// so you can set the logged-out commands for everyone +await loggedOutCommands.setCommands(bot); +``` + +::: + +This way, when a user calls `/login`, they'll have their command list changed to contain only the `logout` command. +Neat, right? + +::: danger Command Name Restrictions +As stated in the [Telegram Bot API documentation](https://core.telegram.org/bots/api#botcommand), command names must consist of: + +1. Between 1 and 32 characters. +2. Only lowercase English letters (a-z), digits (0-9), and underscores (_). + +Therefore, calling `setCommands` or `setMyCommands` with invalid command names will throw an exception. +Commands that don't follow these rules can still be registered and handled, but won't appear in the user command menu. +::: + +**Be aware** that `setCommands` and `setMyCommands` only affect the commands displayed in the user's commands menu, and not the actual access to them. +You will learn how to implement restricted command access in the [Scoped Commands](#scoped-commands) section. + +### Grouping Commands + +Since we can split and group our commands into different instances, it allows for a much more idiomatic command file organization. + +Let's say we want to have developer-only commands. +We can achieve that with the following code structure: + +```ascii +. +├── types.ts +├── bot.ts +└── commands/ + ├── admin.ts + └── users/ + ├── group.ts + ├── say-hello.ts + └── say-bye.ts +``` + +The following code group exemplifies how we could implement a developer only command group, and update the Telegram client command menu accordingly. +Make sure you take notice of the different patterns being used in the `admin.ts` and `group.ts` file-tabs. + +::: code-group + +```ts [types.ts] +import type { Context } from "grammy"; +import type { CommandsFlavor } from "grammy_commands"; + +export type MyContext = Context & CommandsFlavor; +``` + +```ts [bot.ts] +import { devCommands } from "./commands/admin.ts"; +import { userCommands } from "./commands/users/group.ts"; +import type { MyContext } from "./types.ts"; + +export const bot = new Bot(""); // <-- put your bot token between the "" (https://t.me/BotFather) + +bot.use(commands()); + +bot.use(userCommands); +bot.use(devCommands); +``` + +```ts [admin.ts] +import { userCommands } from './users/group.ts'; +import type { MyContext } from '../types.ts'; + +export const devCommands = new CommandGroup(); + +devCommands.command('devlogin', 'Login', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply('Hello, fellow developer! Are we having coffee today too?'); + await ctx.setMyCommands(userCommands, devCommands); + } else { + await next(); + } +}); + +devCommands.command('usercount', 'Count the active users', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply( `Active users: ${/** Your business logic */}`); + } else { + await next(); + } +}); + +devCommands.command('devlogout', 'Logout', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply('Until next commit!'); + await ctx.setMyCommands(userCommands); + } else { + await next(); + } + }); +``` + +```ts [group.ts] +import sayHello from "./say-hello.ts"; +import sayBye from "./say-bye.ts"; +import type { MyContext } from "../../types.ts"; + +export const userCommands = new CommandGroup() + .add([sayHello, sayBye]); +``` + +```ts [say-hello.ts] +import type { MyContext } from "../../types.ts"; + +export default new Command("hello", "Say hello", async (ctx) => { + await ctx.reply("Hello, little user!"); +}); +``` + +```ts [say-bye.ts] +import type { MyContext } from "../../types.ts"; + +export default new Command("bye", "Say bye", async (ctx) => { + await ctx.reply("Goodbye :)"); +}); +``` + +::: + +Did you know that, as shown in the example above, you can create commands either by using the `.command(...)` method directly or by registering initialized `Commands` into a `CommandGroup` instance with the `.add` method? +This approach lets you keep everything in a single file, like in `admin.ts`, or organize your commands across multiple files, like in `group.ts`. + +::: tip Always Use Command Groups + +When creating and exporting commands using the `Command` constructor, it's mandatory to register them onto a `CommandGroup` instance via the `.add` method. +On their own they are useless, so make sure you do that at some point. + +::: + +The plugin also ensures that a `CommandGroup` and its `Commands` share the same `Context` type, so you can avoid that kind of silly mistake at first glance! + +Combining this knowledge with the following section will get your command-game to the next level. + +## Scoped Commands + +Did you know you can show different commands in various chats based on the chat type, language, and even user status within a chat group? +That's what Telegram refers to as [**command scopes**](https://core.telegram.org/bots/features#command-scopes). + +Now, command scopes are a cool feature, but using them by hand can get really messy since it's hard to keep track of all the scopes and the commands they present. +Plus, by using command scopes on their own, you have to do manual filtering inside each command to ensure they run only for the correct scopes. +Syncing those two things up can be a nightmare, which is why this plugin exists. +Let's check how it's done. + +The `Command` class returned by the `command` method exposes a method called `addToScope`. +This method takes in a [`BotCommandScope`](/ref/types/botcommandscope) together with one or more handlers, and registers those handlers to be run at that specific scope. + +You don't even need to worry about calling `filter`, the `addToScope` method will guarantee that your handler only gets called if the context is right. + +Here's an example of a scoped command: + +```ts +const myCommands = new CommandGroup(); + +myCommands + .command("hello", "Say hello") + .addToScope( + { type: "all_private_chats" }, + (ctx) => ctx.reply(`Hello, ${ctx.chat.first_name}!`), + ) + .addToScope( + { type: "all_group_chats" }, + (ctx) => ctx.reply(`Hello, members of ${ctx.chat.title}!`), + ); +``` + +The `hello` command can now be called from both private and group chats, and it will give a different response depending on where it gets called from. +Now, if you call `myCommands.setCommands`, the `hello` command menu will be displayed in both private and group chats. + +Here's an example of a command that's only accessible to group admins. + +```js +adminCommands + .command("secret", "Admin only") + .addToScope( + { type: "all_chat_administrators" }, + (ctx) => ctx.reply("Free cake!"), + ); +``` + +And here is an example of a command that's only accessible in groups. + +```js +groupCommands + .command("fun", "Laugh") + .addToScope( + { type: "all_group_chats" }, + (ctx) => ctx.reply("Haha"), + ); +``` + +Notice that the `command` method could receive the handler too. +If you give it a handler, that handler will apply to the `default` scope of that command. +Calling `addToScope` on that command will then add a new handler, which will be filtered for that scope. +Take a look at this example. + +```ts +myCommands + .command( + "default", + "Default command", + // This will be called when not in a group chat + (ctx) => ctx.reply("Hello from default scope"), + ) + .addToScope( + { type: "all_group_chats" }, + // This will only be called for non-admin users in a group + (ctx) => ctx.reply("Hello, group chat!"), + ) + .addToScope( + { type: "all_chat_administrators" }, + // This will be called for group admins, when inside that group + (ctx) => ctx.reply("Hello, admin!"), + ); +``` + +## Command Translations + +Another powerful feature is the ability to set different names and their respective descriptions for the same command based on the user language. +The commands plugin makes that easy by providing the `localize` method. +Check it out: + +```js +myCommands + // You need to set a default name and description + .command("hello", "Say hello") + // And then you can set the localized ones + .localize("pt", "ola", "Dizer olá"); +``` + +Add as many as you want! +The plugin will take care of registering them for you when you call `myCommands.setCommands`. + +For convenience, grammY exports a `LanguageCodes` enum-like object, which you can use to create a more idiomatic approach. + +::: code-group + +```ts [TypeScript] +import { LanguageCodes } from "grammy/types"; + +myCommands.command( + "chef", + "Steak delivery", + (ctx) => ctx.reply("Steak on the plate!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Bife a domicilio", + ); +``` + +```js [JavaScript] +const { LanguageCodes } = require("grammy/types"); + +myCommands.command( + "chef", + "Steak delivery", + (ctx) => ctx.reply("Steak on the plate!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Bife a domicilio", + ); +``` + +```ts [Deno] +import { LanguageCodes } from "https://deno.land/x/grammy/types.ts"; + +myCommands.command( + "chef", + "Steak delivery", + (ctx) => ctx.reply("Steak on the plate!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Bife a domicilio", + ); +``` + +::: + +### Localizing Commands with the Internationalization Plugin + +If you are looking to have your localized command names and descriptions bundled inside your `.ftl` files, you could make use of the following approach: + +```ts +function addLocalizations(command: Command) { + i18n.locales.forEach((locale) => { + command.localize( + locale, + i18n.t(locale, `${command.name}.command`), + i18n.t(locale, `${command.name}.description`), + ); + }); + return command; +} + +myCommands.commands.forEach(addLocalizations); +``` + +## Finding the Nearest Command + +Telegram can automatically complete registered commands. +However, sometimes users still type these commands manually and may make mistakes. + +To help with this, the commands plugin suggests a command that the user might have intended to use. + +This plugin works with custom prefixes, so you don’t need to worry about compatibility. +Plus, it’s easy to use. + +::: code-group + +```ts [TypeScript] +// Use the flavor to create a custom context +type MyContext = Context & CommandsFlavor; + +// Use the new context to instantiate your bot +const bot = new Bot(""); // <-- put your bot token between the "" (https://t.me/BotFather) +const myCommands = new CommandGroup(); + +// ... Register the commands + +bot + // Check if there is a command + .filter(commandNotFound(myCommands)) + // If so, that means it wasn't handled by any of our commands + .use(async (ctx) => { + // We found a potential match + if (ctx.commandSuggestion) { + await ctx.reply( + `Hmm... I don't know that command. Did you mean ${ctx.commandSuggestion}?`, + ); + } + + // Nothing seems to come close to what the user typed + await ctx.reply("Oops... I don't know that command :/"); + }); +``` + +```js [JavaScript] +const bot = new Bot(""); // <-- put your bot token between the "" (https://t.me/BotFather) +const myCommands = new CommandGroup(); + +// ... Register the commands + +bot + // Check if there is a command + .filter(commandNotFound(myCommands)) + // If so, that means it wasn't handled by any of our commands + .use(async (ctx) => { + // We found a potential match + if (ctx.commandSuggestion) { + await ctx.reply( + `Hmm... I don't know that command. Did you mean ${ctx.commandSuggestion}?`, + ); + } + + // Nothing seems to come close to what the user typed + await ctx.reply("Oops... I don't know that command :/"); + }); +``` + +::: + +The `commandNotFound` gives you some options to configure: + +- `ignoreLocalization`: By default, `commandNotFound` prioritizes commands that match the user language. + To opt-out, set this option to `true`. +- `ignoreCase`: Allows the plugin to ignore letter casing when searching for similar commands. +- `similarityThreshold`: Determines how similar a command name must be to the user input in order to be suggested. + +Additionally, you can search across multiple `CommandGroup` instances by providing an array of `CommandGroup` instead of just one instance. + +The `commandNotFound` function will only trigger for updates which contain command-like text similar to your registered commands. +For example, if you only have registered [commands with a custom prefix](#prefix) like `?`, it will trigger the handler for anything that looks like your commands, e.g. `?sayhi` but not `/definitely_a_command`. + +Same goes the other way, if you only have commands with the default prefix, it will only trigger on updates that look like `/regular` and `/commands`. + +The recommended commands will only come from the `CommandGroup` instances you pass to the function. +This means you can separate the checks into multiple, separate filters. + +Now, let's apply this understanding to the next example. + +```ts +const myCommands = new CommandGroup(); +myCommands.command("dad", "calls dad", () => {}, { prefix: "?" }) + .localize("es", "papa", "llama a papa") + .localize("fr", "pere", "appelle papa"); + +const otherCommands = new CommandGroup(); +otherCommands.command("bread", "eat a toast", () => {}) + .localize("es", "pan", "come un pan") + .localize("fr", "pain", "manger du pain"); + +// Register each language-specific command group + +// Let's assume the user is French and typed '/Papi' +bot + // This filter will trigger for any command-like as '/regular' or '?custom' + .filter(commandNotFound([myCommands, otherCommands], { + ignoreLocalization: true, + ignoreCase: true, + })) + .use(async (ctx) => { + ctx.commandSuggestion === "?papa"; // Evaluates to true + }); +``` + +If the `ignoreLocalization` were set to false, then `ctx.commandSuggestion` would equal `/pain`. + +We could also add more filters similar to the one mentioned earlier by using different parameters or `CommandGroup`s to check against. + +There are many possibilities for how we can customize this! + +## Command Options + +There are a few options that can be specified per command, per scope, or globally for a `CommandGroup` instance. +These options allow you to further customize how your bot handles commands, giving you more flexibility. + +### `ignoreCase` + +By default, commands match user input in a case-sensitive manner. +When this flag is set, a command like `/dandy` will match variations such as `/DANDY` or `/dandY`, regardless of case. + +### `targetedCommands` + +When users invoke a command, they can optionally tag your bot, like so: `/command@bot_username`. +You can decide what to do with these commands by using the `targetedCommands` config option. +With this option, you can choose between three different behaviors: + +- `ignored`: Ignores commands that mention your bot's username. +- `optional`: Handles both commands that mention the bot's username and ones that don't. +- `required`: Only handles commands that mention the bot's username. + +### `prefix` + +Currently, only commands starting with `/` are recognized by Telegram and, consequently, by the [command handling done by the grammY core library](../guide/commands). +In some occasions, you might want to change that and use a custom prefix for your bot. +That is made possible by the `prefix` option, which will tell the commands plugin to look for that prefix when trying to identify a command. + +If you ever need to retrieve `botCommand` entities from an update and need them to be hydrated with the custom prefix you have registered, there is a method specifically tailored for that, called `ctx.getCommandEntities(yourCommands)`, which returns the same interface as `ctx.entities('bot_command')` + +::: tip + +Commands with custom prefixes cannot be shown in the Commands Menu. + +::: + +### `matchOnlyAtStart` + +When [handling commands](../guide/commands), the grammY core library recognizes commands only if they start at the first character of a message. +The commands plugin, however, allows you to listen for commands in the middle of the message text, or in the end, it doesn't matter! +Simply set the `matchOnlyAtStart` option to `false`, and the plugin will handle the rest. + +## RegExp Commands + +This feature is for those who want to go wild. +It allows you to create command handlers based on regular expressions instead of static strings. +A basic example would look like this: + +```ts +myCommands + .command( + /delete_([a-zA-Z]+)/, + (ctx) => ctx.reply(`Deleting ${ctx.msg?.text?.split("_")[1]}`), + ); +``` + +This command handler will trigger on `/delete_me` the same as on `/delete_you`, and it will reply `Deleting me` in the first case and `Deleting you` in the second, but will not trigger on `/delete_` nor `/delete_123xyz`, passing through as if it wasn't there. ## Plugin Summary - Name: `commands` - [Source](https://github.com/grammyjs/commands) -- Reference +- [Reference](/ref/commands/) diff --git a/site/docs/ru/guide/introduction.md b/site/docs/ru/guide/introduction.md index 24f8acf40..7d0413a90 100644 --- a/site/docs/ru/guide/introduction.md +++ b/site/docs/ru/guide/introduction.md @@ -183,7 +183,7 @@ code ./my-bot В общем, вот что вам нужно сделать для Node.js: -1. Создайте исходный файл `bot.ts` с кодом TypeScript, например, с помощью [VS Code](https://code.visualstudio.com/) или любого другого редактора кода. +1. Создайте исходный файл `bot.ts` с кодом TypeScript, например, с помощью [VS Code](https://code.visualstudio.com) или любого другого редактора кода. 2. Скомпилируйте код, выполнив команду в терминале. В результате будет создан файл `bot.js`. 3. Запустите `bot.js` с помощью Node.js, опять же из терминала. diff --git a/site/docs/ru/hosting/heroku.md b/site/docs/ru/hosting/heroku.md index 137cc6813..5afe6fff1 100644 --- a/site/docs/ru/hosting/heroku.md +++ b/site/docs/ru/hosting/heroku.md @@ -274,7 +274,7 @@ npx tsc ### Установите `Procfile` -На данный момент у `Heroku` есть несколько [типов dyno](https://devcenter.heroku.com/articles/dyno-types). +На данный момент у `Heroku` есть несколько [типов dyno](https://devcenter.heroku.com/articles/dynos#use-cases). Два из них: - **Web dynos**: diff --git a/site/docs/ru/plugins/commands.md b/site/docs/ru/plugins/commands.md index b7986732a..f6a4f8d40 100644 --- a/site/docs/ru/plugins/commands.md +++ b/site/docs/ru/plugins/commands.md @@ -5,10 +5,632 @@ next: false # Команды (`commands`) -Скоро будет, возвращайтесь позднее. +Обработка команд на стероидах. + +Этот плагин предоставляет различные возможности для работы с командами, которых нет в [основной библиотеке для обработки команд](../guide/commands). +Вот краткий обзор возможностей, которые вы получаете с этим плагином: + +- Улучшенная читаемость кода за счет инкапсуляции middleware с определениями команд +- Синхронизация меню команд через `setMyCommands` +- Улучшенная группировка и организация команд +- Возможность ограничить область действия команды, например: доступ только + администраторам группы или в каналах и т.д. +- Создание переводов для команды +- Функция `Возможно, вы имели в виду ...?`, которая находит ближайшую команду при + ошибочном вводе пользователем +- Нечувствительность к регистру при сравнении команд +- Настройка пользовательского поведения для команд, которые явно упоминают вашего бота, + например: `/start@your_bot` +- Пользовательские префиксы для команд, например: `+`, `?` или любой другой символ вместо `/` +- Поддержка команд, которые находятся не в начале сообщения +- Команды с использованием регулярных выражений! + +Все эти возможности реализуются благодаря тому, что вы будете определять одну или несколько центральных структур команд, описывающих команды вашего бота. + +## Основное использование + +Прежде чем углубляться, давайте посмотрим, как зарегистрировать и обработать команду с помощью плагина: + +```typescript +const myCommands = new CommandGroup(); + +myCommands.command( + "hello", + "Поздороваться", + (ctx) => ctx.reply(`Привет, мир!`), +); + +bot.use(myCommands); +``` + +Эта команда регистрирует новую команду `/hello` для вашего бота, которая будет обрабатываться переданным middleware. + +Теперь давайте рассмотрим дополнительные инструменты, которые предоставляет этот плагин. + +## Импортирование + +Прежде всего, вот как вы можете импортировать все необходимые типы и классы, которые предоставляет плагин. + +::: code-group + +```ts [TypeScript] +import { + CommandGroup, + commandNotFound, + commands, + type CommandsFlavor, +} from "@grammyjs/commands"; +``` + +```js [JavaScript] +const { CommandGroup, commands, commandNotFound } = require( + "@grammyjs/commands", +); +``` + +```ts [Deno] +import { + CommandGroup, + commandNotFound, + commands, + type CommandsFlavor, +} from "https://deno.land/x/grammy_commands/mod.ts"; +``` + +::: + +Теперь, когда с импортом разобрались, давайте посмотрим, как сделать наши команды видимыми для пользователей. + +## Настройка пользовательского меню команд + +После того как вы определили свои команды с помощью экземпляра класса `CommandGroup`, вы можете вызвать метод `setCommands`, который зарегистрирует все заданные команды для вашего бота. + +```typescript +const myCommands = new CommandGroup(); + +myCommands.command("hello", "Поздороваться", (ctx) => ctx.reply("Привет!")); +myCommands.command("start", "Запустить бота", (ctx) => ctx.reply("Запуск...")); + +bot.use(myCommands); + +await myCommands.setCommands(bot); +``` + +Это позволит отображать каждую зарегистрированную вами команду в меню в приватном чате с вашим ботом или когда пользователи набирают `/` в чате, где присутствует ваш бот. + +### Контекстные команды + +Что, если вы хотите, чтобы некоторые команды отображались только для определённых пользователей? Например, представьте, что у вас есть команды `login` и `logout`. +Команда `login` должна отображаться только для незарегистрированных пользователей, и наоборот. +Вот как это можно реализовать с помощью плагина команд: + +::: code-group + +```typescript [TypeScript] +// Используйте расширитель для создания собственного контекста +type MyContext = Context & CommandsFlavor; + +// Используйте новый контекст для создания экземпляра бота +const bot = new Bot("токен"); + +// Регистрируем контекстную команду +bot.use(commands()); + +const loggedOutCommands = new CommandGroup(); +const loggedInCommands = new CommandGroup(); + +loggedOutCommands.command( + "login", + "Начать сессию с ботом", + async (ctx) => { + await ctx.setMyCommands(loggedInCommands); + await ctx.reply("Добро пожаловать! Сессия начата!"); + }, +); + +loggedInCommands.command( + "logout", + "Завершить сессию с ботом", + async (ctx) => { + await ctx.setMyCommands(loggedOutCommands); + await ctx.reply("До свидания :)"); + }, +); + +bot.use(loggedInCommands); +bot.use(loggedOutCommands); + +// По умолчанию пользователи не авторизованы, +// поэтому можно установить команды для незарегистрированных +await loggedOutCommands.setCommands(bot); +``` + +```javascript [JavaScript] +// Регистрируем контекстную команду +bot.use(commands()); + +const loggedOutCommands = new CommandGroup(); +const loggedInCommands = new CommandGroup(); + +loggedOutCommands.command( + "login", + "Начать сессию с ботом", + async (ctx) => { + await ctx.setMyCommands(loggedInCommands); + await ctx.reply("Добро пожаловать! Сессия начата!"); + }, +); + +loggedInCommands.command( + "logout", + "Завершить сессию с ботом", + async (ctx) => { + await ctx.setMyCommands(loggedOutCommands); + await ctx.reply("До свидания :)"); + }, +); + +bot.use(loggedInCommands); +bot.use(loggedOutCommands); + +// По умолчанию пользователи не авторизованы, +// поэтому можно установить команды для незарегистрированных +await loggedOutCommands.setCommands(bot); +``` + +::: + +Таким образом, когда пользователь вызывает `/login`, его список команд изменится и будет содержать только команду `logout`. +Удобно, правда? + +::: danger Ограничения на имена команд +Как указано в [документации API Telegram Bot](https://core.telegram.org/bots/api#botcommand), имена команд могут состоять только из: + +> 1-32 символов. +> Разрешены только строчные английские буквы, цифры и подчеркивания. + +Таким образом, вызов `setCommands` или `setMyCommands` с именем команды, не соответствующим нижнему регистру, вызовет исключение. +Команды, не соответствующие этим правилам, всё равно могут быть зарегистрированы, использованы и обработаны, но не будут отображаться в меню пользователя. +::: + +**Учтите**, что `setCommands` и `setMyCommands` влияют только на отображаемые команды в меню команд пользователя и не ограничивают доступ к ним. +Вы узнаете, как реализовать ограничение доступа к командам в разделе [Команды с областью видимости](#команды-с-областью-видимости). + +### Группировка команд + +Поскольку мы можем разделять и группировать команды в разные экземпляры, это позволяет намного более удобно организовывать файлы команд. + +Допустим, мы хотим создать команды, доступные только для разработчиков. +Мы можем реализовать это с помощью следующей структуры кода: + +```ascii +src/ +├─ commands/ +│ ├─ admin.ts +│ ├─ users/ +│ │ ├─ group.ts +│ │ ├─ say-hi.ts +│ │ ├─ say-bye.ts +│ │ ├─ ... +├─ bot.ts +├─ types.ts +tsconfig.json +``` + +Следующий блок кода демонстрирует, как можно реализовать группу команд, доступных только для разработчиков, и обновить меню команд в клиенте Telegram соответствующим образом. +Обратите внимание на разные шаблоны, используемые в файлах `admin.ts` и `group.ts`. + +::: code-group + +```ts [types.ts] +export type MyContext = Context & CommandsFlavor; +``` + +```ts [bot.ts] +import { devCommands } from "./commands/admin.ts"; +import { userCommands } from "./commands/users/group.ts"; +import type { MyContext } from "./types.ts"; + +export const bot = new Bot("токен"); + +bot.use(commands()); + +bot.use(userCommands); +bot.use(devCommands); +``` + +```ts [admin.ts] +import { userCommands } from './users/group.ts' +import type { MyContext } from '../types.ts' + +export const devCommands = new CommandGroup() + +devCommands.command('devlogin', 'Приветствие', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply('Привет мне') + await ctx.setMyCommands(userCommands, devCommands) + } else { + await next() + } +}) + +devCommands.command('usercount', 'Приветствие', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply( + `Активные пользователи: ${/** Ваша логика здесь */}` + ) + } else { + await next() + } +}) + +devCommands.command('devlogout', 'Приветствие', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply('Пока мне') + await ctx.setMyCommands(userCommands) + } else { + await next() + } + }) +``` + +```ts [group.ts] +import sayHi from "./say-hi.ts"; +import sayBye from "./say-bye.ts"; +import etc from "./another-command.ts"; +import type { MyContext } from "../../types.ts"; + +export const userCommands = new CommandGroup() + .add([sayHi, sayBye]); +``` + +```ts [say-hi.ts] +import type { MyContext } from "../../types.ts"; + +export default new Command("sayhi", "Приветствие", async (ctx) => { + await ctx.reply("Привет, маленький пользователь!"); +}); +``` + +::: + +Вы заметили, что вы можете регистрировать отдельные команды в экземпляр `CommandGroup` с помощью метода `.add` или напрямую через метод `.command(...)`? +Это позволяет создавать как структуру из одного файла, как в файле `admin.ts`, так и более распределенную файловую структуру, как в файле `group.ts`. + +::: tip Всегда используйте группы команд + +При создании и экспорте команд с использованием конструктора `Command`, обязательно регистрируйте их в экземпляре `CommandGroup` с помощью метода `.add`. +Без этого они бесполезны, так что не забудьте сделать это на каком-то этапе. + +::: + +Плагин также требует, чтобы для заданного `CommandGroup` и его соответствующих `Commands` использовался один и тот же тип контекста, что помогает избежать подобных ошибок на раннем этапе! + +Сочетание этих знаний с информацией из следующего раздела выведет вашу работу с командами на новый уровень. + +## Команды с областью видимости + +Знаете ли вы, что можно показывать разные команды в разных чатах в зависимости от типа чата, языка и даже статуса пользователя в группе? Это то, что Telegram называет [**Области видимости команд**](https://core.telegram.org/bots/features#command-scopes). + +Области видимости команд --- это отличная функция, но использовать её вручную может быть очень сложно, поскольку трудно отслеживать все области и команды, которые они предоставляют. +Кроме того, при использовании только областей команд вам приходится вручную добавлять фильтрацию внутри каждой команды, чтобы убедиться, что они будут выполняться только для нужных областей видимости. +Синхронизировать эти два момента бывает непросто, и именно поэтому существует этот плагин. +Посмотрите, как это делается. + +Класс `Command`, возвращаемый методом `command`, предоставляет метод под названием `addToScope`. +Этот метод принимает в качестве параметров [BotCommandScope](/ref/types/botcommandscope) вместе с одним или несколькими обработчиками и регистрирует эти обработчики для выполнения в указанной области. + +Вам даже не нужно беспокоиться о вызове `filter`, метод `addToScope` гарантирует, что ваш обработчик будет вызываться только в нужном контексте. + +Вот пример команды с определённой областью: + +```ts +const myCommands = new CommandGroup(); + +myCommands + .command("start", "Инициализирует конфигурацию бота") + .addToScope( + { type: "all_private_chats" }, + (ctx) => ctx.reply(`Привет, ${ctx.chat.first_name}!`), + ) + .addToScope( + { type: "all_group_chats" }, + (ctx) => ctx.reply(`Привет, участники ${ctx.chat.title}!`), + ); +``` + +Команду `start` теперь можно вызывать как из личных, так и из групповых чатов, и ответ будет отличаться в зависимости от того, откуда поступил вызов. +Теперь, если вызвать `myCommands.setCommands`, команда `start` будет зарегистрирована как в личных, так и в групповых чатах. + +Вот пример команды, доступной только администраторам группы. + +```js +adminCommands + .command("secret", "Только для админов") + .addToScope( + { type: "all_chat_administrators" }, + (ctx) => ctx.reply("Бесплатный торт!"), + ); +``` + +А вот пример команды, доступной только в группах + +```js +myCommands + .command("fun", "Смех") + .addToScope( + { type: "all_group_chats" }, + (ctx) => ctx.reply("Хаха"), + ); +``` + +Обратите внимание, что при вызове метода `command` создаётся новая команда. +Если вы добавите обработчик, он будет применяться к области этой команды `по умолчанию`. +Вызов `addToScope` для этой команды добавит новый обработчик, который будет фильтроваться по заданной области видимости. +Посмотрите на этот пример. + +```ts +myCommands + .command( + "default", + "Команда по умолчанию", + // Эта функция будет вызываться, если вы не находитесь в групповом чате или если пользователь не является администратором + (ctx) => ctx.reply("Привет из области видимости по умолчанию"), + ) + .addToScope( + { type: "all_group_chats" }, + // Эта функция будет вызываться только для не админов в группе + (ctx) => ctx.reply("Привет, групповой чат!"), + ) + .addToScope( + { type: "all_chat_administrators" }, + // Эта функция будет вызываться для администраторов групп, когда они находятся внутри этой группы + (ctx) => ctx.reply("Привет, админ!"), + ); +``` + +## Переводы команд + +Ещё одной мощной функцией является возможность задавать разные названия для одной и той же команды и соответствующие описания в зависимости от языка пользователя. +Плагин команд упрощает это с помощью метода `localize`. +Вот пример: + +```js +myCommands + // Сначала нужно установить название и описание по умолчанию + .command("hello", "Поздороваться") + // А затем можно задать локализованные варианты + .localize("pt", "ola", "Dizer olá"); +``` + +Добавьте столько вариантов, сколько хотите! Плагин сам позаботится об их регистрации, когда вы вызовете `myCommands.setCommands`. + +Для удобства grammY экспортирует объект, аналогичный перечислению `LanguageCodes`, который можно использовать для более идиоматического подхода: + +::: code-group + +```ts [TypeScript] +import { LanguageCodes } from "grammy/types"; + +myCommands.command( + "chef", + "Доставка стейка", + (ctx) => ctx.reply("Стейк на тарелке!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Bife a domicilio", + ); +``` + +```js [JavaScript] +const { LanguageCodes } = require("grammy/types"); + +myCommands.command( + "chef", + "Доставка стейка", + (ctx) => ctx.reply("Стейк на тарелке!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Bife a domicilio", + ); +``` + +```ts [Deno] +import { LanguageCodes } from "https://deno.land/x/grammy/types.ts"; + +myCommands.command( + "chef", + "Доставка стейка", + (ctx) => ctx.reply("Стейк на тарелке!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Bife a domicilio", + ); +``` + +::: + +### Локализация команд с помощью плагина Интернационализации + +Если вы хотите, чтобы локализованные имена команд и их описания хранились в файлах `.ftl`, воспользуйтесь следующей идеей: + +```ts +function addLocalizations(command: Command) { + i18n.locales.forEach((locale) => { + command.localize( + locale, + i18n.t(locale, `${command.name}.command`), + i18n.t(locale, `${command.name}.description`), + ); + }); + return command; +} + +myCommands.commands.forEach(addLocalizations); +``` + +## Поиск ближайшей команды + +Хотя Telegram может автоматически подставляет зарегистрированные команды, иногда пользователи вводят их вручную и допускают ошибки. +Плагин команд помогает справиться с этим, предлагая команду, которую пользователь, возможно, хотел ввести с самого начала. +Он совместим с пользовательскими префиксами, так что о них можно не беспокоиться, и его использование довольно просто: + +::: code-group + +```ts [TypeScript] +// Используйте расширитель для создания собственного контекста +type MyContext = Context & CommandsFlavor; + +// Используйте новый контекст для инициализации бота +const bot = new Bot("токен"); +const myCommands = new CommandGroup(); + +// ... Регистрируем команды + +bot + // Проверяем, существует ли команда + .filter(commandNotFound(myCommands)) + // Если да, значит, она не была обработана нашими командами + .use(async (ctx) => { + // Мы нашли возможное совпадение + if (ctx.commandSuggestion) { + await ctx.reply( + `Хм... Я не знаю такой команды. Возможно, вы имели в виду ${ctx.commandSuggestion}?`, + ); + } + + // Похоже, ничего не похоже на то, что ввёл пользователь + await ctx.reply("Упс... Я не знаю такой команды :/"); + }); +``` + +```js [JavaScript] +// Используйте новый контекст для инициализации бота +const bot = new Bot("токен"); +const myCommands = new CommandGroup(); + +// ... Регистрируем команды + +bot + // Проверяем, существует ли команда + .filter(commandNotFound(myCommands)) + // Если да, значит, она не была обработана нашими командами + .use(async (ctx) => { + // Мы нашли возможное совпадение + if (ctx.commandSuggestion) { + await ctx.reply( + `Хм... Я не знаю такой команды. Возможно, вы имели в виду ${ctx.commandSuggestion}?`, + ); + } + + // Похоже, ничего не похоже на то, что ввёл пользователь + await ctx.reply("Упс... Я не знаю такой команды :/"); + }); +``` + +::: + +Под капотом, `commandNotFound` использует метод контекста `getNearestCommand`, который по умолчанию отдаёт приоритет командам, соответствующим языку пользователя. +Если вы хотите отключить такое поведение, установите параметр `ignoreLocalization` в значение `true`. +Можно искать по нескольким экземплярам `CommandGroup`, и `ctx.commandSuggestion` будет содержать наиболее подходящую команду, если таковая есть. +Также можно установить флаг `ignoreCase`, чтобы игнорировать регистр при поиске похожей команды, и флаг `similarityThreshold`, который контролирует, насколько название команды должно быть похоже на ввод пользователя, чтобы быть рекомендованным. + +Функция `commandNotFound` будет срабатывать только для обновлений, содержащих текст, похожий на зарегистрированные команды. +Например, если у вас зарегистрированы только [команды с пользовательским префиксом](#prefix), например, `?`, она вызовет обработчик для всего, что похоже на ваши команды, например: `?sayhi`, но не для `/definitely_a_command`. +Также работает и обратное: если у вас зарегистрированы только команды с префиксом по умолчанию, она сработает только на обновления, которые выглядят как `/regular` или `/commands`. + +Рекомендуемые команды будут исходить только от экземпляров `CommandGroup`, переданных функции. Поэтому можно проверять команды, применяя несколько фильтров по отдельности. + +Используем полученные знания для рассмотрения следующего примера: + +```ts +const myCommands = new CommandGroup(); +myCommands.command("dad", "звонит папе", () => {}, { prefix: "?" }) + .localize("es", "papa", "llama a papa") + .localize("fr", "pere", "appelle papa"); + +const otherCommands = new CommandGroup(); +otherCommands.command("bread", "съесть тост", () => {}) + .localize("es", "pan", "come un pan") + .localize("fr", "pain", "manger du pain"); + +// Регистрируем каждую языковую группу команд + +// Допустим, пользователь — француз и ввёл /Papi +bot + // этот фильтр сработает для всех команд, похожих на '/regular' или '?custom' + .filter(commandNotFound([myCommands, otherCommands], { + ignoreLocalization: true, + ignoreCase: true, + })) + .use(async (ctx) => { + ctx.commandSuggestion === "?papa"; // возвращает true + }); +``` + +Если бы `ignoreLocalization` был ложным, мы бы получили "`ctx.commandSuggestion` equals `/pain`". +Мы могли бы добавить больше фильтров, подобных приведённому выше, с разными параметрами или `CommandGroups` для проверки. +Возможностей множество! + +## Опции команд + +Существует несколько опций, которые можно задать для каждой команды, для каждой области или глобально для экземпляра `CommandGroup`. +Эти опции позволяют гибко настраивать, как ваш бот обрабатывает команды. + +### ignoreCase + +По умолчанию команды будут сопоставляться с пользовательским вводом с учётом регистра. +Установив этот флаг, команда с именем `/dandy` будет воспринимать `/DANDY` так же, как `/dandY` или любую другую вариацию, различающуюся только регистром. + +### targetedCommands + +При вызове команды пользователи могут упомянуть вашего бота, например: `/command@bot_username`. С помощью параметра `targetedCommands` можно задать, как бот будет обрабатывать такие команды. +Доступны три варианта поведения: + +- `ignored`: Игнорирует команды, которые упоминают бота +- `optional`: Обрабатывает команды как с упоминанием бота, так и без него +- `required`: Обрабатывает только команды, в которых упоминается бот + +### prefix + +В настоящее время Telegram распознает только команды, начинающиеся с символа `/`, и, соответственно, [обработка команд в основной библиотеке grammY](../guide/commands) также выполняется с этим префиксом. +Однако иногда может потребоваться использовать для бота другой префикс. +Это становится возможным благодаря параметру `prefix`, которая позволяет плагину команд распознавать команды с указанным префиксом. + +Если вам нужно получить сущности `botCommand` из обновления и требуется, чтобы они учитывали зарегистрированный вами пользовательский префикс, существует метод, специально предназначенный для этого --- `ctx.getCommandEntities(вашиКоманды)`, который возвращает тот же интерфейс, что и `ctx.entities('bot_command')`. + +:::tip +Команды с пользовательскими префиксами не могут быть показаны в меню команд. +::: + +### matchOnlyAtStart + +При [обработке команд](../guide/commands) основная библиотека grammY распознает команды только в том случае, если они начинаются с первого символа сообщения. +Однако плагин команд позволяет реагировать на команды, расположенные в середине текста сообщения или в его конце --- это не имеет значения! +Всё, что нужно сделать --- установить параметр `matchOnlyAtStart` на `false`, и плагин позаботится обо всём остальном. + +## Команды с использованием регулярных выражений + +Эта функция подходит для тех, кто хочет задать более гибкие команды, поскольку она позволяет создавать обработчики команд на основе регулярных выражений вместо статических строк. Пример простейшего использования: + +```ts +myCommands + .command( + /delete_([a-zA-Z]+)/, + (ctx) => ctx.reply(`Удаление ${ctx.msg?.text?.split("_")[1]}`), + ); +``` + +Этот обработчик команды сработает как на `/delete_me`, так и на `/delete_you`, и ответит "Удаление me" в первом случае и "Удаление you" во втором, но не сработает на `/delete_` или `/delete_123xyz`, пройдя мимо, как если бы его и не было. ## Краткая информация о плагине - Название: `commands` - [Исходник](https://github.com/grammyjs/commands) -- Документация +- [Ссылка](/ref/commands/) diff --git a/site/docs/uk/guide/introduction.md b/site/docs/uk/guide/introduction.md index 6f1412c86..b6e66262c 100644 --- a/site/docs/uk/guide/introduction.md +++ b/site/docs/uk/guide/introduction.md @@ -188,7 +188,7 @@ code . Підсумовуючи, ось що вам потрібно зробити для Node.js: -1. Створіть вихідний файл `bot.ts` з кодом TypeScript, наприклад за допомогою [VS Code](https://code.visualstudio.com/) або будь-якого іншого редактора коду. +1. Створіть вихідний файл `bot.ts` з кодом TypeScript, наприклад за допомогою [VS Code](https://code.visualstudio.com) або будь-якого іншого редактора коду. 2. Скомпілюйте код, виконавши команду в терміналі. Це згенерує файл під назвою `bot.js`. 3. Запустіть `bot.js` за допомогою Node.js з вашого терміналу. diff --git a/site/docs/uk/hosting/heroku.md b/site/docs/uk/hosting/heroku.md index 0243ee824..398943f35 100644 --- a/site/docs/uk/hosting/heroku.md +++ b/site/docs/uk/hosting/heroku.md @@ -274,7 +274,7 @@ npx tsc ### Налаштування `Procfile` -Наразі у `Heroku` є кілька [типів dyno](https://devcenter.heroku.com/articles/dyno-types). +Наразі у `Heroku` є кілька [типів dyno](https://devcenter.heroku.com/articles/dynos#use-cases). Два з них: - **Веб dyno**: diff --git a/site/docs/uk/plugins/commands.md b/site/docs/uk/plugins/commands.md index 598c49585..e7836a5d6 100644 --- a/site/docs/uk/plugins/commands.md +++ b/site/docs/uk/plugins/commands.md @@ -5,10 +5,640 @@ next: false # Команди (`commands`) -Скоро буде додано, будь ласка, поверніться пізніше. +Обробка команд на стероїдах. + +Цей плагін надає різні можливості, повʼязані з обробкою команд, які не містяться у [стандартних засобах головної бібліотеки](../guide/commands). +Ось короткий огляд того, що ви отримаєте за допомогою цього плагіна: + +- Краща читабельність коду завдяки інкапсуляції проміжних обробників з визначеннями команд +- Синхронізація меню команд користувача за допомогою `setMyCommands` +- Покращене групування та організація команд +- Можливість обмежити доступ до команд, які, наприклад, доступні лише адміністраторам груп або каналів тощо +- Визначення перекладу команд +- Функція `Може, ви мали на увазі ...?`, яка знаходить найближчу існуючу команду до заданого пропущеного користувачем вводу +- Відповідність команд без урахування регістру +- Налаштування кастомної поведінки для команд, в яких явно згадується ваш бот, наприклад, `/start@your_bot`. +- Користувацькі префікси команд, наприклад: `+`, `?` або будь-який інший символ замість `/` +- Підтримка команд, які не стоять на початку повідомлення +- Команди з регулярними виразами! + +Всі ці функції стають можливими завдяки тому, що ви визначаєте одну або кілька центральних структур, які визначають команди вашого бота. + +## Базове використання + +Перш ніж ми зануримося в роботу, подивіться, як ви можете зареєструватися і працювати з командами за допомогою плагіна: + +```ts +const myCommands = new CommandGroup(); + +myCommands.command( + "hello", + "Привітатися", + (ctx) => ctx.reply(`Привіт, світе!`), +); + +bot.use(myCommands); +``` + +Це зареєструє нову команду `/hello` для вашого бота, яка буде оброблятися даним проміжним обробником. + +Тепер давайте розглянемо деякі додаткові інструменти, які може запропонувати цей плагін. + +## Імпортування + +Перш за все, ось як ви можете імпортувати всі необхідні типи і класи, які надає плагін. + +::: code-group + +```ts [TypeScript] +import { + CommandGroup, + commandNotFound, + commands, + type CommandsFlavor, +} from "@grammyjs/commands"; +``` + +```js [JavaScript] +const { CommandGroup, commands, commandNotFound } = require( + "@grammyjs/commands", +); +``` + +```ts [Deno] +import { + CommandGroup, + commandNotFound, + commands, + type CommandsFlavor, +} from "https://deno.land/x/grammy_commands/mod.ts"; +``` + +::: + +Тепер, коли імпортування налагоджено, давайте подивимося, як ми можемо зробити наші команди видимими для наших користувачів. + +## Налаштування меню команд користувача + +Після того, як ви визначили свої команди за допомогою екземпляра класу `CommandGroup`, ви можете викликати метод `setCommands`, який зареєструє всі визначені команди для вашого бота. + +```ts +const myCommands = new CommandGroup(); + +myCommands.command("hello", "Привітатися", (ctx) => ctx.reply("Привіт!")); +myCommands.command("start", "Запустити бота", (ctx) => ctx.reply("Запуск...")); + +bot.use(myCommands); + +await myCommands.setCommands(bot); +``` + +Тепер кожна зареєстрована вами команда відображатиметься в меню приватного чату з вашим ботом або коли користувачі набиратимуть `/` у чаті, учасником якого є ваш бот. + +### Context Shortcut + +Що робити, якщо ви хочете, щоб деякі команди відображалися тільки для певних користувачів? +Наприклад, уявіть, що у вас є команди `login` і `logout`. +Команда `login` повинна відображатися тільки для користувачів, які вийшли з системи, і навпаки. +Ось як це можна зробити за допомогою плагіна: + +::: code-group + +```ts [TypeScript] +// Використовуйте розширювач, щоб створити власний контекст. +type MyContext = Context & CommandsFlavor; + +// Використовуйте новий контекст для створення бота. +const bot = new Bot(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather) + +// Зареєструйте плагін. +bot.use(commands()); + +const loggedOutCommands = new CommandGroup(); +const loggedInCommands = new CommandGroup(); + +loggedOutCommands.command( + "login", + "Розпочати сесію з ботом", + async (ctx) => { + await ctx.setMyCommands(loggedInCommands); + await ctx.reply("Ласкаво просимо! Сесія розпочалася!"); + }, +); + +loggedInCommands.command( + "logout", + "Завершити сесію з ботом", + async (ctx) => { + await ctx.setMyCommands(loggedOutCommands); + await ctx.reply("Бувайте :)"); + }, +); + +bot.use(loggedInCommands); +bot.use(loggedOutCommands); + +// Типово, користувачі не ввійшли до системи, +// тому ви можете задати команди для всіх, хто вийшов з системи. +await loggedOutCommands.setCommands(bot); +``` + +```js [JavaScript] +const bot = new Bot(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather) + +// Зареєструйте плагін. +bot.use(commands()); + +const loggedOutCommands = new CommandGroup(); +const loggedInCommands = new CommandGroup(); + +loggedOutCommands.command( + "login", + "Розпочати сесію з ботом", + async (ctx) => { + await ctx.setMyCommands(loggedInCommands); + await ctx.reply("Ласкаво просимо! Сесія розпочалася!"); + }, +); + +loggedInCommands.command( + "logout", + "Завершити сесію з ботом", + async (ctx) => { + await ctx.setMyCommands(loggedOutCommands); + await ctx.reply("Бувайте :)"); + }, +); + +bot.use(loggedInCommands); +bot.use(loggedOutCommands); + +// Типово, користувачі не ввійшли до системи, +// тому ви можете задати команди для всіх, хто вийшов з системи. +await loggedOutCommands.setCommands(bot); +``` + +::: + +Тепер, коли користувач викликає `/login`, його список команд буде змінено на команду `logout`. +Зручно, чи не так? + +::: danger Обмеження щодо назв команд +Як зазначено в [документації Telegram Bot API](https://core.telegram.org/bots/api#botcommand), назви команд можуть бути створені тільки з: + +1. 1-32 символів. +2. Може містити лише малі англійські літери, цифри та підкреслення. + +Тому виклик `setCommands` або `setMyCommands` з будь-яким параметром, окрім чогось типу `lower_c4s3_commands`, спричинить помилку. +Команди, які не відповідають цим правилам, все одно можна реєструвати, використовувати та обробляти, але вони ніколи не будуть показані у меню користувача. +::: + +**Майте на увазі**, що `setCommands` і `setMyCommands` впливають лише на команди, що відображаються у меню команд користувача, а не на фактичний доступ до них. +Про те, як реалізувати обмежений доступ до команд, ви дізнаєтеся у розділі [обмежені команди](#команди-обмежені-областю-видимості). + +### Групування команд + +Оскільки ми можемо розбивати і групувати наші команди на різні екземпляри, це дозволяє набагато ефективніше організувати файл команд. + +Припустимо, ми хочемо мати команди тільки для розробників. +Ми можемо досягти цього за допомогою наступної структури коду: + +```ascii +src/ +├─ commands/ +│ ├─ admin.ts +│ ├─ users/ +│ │ ├─ group.ts +│ │ ├─ say-hi.ts +│ │ ├─ say-bye.ts +│ │ ├─ ... +├─ bot.ts +├─ types.ts +tsconfig.json +``` + +Наведений нижче код демонструє, як можна реалізувати групу команд тільки для розробників і відповідно оновити меню команд клієнта Telegram. +Переконайтеся, що ви звернули увагу на різні шаблони, які використовуються у вкладках файлів `admin.ts` і `group.ts`. + +::: code-group + +```ts [types.ts] +export type MyContext = Context & CommandsFlavor; +``` + +```ts [bot.ts] +import { devCommands } from "./commands/admin.ts"; +import { userCommands } from "./commands/users/group.ts"; +import type { MyContext } from "./types.ts"; + +export const bot = new Bot("MyBotToken"); + +bot.use(commands()); + +bot.use(userCommands); +bot.use(devCommands); +``` + +```ts [admin.ts] +import { userCommands } from './users/group.ts' +import type { MyContext } from '../types.ts' + +export const devCommands = new CommandGroup() + +devCommands.command('devlogin', 'Привітання', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply('Привіт мені') + await ctx.setMyCommands(userCommands, devCommands) + } else { + await next() + } +}) + +devCommands.command('usercount', 'Активні користувачі', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply( + `Активні користувачі: ${/** Ваша бізнес-логіка */}` + ) + } else { + await next() + } +}) + +devCommands.command('devlogout', 'Прощання', async (ctx, next) => { + if (ctx.from?.id === ctx.env.DEVELOPER_ID) { + await ctx.reply('До побачення мені') + await ctx.setMyCommands(userCommands) + } else { + await next() + } + }) +``` + +```ts [group.ts] +import sayHi from "./say-hi.ts"; +import sayBye from "./say-bye.ts"; +import etc from "./another-command.ts"; +import type { MyContext } from "../../types.ts"; + +export const userCommands = new CommandGroup() + .add([sayHi, sayBye]); +``` + +```ts [say-hi.ts] +import type { MyContext } from "../../types.ts"; + +export default new Command("sayhi", "Привітання", async (ctx) => { + await ctx.reply("Привіт, юний користуваче!"); +}); +``` + +::: + +Чи помітили ви, що можна реєструвати окремі ініціалізовані команди за допомогою методу `.add` в екземплярі `CommandGroup` або безпосередньо за допомогою методу `.command(...)`? +Це дозволяє створити структуру з одного файлу, як у файлі `admin.ts`, або більш розподілену файлову структуру, як у файлі `group.ts`. + +::: tip Завжди використовуйте групи команд + +При створенні та експорті команд за допомогою конструктора `Command` обовʼязково потрібно зареєструвати їх в екземплярі `CommandGroup` за допомогою методу `.add`. +Самі по собі вони не приносять користі, тому обовʼязково зробіть це колись. + +::: + +Плагін також змушує вас мати той самий тип контексту для заданої `CommandGroup` та їхніх відповідних `Commands`, щоб ви могли уникнути такої, на перший погляд, безглуздої помилки! + +Поєднання цих знань з наступним розділом підніме вашу роботу з командами на новий рівень. + +## Команди, обмежені областю видимості + +Чи знаєте ви, що можете дозволити показувати різні команди в різних чатах залежно від типу чату, мови і навіть статусу користувача в групі? +Це те, що в Telegram називається [**областями видимості команд**](https://core.telegram.org/bots/features#command-scopes). + +Області видимості команд --- це класна функція, але використання її вручну може бути дуже заплутаним, оскільки важко відстежити всі області та команди, які вони представляють. +Крім того, використовуючи області видимості команд самостійно, вам доведеться вручну фільтрувати кожну команду, щоб переконатися, що вона буде виконуватися тільки для правильних областей видимості. +Синхронізація цих двох речей може перетворитися на справжній жах, і саме тому існує цей плагін. +Погляньте, як це робиться. + +Клас `Command`, що повертається методом `command`, містить метод з назвою `addToScope`. +Цей метод отримує [`BotCommandScope`](/ref/types/botcommandscope) разом з одним або декількома обробниками і реєструє ці обробники для виконання у вказаній області видимості. + +Вам навіть не потрібно турбуватися про виклик `filter`. +Метод `addToScope` гарантує, що ваш обробник буде викликано лише за умови правильного контексту. + +Ось приклад команди з областю видимості: + +```ts +const myCommands = new CommandGroup(); + +myCommands + .command("start", "Ініціалізує налаштування бота") + .addToScope( + { type: "all_private_chats" }, + (ctx) => ctx.reply(`Привіт, ${ctx.chat.first_name}!`), + ) + .addToScope( + { type: "all_group_chats" }, + (ctx) => ctx.reply(`Привіт, член ${ctx.chat.title}!`), + ); +``` + +Команду `start` тепер можна викликати як з приватних, так і з групових чатів, і вона даватиме різну відповідь залежно від того, звідки її викликано. +Тепер, якщо ви викличете `myCommands.setCommands`, команда `start` буде зареєстрована як в приватних, так і в групових чатах. + +Ось приклад команди, яка доступна лише адміністраторам груп: + +```js +adminCommands + .command("secret", "Виклюючно для адміністраторів") + .addToScope( + { type: "all_chat_administrators" }, + (ctx) => ctx.reply("Free cake!"), + ); +``` + +А ось приклад команди, яка доступна лише в групах: + +```js +myCommands + .command("fun", "Сміх") + .addToScope( + { type: "all_group_chats" }, + (ctx) => ctx.reply("Хаха"), + ); +``` + +Зверніть увагу, що коли ви викликаєте метод `command`, він відкриває нову команду. +Якщо ви надасте їй обробник, цей обробник буде застосовано до області видимості `default` цієї команди. +Виклик `addToScope` для цієї команди додасть новий обробник, який буде відфільтровано для цієї області видимості. +Погляньте на цей приклад: + +```ts +myCommands + .command( + "default", + "Типова команда", + // Ця команда буде викликана, якщо користувач не перебуває в груповому чаті або якщо він не є адміністратором. + (ctx) => ctx.reply("Привіт з типової області видимості"), + ) + .addToScope( + { type: "all_group_chats" }, + // Ця команда буде викликана лише для користувачів, які не є адміністраторами в групі. + (ctx) => ctx.reply("Привіт, груповий чате!"), + ) + .addToScope( + { type: "all_chat_administrators" }, + // Ця команда буде викликана для адміністраторів в цій групі. + (ctx) => ctx.reply("Привіт, адміне!"), + ); +``` + +## Переклади команд + +Ще однією потужною можливістю є встановлення різних назв для однієї і тієї ж команди та їхніх описів, що базуються на мові користувача. +Плагін команд полегшує це завдання за допомогою методу `localize`. +Погляньте: + +```js +myCommands + // Вам потрібно встановити типову назву та опис. + .command("hello", "Привітатися") + // А потім ви можете встановити локалізовані версії. + .localize("pt", "ola", "Dizer olá"); +``` + +Додавайте скільки завгодно! Плагін подбає про їхню реєстрацію, коли ви викличете `myCommands.setCommands`. + +Для зручності grammY експортує обʼєкт, подібний до переліку `LanguageCodes`, який ви можете використовувати для кращої зрозумілості коду: + +::: code-group + +```ts [TypeScript] +import { LanguageCodes } from "grammy/types"; + +myCommands.command( + "chef", + "Доставка стейків", + (ctx) => ctx.reply("Стейк на тарілці!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Bife a domicilio", + ); +``` + +```js [JavaScript] +const { LanguageCodes } = require("grammy/types"); + +myCommands.command( + "chef", + "Доставка стейків", + (ctx) => ctx.reply("Стейк на тарілці!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Bife a domicilio", + ); +``` + +```ts [Deno] +import { LanguageCodes } from "https://deno.land/x/grammy/types.ts"; + +myCommands.command( + "chef", + "Доставка стейків", + (ctx) => ctx.reply("Стейк на тарілці!"), +) + .localize( + LanguageCodes.Spanish, + "cocinero", + "Bife a domicilio", + ); +``` + +::: + +### Локалізація команд за допомогою плагіна інтернаціоналізації + +Якщо ви хочете, щоб ваші локалізовані назви команд та описи до них містилися у ваших файлах `.ftl`, ви можете скористатися наступною ідеєю: + +```ts +function addLocalizations(command: Command) { + i18n.locales.forEach((locale) => { + command.localize( + locale, + i18n.t(locale, `${command.name}.command`), + i18n.t(locale, `${command.name}.description`), + ); + }); + return command; +} + +myCommands.commands.forEach(addLocalizations); +``` + +## Пошук найближчої команди + +Незважаючи на те, що Telegram вміє автоматично завершувати зареєстровані команди, іноді користувачі вводять їх вручну і, в деяких випадках, роблять помилки. +Плагін команд допоможе вам впоратися з цим, дозволяючи запропонувати команду, яка може бути саме тією, яку користувач хотів отримати в першу чергу. +Він сумісний з користувацькими префіксами, тож вам не доведеться про це турбуватися. +Користуватися цим доволі просто: + +::: code-group + +```ts [TypeScript] +// Використовуйте розширювач, щоб створити власний контекст. +type MyContext = Context & CommandsFlavor; + +// Використовуйте новий контекст для створення бота. +const bot = new Bot(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather) +const myCommands = new CommandGroup(); + +// Зареєструйте команди. + +bot + // Перевірте, чи така команда не існує. + .filter(commandNotFound(myCommands)) + // Якщо так, то це означає, що її не обробив жоден з наших обробників команд. + .use(async (ctx) => { + // Ми знайшли потенційний збіг. + if (ctx.commandSuggestion) { + await ctx.reply( + `Хм... Я не знаю цієї команди. Може, ви мали на увазі ${ctx.commandSuggestion}?`, + ); + } + + // Здається, ніщо не збігається з тим, що ввів користувач. + await ctx.reply("Упс... Я не знаю цієї команди. :/"); + }); +``` + +```js [JavaScript] +const bot = new Bot(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather) +const myCommands = new CommandGroup(); + +// Зареєструйте команди. + +bot + // Перевірте, чи така команда не існує. + .filter(commandNotFound(myCommands)) + // Якщо так, то це означає, що її не обробив жоден з наших обробників команд. + .use(async (ctx) => { + // Ми знайшли потенційний збіг. + if (ctx.commandSuggestion) { + await ctx.reply( + `Хм... Я не знаю цієї команди. Може, ви мали на увазі ${ctx.commandSuggestion}?`, + ); + } + + // Здається, ніщо не збігається з тим, що ввів користувач. + await ctx.reply("Упс... Я не знаю цієї команди. :/"); + }); +``` + +::: + +За лаштунками `commandNotFound` використовуватиме метод контексту `getNearestCommand`, який за замовчуванням надаватиме пріоритет командам, що відповідають мові користувача. +Якщо ви хочете відмовитися від такої поведінки, ви можете передати прапорець `ignoreLocalization`, встановлений у `true`. +Можна шукати у декількох екземплярах `CommandGroup`, і `ctx.commandSuggestion` буде найбільш схожою командою, якщо така є, у всіх екземплярах. +Також можна встановити прапорець `ignoreCase`, який ігноруватиме регістр під час пошуку схожої команди, і прапорець `similarityThreshold`, який контролює, наскільки назва команди має бути схожою на введену користувачем, щоб її було рекомендовано. + +Функція `commandNotFound` спрацьовуватиме лише для оновлень, які містять текст, схожий на ваші зареєстровані команди. +Наприклад, якщо ви зареєстрували лише [команди з власним префіксом](#prefix) на кшталт `?`, вона спрацює для всього, що схоже на ваші команди, наприклад: `?sayhi`, але не `/definitely_a_command`. +Те ж саме відбудеться і в зворотному випадку, якщо у вас є лише команди з префіксом за замовчуванням, він спрацює лише для оновлень, які виглядають як `/regular` і `/commands`. + +Рекомендовані команди надходитимуть лише з екземплярів `CommandGroup`, які ви передали до функції. +Отже, ви можете винести перевірку у декілька окремих фільтрів. + +Давайте використаємо попередні знання для розгляду наступного прикладу: + +```ts +const myCommands = new CommandGroup(); +myCommands.command("dad", "Подзвонити татові", () => {}, { prefix: "?" }) + .localize("es", "papa", "llama a papa") + .localize("fr", "pere", "appelle papa"); + +const otherCommands = new CommandGroup(); +otherCommands.command("bread", "Зʼїсти тост", () => {}) + .localize("es", "pan", "come un pan") + .localize("fr", "pain", "manger du pain"); + +// Зареєструйте кожну групу команд для кожної мови. + +// Припустимо, що користувач є французом і ввів `/Papi`. +bot + // Цей фільтр спрацює для будь-якої команди, подібної до `/regular` або `?custom`. + .filter(commandNotFound([myCommands, otherCommands], { + ignoreLocalization: true, + ignoreCase: true, + })) + .use(async (ctx) => { + ctx.commandSuggestion === "?papa"; // Повертає true. + }); +``` + +Якщо значення `ignoreLocalization` було б `false`, ми отримали б, що `ctx.commandSuggestion` дорівнює `/pain`. +Ми можемо додати більше фільтрів, подібних до наведеного вище, з різними параметрами або `CommandGroup` для перевірки. + +Існує безліч можливостей! + +## Параметри команд + +Існує кілька параметрів, які можна вказати для кожної команди, області видимості або глобально для екземпляра `CommandGroup`. +Ці параметри дозволяють вам додатково налаштувати те, як ваш бот обробляє команди, надаючи вам більшої гнучкості. + +### `ignoreCase` + +За замовчуванням команди будуть відповідати введеним користувачем даним з урахуванням регістру. +Якщо цей прапорець встановлено, наприклад, у команді з назвою `/dandy`, то `/DANDY` відповідатиме так само, як `/dandY` або будь-якій іншій варіації, що враховує регістр. + +### `targetedCommands` + +Коли користувачі викликають команду, вони можуть за бажанням позначити вашого бота, наприклад, так: `/command@bot_username`. +Ви можете вирішити, що робити з цими командами, за допомогою конфігураційного параметра `targetedCommands`. +За допомогою цього параметра ви можете вибрати один з трьох варіантів поведінки: + +- `ignored`: ігнорує команди, в яких згадується ваш бот. +- `optional`: обробляє як команди, що згадують, так і команди, що не згадують бота. +- `required`: обробляє тільки команди, в яких згадується бот. + +### `prefix` + +Наразі Telegram розпізнає лише команди, що починаються з `/`, а отже, і [обробку команд виконує grammY](../guide/commands). +У деяких випадках ви можете змінити це і використовувати власний префікс для вашого бота. +Це можливо за допомогою параметра `prefix`, яка вкаже плагіну команд шукати цей префікс при спробі ідентифікувати команду. + +Якщо вам коли-небудь знадобиться отримати сутності `botCommand` з оновлення і потрібно, щоб вони були гідратовані з зареєстрованим вами власним префіксом, існує метод, спеціально розроблений для цього, який називається `ctx.getCommandEntities(yourCommands)`, який повертає той самий інтерфейс, що і `ctx.entities('bot_command')`. + +::: tip + +Команди з власними префіксами не відображаються у меню команд. + +::: + +### `matchOnlyAtStart` + +При [обробці команд](../guide/commands) grammY розпізнає лише команди, які починаються з першого символу повідомлення. +Плагін команд, однак, дозволяє вам прослуховувати команди в середині тексту повідомлення, або в кінці, це не має значення! +Усе, що вам потрібно зробити, це встановити опцію `matchOnlyAtStart` у значення `false`, а все інше плагін зробить сам. + +## Команди з регулярними виразами + +Ця функція для тих, хто дійсно хоче розгулятись. +Вона дозволяє створювати обробники команд на основі регулярних виразів замість статичних рядків, базовий приклад виглядатиме ось так: + +```ts +myCommands + .command( + /delete_([a-zA-Z]+)/, + (ctx) => ctx.reply(`Видалення ${ctx.msg?.text?.split("_")[1]}`), + ); +``` + +Цей обробник команд спрацює на `/delete_me` так само, як і на `/delete_you`, і відповість "Видалення me" у першому випадку і "Видалення you" у другому, але не спрацює на `/delete_` або `/delete_123xyz`, пропускаючи їх так, ніби їх там не було. ## Загальні відомості про плагін - Назва: `commands` - [Джерело](https://github.com/grammyjs/commands) -- Довідка +- [Довідка](/ref/commands/) diff --git a/site/docs/zh/guide/introduction.md b/site/docs/zh/guide/introduction.md index 6288f343e..9bfb4a106 100644 --- a/site/docs/zh/guide/introduction.md +++ b/site/docs/zh/guide/introduction.md @@ -186,7 +186,7 @@ code . 你将 Node.js 所要做的事情有下面这些: -1. 用 TypeScript 代码创建一个源文件 `bot.ts` ,例如使用 [VS Code](https://code.visualstudio.com/) (或任何其他代码编辑器)。 +1. 用 TypeScript 代码创建一个源文件 `bot.ts` ,例如使用 [VS Code](https://code.visualstudio.com) (或任何其他代码编辑器)。 2. 通过在你的终端运行一个命令来编译代码。这将生成一个名为 `bot.js` 的文件。 3. 同样从你的终端,使用 Node.js 运行 `bot.js`。 diff --git a/site/docs/zh/hosting/heroku.md b/site/docs/zh/hosting/heroku.md index 1e1806537..3dd9efb69 100644 --- a/site/docs/zh/hosting/heroku.md +++ b/site/docs/zh/hosting/heroku.md @@ -271,7 +271,7 @@ npx tsc ### 设置 `Procfile` -目前 `Heroku` 有好几种 [dynos 类型](https://devcenter.heroku.com/articles/dyno-types)。 +目前 `Heroku` 有好几种 [dynos 类型](https://devcenter.heroku.com/articles/dynos#use-cases)。 其中两个是: - **Web dynos**: diff --git a/site/modules.ts b/site/modules.ts index a1591bb34..7ce808550 100644 --- a/site/modules.ts +++ b/site/modules.ts @@ -125,6 +125,16 @@ export const modules: ModuleConfig[] = [ ), shortdescription: sdesc("the [internationalization plugin](/plugins/i18n)"), }, + { + repo: "commands", + slug: "commands", + name: "Commands", + description: desc( + "the [commands plugin](/plugins/commands)", + "the commands plugin", + ), + shortdescription: sdesc("the [commands plugin](/plugins/commands)"), + }, { repo: "router", slug: "router",