Команди (commands
)
Обробка команд на стероїдах.
Цей плагін надає різні можливості, повʼязані з обробкою команд, які не містяться у стандартних засобах головної бібліотеки. Ось короткий огляд того, що ви отримаєте за допомогою цього плагіна:
- Краща читабельність коду завдяки інкапсуляції проміжних обробників з визначеннями команд
- Синхронізація меню команд користувача за допомогою
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
для вашого бота, яка буде оброблятися даним проміжним обробником.
Тепер давайте розглянемо деякі додаткові інструменти, які може запропонувати цей плагін.
Імпортування
Перш за все, ось як ви можете імпортувати всі необхідні типи і класи, які надає плагін.
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
Тепер кожна зареєстрована вами команда відображатиметься в меню приватного чату з вашим ботом або коли користувачі набиратимуть /
у чаті, учасником якого є ваш бот.
Context Shortcut
Що робити, якщо ви хочете, щоб деякі команди відображалися тільки для певних користувачів? Наприклад, уявіть, що у вас є команди login
і logout
. Команда login
повинна відображатися тільки для користувачів, які вийшли з системи, і навпаки. Ось як це можна зробити за допомогою плагіна:
// Використовуйте розширювач, щоб створити власний контекст.
type MyContext = Context & CommandsFlavor;
// Використовуйте новий контекст для створення бота.
const bot = new Bot<MyContext>(""); // <-- Помістіть токен свого бота між "" (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
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
. Зручно, чи не так?
Обмеження щодо назв команд
Як зазначено в документації Telegram Bot API, назви команд можуть бути створені тільки з:
- 1-32 символів.
- Може містити лише малі англійські літери, цифри та підкреслення.
Тому виклик set
або set
з будь-яким параметром, окрім чогось типу lower
, спричинить помилку. Команди, які не відповідають цим правилам, все одно можна реєструвати, використовувати та обробляти, але вони ніколи не будуть показані у меню користувача.
Майте на увазі, що 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>("MyBotToken");
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
, він відкриває нову команду. Якщо ви надасте їй обробник, цей обробник буде застосовано до області видимості default
цієї команди. Виклик 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("uk", "pryvit", "Привітатися");
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.Ukrainian,
"shefkukhar",
"Доставка стейків",
);
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.Ukrainian,
"shefkukhar",
"Доставка стейків",
);
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.Ukrainian,
"shefkukhar",
"Доставка стейків",
);
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>(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather)
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(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather)
const myCommands = new CommandGroup();
// Зареєструйте команди.
bot
// Перевірте, чи така команда не існує.
.filter(commandNotFound(myCommands))
// Якщо так, то це означає, що її не обробив жоден з наших обробників команд.
.use(async (ctx) => {
// Ми знайшли потенційний збіг.
if (ctx.commandSuggestion) {
await ctx.reply(
`Хм... Я не знаю цієї команди. Може, ви мали на увазі ${ctx.commandSuggestion}?`,
);
}
// Здається, ніщо не збігається з тим, що ввів користувач.
await ctx.reply("Упс... Я не знаю цієї команди. :/");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
За лаштунками 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("uk", "tato", "подзвонити татові")
.localize("es", "papa", "llama a papa")
.localize("fr", "pere", "appelle papa");
const otherCommands = new CommandGroup();
otherCommands.command("bread", "eat a toast", () => {})
.localize("uk", "khlib", "зʼїсти тост")
.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
було б false
, ми отримали б, що ctx
дорівнює /pain
. Ми можемо додати більше фільтрів, подібних до наведеного вище, з різними параметрами або Command
для перевірки.
Існує безліч можливостей!
Параметри команд
Існує кілька параметрів, які можна вказати для кожної команди, області видимості або глобально для екземпляра Command
. Ці параметри дозволяють вам додатково налаштувати те, як ваш бот обробляє команди, надаючи вам більшої гнучкості.
ignoreCase
За замовчуванням команди будуть відповідати введеним користувачем даним з урахуванням регістру. Якщо цей прапорець встановлено, наприклад, у команді з назвою /dandy
, то /DANDY
відповідатиме так само, як /dandY
або будь-якій іншій варіації, що враховує регістр.
targetedCommands
Коли користувачі викликають команду, вони можуть за бажанням позначити вашого бота, наприклад, так: /command@bot
. Ви можете вирішити, що робити з цими командами, за допомогою конфігураційного параметра 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
, пропускаючи їх так, ніби їх і не було.