Сессии и хранение данных (встроенно)
Хотя вы всегда можете просто написать свой собственный код для подключения к хранилищу данных по вашему выбору, grammY поддерживает очень удобный паттерн хранения данных, называемый сессиями.
Перейдите вниз, если вы знаете, как работают сессии.
Почему мы должны думать о хранении?
В отличие от обычных пользовательских аккаунтов в Telegram, боты имеют ограниченное облачное хранилище. В результате есть несколько вещей, которые вы не можете делать с помощью ботов:
- Вы не можете получить доступ к старым сообщениям, которые получил ваш бот.
- Вы не можете получить доступ к старым сообщениям, которые ваш бот отправил.
- Вы не можете получить список всех чатов с вашим ботом.
- Другие проблемы, например, нет обзора медиа и т.д.
По сути, все сводится к тому, что бот имеет доступ только к информации текущего входящего обновления (например, сообщения), т.е. к той информации, которая доступна в объекте контекста ctx
.
Следовательно, если вы хотите получить доступ к старым данным, вы должны хранить их сразу же после поступления. Это означает, что у вас должно быть хранилище данных, например, файл, база данных или хранилище в памяти.
Конечно, grammY позаботился об этом: вам не нужно размещать это самостоятельно. Вы можете просто использовать сессионное хранилище grammY, которое не нуждается в настройке и остается бесплатным навсегда.
Естественно, существует множество других сервисов, предлагающих хранение данных как услугу, и grammY также легко интегрируется с ними. Если вы хотите запустить собственную базу данных, будьте уверены, что grammY поддерживает и это. Прокрутите вниз, чтобы узнать, какие интеграции доступны в настоящее время.
Что такое сессии?
Очень часто боты хранят некоторые данные в чате. Например, допустим, мы хотим создать бота, который будет подсчитывать количество раз, когда в тексте сообщения появляется эмодзи пиццы 🍕. Этого бота можно добавить в группу, и он сможет рассказать вам, насколько вы и ваши друзья любите пиццу.
Когда наш пицца-бот получает сообщение, он должен вспомнить, сколько раз он видел 🍕 в этом чате раньше. Количество пицц, конечно, не должно измениться, когда ваша сестра добавит пицца-бота в свой групповой чат, так что на самом деле мы хотим хранить один счетчик на каждый чат.
Сессии — это элегантный способ хранения данных в каждом чате. В качестве ключа в базе данных используется идентификатор чата, а в качестве значения - счетчик. В данном случае мы будем называть идентификатор чата ключом сессии. (Подробнее о ключах сессий вы можете прочитать здесь). По сути, ваш бот будет хранить карту от идентификатора чата к некоторым пользовательским данным сессии, т.е. что-то вроде этого:
{
"424242": { "pizzaCount": 24 },
"987654": { "pizzaCount": 1729 }
}
2
3
4
Когда мы говорим “база данных”, мы на самом деле имеем в виду любое решение для хранения данных. Это и файлы, и облачные хранилища, и все остальное.
Хорошо, но что такое сессии сейчас?
Мы можем установить на бота middleware, который будет предоставлять данные о сессии чата в ctx
при каждом обновлении. Установленный плагин будет делать что-то до и после вызова наших обработчиков:
- Перед нашим middleware. Плагин сессии загружает данные сессии для текущего чата из базы данных. Он сохраняет данные в объекте контекста под именем
ctx
..session - Во время работы нашего middleware. Мы можем читать
ctx
, чтобы узнать, какое значение было в базе данных. Например, если в чат отправлено сообщение с идентификатором.session 424242
, то оно будетctx
во время работы нашего middleware (по крайней мере, с приведенным выше примером состояния базы данных). Мы также можем модифицировать.session = { pizza Count: 24 } ctx
произвольным образом, так что мы можем добавлять, удалять и изменять поля по своему усмотрению..session - После нашего middleware. Middleware сессии следит за тем, чтобы данные были записаны обратно в базу данных. Каким бы ни было значение
ctx
после завершения работы middleware, оно будет сохранено в базе данных..session
В результате нам больше не нужно беспокоиться о взаимодействии с хранилищем данных. Мы просто изменяем данные в ctx
, а плагин позаботится обо всем остальном.
Когда использовать сессии?
Пропустите, если вы уже знаете, что хотите использовать сессии.
Вы думаете: “Это здорово, мне больше не придется беспокоиться о базах данных!” И вы будете правы, сессии — это идеальное решение, но только для некоторых типов данных.
По нашему опыту, есть случаи, когда сессии действительно великолепны. С другой стороны, есть случаи, когда традиционная база данных может быть более подходящей.
Это сравнение может помочь вам решить, стоит ли использовать сессии или нет.
Сессии | База данных | |
---|---|---|
Доступ | одно изолированное хранилище на чат. | Доступ к одним и тем же данным из разных чатов. |
Совместный доступ | данные используются только ботом. | данные используются другими системами (например, подключенным веб-сервером) |
Формат | любые объекты JavaScript: строки, числа, массивы и так далее | любые данные (бинарные, файлы, структурированные и т.д.) |
Размер одного чата | оптимально менее ~3 МБ на чат | любой размер |
Эксклюзивная функция | Требуется некоторым плагинам grammY. | Поддерживает транзакции базы данных. |
Это не означает, что вещи не могут работать, если вы выбираете сессии/базы данных вместо других. Например, вы, конечно, можете хранить большие бинарные данные в сессии. Однако ваш бот будет работать не так хорошо, как мог бы, поэтому мы рекомендуем использовать сессии только там, где это имеет смысл.
Как использовать сессии?
Вы можете добавить поддержку сессий в grammY, используя middleware для сессий.
Пример использования
Вот пример бота, который подсчитывает сообщения, содержащие эмодзи пиццы 🍕:
import { Bot, Context, session, SessionFlavor } from "grammy";
// Определите форму нашей сессии.
interface SessionData {
pizzaCount: number;
}
// Расширьте тип контекста, чтобы включить в него сессии.
type MyContext = Context & SessionFlavor<SessionData>;
const bot = new Bot<MyContext>("");
// Установите middleware для сессии и определите начальное значение.
function initial(): SessionData {
return { pizzaCount: 0 };
}
bot.use(session({ initial }));
bot.command("hunger", async (ctx) => {
const count = ctx.session.pizzaCount;
await ctx.reply(`Ваш уровень голода ${count}!`);
});
bot.hears(/.*🍕.*/, (ctx) => ctx.session.pizzaCount++);
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
const { Bot, session } = require("grammy");
const bot = new Bot("");
// Установите middleware для сессии и определите начальное значение.
function initial() {
return { pizzaCount: 0 };
}
bot.use(session({ initial }));
bot.command("hunger", async (ctx) => {
const count = ctx.session.pizzaCount;
await ctx.reply(`Ваш уровень голода ${count}!`);
});
bot.hears(/.*🍕.*/, (ctx) => ctx.session.pizzaCount++);
bot.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {
Bot,
Context,
session,
SessionFlavor,
} from "https://deno.land/x/grammy@v1.33.0/mod.ts";
// Определите форму нашей сессии.
interface SessionData {
pizzaCount: number;
}
// Расширьте тип контекста, чтобы включить в него сессии.
type MyContext = Context & SessionFlavor<SessionData>;
const bot = new Bot<MyContext>("");
// Установите middleware для сессии и определите начальное значение.
function initial(): SessionData {
return { pizzaCount: 0 };
}
bot.use(session({ initial }));
bot.command("hunger", async (ctx) => {
const count = ctx.session.pizzaCount;
await ctx.reply(`Ваш уровень голода ${count}!`);
});
bot.hears(/.*🍕.*/, (ctx) => ctx.session.pizzaCount++);
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
29
30
31
Обратите внимание, что нам также нужно настроить тип контекста, чтобы сделать сессию доступной на нем. Расширитель контекста называется Session
.
Первоначальные данные сессии
Когда пользователь впервые обращается к вашему боту, у него нет данных о сессии. Поэтому важно указать параметр initial
для middleware сессии. Передайте функцию, которая генерирует новый объект с начальными данными сессии для новых чатов.
// Создает новый объект, который будет использоваться в качестве начальных данных сессии.
function createInitialSessionData() {
return {
pizzaCount: 0,
// другие данные здесь
};
}
bot.use(session({ initial: createInitialSessionData }));
2
3
4
5
6
7
8
Тоже самое, но короче:
bot.use(session({ initial: () => ({ pizzaCount: 0 }) }));
Совместное использование объектов Убедитесь, что всегда создаете
новый объект. Не делайте этого НЕ:
// ОПАСНОСТЬ, ПЛОХО, НЕПРАВИЛЬНО, СТОП
const initialData = { pizzaCount: 0 }; // НЕТ
bot.use(session({ initial: () => initialData })); // ЗЛО
2
3
Если это сделать, то несколько чатов могут совместно использовать один и тот же объект сессии в памяти. Таким образом, изменение данных сессии в одном чате может случайно повлиять на данные сессии в другом чате.
Вы также можете полностью опустить опцию initial
, хотя вам советуют этого не делать. Если вы не укажете его, чтение ctx
будет вызывать ошибку у новых пользователей.
Ключи сессии
В этом разделе описывается расширенная функция, о которой большинству людей не нужно беспокоиться. Возможно, вы захотите продолжить в разделе о хранении ваших данных.
Вы можете указать, какой ключ сессии использовать, передав функцию get
в настройки. Таким образом, вы можете кардинально изменить принцип работы плагина сессий. По умолчанию данные хранятся в каждом чате. Использование get
позволяет хранить данные для каждого пользователя, или для комбинации пользователь-чат, или как вам угодно. Вот три примера:
// Сохраняет данные в каждом чате (по умолчанию).
function getSessionKey(ctx: Context): string | undefined {
// Пусть все пользователи в групповом чате используют одну и ту же сессию,
// но в личных чатах каждому пользователю предоставляется отдельная приватная сессия.
return ctx.chat?.id.toString();
}
// Хранит данные для каждого пользователя.
function getSessionKey(ctx: Context): string | undefined {
// Дайте каждому пользователю его личное хранилище сессий
// (будет распространяться по группам и в личном чате)
return ctx.from?.id.toString();
}
// Хранит данные по каждой комбинации пользователь-чат.
function getSessionKey(ctx: Context): string | undefined {
// Предоставьте каждому пользователю одно личное хранилище сессий для общения с ботом
// (независимая сессия для каждой группы и их приватного чата)
return ctx.from === undefined || ctx.chat === undefined
? undefined
: `${ctx.from.id}/${ctx.chat.id}`;
}
bot.use(session({ getSessionKey }));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Сохраняет данные в каждом чате (по умолчанию).
function getSessionKey(ctx) {
// Пусть все пользователи в групповом чате используют одну и ту же сессию,
// но в личных чатах каждому пользователю предоставляется отдельная приватная сессия.
return ctx.chat?.id.toString();
}
// Хранит данные для каждого пользователя.
function getSessionKey(ctx) {
// Дайте каждому пользователю его личное хранилище сессий
// (будет распространяться по группам и в личном чате)
return ctx.from?.id.toString();
}
// Хранит данные по каждой комбинации пользователь-чат.
function getSessionKey(ctx) {
// Предоставьте каждому пользователю одно личное хранилище сессий для общения с ботом
// (независимая сессия для каждой группы и их приватного чата)
return ctx.from === undefined || ctx.chat === undefined
? undefined
: `${ctx.from.id}/${ctx.chat.id}`;
}
bot.use(session({ getSessionKey }));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Если get
возвращает undefined
, то ctx
будет undefined
. Например, стандартный преобразователь сеансовых ключей не будет работать для обновлений poll
/poll
или обновлений inline
, потому что они не принадлежат чату (ctx
является undefined
).
Ключи сеансов и вебхуки.
Если вы запускаете бота на вебхуках, вам следует избегать использования опции get
. Telegram отправляет вебхуки последовательно в каждый чат, поэтому стандартный преобразователь сеансовых ключей — единственная реализация, которая гарантированно не приведет к потере данных.
Если вы должны использовать эту опцию (что, конечно, все еще возможно), вы должны знать, что вы делаете. Убедитесь, что вы понимаете последствия такой конфигурации, прочитав статью здесь и особенно здесь.
Вы также можете указать префикс, если хотите добавить дополнительное пространство имён к вашим ключам сессий. Например, вот как можно сохранять данные сессии для каждого пользователя с использованием префикса user
.
bot.use(session({
getSessionKey: (ctx) => ctx.from?.id,
prefix: "user-",
}));
2
3
4
Для пользователя с идентификатором 424242
ключ сессии теперь будет иметь вид user
.
Миграции чата
Если вы используете сессии для групп, вам следует знать, что при определенных обстоятельствах Telegram переносит обычные группы в супергруппы (например, здесь).
Эта миграция происходит только один раз для каждой группы, но она может привести к несоответствиям. Это происходит потому, что перенесенный чат технически является совершенно другим чатом, имеющим другой идентификатор, и, следовательно, его сессия будет идентифицироваться по-другому.
В настоящее время не существует безопасного решения этой проблемы, поскольку сообщения из двух чатов также идентифицируются по-разному. Это может привести к скачкам данных. Однако существует несколько способов решения этой проблемы:
Игнорирование проблемы. При переносе группы, данные сеанса бота фактически обнуляются. Простое, надежное, стандартное поведение, но потенциально неожиданное один раз в чате. Например, если миграция произойдет, когда пользователь находится в беседе, управляемой плагином conversations, беседа будет сброшена.
Храните в сессии только временные данные (или данные с таймаутом), а для важных вещей, которые необходимо перенести при миграции чата, используйте базу данных. Затем можно использовать транзакции и пользовательскую логику для обработки одновременного доступа к данным из старого и нового чата. Это требует больших усилий и требует затрат на производительность, но это единственный по-настоящему надежный способ решить эту проблему.
Теоретически возможно реализовать обходной путь, который будет соответствовать обоим чатам без гарантии надежности. Telegram Bot API отправляет обновление миграции для каждого из двух чатов, как только миграция была запущена (см. свойства
migrate
или_to _chat _id migrate
в документации Telegram API). Проблема в том, что нет никакой гарантии, что эти сообщения будут отправлены до появления нового сообщения в супергруппе. Следовательно, бот может получить сообщение из новой супергруппы до того, как узнает о переходе, и, таким образом, не сможет сопоставить два чата, что приведет к вышеупомянутым проблемам._from _chat _id Другим обходным решением было бы ограничить бота только для супергрупп с помощью фильтров (или ограничить только функции, связанные с сессиями, для супергрупп). Однако это перекладывает проблему/неудобство на пользователей.
Предоставление пользователям возможности принимать решение в явном виде. (“Этот чат был перенесен, хотите ли вы перенести данные бота?”). Гораздо надежнее и прозрачнее автоматических миграций за счет искусственно добавленной задержки, но хуже пользовательский опыт.
И наконец, разработчик сам решает, как поступить в этом случае. В зависимости от функциональности бота можно выбрать тот или иной способ. Если данные недолговечны (например, временные, с таймаутами), миграция не представляет особой проблемы. Пользователь воспримет миграцию как заминку (если время неудачно выбрано) и просто запустит функцию заново.
Игнорировать проблему, конечно, проще всего, но все же важно знать о таком поведении. В противном случае это может привести к путанице и стоить часов времени на отладку.
Хранение ваших данных
Во всех приведенных выше примерах данные сессии хранятся в оперативной памяти, поэтому при остановке бота все данные будут потеряны. Это удобно, когда вы разрабатываете бота или запускаете автоматические тесты (не нужно настраивать базу данных), однако это, скорее всего, нежелательно в production. В production билде вы захотите сохранить данные, например, в файле, базе данных или другом хранилище.
Вам следует использовать опцию storage
в middleware сессии, чтобы подключить его к вашему хранилищу данных. Возможно, для grammY уже написан адаптер хранения, который вы можете использовать (см. ниже), но если это не так, то обычно требуется всего 5 строк кода, чтобы реализовать его самостоятельно.
Известные адаптеры хранения
По умолчанию сессии будут храниться в вашей памяти с помощью встроенного адаптера хранения. Вы также можете использовать постоянные сессии, которые grammY предлагает бесплатно, или подключаться к внешним хранилищам.
Вот как можно установить один из адаптеров хранения данных снизу.
const storageAdapter = ... // зависит от настроек
bot.use(session({
initial: ...
storage: storageAdapter,
}));
2
3
4
5
6
Оперативная память (по умолчанию)
По умолчанию все данные будут храниться в оперативной памяти. Это означает, что все сессии будут потеряны, как только ваш бот остановится.
Вы можете использовать класс Memory
(документация API) из пакета ядра grammY, если хотите настроить дополнительные параметры хранения данных в оперативной памяти.
bot.use(session({
initial: ...
storage: new MemorySessionStorage() // также значение по умолчанию
}));
2
3
4
Бесплатное хранилище
Бесплатное хранилище предназначено для использования в хобби проектах. Приложениям производственного масштаба следует размещать собственную базу данных. Список поддерживаемых интеграций внешних решений для хранения данных находится внизу.
Преимущество использования grammY заключается в том, что вы получаете доступ к бесплатному облачному хранилищу. Оно не требует настройки - вся аутентификация осуществляется с помощью токена вашего бота. Загляните в репозиторий!
Он очень прост в использовании:
import { freeStorage } from "@grammyjs/storage-free";
bot.use(session({
initial: ...
storage: freeStorage<SessionData>(bot.token),
}));
2
3
4
5
6
const { freeStorage } = require("@grammyjs/storage-free");
bot.use(session({
initial: ...
storage: freeStorage(bot.token),
}));
2
3
4
5
6
import { freeStorage } from "https://deno.land/x/grammy_storages@v2.4.2/free/src/mod.ts";
bot.use(session({
initial: ...
storage: freeStorage<SessionData>(bot.token),
}));
2
3
4
5
6
Готово! Теперь ваш бот будет использовать постоянное хранилище данных.
Здесь приведен полный пример бота, который вы можете скопировать, чтобы опробовать его.
import { Bot, Context, session, SessionFlavor } from "grammy";
import { freeStorage } from "@grammyjs/storage-free";
// Определите структуру сессии.
interface SessionData {
count: number;
}
type MyContext = Context & SessionFlavor<SessionData>;
// Создайте бота и зарегистрируйте middleware сессии.
const bot = new Bot<MyContext>("");
bot.use(
session({
initial: () => ({ count: 0 }),
storage: freeStorage<SessionData>(bot.token),
}),
);
// Используйте постоянные данные сессии в обработчиках обновлений.
bot.on("message", async (ctx) => {
ctx.session.count++;
await ctx.reply(`Количество сообщений: ${ctx.session.count}`);
});
bot.catch((err) => console.error(err));
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
const { Bot, session } = require("grammy");
const { freeStorage } = require("@grammyjs/storage-free");
// Создайте бота и зарегистрируйте middleware сессии.
const bot = new Bot("");
bot.use(
session({
initial: () => ({ count: 0 }),
storage: freeStorage(bot.token),
}),
);
// Используйте постоянные данные сессии в обработчиках обновлений.
bot.on("message", async (ctx) => {
ctx.session.count++;
await ctx.reply(`Количество сообщений: ${ctx.session.count}`);
});
bot.catch((err) => console.error(err));
bot.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {
Bot,
Context,
session,
SessionFlavor,
} from "https://deno.land/x/grammy@v1.33.0/mod.ts";
import { freeStorage } from "https://deno.land/x/grammy_storages@v2.4.2/free/src/mod.ts";
// Определите структуру сессии.
interface SessionData {
count: number;
}
type MyContext = Context & SessionFlavor<SessionData>;
// Создайте бота и зарегистрируйте middleware сессии.
const bot = new Bot<MyContext>("");
bot.use(
session({
initial: () => ({ count: 0 }),
storage: freeStorage<SessionData>(bot.token),
}),
);
// Используйте постоянные данные сессии в обработчиках обновлений.
bot.on("message", async (ctx) => {
ctx.session.count++;
await ctx.reply(`Количество сообщений: ${ctx.session.count}`);
});
bot.catch((err) => console.error(err));
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
29
30
31
32
Внешние решения для хранения данных
Мы поддерживаем коллекцию официальных адаптеров для хранения данных, которые позволяют хранить данные о сеансах в различных местах. Каждый из них потребует от вас регистрации у хостинг-провайдера или размещения собственного решения для хранения данных.
Посетите это место, чтобы посмотреть список поддерживаемых в настоящее время адаптеров и получить рекомендации по их использованию.
Ваше хранилище не поддерживается? Не беда! Создать собственный адаптер
хранилища очень просто. Опция storage
работает с любым объектом, который соответствует этому интерфейсу, так что вы можете подключиться к своему хранилищу всего в нескольких строчках кода.
Если вы опубликовали свой собственный адаптер хранения, не стесняйтесь редактировать эту страницу и ссылаться на нее, чтобы другие люди могли использовать его.
Все адаптеры для хранения данных устанавливаются одинаково. Во-первых, необходимо обратить внимание на имя пакета выбранного вами адаптера. Например, адаптер хранения для Supabase называется supabase
.
На Node.js вы можете установить адаптеры с помощью команды npm i @grammyjs
. Например, адаптер хранения для Supabase можно установить через npm i @grammyjs
.
На Deno все адаптеры хранения публикуются в одном модуле Deno. Вы можете импортировать нужный вам адаптер из его подпапки по адресу https://
. Например, адаптер хранения для Supabase можно импортировать из https://
.
Ознакомьтесь с соответствующими репозиториями, посвященными каждой отдельной настройке. В них содержится информация о том, как подключить их к вашему решению для хранения данных.
Вы также можете прокрутить страницу вниз, чтобы узнать, как плагин сессий может улучшить любой адаптер хранения.
Мульти сессии
Плагин сессий способен хранить различные фрагменты данных сессии в разных местах. В принципе, это работает так, как если бы вы установили несколько независимых экземпляров плагина сессий, каждый из которых имеет свою конфигурацию.
Каждый из этих фрагментов данных будет иметь имя, под которым он может хранить свои данные. Вы сможете получить доступ к ctx
и ctx
, причем эти значения были загружены из разных хранилищ данных, и они же будут записаны обратно в разные хранилища данных. Естественно, вы можете использовать одно и то же хранилище с разной конфигурацией.
Также можно использовать разные ключи сессий для каждого фрагмента. В результате вы можете хранить часть данных для каждого чата, а часть - для каждого пользователя.
Если вы используете grammY runner, убедитесь, что вы правильно настроили
sequentialize
, возвращая все сессионные ключи в качестве ограничений из функции.
Вы можете использовать эту возможность, передав type:
в конфигурацию сессии. В свою очередь, вам нужно будет настроить каждый фрагмент со своим собственным конфигом.
bot.use(
session({
type: "multi",
foo: {
// Это также значения по умолчанию
storage: new MemorySessionStorage(),
initial: () => undefined,
getSessionKey: (ctx) => ctx.chat?.id.toString(),
prefix: "",
},
bar: {
initial: () => ({ prop: 0 }),
storage: freeStorage(bot.token),
},
baz: {},
}),
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Обратите внимание, что вы должны добавить запись конфигурации для каждого фрагмента, который вы хотите использовать. Если вы хотите использовать конфигурацию по умолчанию, вы можете указать пустой объект (как мы сделали для baz
в примере выше).
Данные вашей сессии все равно будут состоять из объекта с несколькими свойствами. Поэтому ваш расширитель контекста не изменится. В приведенном выше примере можно использовать этот интерфейс при настройке объекта контекста:
interface SessionData {
foo?: string;
bar: { prop: number };
baz: { width?: number; height?: number };
}
2
3
4
5
После этого вы можете продолжать использовать Session
для своего контекстного объекта.
Ленивые сессии
В этом разделе описывается оптимизация производительности, о которой большинству людей не нужно беспокоиться.
Ленивые сессии — это альтернативная реализация сессий, которая может значительно снизить трафик базы данных вашего бота, пропуская лишние операции чтения и записи.
Предположим, что ваш бот находится в групповом чате, где он не отвечает на обычные текстовые сообщения, а только на команды. Без сессий это будет выглядеть следующим образом:
- Вашему боту отправляется обновление с новым текстовым сообщением.
- Никакой обработчик не вызывается, поэтому никаких действий не происходит.
- middleware завершает работу немедленно.
Как только вы устанавливаете стандартные (строгие) сессии, которые напрямую предоставляют данные сессии в объект контекста, это происходит:
- Обновление с новым текстовым сообщением будет отправлено вашему боту.
- Данные сессии загружаются из хранилища сессий (например, базы данных).
- Обработчик не вызывается, поэтому никаких действий не происходит.
- Идентичные данные сеанса записываются обратно в хранилище сеанса.
- middleware завершает работу, выполнив чтение и запись в хранилище данных.
В зависимости от характера вашего бота, это может привести к большому количеству лишних чтений и записей. Ленивые сессии позволяют пропустить шаги 2. и 4., если окажется, что ни одному вызванному обработчику не нужны данные сессии. В этом случае данные не будут ни считываться из хранилища данных, ни записываться в него.
Это достигается путем перехвата доступа к ctx
. Если обработчик не вызван, то к ctx
никогда не будет получен доступ. Ленивые сессии используют это как индикатор для предотвращения связи с базой данных.
На практике вместо того, чтобы иметь данные сессии, доступные в ctx
, вы теперь будете иметь данные сессии в виде Promise
, доступные в ctx
.
// Сессии по умолчанию (строгие сессии)
bot.command("settings", async (ctx) => {
// `session` - это данные сессии
const session = ctx.session;
});
// Ленивые сессии
bot.command("settings", async (ctx) => {
// `promise` - это Promise данных сессии, и
const promise = ctx.session;
// `session` - это данные сессии
const session = await ctx.session;
});
2
3
4
5
6
7
8
9
10
11
12
13
Если вы никогда не обращаетесь к ctx
, то никаких операций не будет, но как только вы обратитесь к свойству session
контекстного объекта, будет запущена операция чтения. Если вы никогда не вызываете операцию чтения (или напрямую присваиваете новое значение ctx
), мы знаем, что нам также не придется записывать данные обратно, поскольку они никак не могли быть изменены. Следовательно, мы пропускаем и операцию записи. В результате мы получаем минимум операций чтения и записи, но вы можете использовать сессию почти так же, как и раньше, просто добавив в код несколько ключевых слов async
и await
.
Так что же нужно для использования ленивых сессий вместо стандартных (строгих)? В основном вам нужно сделать три вещи:
- Используйте для расширения контекста
Lazy
вместоSession Flavor Session
. Они работают одинаково, просто для ленивого вариантаFlavor ctx
обернута в.session Promise
. - Используйте
lazy
вместоSession session
для регистрации middleware сессии. - Всегда ставьте строку
await ctx
вместо.session ctx
везде в вашем middleware, как для чтения, так и для записи. Не волнуйтесь: вы можете.session await
promise с данными сессии столько раз, сколько захотите, но вы всегда будете ссылаться на одно и то же значение, поэтому никогда не будет дублирования чтения для обновления.
Обратите внимание, что при использовании ленивых сессий вы можете присваивать ctx
как объекты, так и promise объектов. Если вы зададите ctx
как promise, то оно будет await
перед записью данных обратно в хранилище данных. Это позволит использовать следующий код:
bot.command("reset", async (ctx) => {
// Гораздо короче, чем если бы сначала нужно было `await ctx.session`:
ctx.session = ctx.session.then((stats) => {
stats.counter = 0;
});
});
2
3
4
5
6
Можно долго доказывать, что явное использование await
предпочтительнее, чем назначение promise на ctx
, но суть в том, что вы можете сделать это, если вам по какой-то причине больше нравится такой стиль.
Плагины, которым нужны сессии Разработчики плагинов, использующих
ctx
, должны всегда разрешать пользователям передавать Session
и, следовательно, поддерживать оба режима. В коде плагина просто постоянно await ctx
: если передается объект, не являющийся promise, он просто будет оценен сам по себе, так что вы эффективно пишете код только для ленивых сессий и, таким образом, автоматически поддерживаете строгие сессии.
Усовершенствования для хранилищ
Плагин сессий способен расширить возможности любого адаптера хранилища, добавив к нему дополнительные функции: таймауты и миграции.
Их можно установить с помощью функции enhance
.
// Используйте улучшенный адаптер для хранения данных.
bot.use(
session({
storage: enhanceStorage({
storage: freeStorage(bot.token), // настройте это
// другие настройки здесь
}),
}),
);
2
3
4
5
6
7
8
9
Вы также можете использовать оба варианта одновременно.
Таймауты
Улучшение таймаутов позволяет добавить дату истечения срока действия к данным сессии. Это означает, что вы можете указать период времени, и если в течение этого времени сессия не будет изменена, данные для конкретного чата будут удалены.
Вы можете использовать тайм-ауты сессий с помощью опции milliseconds
.
const enhanced = enhanceStorage({
storage,
millisecondsToLive: 30 * 60 * 1000, // 30 минут
});
2
3
4
Обратите внимание, что фактическое удаление данных произойдет только при следующем чтении данных соответствующей сессии.
Миграции
Миграции полезны, если вы развиваете бота дальше, а данные о сессиях уже существуют. Вы можете использовать их, если хотите изменить данные сессии, не ломая все предыдущие данные.
Для этого данным присваиваются номера версий, а затем пишутся небольшие функции миграции. Функции миграции определяют, как обновлять данные сессии от одной версии к другой.
Мы попытаемся проиллюстрировать это на примере. Допустим, вы храните информацию о домашнем животном пользователя. До сих пор вы хранили только имена питомцев в строковом массиве в ctx
.
interface SessionData {
petNames: string[];
}
2
3
Теперь вы понимаете, что хотите также хранить возраст питомцев.
Вы можете сделать следующее:
interface SessionData {
petNames: string[];
petBirthdays?: number[];
}
2
3
4
Это не нарушит существующие данные сессии. Однако это не очень хорошо, потому что имена и дни рождения теперь хранятся в разных местах. В идеале данные сессии должны выглядеть следующим образом:
interface Pet {
name: string;
birthday?: number;
}
interface SessionData {
pets: Pet[];
}
2
3
4
5
6
7
8
Функции миграции позволяют преобразовать старый массив строк в новый массив объектов домашних животных.
interface OldSessionData {
petNames: string[];
}
function addBirthdayToPets(old: OldSessionData): SessionData {
return {
pets: old.petNames.map((name) => ({ name })),
};
}
const enhanced = enhanceStorage({
storage,
migrations: {
1: addBirthdayToPets,
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function addBirthdayToPets(old) {
return {
pets: old.petNames.map((name) => ({ name })),
};
}
const enhanced = enhanceStorage({
storage,
migrations: {
1: addBirthdayToPets,
},
});
2
3
4
5
6
7
8
9
10
11
12
При считывании данных сессии улучшение хранилища проверит, не находятся ли данные сессии в версии 1
. Если версия ниже (или отсутствует, потому что вы не использовали эту функцию раньше), то будет запущена функция миграции. Это обновит данные до версии 1
. Таким образом, в вашем боте вы всегда можете считать, что данные сессии имеют самую актуальную структуру, а улучшение хранилища позаботится об остальном и при необходимости перенесет ваши данные.
С течением времени и дальнейшими изменениями вашего бота вы сможете добавлять все больше и больше функций миграции:
const enhanced = enhanceStorage({
storage,
migrations: {
1: addBirthdayToPets,
2: addIsFavoriteFlagToPets,
3: addUserSettings,
10: extendUserSettings,
10.1: fixUserSettings,
11: compressData,
},
});
2
3
4
5
6
7
8
9
10
11
В качестве версий можно выбрать любые числа JavaScript. Независимо от того, насколько изменились данные сессии для чата, при считывании они будут перемещаться по версиям до тех пор, пока не будет использована самая последняя структура.
Типы для усовершенствования хранилищ
При использовании расширений хранилища адаптер хранилища должен хранить больше данных, чем просто данные сеанса. Например, он должен хранить время, когда сессия была сохранена в последний раз, чтобы правильно просрочить данные по истечении времени. В некоторых случаях TypeScript сможет определить правильные типы для вашего адаптера хранения. Однако чаще всего необходимо явно указать типы данных сессии в нескольких местах.
Следующий пример фрагмента кода иллюстрирует использование улучшения таймаута с правильными типами TypeScript.
interface SessionData {
count: number;
}
type MyContext = Context & SessionFlavor<SessionData>;
const bot = new Bot<MyContext>("");
bot.use(
session({
initial(): SessionData {
return { count: 0 };
},
storage: enhanceStorage({
storage: new MemorySessionStorage<Enhance<SessionData>>(),
millisecondsToLive: 60_000,
}),
}),
);
bot.on(
"message",
(ctx) => ctx.reply(`Счетчик чата теперь: ${ctx.session.count++}`),
);
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
Обратите внимание, что каждый адаптер хранения может принимать параметр типа. Например, для бесплатных сессий можно использовать free
вместо Memory
. То же самое справедливо и для всех остальных адаптеров хранения.
Краткая информация о плагине
Этот плагин встроен в ядро grammY. Вам не нужно ничего устанавливать, чтобы использовать его. Просто импортируйте все из самого grammY.
Кроме того, документация и ссылка на API этого плагина объединены с основным пакетом.