Фильтрующие запросы и bot.on()
Первым аргументом bot
является строка filter query.
Введение
Большинство (все?) других фреймворков позволяют выполнять примитивную форму фильтрации обновлений, например, только on("message")
и тому подобное. Остальная фильтрация сообщений остается на усмотрение разработчика, что часто приводит к бесконечным if
утверждениям в коде.
Напротив, grammY поставляется с собственным языком запросов, который вы можете использовать для фильтрации именно тех сообщений, которые вам нужны.
Это позволяет использовать более 1150 различных фильтров, и мы можем добавить еще больше со временем. Каждый правильный фильтр может быть автоматически заполнен в вашем редакторе кода. Таким образом, вы можете просто набрать bot
и выполнить поиск по всем фильтрам, набрав что-нибудь.
Вывод типов в bot
будет понимать выбранный вами запрос фильтра. Поэтому он подтягивает к контексту несколько типов, о которых известно.
bot.on("message", async (ctx) => {
// Может быть undefined, если полученное сообщение не содержит текста.
const text: string | undefined = ctx.msg.text;
});
bot.on("message:text", async (ctx) => {
// Текст всегда присутствует, потому что этот обработчик вызывается при получении текстового сообщения.
const text: string = ctx.msg.text;
});
2
3
4
5
6
7
8
В некотором смысле, grammY реализует запросы фильтра как во время выполнения, так и на уровне типов.
Примеры запросов
Вот несколько примеров запросов:
Регулярные запросы
Простые фильтры для обновлений и подфильтры:
bot.on("message"); // вызывается при получении любого сообщения
bot.on("message:text"); // только текстовых сообщений
bot.on("message:photo"); // только сообщений содержащих фотографии
2
3
Фильтры для сущностей
Подфильтры, которые позволяют углубиться на один уровень:
bot.on("message:entities:url"); // сообщения содержащие URL
bot.on("message:entities:code"); // сообщения, содержащие блоки кода
bot.on("edited_message:entities"); // редактированные сообщения с любыми сущностями
2
3
Пропущенные фильтры
Вы можете опустить некоторые значения в фильтрующих запросах. Тогда grammY будет перебирать различные значения, чтобы соответствовать вашему запросу.
bot.on(":text"); // любые текстовые сообщения и любые текстовые сообщения каналов
bot.on("message::url"); // сообщения с URL адресом в тексте или подписи (фотографии и т.д.)
bot.on("::email"); // сообщения или посты в канале с электронной почтой в тексте или подписи
2
3
Если опустить первое значение, то совпадут и сообщения, и посты в канале. Помните, что ctx
дает доступ как к сообщениям, так и к постам в канале, в зависимости от того, что соответствует запросу.
Опуская второе значение, можно подобрать как сущности, так и подпись. Вы можете опустить и первую, и вторую часть одновременно.
Сокращения
Механизм запросов grammY позволяет задавать аккуратные сокращения, которые группируют связанные запросы вместе.
msg
Сокращенная запись msg
группирует новые сообщения и новые посты в канале. Другими словами, использование msg
всё равно, что прослушивание событий "message"
и "channel
.
bot.on("msg"); // любое сообщение или сообщение канала
bot.on("msg:text"); // точно так же, как `:text`.
2
edit
Сокращённая запись edit
группирует отредактированные сообщения и отредактированные посты канала. Другими словами, использование edit
эквивалентно прослушиванию событий "edited
и "edited
.
bot.on("edit"); // редактирование любого сообщения или сообщения канала
bot.on("edit:text"); // редактирование текстовых сообщений
bot.on("edit::url"); // редактирование сообщений с URL в тексте или подписи
bot.on("edit:location"); // обновленное местоположение
2
3
4
:media
Сокращённая запись :
группирует фото и видео сообщения. Другими словами, использование :
эквивалентно прослушиванию событий ":
и ":
.
bot.on("message:media"); // фото и видео сообщения
bot.on("edited_channel_post:media"); // редактирование сообщений канала с помощью медиа
bot.on(":media"); // медиа сообщения или посты в канале
2
3
:file
Сокращённая запись :
группирует все сообщения в которых есть файлы. Другими словами, использование :
эквивалентно прослушиванию событий ":
, ":
, ":
, ":
, ":
, ":
, ":
, и ":
. Следовательно, вы можете быть уверены, что await ctx
передаст вам объект файла.
bot.on(":file"); // файлы в сообщениях или сообщениях канала
bot.on("edit:file"); // редактирование сообщений с файлом или постах с файлом в канале
2
Синтаксический сахар
Есть два особых случая для частей запроса, которые делают фильтрацию для пользователей более удобной. Вы можете обнаружить ботов в запросах с помощью части запроса :
. Синтаксический сахар :
можно использовать для ссылки на вашего бота внутри запроса, который будет сравнивать идентификаторы пользователей за вас.
// Системное сообщение о боте, который присоединился к чату
bot.on("message:new_chat_members:is_bot");
// Системное сообщение о том, что ваш бот удален
bot.on("message:left_chat_member:me");
2
3
4
Обратите внимание, что хотя этот синтаксический сахар полезен для работы со служебными сообщениями, его не следует использовать для определения того, присоединяется ли кто-то к чату или покидает его. Служебные сообщения — это сообщения, которые информируют пользователей в чате, и некоторые из них будут видны не во всех случаях. Например, в больших группах не будет никаких служебных сообщений о пользователях, которые присоединяются или покидают чат. Следовательно, ваш бот может не заметить этого. Вместо этого вы должны прослушивать обновления участников чата.
Комбинирование нескольких запросов
Вы можете комбинировать любое количество фильтрующих запросов с помощью логических операций И и ИЛИ.
Сочетание с логическим ИЛИ
Если вы хотите установить какую-то промежуточную программу за конкатенацией двух запросов ИЛИ, вы можете передать их оба в bot
в виде массива.
// Выполняется, если обновление касается сообщения ИЛИ редактирования сообщения
bot.on(["message", "edited_message"] /* , ... */);
// Выполняется, если в тексте или подписи найден хэштег ИЛИ электронная почта ИЛИ упоминание
bot.on(["::hashtag", "::email", "::mention"] /* , ... */);
2
3
4
Middleware будет выполнен, если любой из предоставленных запросов совпадет. Порядок запросов не имеет значения.
Сочетание с логическим И
Если вы хотите установить какую-то промежуточную программу за конкатенацией И двух запросов, вы можете составить цепочку вызовов bot
.
// Поиск пересланных URL-адресов
bot.on("::url").on(":forward_origin" /* , ... */);
// Сопоставляет фотографии, содержащие хэштег в подписи
bot.on(":photo").on("::hashtag" /* , ... */);
2
3
4
Middleware будет выполнен, если все предоставленные запросы совпадают. Порядок запросов не имеет значения.
Построение сложных запросов
Технически возможно объединять запросы фильтров в более сложные формулы, если они находятся в CNF, хотя это вряд ли будет полезно.
bot
// Сопоставляет все сообщения канала или пересланные сообщения ...
.on(["channel_post", ":forward_origin"])
// ... которые содержат текст ...
.on(":text")
// ... с хотя бы с одним URL, хэштегом или кештегом.
.on(["::url", "::hashtag", "::cashtag"] /* , ... */);
2
3
4
5
6
7
Вывод типа ctx
просканирует всю цепочку вызовов и проверит каждый элемент всех трех вызовов .on
. Например, он может определить, что ctx
является необходимым свойством для приведенного выше фрагмента кода.
Полезные советы
Вот несколько менее известных возможностей фильтрующих запросов, которые могут пригодиться. Некоторые из них немного продвинутые, поэтому не стесняйтесь переходить к следующему разделу.
Обновления участников чата
Вы можете использовать следующий запрос фильтра для получения обновлений состояния вашего бота.
bot.on("my_chat_member"); // заблокировал, разблокировал, зашёл или вышел
В личных чатах это срабатывает, когда бот блокируется или разблокируется. В группах это срабатывает, когда бот добавляется или удаляется. Теперь вы можете проверить ctx
, чтобы понять, что именно произошло.
Не следует путать с
bot.on("chat_member");
который можно использовать для обнаружения изменений статуса других участников чата, например, когда люди присоединяются, получают повышение и так далее.
Обратите внимание, что обновления
chat
должны быть включены явно, указав_member allowed
при запуске вашего бота._updates
Комбинирование запросов с другими методами
Вы можете комбинировать фильтрующие запросы с другими методами класса Composer
(документация API), такими как command
или filter
. Это позволяет создавать мощные шаблоны обработки сообщений.
bot.on(":forward_origin").command("help"); // пересланная команда /help
// Отвечайте на команды только в личных чатах
const pm = bot.chatType("private");
pm.command("start");
pm.command("help");
2
3
4
5
6
Фильтрация по типу отправителя сообщения
В Telegram существует пять различных типов авторов сообщений:
- Авторы постов в канале
- Автоматические переадресации из связанных каналов в комментариях группы
- Обычные аккаунты пользователей, включая ботов (т.е. “обычные” сообщения)
- Администраторы, отправляющие сообщения от имени группы (анонимные администраторы)
- Пользователи, отправляющие сообщения в качестве одного из своих каналов.
Вы можете комбинировать запросы фильтров с другими механизмами обработки обновлений, чтобы узнать тип автора сообщения.
// Сообщения канала, отправленные `ctx.senderChat`.
bot.on("channel_post");
// Автоматическая пересылка из канала `ctx.senderChat`:
bot.on("message:is_automatic_forward");
// Регулярные сообщения, отправляемые `ctx.from`.
bot.on("message").filter((ctx) => ctx.senderChat === undefined);
// Анонимный админ в `ctx.chat`
bot.on("message").filter((ctx) => ctx.senderChat?.id === ctx.chat.id);
// Пользователи, отправляющие сообщения от имени своего канала `ctx.senderChat`.
bot.on("message").filter((ctx) =>
ctx.senderChat !== undefined && ctx.senderChat.id !== ctx.chat.id
);
2
3
4
5
6
7
8
9
10
11
12
13
Фильтрация по статусу пользователя
Если вы хотите отфильтровать по другим свойствам пользователя, вам нужно выполнить дополнительный запрос, например, await ctx
для автора сообщения. Фильтрующие запросы не будут тайно выполнять за вас дополнительные запросы к API. Тем не менее, выполнить такую фильтрацию довольно просто:
bot.on("message").filter(
async (ctx) => {
const user = await ctx.getAuthor();
return user.status === "creator" || user.status === "administrator";
},
(ctx) => {
// Обрабатывает сообщения от создателей и админов.
},
);
2
3
4
5
6
7
8
9
Повторное использование логики фильтрующих запросов
Внутри bot
полагается на функцию под названием match
. Она принимает запрос фильтра и компилирует его в функцию-предикат. Предикат просто передается в bot
для фильтрации обновлений.
Вы можете импортировать match
напрямую, если хотите использовать его в своей собственной логике. Например, вы можете решить пропускать все обновления, которые соответствуют определенному запросу:
// Пропускайте все текстовые сообщений или посты в текстовых каналах.
bot.drop(matchFilter(":text"));
2
Аналогичным образом можно использовать типы фильтрующих запросов, которые grammY использует внутренне:
Повторное использование типов фильтрующих запросов
Внутри match
использует сужение типов TypeScript, чтобы сузить тип ctx
. Он берет тип C extends Context
и Q extends Filter
и выдает ctx is Filter<C
. Другими словами, тип Filter
— это то, что вы фактически получаете для вашего ctx
в middleware.
Вы можете импортировать Filter
напрямую, если хотите использовать его в своей собственной логике. Например, вы можете определить функцию-обработчик, которая будет обрабатывать определенные объекты контекста, отфильтрованные с помощью запроса фильтра:
function handler(ctx: Filter<Context, ":text">) {
// обработка суженного объекта контекста
}
bot.on(":text", handler);
2
3
4
5
Посмотрите ссылки на API для
match
,Filter Filter
иFilter
, чтобы прочитать дальше.Query
Язык запросов
Этот раздел предназначен для пользователей, которые хотят получить более глубокое представление о фильтрующих запросах в grammY, но он не содержит никаких знаний, необходимых для создания бота.
Структура запросов
Каждый запрос состоит не более чем из трех частей запроса. В зависимости от количества частей запроса мы различаем запросы L1, L2 и L3, такие как "message"
, "message:
и "message:
соответственно.
Части запроса разделяются двоеточиями (:
). Часть до первого двоеточия или конца строки запроса мы называем L1-частью запроса. Часть от первого двоеточия до второго двоеточия или до конца строки запроса мы называем L2 частью запроса. Часть от второго двоеточия до конца строки запроса мы называем L3 частью запроса.
Например:
Фильтрующий запрос | Часть L1 | Часть L2 | Часть L3 |
---|---|---|---|
"message" | "message" | undefined | undefined |
"message: | "message" | "entities" | undefined |
"message: | "message" | "entities" | "mention" |
Валидация запросов
Несмотря на то, что система типов должна отлавливать все некорректные запросы фильтров во время компиляции, grammY также проверяет все переданные запросы фильтров во время выполнения во время установки. Каждый переданный запрос фильтра сопоставляется со структурой проверки, которая проверяет, является ли он корректным. Хорошо не только то, что ошибки в TypeScript приводят к серьезным проблемам со сложной системой вывода типов, которая обеспечивает работу запросов фильтра. Если такое повторится в будущем, это позволит предотвратить проблемы, которые могли бы возникнуть в противном случае. В этом случае вам будут выдаваться полезные сообщения об ошибках.
Производительность
grammY может проверять каждый запрос фильтра за (амортизированное) постоянное время на одно обновление, независимо от структуры запроса или входящего обновления.
Проверка фильтрующих запросов происходит только один раз, когда бот инициализируется и вызывается bot
.
При запуске grammY извлекает предикатную функцию из фильтрующего запроса, разбивая его на части запроса. Каждая часть будет сопоставлена с функцией, выполняющей одну проверку истинности свойства объекта, или две проверки, если часть опущена и необходимо проверить два значения. Затем эти функции объединяются в предикат, который проверяет только столько значений, сколько необходимо для запроса, без итерации по ключам объекта Update
.
Эта система использует меньше операций, чем некоторые конкурирующие библиотеки, которым при маршрутизации обновлений необходимо выполнять проверку содержимого в массивах. Система фильтрующих запросов grammY работает быстрее, несмотря на то, что она гораздо мощнее.
Безопасность типов
Как упоминалось выше, запросы фильтрации автоматически сужают определенные свойства контекстного объекта. Предикат, полученный из одного или нескольких запросов фильтра, представляет собой предикат типа TypeScript, который выполняет это сужение. В целом, можно доверять тому, что вывод типов работает корректно. Если предполагается, что свойство присутствует, вы можете смело полагаться на него. Если предполагается, что свойство потенциально отсутствует, это означает, что есть определенные случаи его отсутствия. Не стоит выполнять приведение типов с помощью оператора !
.
Для вас может быть неочевидно, что это за случаи. Не стесняйтесь спрашивать в групповом чате, если не можете разобраться.
Вычисление этих типов - сложная задача. В эту часть grammY вошло много знаний о API бота. Если вы хотите больше узнать об основных подходах к вычислению этих типов, вы можете посмотреть видео на You