Контекст
Объект Context
(ссылка на grammY API) является важной частью grammY.
Всякий раз, когда вы регистрируете слушателя на объекте бота, этот слушатель получает объект контекста.
bot.on("message", async (ctx) => {
// `ctx` это объект `Context`.
});
2
3
Вы можете использовать объект контекста, чтобы
Обратите внимание, что объекты контекста обычно называются ctx
.
Доступная информация
Когда пользователь отправляет сообщение вашему боту, вы можете получить доступ к нему через ctx
. Например, чтобы получить текст сообщения, вы можете сделать следующее:
bot.on("message", async (ctx) => {
// При обработке текстовых сообщений `txt` будет `строкой`.
// Оно будет `undefined`, если в полученном сообщении нет текста сообщения,
// например, фотографии, стикеры и другие сообщения.
const txt = ctx.message.text;
});
2
3
4
5
6
Аналогичным образом вы можете получить доступ к другим свойствам объекта сообщения, например, к ctx
для получения информации о чате, в который было отправлено сообщение. Посмотрите часть о Message
в документации Telegram Bot API, чтобы узнать, какие данные доступны. Кроме того, вы можете просто использовать автодополнение в редакторе кода, чтобы увидеть возможные варианты.
Если вы зарегистрируете свой слушатель для других типов, ctx
также предоставит вам информацию о них. Пример:
bot.on("edited_message", async (ctx) => {
// Получите новый, отредактированный текст сообщения.
const editedText = ctx.editedMessage.text;
});
2
3
4
Более того, вы можете получить доступ к необработанному объекту Update
(документация Telegram Bot API), который Telegram отправляет вашему боту. Этот объект обновления (ctx
) содержит все данные, которые являются источниками ctx
и т. п.
Объект контекста всегда содержит информацию о вашем боте, доступную через ctx
.
Краткая запись
На контекстном объекте установлено несколько кратких записей.
Параметр | Описание |
---|---|
ctx | Получает объект сообщения, а также отредактированные |
ctx | Получает идентификатор сообщения для сообщений или реакций |
ctx | Получает объект чата |
ctx | Получает идентификатор чата из ctx или из обновлений business . |
ctx | Получает объект чата отправителя из ctx (для анонимных сообщений канала/группы). |
ctx | Получает автора сообщения, запроса обратного вызова или других вещей |
ctx | Получает идентификатор сообщения для callback queries или выбранных inline результатов |
ctx | Получает идентификатор бизнес-соединения для сообщений или обновлений бизнес-соединения |
ctx | Получает сущности сообщения и их текст, опционально отфильтрованный по типу сущности |
ctx | Получает реакции от обновления в удобном для работы виде |
Другими словами, вы также можете сделать это:
bot.on("message", async (ctx) => {
// Получите текст сообщения.
const text = ctx.msg.text;
});
bot.on("edited_message", async (ctx) => {
// Получите новый, отредактированный текст сообщения.
const editedText = ctx.msg.text;
});
bot.on("message:entities", async (ctx) => {
// Получите все сущности.
const entities = ctx.entities();
// Получите текст первой сущности.
entities[0].text;
// Получать сущности которые являются электронной почтой
const emails = ctx.entities("email");
// Получать сущности которые являются электронной почтой и номером телефона
const phonesAndEmails = ctx.entities(["email", "phone"]);
});
bot.on("message_reaction", (ctx) => {
const { emojiAdded } = ctx.reactions();
if (emojiAdded.includes("🎉")) {
await ctx.reply("вечеринОчка :D");
}
});
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
Перейдите к Реакциям, если они вас интересуют.
Таким образом, если вы хотите, вы можете забыть о ctx
, ctx
и ctx
и так далее, и просто всегда использовать ctx
вместо этого.
Поиск информации с помощью проверок
У объекта контекста есть несколько методов, которые позволяют исследовать содержащиеся в нем данные на предмет определенных вещей. Например, вы можете вызвать ctx
, чтобы узнать, содержит ли объект контекста команду /start
. Именно поэтому методы получили общее название has checks.
Знайте, когда использовать has checks
Это точно такая же логика, которая используется в bot
. Обратите внимание, что обычно следует использовать фильтрующие запросы и подобные методы. Использование has checks лучше всего работает в плагине conversations.
Проверки has checks сужают тип контекста. Это означает, что проверка наличия в контексте данных запроса обратного вызова сообщит TypeScript, что в контексте присутствует поле ctx
.
if (ctx.hasCallbackQuery(/query-data-\d+/)) {
// Известно, что `ctx.callbackQuery.data` присутствует здесь
const data: string = ctx.callbackQuery.data;
}
2
3
4
То же самое относится и ко всем другим has checks. Посмотрите API объекта контекста, чтобы увидеть список всех проверок has. Также ознакомьтесь со статическим свойством Context
в документации API, которое позволяет создавать эффективные предикатные функции для проверки большого количества объектов контекста.
Доступные действия
Если вы хотите ответить на сообщение пользователя, вы можете написать следующее:
bot.on("message", async (ctx) => {
// Получите идентификатор чата.
const chatId = ctx.msg.chat.id;
// Текст для ответа
const text = "Я получил ваше сообщение!";
// Отправить ответ.
await bot.api.sendMessage(chatId, text);
});
2
3
4
5
6
7
8
Вы можете заметить две неоптимальные вещи:
- Мы должны иметь доступ к объекту
bot
. Это означает, что мы должны передавать объектbot
по всему нашему коду, чтобы получить ответ, что раздражает, когда у вас несколько исходных файлов и вы определяете слушателя в другом месте. - Нам приходится извлекать идентификатор чата из контекста и снова явно передавать его в `sendMessage. Это тоже раздражает, потому что вы, скорее всего, всегда хотите ответить тому же пользователю, который отправил сообщение. Представьте, как часто вы будете набирать одно и то же сообщение снова и снова!
Что касается пункта 1. Объект контекста просто предоставляет вам доступ к тому же объекту API, который вы найдете в bot
, он называется ctx
. Теперь вы можете написать ctx
вместо этого, и вам больше не придется передавать объект bot
. Легко.
Однако настоящая сила заключается в исправлении пункта 2. Объект контекста позволяет вам просто отправить ответ, например, так:
bot.on("message", async (ctx) => {
await ctx.reply("Я получил ваше сообщение!");
});
// Или даже короче:
bot.on("message", (ctx) => ctx.reply("Попался!"));
2
3
4
5
6
Отлично! 🎉
Под капотом контекст уже знает идентификатор чата (а именно ctx
), поэтому он предоставляет вам метод reply
, чтобы просто отправить сообщение обратно в тот же чат. Внутри, reply
снова вызывает send
с предварительно заполненным идентификатором чата.
Следовательно, все методы на объекте контекста принимают объекты опций типа Other
, как объяснялось ранее. Это можно использовать для передачи дополнительных настроек при каждом вызове API.
Функция ответа в Telegram
Несмотря на то, что в grammY (и многих других фреймворках) метод называется ctx
, он не использует функцию ответа в Telegram, при которой происходит привязка к предыдущему сообщению.
Если вы посмотрите, что может делать send
в документации API бота, вы увидите ряд опций, таких как parse
, link
и reply
. Последняя может быть использована для превращения сообщения в ответ:
await ctx.reply("^ Это сообщение!", {
reply_parameters: { message_id: ctx.msg.message_id },
});
2
3
Один и тот же объект options может быть передан в bot
и ctx
. Используйте автодополнение, чтобы увидеть доступные параметры прямо в редакторе кода.
Естественно, каждый другой метод в ctx
имеет ярлык с правильными предварительно заполненными значениями, например ctx
для ответа с фотографией или ctx
для получения ссылки на приглашение в соответствующий чат. Если вы хотите получить представление о том, какие ярлыки существуют, то автодополнение в редакторе кода — ваш друг, а также документация grammY API.
Обратите внимание, что вы можете не захотеть всегда реагировать в одном и том же чате. В этом случае вы можете просто вернуться к использованию методов ctx
и указать все опции при их вызове. Например, если вы получили сообщение от Алисы и хотите отреагировать на него, отправив сообщение Бобу, то вы не можете использовать ctx
, потому что он всегда будет отправлять сообщения в чат с Алисой. Вместо этого вызовите ctx
и укажите идентификатор чата Боба.
Как создаются контекстные объекты
Всякий раз, когда ваш бот получает новое сообщение от Telegram, оно оборачивается в объект обновления. На самом деле, объекты обновлений могут содержать не только новые сообщения, но и все остальные вещи, такие как редактирование сообщений, ответы на опросы и многое другое.
Свежий объект контекста создается ровно один раз для каждого входящего обновления. Контексты для разных обновлений являются совершенно несвязанными объектами, они лишь ссылаются на одну и ту же информацию о боте через ctx
.
Один и тот же объект контекста для одного обновления будет общим для всех установленных на боте промежуточных программ (документация).
Кастомизация объекта контекста
Если вы новичок в работе с контекстными объектами, вам не нужно беспокоиться об остальной части этой страницы.
При желании вы можете установить собственные свойства для контекстного объекта.
Через Middleware (Рекомендуется)
Настройки можно легко выполнить в middleware.
Middleчто?
Этот раздел требует понимания middleware, поэтому, если вы еще не перешли к этому разделу, вот очень краткое описание.
Все, что вам действительно нужно знать, это то, что несколько обработчиков могут обрабатывать один и тот же объект контекста. Существуют специальные обработчики, которые могут изменять ctx
до запуска других обработчиков, и изменения первого обработчика будут видны всем последующим обработчикам.
Идея заключается в том, чтобы установить middleware до того, как вы зарегистрируете другие слушатели. Затем вы можете установить нужные вам свойства внутри этих обработчиков. Если вы сделаете ctx
внутри такого обработчика, то свойство ctx
будет доступно и в остальных обработчиках.
Для наглядности предположим, что вы хотите установить свойство ctx
для объекта контекста. В этом примере мы будем использовать его для хранения некоторой конфигурации о проекте, чтобы все обработчики имели к ней доступ. С помощью конфигурации будет легче определить, используется ли бот его разработчиком или обычными пользователями.
Сразу после создания бота сделайте следующее:
const BOT_DEVELOPER = 123456; // идентификатор чата разработчика бота
bot.use(async (ctx, next) => {
// Измените здесь объект контекста, установив параметры для config.
ctx.config = {
botDeveloper: BOT_DEVELOPER,
isDeveloper: ctx.from?.id === BOT_DEVELOPER,
};
// Запустите оставшиеся обработчики.
await next();
});
2
3
4
5
6
7
8
9
10
11
После этого вы можете использовать ctx
в остальных обработчиках.
bot.command("start", async (ctx) => {
// Работайте с измененным контекстом!
if (ctx.config.isDeveloper) await ctx.reply("Привет, мам!! <3");
else await ctx.reply("Здравствуй, человек!");
});
2
3
4
5
Однако вы заметите, что TypeScript не знает о наличии ctx
, хотя мы правильно назначаем свойство. Поэтому, хотя код и работает во время выполнения, он не компилируется. Чтобы исправить это, нам нужно изменить тип контекста и добавить свойство.
interface BotConfig {
botDeveloper: number;
isDeveloper: boolean;
}
type MyContext = Context & {
config: BotConfig;
};
2
3
4
5
6
7
8
Новый тип My
теперь точно описывает объекты контекста, с которыми на самом деле работает наш бот.
Вам нужно будет убедиться, что типы синхронизированы со свойствами, которые вы инициализируете.
Мы можем использовать новый тип, передав его конструктору Bot
.
const bot = new Bot<MyContext>("");
В общем, настройка будет выглядеть следующим образом:
const BOT_DEVELOPER = 123456; // идентификатор чата разработчика бота
// Определите пользовательский тип контекста.
interface BotConfig {
botDeveloper: number;
isDeveloper: boolean;
}
type MyContext = Context & {
config: BotConfig;
};
const bot = new Bot<MyContext>("");
// Установка пользовательских свойств для объектов контекста.
bot.use(async (ctx, next) => {
ctx.config = {
botDeveloper: BOT_DEVELOPER,
isDeveloper: ctx.from?.id === BOT_DEVELOPER,
};
await next();
});
// Определите обработчики для объектов пользовательского контекста.
bot.command("start", async (ctx) => {
if (ctx.config.isDeveloper) await ctx.reply("Привет, мам!");
else 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
25
26
27
const BOT_DEVELOPER = 123456; // идентификатор чата разработчика бота
const bot = new Bot("");
// Установка пользовательских свойств для объектов контекста.
bot.use(async (ctx, next) => {
ctx.config = {
botDeveloper: BOT_DEVELOPER,
isDeveloper: ctx.from?.id === BOT_DEVELOPER,
};
await next();
});
// Определите обработчики для объектов пользовательского контекста.
bot.command("start", async (ctx) => {
if (ctx.config.isDeveloper) await ctx.reply("Привет, мам!");
else await ctx.reply("Добро пожаловать");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Естественно, пользовательский тип контекста можно передавать и другим вещам, которые работают с middleware, например Composer.
const composer = new Composer<MyContext>();
Некоторые плагины также требуют передачи пользовательского типа контекста, например, плагин router или menu. Ознакомьтесь с их документацией, чтобы узнать, как они могут использовать пользовательский тип контекста. Эти типы называются контекстными вкусами, как описано здесь внизу.
Через наследование
Помимо установки пользовательских свойств для объекта контекста, вы можете подклассифицировать класс Context
.
class MyContext extends Context {
// т.д.
}
2
3
Однако мы рекомендуем настраивать контекстный объект через middle, потому что это намного гибче и лучше работает, если вы хотите установить плагины.
Сейчас мы рассмотрим, как использовать пользовательские классы для контекстных объектов.
При создании бота вы можете передать ему пользовательский конструктор контекста, который будет использоваться для инстанцирования объектов контекста. Обратите внимание, что ваш класс должен расширять Context
.
import { Bot, Context } from "grammy";
import type { Update, UserFromGetMe } from "grammy/types";
// Определите класс пользовательского контекста.
class MyContext extends Context {
// Установите некоторые пользовательские свойства.
public readonly customProp: number;
constructor(update: Update, api: Api, me: UserFromGetMe) {
super(update, api, me);
this.customProp = me.username.length * 42;
}
}
// Передайте конструктор класса пользовательского контекста в качестве параметра.
const bot = new Bot("", {
ContextConstructor: MyContext,
});
bot.on("message", async (ctx) => {
// `ctx` теперь имеет тип `MyContext`.
const prop = ctx.customProp;
});
bot.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { Bot, Context } = require("grammy");
// Определите класс пользовательского контекста.
class MyContext extends Context {
// Установите некоторые пользовательские свойства.
public readonly customProp;
constructor(update, api, me) {
super(update, api, me);
this.customProp = me.username.length * 42;
}
}
// Передайте конструктор класса пользовательского контекста в качестве параметра.
const bot = new Bot("", {
ContextConstructor: MyContext,
});
bot.on("message", async (ctx) => {
// `ctx` теперь имеет тип `MyContext`.
const prop = ctx.customProp;
});
bot.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Bot, Context } from "https://deno.land/x/grammy@v1.30.0/mod.ts";
import type {
Update,
UserFromGetMe,
} from "https://deno.land/x/grammy@v1.30.0/types.ts";
// Определите класс пользовательского контекста.
class MyContext extends Context {
// Установите некоторые пользовательские свойства.
public readonly customProp: number;
constructor(update: Update, api: Api, me: UserFromGetMe) {
super(update, api, me);
this.customProp = me.username.length * 42;
}
}
// Передайте конструктор класса пользовательского контекста в качестве параметра.
const bot = new Bot("", {
ContextConstructor: MyContext,
});
bot.on("message", async (ctx) => {
// `ctx` теперь имеет тип `MyContext`.
const prop = ctx.customProp;
});
bot.start();
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
Обратите внимание, что при использовании подкласса пользовательский тип контекста будет определяться автоматически. Вам больше не нужно писать Bot<My
, потому что вы уже указали конструктор вашего подкласса в объекте options new Bot()
.
Однако это сильно затрудняет (если не делает невозможным) установку плагинов, т.к. они часто требуют установки расширителей контекста.
Расширители контекста
Контекстные расширители — это способ сообщить TypeScript о новых свойствах вашего контекстного объекта. Эти новые свойства могут поставляться в плагинах или других модулях, а затем устанавливаться на вашего бота.
Контекстные расширители также могут преобразовывать типы существующих свойств с помощью автоматических процедур, которые определяются плагинами.
Дополнительные расширители контекста
Как подразумевалось выше, существует два различных вида расширителей контекста. Основной из них называется дополнительным расширителем контекста, и всякий раз, когда мы говорим о расширителях контекста, мы имеем в виду только эту основную форму. Давайте посмотрим, как это работает.
Например, когда у вас есть данные о сессии, вы должны зарегистрировать ctx
в типе контекста. В противном случае,
- вы не сможете установить встроенный плагин сессий, и
- у вас не будет доступа к
ctx
в ваших слушателях..session
Несмотря на то, что мы будем использовать сессии в качестве примера, подобные вещи применимы и ко многим другим. На самом деле, большинство плагинов предоставляют вам контекст, который вы должны использовать.
Расширитель контекста — это просто небольшой новый тип, определяющий свойства, которые должны быть добавлены к типу контекста. Давайте рассмотрим пример такого типа.
interface SessionFlavor<S> {
session: S;
}
2
3
Тип Session
(документация API) прост: он определяет только свойство session
. Он принимает параметр типа, который определяет фактическую структуру данных сессии.
Чем это полезно? Так вы можете расширять свой контекст данными сессии:
import { Context, SessionFlavor } from "grammy";
// Объявите `ctx.session` типом `string`.
type MyContext = Context & SessionFlavor<string>;
2
3
4
Теперь вы можете использовать плагин сессий, и у вас есть доступ к ctx
:
bot.on("message", async (ctx) => {
// Теперь `str` имеет тип `string`.
const str = ctx.session;
});
2
3
4
Преобразованные расширители контекста
Другая разновидность расширителей контекста более мощная. Вместо того чтобы устанавливать их с помощью оператора &
, их нужно устанавливать следующим образом:
import { Context } from "grammy";
import { SomeFlavorA } from "my-plugin";
type MyContext = SomeFlavorA<Context>;
2
3
4
Все остальное работает точно так же.
Каждый (официальный) плагин указывает в своей документации, должен ли он использоваться через дополнительный или через преобразованных расширители контекста.
Комбинирование разных расширителей контекста
Если у вас есть разные дополнительных расширителей контекста, вы можете просто установить их следующим образом:
type MyContext = Context & FlavorA & FlavorB & FlavorC;
Порядок следования расширителей контекста не имеет значения, вы можете комбинировать их в любом порядке.
Можно также комбинировать несколько преобразованных расширителей контекста:
type MyContext = FlavorX<FlavorY<FlavorZ<Context>>>;
Здесь порядок может иметь значение, поскольку FlavorZ
сначала преобразует Context
, затем FlavorY
, а результат этого преобразования будет снова преобразован FlavorX
.
Вы даже можете смешивать дополнительные и преобразованные расширители:
type MyContext = FlavorX<
FlavorY<
FlavorZ<
Context & FlavorA & FlavorB & FlavorC
>
>
>;
2
3
4
5
6
7
Обязательно следуйте этому шаблону при установке нескольких плагинов. Существует ряд ошибок типа, которые возникают из-за неправильного сочетания расширителей контекста.