Команды (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, commands, commandNotFound } = require(
"@grammyjs/commands",
);
2
3
import {
CommandGroup,
commandNotFound,
commands,
type CommandsFlavor,
} from "https://deno.land/x/grammy_commands@v1.0.3/mod.ts";
2
3
4
5
6
Теперь, когда с импортом разобрались, давайте посмотрим, как сделать наши команды видимыми для пользователей.
Настройка пользовательского меню команд
После того как вы определили свои команды с помощью экземпляра класса Command
, вы можете вызвать метод set
, который зарегистрирует все заданные команды для вашего бота.
const myCommands = new CommandGroup();
myCommands.command("hello", "Поздороваться", (ctx) => ctx.reply("Привет!"));
myCommands.command("start", "Запустить бота", (ctx) => ctx.reply("Запуск..."));
bot.use(myCommands);
await myCommands.setCommands(bot);
2
3
4
5
6
7
8
Это позволит отображать каждую зарегистрированную вами команду в меню в приватном чате с вашим ботом или когда пользователи набирают /
в чате, где присутствует ваш бот.
Контекстные команды
Что, если вы хотите, чтобы некоторые команды отображались только для определённых пользователей? Например, представьте, что у вас есть команды login
и logout
. Команда login
должна отображаться только для незарегистрированных пользователей, и наоборот. Вот как это можно реализовать с помощью плагина команд:
// Используйте расширитель для создания собственного контекста
type MyContext = Context & CommandsFlavor;
// Используйте новый контекст для создания экземпляра бота
const bot = new Bot<MyContext>("токен");
// Регистрируем контекстную команду
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
33
34
35
36
// Регистрируем контекстную команду
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
Таким образом, когда пользователь вызывает /login
, его список команд изменится и будет содержать только команду logout
. Удобно, правда?
Ограничения на имена команд
Как указано в документации API Telegram Bot, имена команд могут состоять только из:
1-32 символов. Разрешены только строчные английские буквы, цифры и подчеркивания.
Таким образом, вызов set
или set
с именем команды, не соответствующим нижнему регистру, вызовет исключение. Команды, не соответствующие этим правилам, всё равно могут быть зарегистрированы, использованы и обработаны, но не будут отображаться в меню пользователя.
Учтите, что set
и set
влияют только на отображаемые команды в меню команд пользователя и не ограничивают доступ к ним. Вы узнаете, как реализовать ограничение доступа к командам в разделе Команды с областью видимости.
Группировка команд
Поскольку мы можем разделять и группировать команды в разные экземпляры, это позволяет намного более удобно организовывать файлы команд.
Допустим, мы хотим создать команды, доступные только для разработчиков. Мы можем реализовать это с помощью следующей структуры кода:
src/
├─ commands/
│ ├─ admin.ts
│ ├─ users/
│ │ ├─ group.ts
│ │ ├─ say-hi.ts
│ │ ├─ say-bye.ts
│ │ ├─ ...
├─ bot.ts
├─ types.ts
tsconfig.json
2
3
4
5
6
7
8
9
10
11
Следующий блок кода демонстрирует, как можно реализовать группу команд, доступных только для разработчиков, и обновить меню команд в клиенте Telegram соответствующим образом. Обратите внимание на разные шаблоны, используемые в файлах admin
и group
.
export type MyContext = Context & CommandsFlavor<MyContext>;
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>("токен");
bot.use(commands());
bot.use(userCommands);
bot.use(devCommands);
2
3
4
5
6
7
8
9
10
import { userCommands } from './users/group.ts'
import type { MyContext } from '../types.ts'
export const devCommands = new CommandGroup<MyContext>()
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()
}
})
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
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<MyContext>()
.add([sayHi, sayBye]);
2
3
4
5
6
7
import type { MyContext } from "../../types.ts";
export default new Command<MyContext>("sayhi", "Приветствие", async (ctx) => {
await ctx.reply("Привет, маленький пользователь!");
});
2
3
4
5
Вы заметили, что вы можете регистрировать отдельные команды в экземпляр Command
с помощью метода .add
или напрямую через метод .command(
? Это позволяет создавать как структуру из одного файла, как в файле admin
, так и более распределенную файловую структуру, как в файле group
.
Всегда используйте группы команд
При создании и экспорте команд с использованием конструктора Command
, обязательно регистрируйте их в экземпляре Command
с помощью метода .add
. Без этого они бесполезны, так что не забудьте сделать это на каком-то этапе.
Плагин также требует, чтобы для заданного Command
и его соответствующих Commands
использовался один и тот же тип контекста, что помогает избежать подобных ошибок на раннем этапе!
Сочетание этих знаний с информацией из следующего раздела выведет вашу работу с командами на новый уровень.
Команды с областью видимости
Знаете ли вы, что можно показывать разные команды в разных чатах в зависимости от типа чата, языка и даже статуса пользователя в группе? Это то, что Telegram называет Области видимости команд.
Области видимости команд — это отличная функция, но использовать её вручную может быть очень сложно, поскольку трудно отслеживать все области и команды, которые они предоставляют. Кроме того, при использовании только областей команд вам приходится вручную добавлять фильтрацию внутри каждой команды, чтобы убедиться, что они будут выполняться только для нужных областей видимости. Синхронизировать эти два момента бывает непросто, и именно поэтому существует этот плагин. Посмотрите, как это делается.
Класс Command
, возвращаемый методом command
, предоставляет метод под названием add
. Этот метод принимает в качестве параметров Bot
Вам даже не нужно беспокоиться о вызове filter
, метод add
гарантирует, что ваш обработчик будет вызываться только в нужном контексте.
Вот пример команды с определённой областью:
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}!`),
);
2
3
4
5
6
7
8
9
10
11
12
Команду start
теперь можно вызывать как из личных, так и из групповых чатов, и ответ будет отличаться в зависимости от того, откуда поступил вызов. Теперь, если вызвать my
, команда start
будет зарегистрирована как в личных, так и в групповых чатах.
Вот пример команды, доступной только администраторам группы.
adminCommands
.command("secret", "Только для админов")
.addToScope(
{ type: "all_chat_administrators" },
(ctx) => ctx.reply("Бесплатный торт!"),
);
2
3
4
5
6
А вот пример команды, доступной только в группах
myCommands
.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_group_chats" },
// Эта функция будет вызываться только для не админов в группе
(ctx) => ctx.reply("Привет, групповой чат!"),
)
.addToScope(
{ type: "all_chat_administrators" },
// Эта функция будет вызываться для администраторов групп, когда они находятся внутри этой группы
(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 "grammy/types";
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("grammy/types");
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@v1.33.0/types.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 может автоматически подставляет зарегистрированные команды, иногда пользователи вводят их вручную и допускают ошибки. Плагин команд помогает справиться с этим, предлагая команду, которую пользователь, возможно, хотел ввести с самого начала. Он совместим с пользовательскими префиксами, так что о них можно не беспокоиться, и его использование довольно просто:
// Используйте расширитель для создания собственного контекста
type MyContext = Context & CommandsFlavor;
// Используйте новый контекст для инициализации бота
const bot = new Bot<MyContext>("токен");
const myCommands = new CommandGroup<MyContext>();
// ... Регистрируем команды
bot
// Проверяем, существует ли команда
.filter(commandNotFound(myCommands))
// Если да, значит, она не была обработана нашими командами
.use(async (ctx) => {
// Мы нашли возможное совпадение
if (ctx.commandSuggestion) {
await 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
24
// Используйте новый контекст для инициализации бота
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("Упс... Я не знаю такой команды :/");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Под капотом, command
использует метод контекста get
, который по умолчанию отдаёт приоритет командам, соответствующим языку пользователя. Если вы хотите отключить такое поведение, установите параметр ignore
в значение true
. Можно искать по нескольким экземплярам Command
, и ctx
будет содержать наиболее подходящую команду, если таковая есть. Также можно установить флаг ignore
, чтобы игнорировать регистр при поиске похожей команды, и флаг similarity
, который контролирует, насколько название команды должно быть похоже на ввод пользователя, чтобы быть рекомендованным.
Функция 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");
// Регистрируем каждую языковую группу команд
// Допустим, пользователь - француз и ввёл /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
Если бы ignore
был ложным, мы бы получили, что ctx
равен /pain
. Мы могли бы добавить больше фильтров, подобных приведённому выше, с разными параметрами или Command
для проверки. Возможностей множество!
Параметры команд
Существует несколько параметров, которые можно задать для каждой команды, для каждой области или глобально для экземпляра Command
. Эти параметры позволяют гибко настраивать, как ваш бот обрабатывает команды.
ignoreCase
По умолчанию команды будут сопоставляться с пользовательским вводом с учётом регистра. Установив этот флаг, команда с именем /dandy
будет воспринимать /DANDY
так же, как /dandY
или любую другую вариацию, различающуюся только регистром.
targetedCommands
При вызове команды пользователи могут упомянуть вашего бота, например: /команда@имя
. С помощью параметра targeted
можно задать, как бот будет обрабатывать такие команды. Доступны три варианта поведения:
ignored
: Игнорирует команды, которые упоминают ботаoptional
: Обрабатывает команды как с упоминанием бота, так и без негоrequired
: Обрабатывает только команды, в которых упоминается бот
prefix
В настоящее время Telegram распознает только команды, начинающиеся с символа /
, и, соответственно, обработка команд в основной библиотеке grammY также выполняется с этим префиксом. Однако иногда может потребоваться использовать для бота другой префикс. Это становится возможным благодаря параметру prefix
, которая позволяет плагину команд распознавать команды с указанным префиксом.
Если вам нужно получить сущности bot
из обновления и требуется, чтобы они учитывали зарегистрированный вами пользовательский префикс, существует метод, специально предназначенный для этого — ctx
, который возвращает тот же интерфейс, что и ctx
.
TIP
Команды с пользовательскими префиксами не могут быть показаны в меню команд.
matchOnlyAtStart
При обработке команд основная библиотека grammY распознает команды только в том случае, если они начинаются с первого символа сообщения. Однако плагин команд позволяет реагировать на команды, расположенные в середине текста сообщения или в его конце — это не имеет значения! Всё, что нужно сделать — установить параметр match
на false
, и плагин позаботится обо всём остальном.
Команды с использованием регулярных выражений
Эта функция подходит для тех, кто хочет задать более гибкие команды, поскольку она позволяет создавать обработчики команд на основе регулярных выражений вместо статических строк. Пример простейшего использования:
myCommands
.command(
/delete_([a-zA-Z]+)/,
(ctx) => ctx.reply(`Удаление ${ctx.msg?.text?.split("_")[1]}`),
);
2
3
4
5
Этот обработчик команды сработает как на /delete
, так и на /delete
, и ответит “Удаление me” в первом случае и “Удаление you” во втором, но не сработает на /delete
или /delete
, пройдя мимо, как если бы его и не было.