Команды (commands
)
Обработка команд на стероидах.
Этот плагин расширенные функции обработки команд, которых нет в основной библиотеке для обработки команд. Вот краткий обзор возможностей, которые вы получаете с этим плагином:
- Улучшенная читаемость кода за счет инкапсуляции middleware с определениями команд.
- Синхронизация меню команд для пользователя через
set
.MyCommands - Улучшенная группировка и организация команд.
- Возможность ограничить использования команды, т.е. только для админов, или только для групп.
- Создание переводов для команды.
- Функция
Возможно
, которая находит ближайшую команду при ошибочном вводе пользователем., вы имели в виду .. .? - Нечувствительность к регистру при сравнении команд.
- Настройка команд для пользователей, которые явно упоминают вашего бота, например:
/start@your
._bot - Пользовательские префиксы для команд, например:
+
,?
или любой другой символ вместо/
. - Поддержка команд, которые находятся не в начале сообщения.
- Команды с использованием регулярных выражений!
Все эти возможности реализуются благодаря тому, что вы будете определять одну или несколько центральных структур команд, описывающих команды вашего бота.
Основное использование
Прежде чем углубляться, давайте посмотрим, как зарегистрировать и обработать команду с помощью плагина:
const myCommands = new CommandGroup();
myCommands.command(
"hello",
"Поздороваться",
(ctx) => ctx.reply(`Привет, мир!`),
);
bot.use(myCommands);
2
3
4
5
6
7
8
9
Эта команда регистрирует новую команду /hello
для вашего бота, которая будет обрабатываться переданным middleware.
Теперь давайте рассмотрим дополнительные инструменты, которые предоставляет этот плагин.
Импортирование
Прежде всего, вот как вы можете импортировать все необходимые типы и классы, которые предоставляет плагин.
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.0.9/mod.ts";
2
3
4
5
6
Теперь, когда с импортом разобрались, давайте посмотрим, как сделать наши команды видимыми для пользователей.
Настройка пользовательского меню команд
После того как вы определили свои команды с помощью класса Command
, вы можете вызвать метод set
, который зарегистрирует все заданные команды для пользователя.
const myCommands = new CommandGroup();
myCommands.command("hello", "Поздороваться", (ctx) => ctx.reply("Привет!"));
bot.use(myCommands);
// Обновить команды у пользователя
await myCommands.setCommands(bot);
2
3
4
5
6
7
8
Это гарантирует, что каждая зарегистрированная команда будет отображаться в личном чате с вашим ботом или когда пользователи вводят /
в чате, в котором есть ваш бот.
Контекстные команды
Что делать, если вы хотите, чтобы некоторые команды отображались только для определенных пользователей? Например, представьте, что у вас есть команды login
и logout
. Команда login
должна отображаться только для пользователей, не вошедших в систему, и наоборот. Вот как это сделать с помощью плагина команд:
// Используйте расширитель для создания собственного контекста
type MyContext = CommandsFlavor<Context>;
// Используйте новый контекст для создания экземпляра бота
const bot = new Bot<MyContext>(""); // <-- поместите сюда токен вашего бота (https://t.me/BotFather)
// Регистрируем контекстную команду
bot.use(commands());
const loggedOutCommands = new CommandGroup<MyContext>();
const loggedInCommands = new CommandGroup<MyContext>();
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);
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(""); // <-- поместите сюда токен вашего бота (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);
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
Таким образом, когда пользователь вызывает /login
, его список команд изменится и будет содержать только команду logout
. Удобно, правда?
Ограничения на имена команд
Как указано в документации API Telegram Bot, имена команд должны состоять только из:
От 1 до 32 символов. Разрешены только строчные английские буквы (a-z), цифры (0-9) и нижнее подчёркивание.
Поэтому вызов set
или set
с неправильным именем команд приведет к особому случаю.Команды, не соответствующие этим правилам, будут зарегистрированы и обработаны, но не будут отображаться в меню команд пользователя.
Учтите, что set
и set
влияют только на отображаемые команды в меню команд пользователя и не ограничивают доступ к ним. Вы узнаете, как реализовать ограничение доступа к командам в разделе Команды с областью видимости.
Группировка команд
Поскольку мы можем разделять и группировать команды в разные экземпляры, это позволяет намного более удобно организовывать файлы команд.
Допустим, мы хотим создать команды, доступные только для разработчиков. Мы можем реализовать это с помощью следующей структуры кода:
.
├── bot.ts
├── types.ts
└── commands/
├── admin.ts
└── users/
├── group.ts
├── say-hello.ts
└── say-bye.ts
2
3
4
5
6
7
8
9
Следующий блок кода демонстрирует, как можно реализовать группу команд, доступных только для разработчиков, и обновить меню команд в клиенте Telegram соответствующим образом. Обратите внимание на разные шаблоны, используемые в файлах admin
и group
.
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>(""); // <-- поместите сюда токен вашего бота (https://t.me/BotFather)
bot.use(commands());
bot.use(userCommands);
bot.filter((ctx) => ctx.from?.id == /** Используйте свой ID здесь **/)
.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", "Перевести меню команд в режим разработчика", async (ctx, next) => {
await ctx.reply("Здравствуйте, коллега! Сегодня тоже будем пить кофе?");
await ctx.setMyCommands(userCommands, devCommands);
});
devCommands.command("usercount", "Отобразить число пользователей", async (ctx, next) => {
await ctx.reply(`Всего пользователей: ${/** Ваша бизнес логика здесь */}`);
});
devCommands.command("devlogout", "Перевести меню команд в режим пользователя", async (ctx, next) => {
await ctx.reply("До следующего коммита!");
await ctx.setMyCommands(userCommands);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import sayBye from "./say-bye.ts";
import sayHello from "./say-hello.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", "Приветствие", async (ctx) => {
await ctx.reply("Привет, маленький пользователь!");
});
2
3
4
5
import type { MyContext } from "../../types.ts";
export default new Command<MyContext>("bye", "Попрощаться", async (ctx) => {
await ctx.reply("Пока-пока :(");
});
2
3
4
5
Знаете ли вы, что, как показано в примере выше, вы можете создавать команды либо напрямую с помощью метода .command(
, либо регистрируя инициализированные Commands
в Command
с помощью метода .add
? Такой подход позволяет хранить все в одном файле, как в admin
, или организовывать команды по нескольким файлам, как в group
.
Всегда используйте группы команд
При создании и экспорте команд с использованием конструктора Command
, обязательно регистрируйте их в экземпляре Command
с помощью метода .add
. Без этого они бесполезны, так что не забудьте сделать это на каком-то этапе.
Плагин также гарантирует, что Command
и его Commands
имеют один и тот же тип Context
, так что вы можете избежать таких глупых ошибок с первого взгляда! Сочетание этих знаний с информацией из следующего раздела выведет вашу работу с командами на новый уровень.
Команды с областью видимости
Знаете ли вы, что можно показывать разные команды в разных чатах в зависимости от типа чата, языка и даже статуса пользователя в группе? Это то, что Telegram называет Области видимости команд.
Теперь, области видимости команд это реально крутая тема, но использовать её вручную может быть очень сложно, поскольку трудно отслеживать все области и команды, которые они предоставляют. Кроме того, при использовании только областей команд вам приходится вручную добавлять фильтрацию внутри каждой команды, чтобы убедиться, что они будут выполняться только для нужных областей видимости. Синхронизировать эти два момента бывает непросто, и именно поэтому существует этот плагин. Давайте посмотрим, как это делается.
Класс Command
, возвращаемый методом command
, предоставляет метод под названием add
. Этот метод принимает в качестве параметров Bot
вместе с одним или несколькими обработчиками и регистрирует эти обработчики для выполнения в указанной области.
Вам даже не нужно беспокоиться о вызове filter
, метод add
гарантирует, что ваш обработчик будет вызываться только в нужном контексте.
Вот пример команды с определённой областью:
const myCommands = new CommandGroup();
myCommands
.command("hello", "Поздороваться")
.addToScope(
{ type: "all_group_chats" },
(ctx) => ctx.reply(`Привет, участники ${ctx.chat.title}!`),
)
.addToScope(
{ type: "all_private_chats" },
(ctx) => ctx.reply(`Привет, ${ctx.chat.first_name}!`),
);
2
3
4
5
6
7
8
9
10
11
12
Команду hello
теперь можно вызывать как из личного чата с ботом, так и из групповых чатов, и ответ будет отличаться в зависимости от того, где она была использована. Теперь, если вызвать my
, команда hello
будет зарегистрирована как в личных, так и в групповых чатах.
Вот пример команды, доступной только администраторам группы.
adminCommands
.command("secret", "Только для админов")
.addToScope(
{ type: "all_chat_administrators" },
(ctx) => ctx.reply("Бесплатный торт!"),
);
2
3
4
5
6
А вот пример команды, доступной только в группах
groupCommands
.command("fun", "Смеяться")
.addToScope(
{ type: "all_group_chats" },
(ctx) => ctx.reply("Хаха"),
);
2
3
4
5
6
Обратите внимание, что метод command
тоже может получить обработчик. Если вы добавите обработчик, он будет применяться к области этой команды по умолчанию
. Вызов add
для этой команды добавит новый обработчик, который будет фильтроваться по заданной области видимости. Посмотрите на этот пример.
myCommands
.command(
"default",
"Команда по умолчанию",
// Эта функция будет вызываться, если вы не находитесь в групповом чате или если пользователь не является администратором
(ctx) => ctx.reply("Привет из области видимости по умолчанию"),
)
.addToScope(
{ type: "all_chat_administrators" },
// Эта функция будет вызываться для администраторов групп, когда они находятся внутри этой группы
(ctx) => ctx.reply("Привет, админ!"),
)
.addToScope(
{ type: "all_group_chats" },
// Эта функция будет вызываться только для не админов в группе
(ctx) => ctx.reply("Привет, групповой чат!"),
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Переводы команд
Ещё одной мощной функцией является возможность задавать разные названия для одной и той же команды и соответствующие описания в зависимости от языка пользователя. Плагин команд упрощает это с помощью метода localize
. Вот пример:
myCommands
// Сначала нужно установить название и описание по умолчанию
.command("hello", "Say hello")
// А затем можно задать локализованные варианты
.localize("ru", "privet", "Поздороваться");
2
3
4
5
Добавьте столько вариантов, сколько хотите! Плагин сам позаботится об их регистрации, когда вы вызовете my
.
Для удобства grammY экспортирует объект, аналогичный перечислению Language
, который можно использовать для более идиоматического подхода:
import { LanguageCodes } from "@grammyjs/commands";
myCommands.command(
"chef",
"Steak delivery",
(ctx) => ctx.reply("Steak on the plate!"),
)
.localize(
LanguageCodes.Russian,
"chefpovar",
"Стейк на тарелке!",
);
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.Russian,
"chefpovar",
"Стейк на тарелке!",
);
2
3
4
5
6
7
8
9
10
11
12
import { LanguageCodes } from "https://deno.land/x/grammy_commands@v1.0.9/mod.ts";
myCommands.command(
"chef",
"Steak delivery",
(ctx) => ctx.reply("Steak on the plate!"),
)
.localize(
LanguageCodes.Russian,
"chefpovar",
"Стейк на тарелке!",
);
2
3
4
5
6
7
8
9
10
11
12
Локализация команд с помощью плагина Интернационализации
Если вы хотите, чтобы локализованные имена команд и их описания хранились в файлах .ftl
, воспользуйтесь следующей подходом:
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
Поиск ближайшей команды
Telegram автоматически предлагает команды при наборе текста. Однако иногда пользователи все равно набирают эти команды полностью вручную и могут допустить ошибки.
Чтобы помочь в этом, плагин Commands
предлагает команду, которую пользователь, возможно, собирался использовать.
Эта функция работает с пользовательскими префиксами, поэтому вам не нужно беспокоиться о совместимости. Кроме того, это просто в использовании.
// Используйте расширитель для создания собственного контекста
type MyContext = Context & CommandsFlavor;
// Используйте новый контекст для инициализации бота
const bot = new Bot<MyContext>(""); // <-- поместите сюда токен вашего бота (https://t.me/BotFather)
const myCommands = new CommandGroup<MyContext>();
// ... Регистрируем команды
bot
// Проверяем, существует ли команда
.filter(commandNotFound(myCommands))
// Если да, значит, она не была обработана нашими командами
.use(async (ctx) => {
if (ctx.commandSuggestion) {
// Мы нашли возможное совпадение
return ctx.reply(
`Хм... Я не знаю такой команды. Возможно, вы имели в виду ${ctx.commandSuggestion}?`,
);
}
// Похоже, ничего не похоже на то, что ввёл пользователь
await ctx.reply("Упс... Я не знаю такой команды :/");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Используйте новый контекст для инициализации бота
const bot = new Bot(""); // <-- поместите сюда токен вашего бота (https://t.me/BotFather)
const myCommands = new CommandGroup();
// ... Регистрируем команды
bot
// Проверяем, существует ли команда
.filter(commandNotFound(myCommands))
// Если да, значит, она не была обработана нашими командами
.use(async (ctx) => {
if (ctx.commandSuggestion) {
// Мы нашли возможное совпадение
return ctx.reply(
`Хм... Я не знаю такой команды. Возможно, вы имели в виду ${ctx.commandSuggestion}?`,
);
}
// Похоже, ничего не похоже на то, что ввёл пользователь
await ctx.reply("Упс... Я не знаю такой команды :/");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
command
принимает некоторые опции для настройки своего поведения:
ignore
: Не назначать приоритет командам, которые соответствуют языку пользователя.Localization ignore
: Позволяет плагину игнорировать регистр букв при поиске похожих команд.Case similarity
: Определяет, насколько название команды должно быть похоже на введенное пользователем, чтобы быть предложенным.Threshold
Кроме того, вы можете искать по нескольким классам Command
, предоставив целый массив таких групп, вместо одной.
Функция command
будет срабатывать только для обновлений, содержащих текст, похожий на зарегистрированные команды. Например, если у вас зарегистрированы только команды с пользовательским префиксом, например ?
, то обработчик будет срабатывать на всё, что похоже на ваши команды, например ?sayhi
, но не на /definitely
.
Если у вас есть только команды с префиксом по умолчанию, то обработчик будет срабатывать только на обновления, похожие на /regular
и /commands
.
Рекомендуемые команды будут исходить только от экземпляров Command
, переданных функции. Поэтому можно проверять команды, применяя несколько фильтров по отдельности.
Теперь давайте применим это к следующему примеру.
const myCommands = new CommandGroup();
myCommands.command("dad", "calls dad", () => {}, { prefix: "?" })
.localize("ru", "papa", "звонит папе")
.localize("es", "papa", "llama a papa")
.localize("fr", "pere", "appelle papa");
const otherCommands = new CommandGroup();
otherCommands.command("bread", "eat a toast", () => {})
.localize("ru", "hleb", "съесть хлеб")
.localize("es", "pan", "come un pan")
.localize("fr", "pain", "manger du pain");
bot.use(myCommands);
bot.use(otherCommands);
// Допустим, пользователь - француз и ввёл '/Papi'
bot
// этот фильтр сработает для всех команд, похожих на '/regular' или '?custom'
.filter(commandNotFound([myCommands, otherCommands], {
ignoreLocalization: true,
ignoreCase: true,
}))
.use(async (ctx) => {
ctx.commandSuggestion === "?papa"; // Оценивается как 'true'
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Если бы ignore
было установлено значение false
, то ctx
было бы равно /pain
.
Мы также можем добавить больше фильтров, подобных упомянутому ранее, используя различные параметры или Command
для проверки.
Существует множество возможностей для настройки этого!
Параметры команд
Существует несколько параметров, которые можно задать для каждой команды, для каждой области или глобально для экземпляра Command
. Эти параметры позволяют гибко настраивать, как ваш бот обрабатывает команды.
ignoreCase
По умолчанию команды соответствуют введенным пользователем данным с учетом регистра. Когда этот флаг установлен, команда типа /dandy
будет соответствовать таким вариантам, как /DANDY
или /dandY
, независимо от регистра.
targetedCommands
При вызове команды пользователи могут упомянуть вашего бота, например: /команда@имя
. С помощью параметра targeted
можно задать, как бот будет обрабатывать такие команды. Доступны три варианта поведения:
ignored
: Игнорирует команды, которые упоминают ботаoptional
: Обрабатывает команды как с упоминанием бота, так и без негоrequired
: Обрабатывает только команды, в которых упоминается бот
prefix
В настоящее время Telegram распознает только команды, начинающиеся с символа /
, и, соответственно, обработка команд в основной библиотеке grammY также выполняется с этим префиксом. Однако иногда может потребоваться использовать для бота другой префикс. Это становится возможным благодаря параметру prefix
, которая позволяет плагину команд распознавать команды с указанным префиксом.
Если вам нужно получить сущности bot
из обновления и требуется, чтобы они учитывали зарегистрированный вами пользовательский префикс, существует метод, специально предназначенный для этого — ctx
, который возвращает тот же интерфейс, что и ctx
.
DANGER
Команды с пользовательскими префиксами не могут быть показаны в меню команд.
matchOnlyAtStart
При обработке команд основная библиотека grammY распознает команды только в том случае, если они начинаются с первого символа сообщения. Однако плагин команд позволяет реагировать на команды, расположенные в середине текста сообщения или в его конце — это не имеет значения! Всё, что нужно сделать — установить параметр match
на false
, и плагин позаботится обо всём остальном.
Команды с использованием регулярных выражений
Эта функция подходит для тех, кто хочет задать более гибкие команды, поскольку она позволяет создавать обработчики команд на основе регулярных выражений вместо статических строк. Пример простейшего использования:
myCommands
.command(
/delete_([a-zA-Z]+)/,
"Удалить это",
(ctx) => ctx.reply(`Удаление ${ctx.msg?.text?.split("_")[1]}`),
);
2
3
4
5
6
Этот обработчик команды сработает как на /delete
, так и на /delete
, и ответит “Удаление me” в первом случае и “Удаление you” во втором, но не сработает на /delete
или /delete
, пройдя мимо, как если бы его и не было.