Commands (commands
)
Command handling on steroids.
This plugin offers advanced command-handling features beyond the core library’s command handling. 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
set
.MyCommands - 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:
const myCommands = new CommandGroup();
myCommands.command("hello", "Say hello", (ctx) => ctx.reply(`Hello, world!`));
bot.use(myCommands);
2
3
4
5
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.
import {
CommandGroup,
commandNotFound,
commands,
type CommandsFlavor,
} from "@grammyjs/commands";
2
3
4
5
6
const { CommandGroup, commandNotFound, commands } = require(
"@grammyjs/commands",
);
2
3
import {
CommandGroup,
commandNotFound,
commands,
type CommandsFlavor,
} from "https://deno.land/x/grammy_commands@v1.2.0/mod.ts";
2
3
4
5
6
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 Command
class, you can call the set
method to add all the defined commands to the user command menu.
const myCommands = new CommandGroup();
myCommands.command("hello", "Say hello", (ctx) => ctx.reply(`Hello, world!`));
bot.use(myCommands);
// Update the user command menu
await myCommands.setCommands(bot);
2
3
4
5
6
7
8
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:
// Use the flavor to create a custom context
type MyContext = CommandsFlavor<Context>;
// Use the new context to instantiate your bot
const bot = new Bot<MyContext>(""); // <-- put your bot token between the "" (https://t.me/BotFather)
// Register the context shortcut
bot.use(commands());
const loggedOutCommands = new CommandGroup<MyContext>();
const loggedInCommands = new CommandGroup<MyContext>();
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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
This way, when a user calls /login
, they’ll have their command list changed to contain only the logout
command. Neat, right?
Command Name Restrictions
As stated in the Telegram Bot API documentation, command names must consist of:
- Between 1 and 32 characters.
- Only lowercase English letters (a-z), digits (0-9), and underscores (_).
Therefore, calling set
or set
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 set
and set
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 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:
.
├── bot.ts
├── types.ts
└── commands/
├── admin.ts
└── users/
├── group.ts
├── say-hello.ts
└── say-bye.ts
2
3
4
5
6
7
8
9
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
and group
file-tabs.
import { devCommands } from "./commands/admin.ts";
import { userCommands } from "./commands/users/group.ts";
import type { MyContext } from "./types.ts";
export const bot = new Bot<MyContext>(""); // <-- put your bot token between the "" (https://t.me/BotFather)
bot.use(commands());
bot.use(userCommands);
bot.filter((ctx) => ctx.from?.id == /** Put your ID here **/)
.use(devCommands);
2
3
4
5
6
7
8
9
10
11
import type { Context } from "grammy";
export type MyContext = CommandsFlavor<Context>;
2
3
import { userCommands } from './users/group.ts';
import type { MyContext } from '../types.ts';
export const devCommands = new CommandGroup<MyContext>();
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) => {
await ctx.reply(`Total users: ${/** Your business logic */}`);
});
devCommands.command("devlogout", "Reset command menu to user-mode", async (ctx, next) => {
await ctx.reply("Until next commit!");
await ctx.setMyCommands(userCommands);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import sayHello from "./say-hello.ts";
import sayBye from "./say-bye.ts";
import type { MyContext } from "../../types.ts";
export const userCommands = new CommandGroup<MyContext>()
.add([sayHello, sayBye]);
2
3
4
5
6
import type { MyContext } from "../../types.ts";
export default new Command<MyContext>("hello", "Say hello", async (ctx) => {
await ctx.reply("Hello, little user!");
});
2
3
4
5
import type { MyContext } from "../../types.ts";
export default new Command<MyContext>("bye", "Say bye", async (ctx) => {
await ctx.reply("Goodbye :)");
});
2
3
4
5
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 Command
instance with the .add
method? This approach lets you keep everything in a single file, like in admin
, or organize your commands across multiple files, like in group
.
Always Use Command Groups
When creating and exporting commands using the Command
constructor, it’s mandatory to register them onto a Command
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 Command
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.
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 add
. This method takes in a Bot
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 add
method will guarantee that your handler only gets called if the context is right.
Here’s an example of a scoped command:
const myCommands = new CommandGroup();
myCommands
.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}!`),
);
2
3
4
5
6
7
8
9
10
11
12
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 my
, 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.
adminCommands
.command("secret", "Admin only")
.addToScope(
{ type: "all_chat_administrators" },
(ctx) => ctx.reply("Free cake!"),
);
2
3
4
5
6
And here is an example of a command that’s only accessible in groups.
groupCommands
.command("fun", "Laugh")
.addToScope(
{ type: "all_group_chats" },
(ctx) => ctx.reply("Haha"),
);
2
3
4
5
6
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 add
on that command will then add a new handler, which will be filtered for that scope. Take a look at this example.
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_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!"),
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:
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á");
2
3
4
5
Add as many as you want! The plugin will take care of registering them for you when you call my
.
For convenience, grammY exports a Language
enum-like object, which you can use to create a more idiomatic approach.
import { LanguageCodes } from "@grammyjs/commands";
myCommands.command(
"chef",
"Steak delivery",
(ctx) => ctx.reply("Steak on the plate!"),
)
.localize(
LanguageCodes.Spanish,
"cocinero",
"Bife a domicilio",
);
2
3
4
5
6
7
8
9
10
11
12
const { LanguageCodes } = require("@grammyjs/commands");
myCommands.command(
"chef",
"Steak delivery",
(ctx) => ctx.reply("Steak on the plate!"),
)
.localize(
LanguageCodes.Spanish,
"cocinero",
"Bife a domicilio",
);
2
3
4
5
6
7
8
9
10
11
12
import { LanguageCodes } from "https://deno.land/x/grammy_commands@v1.2.0/mod.ts";
myCommands.command(
"chef",
"Steak delivery",
(ctx) => ctx.reply("Steak on the plate!"),
)
.localize(
LanguageCodes.Spanish,
"cocinero",
"Bife a domicilio",
);
2
3
4
5
6
7
8
9
10
11
12
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:
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);
2
3
4
5
6
7
8
9
10
11
12
Finding the Nearest Command
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.
This functionality works with custom prefixes, so you don’t need to worry about compatibility. Plus, it’s easy to use.
// Use the flavor to create a custom context
type MyContext = Context & CommandsFlavor;
// Use the new context to instantiate your bot
const bot = new Bot<MyContext>(""); // <-- put your bot token between the "" (https://t.me/BotFather)
const myCommands = new CommandGroup<MyContext>();
// ... 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) {
return 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 :/");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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) {
return 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 :/");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
The command
predicate takes in some options to customize its behavior:
ignore
: Do not prioritize commands that match the user language.Localization ignore
: Allows the plugin to ignore letter casing when searching for similar commands.Case similarity
: Determines how similar a command name must be to the user input in order to be suggested.Threshold
Additionally, you can search across multiple Command
instances by providing an array of Command
instead of just one instance.
The command
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 like ?
, it will trigger the handler for anything that looks like your commands, e.g. ?sayhi
but not /definitely
.
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 Command
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.
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");
bot.use(myCommands);
bot.use(otherCommands);
// Let's assume the user is French and typed '/Papi'
bot
// This filter will trigger for any command-like text, such as '/regular' or '?custom'
.filter(commandNotFound([myCommands, otherCommands], {
ignoreLocalization: true,
ignoreCase: true,
}))
.use(async (ctx) => {
ctx.commandSuggestion === "?papa"; // Evaluates to true
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
If the ignore
were set to false, then ctx
would equal /pain
.
We could also add more filters similar to the one mentioned earlier by using different parameters or Command
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 Command
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
. You can decide what to do with these commands by using the targeted
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. 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 bot
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
, which returns the same interface as ctx
DANGER
Commands with custom prefixes cannot be shown in the Commands Menu.
matchOnlyAtStart
When handling 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 match
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:
myCommands
.command(
/delete_([a-zA-Z]+)/,
"Delete this",
(ctx) => ctx.reply(`Deleting ${ctx.msg?.text?.split("_")[1]}`),
);
2
3
4
5
6
This command handler will trigger on /delete
the same as on /delete
, and it will reply Deleting me
in the first case and Deleting you
in the second, but will not trigger on /delete
nor /delete
, passing through as if it wasn’t there.