From f773c6125d355064b8cdd741514d39a5c48cb473 Mon Sep 17 00:00:00 2001 From: MasedMSD <68379695+MasedMSD@users.noreply.github.com> Date: Sun, 9 Feb 2025 22:27:00 +0500 Subject: [PATCH 01/10] only fixes --- site/docs/plugins/commands.md | 354 ++++++++++++++++++---------------- 1 file changed, 189 insertions(+), 165 deletions(-) diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index 0e017f9e3..82d84c140 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -7,25 +7,22 @@ next: false Command handling on steroids. -This plugin provides various features related to command handling that are not contained in the [command handling done by the core library](../guide/commands). +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 -- Ability to scope command reach, e.g: only accessible to group admins or - channels, etc -- Defining command translations -- `Did you mean ...?` feature that finds the nearest existing command to a given - user miss-input -- Case-insensitive command matching -- Setting custom behavior for commands that explicitly mention your bot's user, - like: `/start@your_bot` -- Custom command prefixes, e.g: `+`, `?` or any symbol instead of `/` -- Support for commands that are not in the beginning of the message -- RegExp Commands! - -All of these features are made possible because you will define one or more central command structures that define your bot's commands. +- 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 @@ -39,13 +36,13 @@ myCommands.command("hello", "Say hello", (ctx) => ctx.reply(`Hello, world!`)); bot.use(myCommands); ``` -This registers a new `/hello` command to your bot that will be handled by the given middleware. +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 the plugin provides. +First of all, here's how you can import all the necessary types and classes provided by the plugin. ::: code-group @@ -59,7 +56,7 @@ import { ``` ```js [JavaScript] -const { CommandGroup, commands, commandNotFound } = require( +const { CommandGroup, commandNotFound, commands } = require( "@grammyjs/commands", ); ``` @@ -79,41 +76,42 @@ Now that the imports are settled, let's see how we can make our commands visible ## User Command Menu Setting -Once you defined your commands with an instance of the `CommandGroup` class, you can call the `setCommands` method, which will register all the defined commands to your bot. +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("Hi there!")); -myCommands.command("start", "Start the bot", (ctx) => ctx.reply("Starting...")); +myCommands.command("hello", "Say hello", (ctx) => ctx.reply(`Hello, world!`)); bot.use(myCommands); -await myCommands.setCommands(bot); +// Update the user command menu +await myCommands.setCommands(bot); // [!code highlight] ``` -This will make it so every command you registered is displayed on the menu of a private chat with your bot, or whenever users type `/` on a chat your bot is a member of. +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 to be 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. -This is how you can do that with the commands plugin: +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 = Context & CommandsFlavor; +type MyContext = CommandsFlavor; // Use the new context to instantiate your bot -const bot = new Bot("token"); +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(); +const loggedOutCommands = new CommandGroup(); +const loggedInCommands = new CommandGroup(); loggedOutCommands.command( "login", @@ -137,11 +135,13 @@ bot.use(loggedInCommands); bot.use(loggedOutCommands); // By default, users are not logged in, -// so you can set the logged out commands for everyone +// 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()); @@ -170,26 +170,26 @@ bot.use(loggedInCommands); bot.use(loggedOutCommands); // By default, users are not logged in, -// so you can set the logged out commands for everyone +// 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. +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 can only be form out of: +As stated in the [Telegram Bot API documentation](https://core.telegram.org/bots/api#botcommand), command names must consist of: -> 1-32 characters. -> Can contain only lowercase English letters, digits and underscores. +1. Between 1 and 32 characters. +2. Only lowercase English letters (a-z), digits (0-9), and underscores (_). -Therefore calling `setCommands` or `setMyCommands` with anything but lower_c4s3_commands will throw an exception. -Commands not following this rules can still be registered, used and handled, but will never be displayed on the user menu as such. +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 affects the commands displayed in the user's commands menu, and not the actual access to them. +**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 @@ -200,25 +200,26 @@ Let's say we want to have developer-only commands. We can achieve that with the following code structure: ```ascii -src/ -├─ commands/ -│ ├─ admin.ts -│ ├─ users/ -│ │ ├─ group.ts -│ │ ├─ say-hi.ts -│ │ ├─ say-bye.ts -│ │ ├─ ... -├─ bot.ts -├─ types.ts -tsconfig.json +. +├── 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 use in the `admin.ts` and `group.ts` file-tabs. +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; ``` @@ -227,7 +228,7 @@ 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"); +export const bot = new Bot(""); // <-- put your bot token between the "" (https://t.me/BotFather) bot.use(commands()); @@ -236,83 +237,90 @@ 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', 'Greetings', async (ctx, next) => { - if (ctx.from?.id === ctx.env.DEVELOPER_ID) { - await ctx.reply('Hi to me') - await ctx.setMyCommands(userCommands, devCommands) - } else { - await next() - } -}) - -devCommands.command('usercount', 'Greetings', 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', 'Greetings', async (ctx, next) => { - if (ctx.from?.id === ctx.env.DEVELOPER_ID) { - await ctx.reply('Bye to me') - await ctx.setMyCommands(userCommands) - } else { - await next() - } - }) +import { userCommands } from './users/group.ts'; +import type { MyContext } from '../types.ts'; + +export const devCommands = new CommandGroup(); + +devCommands.command('devlogin', 'Set command menu to dev mode', 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', 'Display user count', 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', 'Reset command menu to user-mode', 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 sayHi from "./say-hi.ts"; +import sayHello from "./say-hello.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]); + .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-hi.ts] +```ts [say-bye.ts] import type { MyContext } from "../../types.ts"; -export default new Command("sayhi", "Greetings", async (ctx) => { - await ctx.reply("Hello little User!"); +export default new Command("bye", "Say bye", async (ctx) => { + await ctx.reply("Goodbye :)"); }); ``` ::: -Did you notice it is possible to register single initialized Commands via the `.add` method into the `CommandGroup` instance or also directly through the `.command(...)` method? -This allows for a one-file-only structure, like in the `admin.ts` file, or a more distributed file structure like in the `group.ts` file. +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 enforce you to have the same Context-type for a given `CommandGroup` and their respective `Commands` so you avoid at first glance that kind of silly mistake! +::: -Combining this knowledge with the following section will get your Command-game to the next level. +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 allow different commands to be shown on different chats depending on the chat type, the language, and even the user status in a chat group? That's what Telegram calls [**Command Scopes**](https://core.telegram.org/bots/features#command-scopes). +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 it by hand can get really messy, since it's hard to keep track of all the scopes and what commands they present. -Plus, by using Command Scopes on their own, you have to do manual filtering inside each command to ensure they'll only run for the correct scopes. -Syncing those two things up can be a nightmare, and that's why this plugin exists. -Check how it's done. +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 ran at that specific scope. +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. @@ -322,19 +330,19 @@ Here's an example of a scoped command: const myCommands = new CommandGroup(); myCommands - .command("start", "Initializes bot configuration") - .addToScope( - { type: "all_private_chats" }, - (ctx) => ctx.reply(`Hello, ${ctx.chat.first_name}!`), - ) + .command("hello", "Say hello") .addToScope( { type: "all_group_chats" }, (ctx) => ctx.reply(`Hello, members of ${ctx.chat.title}!`), + ) + .addToScope( + { type: "all_private_chats" }, + (ctx) => ctx.reply(`Hello, ${ctx.chat.first_name}!`), ); ``` -The `start` 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 `start` command will be registered to both private and group chats. +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. @@ -347,10 +355,10 @@ adminCommands ); ``` -And here is an example of a command that's only accessible in groups +And here is an example of a command that's only accessible in groups. ```js -myCommands +groupCommands .command("fun", "Laugh") .addToScope( { type: "all_group_chats" }, @@ -358,9 +366,9 @@ myCommands ); ``` -Notice that when you call the `command` method, it opens up a new command. +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 to that scope. +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 @@ -368,24 +376,24 @@ myCommands .command( "default", "Default command", - // This will be called when not on a group chat, or when the user is not an admin + // 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!"), + ) + .addToScope( + { type: "all_group_chats" }, + // This will only be called for non-admin users in a group + (ctx) => ctx.reply("Hello, group chat!"), ); ``` ## Command Translations -Another powerful feature is the ability to set different names for the same command, and their respective descriptions based on the user language. +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: @@ -397,9 +405,10 @@ myCommands .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`. +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 that you can use for a more idiomatic approach: +For convenience, grammY exports a `LanguageCodes` enum-like object, which you can use to create a more idiomatic approach. ::: code-group @@ -450,9 +459,9 @@ myCommands.command( ::: -### Localizing Commands With the Internationalization Plugin +### Localizing Commands with the Internationalization Plugin -If you are looking to have your localized command names and descriptions bundle inside your `.ftl` files, you could make use of the following idea: +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) { @@ -471,9 +480,13 @@ myCommands.commands.forEach(addLocalizations); ## Finding the Nearest Command -Even though Telegram is capable of auto completing the registered commands, sometimes users do type them manually and, in some cases, happen to make mistakes. -The commands plugin helps you deal with that by allowing you to suggest a command that might be what the user wanted in the first place. -It is compatible with custom prefixes, so you don't have to worry about that, and its usage is quite straightforward: +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 @@ -482,7 +495,7 @@ It is compatible with custom prefixes, so you don't have to worry about that, an type MyContext = Context & CommandsFlavor; // Use the new context to instantiate your bot -const bot = new Bot("token"); +const bot = new Bot(""); // <-- put your bot token between the "" (https://t.me/BotFather) const myCommands = new CommandGroup(); // ... Register the commands @@ -490,23 +503,22 @@ const myCommands = new CommandGroup(); bot // Check if there is a command .filter(commandNotFound(myCommands)) - // If so, that means it wasn't handled by any of our commands. + // If so, that means it wasn't handled by any of our commands .use(async (ctx) => { + // We found a potential match if (ctx.commandSuggestion) { - // We found a potential match await ctx.reply( `Hmm... I don't know that command. Did you mean ${ctx.commandSuggestion}?`, ); - return; } + // Nothing seems to come close to what the user typed await ctx.reply("Oops... I don't know that command :/"); }); ``` ```js [JavaScript] -// Use the new context to instantiate your bot -const bot = new Bot("token"); +const bot = new Bot(""); // <-- put your bot token between the "" (https://t.me/BotFather) const myCommands = new CommandGroup(); // ... Register the commands @@ -514,15 +526,15 @@ const myCommands = new CommandGroup(); bot // Check if there is a command .filter(commandNotFound(myCommands)) - // If so, that means it wasn't handled by any of our commands. + // If so, that means it wasn't handled by any of our commands .use(async (ctx) => { + // We found a potential match if (ctx.commandSuggestion) { - // We found a potential match await ctx.reply( `Hmm... I don't know that command. Did you mean ${ctx.commandSuggestion}?`, ); - return; } + // Nothing seems to come close to what the user typed await ctx.reply("Oops... I don't know that command :/"); }); @@ -530,18 +542,24 @@ bot ::: -Behind the scenes, `commandNotFound` will use the `getNearestCommand` context method which by default will prioritize commands that correspond to the user language. -If you want to opt-out of this behavior, you can pass the `ignoreLocalization` flag set to true. -It is possible to search across multiple CommandGroup instances, and `ctx.commandSuggestion` will be the most similar command, if any, across them all. -It also allows to set the `ignoreCase` flag, which will ignore casing while looking for a similar command and the `similarityThreshold` flag, which controls how similar a command name has to be to the user input for it to be recommended. +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` `/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. So you could defer the checks into multiple, separate filters. +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. -Let's use the previous knowledge to inspect the next example: +Now, let's apply this understanding to the next example. ```ts const myCommands = new CommandGroup(); @@ -556,63 +574,69 @@ otherCommands.command("bread", "eat a toast", () => {}) // Register each language-specific command group -// Let's assume the user is French and typed /Papi +// Let's assume the user is French and typed '/Papi' bot - // this filter will trigger for any command-like as '/regular' or '?custom' + // 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 + ctx.commandSuggestion === "?papa"; // Evaluates to true }); ``` -If the `ignoreLocalization` was falsy instead we would have gotten "`ctx.commandSuggestion` equals `/pain`". -We could add more filters like the above, with different parameters or `CommandGroups` to check against. -There are a lot of possibilities! +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 +### `ignoreCase` -By default commands will match the user input in a case-sensitive manner. -Having this flag set, for example, in a command named `/dandy` will match `/DANDY` the same as `/dandY` or any other case-only variation. +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 +### `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 it you can choose between three different behaviors: +With this option, you can choose between three different behaviors: -- `ignored`: Ignores commands that mention your bot's user -- `optional`: Handles both commands that do and that don't mention the bot's user -- `required`: Only handles commands that mention the bot's user +- `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, thus, by the [command handling done by the grammY core library](../guide/commands). +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 will only recognize commands that start on the first character of a message. +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! -All you have to do is set the `matchOnlyAtStart` option to `false`, and the rest will be done by the plugin. +Simply set the `matchOnlyAtStart` option to `false`, and the plugin will handle the rest. ## RegExp Commands -This feature is for those who are really looking 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 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 @@ -622,7 +646,7 @@ myCommands ); ``` -This command handler will trigger on `/delete_me` the same as in `/delete_you`, and it will reply "Deleting me" in the first case and "Deleting you" in the later, but will not trigger on `/delete_` nor `/delete_123xyz`, passing through as if it wasn't there. +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 From 08607d9539e8b26314c5eb291dcec97a221a722f Mon Sep 17 00:00:00 2001 From: MasedMSD <68379695+MasedMSD@users.noreply.github.com> Date: Mon, 10 Feb 2025 22:45:36 +0500 Subject: [PATCH 02/10] Apply suggestions from code review --- site/docs/plugins/commands.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index 82d84c140..d5150597d 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -218,7 +218,6 @@ Make sure you take notice of the different patterns being used in the `admin.ts` ```ts [types.ts] import type { Context } from "grammy"; -import type { CommandsFlavor } from "grammy_commands"; export type MyContext = Context & CommandsFlavor; ``` @@ -485,7 +484,7 @@ However, sometimes users still type these commands manually and may make mistake 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. +This functionality works with custom prefixes, so you don’t need to worry about compatibility. Plus, it’s easy to use. ::: code-group @@ -542,10 +541,9 @@ bot ::: -The `commandNotFound` gives you some options to configure: +The `commandNotFound` predicate takes in some options to customize its behavior: -- `ignoreLocalization`: By default, `commandNotFound` prioritizes commands that match the user language. - To opt-out, set this option to `true`. +- `ignoreLocalization`: Do not prioritize commands that match the user language. - `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. @@ -576,7 +574,7 @@ otherCommands.command("bread", "eat a toast", () => {}) // Let's assume the user is French and typed '/Papi' bot - // This filter will trigger for any command-like as '/regular' or '?custom' + // This filter will trigger for any command-like text, such as '/regular' or '?custom' .filter(commandNotFound([myCommands, otherCommands], { ignoreLocalization: true, ignoreCase: true, From b9bacaff9b1b18e986e45bcf783bbc587cd0756f Mon Sep 17 00:00:00 2001 From: MasedMSD <68379695+MasedMSD@users.noreply.github.com> Date: Mon, 10 Feb 2025 22:46:18 +0500 Subject: [PATCH 03/10] little misspell --- site/docs/plugins/commands.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index d5150597d..49aa2426e 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -484,8 +484,8 @@ However, sometimes users still type these commands manually and may make mistake To help with this, the commands plugin suggests a command that the user might have intended to use. -This functionality works with custom prefixes, so you don’t need to worry about compatibility. -Plus, it’s easy to use. +This functionality works with custom prefixes, so you don't need to worry about compatibility. +Plus, it's easy to use. ::: code-group From 5116dbf1e3541600d8a408b9ae8dc85f1e6cdc50 Mon Sep 17 00:00:00 2001 From: MasedMSD <68379695+MasedMSD@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:53:19 +0500 Subject: [PATCH 04/10] Apply suggestions from code review --- site/docs/plugins/commands.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index 49aa2426e..527769b10 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -201,8 +201,8 @@ We can achieve that with the following code structure: ```ascii . -├── types.ts ├── bot.ts +├── types.ts └── commands/ ├── admin.ts └── users/ @@ -219,7 +219,7 @@ Make sure you take notice of the different patterns being used in the `admin.ts` ```ts [types.ts] import type { Context } from "grammy"; -export type MyContext = Context & CommandsFlavor; +export type MyContext = CommandsFlavor; ``` ```ts [bot.ts] @@ -252,7 +252,7 @@ devCommands.command('devlogin', 'Set command menu to dev mode', async (ctx, next devCommands.command('usercount', 'Display user count', async (ctx, next) => { if (ctx.from?.id === ctx.env.DEVELOPER_ID) { - await ctx.reply( `Active users: ${/** Your business logic */}`); + await ctx.reply( `Total users: ${/** Your business logic */}`); } else { await next(); } @@ -479,8 +479,8 @@ 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. +Telegram autocompletes registered commands while typing. +However, sometimes users still type these commands completely by hand and may make mistakes. To help with this, the commands plugin suggests a command that the user might have intended to use. @@ -570,7 +570,8 @@ otherCommands.command("bread", "eat a toast", () => {}) .localize("es", "pan", "come un pan") .localize("fr", "pain", "manger du pain"); -// Register each language-specific command group +bot.use(myCommands) +bot.use(otherCommands) // Let's assume the user is French and typed '/Papi' bot @@ -618,7 +619,7 @@ That is made possible by the `prefix` option, which will tell the commands plugi 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 +::: danger Commands with custom prefixes cannot be shown in the Commands Menu. From ad95c7e8abbbff95bd70dde5f2af46bea6533aac Mon Sep 17 00:00:00 2001 From: MasedMSD <68379695+MasedMSD@users.noreply.github.com> Date: Wed, 12 Feb 2025 22:06:21 +0500 Subject: [PATCH 05/10] Apply suggestions from code review --- site/docs/plugins/commands.md | 42 +++++++++++++---------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index 527769b10..1c4fdb645 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -232,7 +232,7 @@ export const bot = new Bot(""); // <-- put your bot token between the bot.use(commands()); bot.use(userCommands); -bot.use(devCommands); +bot.filter((ctx) => ctx.from?.id == `${/** Put your ID here **/}`).use(devCommands); ``` ```ts [admin.ts] @@ -241,31 +241,19 @@ import type { MyContext } from '../types.ts'; export const devCommands = new CommandGroup(); -devCommands.command('devlogin', 'Set command menu to dev mode', 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("devlogin", "Set command menu to dev mode", async (ctx, next) => { + await ctx.reply("Hello, fellow developer! Are we having coffee today too?"); + await ctx.setMyCommands(userCommands, devCommands); }); -devCommands.command('usercount', 'Display user count', async (ctx, next) => { - if (ctx.from?.id === ctx.env.DEVELOPER_ID) { - await ctx.reply( `Total users: ${/** Your business logic */}`); - } else { - await next(); - } +devCommands.command("usercount", "Display user count", async (ctx, next) => { + await ctx.reply( `Total users: ${/** Your business logic */}`); }); -devCommands.command('devlogout', 'Reset command menu to user-mode', async (ctx, next) => { - if (ctx.from?.id === ctx.env.DEVELOPER_ID) { - await ctx.reply('Until next commit!'); - await ctx.setMyCommands(userCommands); - } else { - await next(); - } - }); +devCommands.command("devlogout", "Reset command menu to user-mode", async (ctx, next) => { + await ctx.reply("Until next commit!"); + await ctx.setMyCommands(userCommands); +}); ``` ```ts [group.ts] @@ -412,7 +400,7 @@ For convenience, grammY exports a `LanguageCodes` enum-like object, which you ca ::: code-group ```ts [TypeScript] -import { LanguageCodes } from "grammy/types"; +import { LanguageCodes } from "@grammyjs/commands"; myCommands.command( "chef", @@ -427,7 +415,7 @@ myCommands.command( ``` ```js [JavaScript] -const { LanguageCodes } = require("grammy/types"); +const { LanguageCodes } = require("@grammyjs/commands"); myCommands.command( "chef", @@ -442,7 +430,7 @@ myCommands.command( ``` ```ts [Deno] -import { LanguageCodes } from "https://deno.land/x/grammy/types.ts"; +import { LanguageCodes } from "https://deno.land/x/grammy_commands/mod.ts"; myCommands.command( "chef", @@ -570,8 +558,8 @@ otherCommands.command("bread", "eat a toast", () => {}) .localize("es", "pan", "come un pan") .localize("fr", "pain", "manger du pain"); -bot.use(myCommands) -bot.use(otherCommands) +bot.use(myCommands); +bot.use(otherCommands); // Let's assume the user is French and typed '/Papi' bot From 08eafad0d605c742cadd861a67debf1c8e2946e3 Mon Sep 17 00:00:00 2001 From: MasedMSD <68379695+MasedMSD@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:06:15 +0500 Subject: [PATCH 06/10] resolved https://github.com/grammyjs/website/pull/1189#discussion_r1958642713 --- site/docs/plugins/commands.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index 1c4fdb645..6a2e1506d 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -216,12 +216,6 @@ Make sure you take notice of the different patterns being used in the `admin.ts` ::: code-group -```ts [types.ts] -import type { Context } from "grammy"; - -export type MyContext = CommandsFlavor; -``` - ```ts [bot.ts] import { devCommands } from "./commands/admin.ts"; import { userCommands } from "./commands/users/group.ts"; @@ -235,6 +229,12 @@ bot.use(userCommands); bot.filter((ctx) => ctx.from?.id == `${/** Put your ID here **/}`).use(devCommands); ``` +```ts [types.ts] +import type { Context } from "grammy"; + +export type MyContext = CommandsFlavor; +``` + ```ts [admin.ts] import { userCommands } from './users/group.ts'; import type { MyContext } from '../types.ts'; From f970894fb1ee4ec6faadb4f447c0a6a08fe3c6f4 Mon Sep 17 00:00:00 2001 From: MasedMSD <68379695+MasedMSD@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:08:00 +0500 Subject: [PATCH 07/10] fix fmt --- site/docs/plugins/commands.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index 6a2e1506d..7a8421d9d 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -226,7 +226,8 @@ export const bot = new Bot(""); // <-- put your bot token between the bot.use(commands()); bot.use(userCommands); -bot.filter((ctx) => ctx.from?.id == `${/** Put your ID here **/}`).use(devCommands); +bot.filter((ctx) => ctx.from?.id == `${/** Put your ID here **/}`) + .use(devCommands); ``` ```ts [types.ts] From c2c2a8fe9c7be3e3275197b58112242d39665b5b Mon Sep 17 00:00:00 2001 From: MasedMSD <68379695+MasedMSD@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:41:08 +0500 Subject: [PATCH 08/10] fix align --- site/docs/plugins/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index 7a8421d9d..5a4cdd122 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -227,7 +227,7 @@ bot.use(commands()); bot.use(userCommands); bot.filter((ctx) => ctx.from?.id == `${/** Put your ID here **/}`) - .use(devCommands); + .use(devCommands); ``` ```ts [types.ts] From e23ba3571db81bc7b577e5244412f341e685ba2d Mon Sep 17 00:00:00 2001 From: MasedMSD <68379695+MasedMSD@users.noreply.github.com> Date: Tue, 18 Feb 2025 23:12:22 +0500 Subject: [PATCH 09/10] changed id example as https://t.me/grammyjs/294621 --- site/docs/plugins/commands.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index 5a4cdd122..e00017768 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -226,7 +226,7 @@ export const bot = new Bot(""); // <-- put your bot token between the bot.use(commands()); bot.use(userCommands); -bot.filter((ctx) => ctx.from?.id == `${/** Put your ID here **/}`) +bot.filter((ctx) => ctx.from?.id == YOUR_ID) .use(devCommands); ``` @@ -248,7 +248,7 @@ devCommands.command("devlogin", "Set command menu to dev mode", async (ctx, next }); devCommands.command("usercount", "Display user count", async (ctx, next) => { - await ctx.reply( `Total users: ${/** Your business logic */}`); + await ctx.reply(`Total users: ${/** Your business logic */}`); }); devCommands.command("devlogout", "Reset command menu to user-mode", async (ctx, next) => { From 6e723a2bf752c61be4407c5638d9678ad6f08898 Mon Sep 17 00:00:00 2001 From: MasedMSD <68379695+MasedMSD@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:41:03 +0500 Subject: [PATCH 10/10] changed variable to comment --- site/docs/plugins/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/docs/plugins/commands.md b/site/docs/plugins/commands.md index e00017768..c087ba71f 100644 --- a/site/docs/plugins/commands.md +++ b/site/docs/plugins/commands.md @@ -226,7 +226,7 @@ export const bot = new Bot(""); // <-- put your bot token between the bot.use(commands()); bot.use(userCommands); -bot.filter((ctx) => ctx.from?.id == YOUR_ID) +bot.filter((ctx) => ctx.from?.id == /** Put your ID here **/) .use(devCommands); ```