Сесії та збереження даних (вбудовано)
Хоча ви завжди можете просто написати власний код для підключення до вибраного вами сховища даних, grammY підтримує дуже зручний шаблон зберігання даних під назвою сесії.
Перейдіть вниз, якщо ви вже знаєте, як працюють сесії.
Чому ми повинні думати про зберігання даних?
На відміну від облікових записів звичайних користувачів у Telegram, боти мають обмежене хмарне сховище у хмарі Telegram. Унаслідок цього, є кілька речей, які ви не можете робити за допомогою ботів:
- Ви не можете отримати доступ до старих повідомлень, які отримав ваш бот.
- Ви не можете отримати доступ до старих повідомлень, надісланих вашим ботом.
- Ви не можете отримати список усіх чатів з вашим ботом.
- Багато інших речей: наприклад, відсутність огляду медіаконтенту тощо.
По суті, це зводиться до того, що бот має доступ лише до інформації поточного оновлення: наприклад, повідомлення, тобто до тієї інформації, яка доступна в обʼєкті контексту ctx
.
Отже, якщо ви хочете отримати доступ до старих даних, ви повинні зберігати їх одразу після надходження. Це означає, що у вас має бути сховище даних: файл, база даних або сховище в памʼяті.
Звісно, grammY має все необхідне для цього, тому вам не потрібно самостійно цим займатися. Ви можете просто використовувати сховище сесій grammY, яке не потребує жодних налаштувань і є завжди безкоштовним.
Звичайно, існує безліч інших сервісів, які пропонують зберігання даних як послугу, і grammY також легко інтегрується з ними. Якщо ви хочете запустити власну базу даних, то можете бути впевнені, що grammY буде працювати з нею так само добре. Прокрутіть вниз, щоб побачити, які інтеграції наразі доступні.
Що таке сесії?
Для ботів дуже поширеним явищем є зберігання певної кількості даних кожного чату. Наприклад, ми хочемо створити бота, який підраховує кількість разів, коли текст повідомлення містить емодзі піци 🍕. Цього бота можна додати в групу, і він розповість вам, наскільки ви і ваші друзі любите піцу.
Коли бот отримує повідомлення, він повинен згадати, скільки разів він бачив 🍕 у цьому чаті раніше. Звичайно, ваш лічильник піци не повинен змінюватися, коли ваша сестра додає бота до свого групового чату, тому ми хочемо зберігати один лічильник для кожного чату.
Сесії — це гарний спосіб зберігання даних для кожного чату. Ви використовуєте ідентифікатор чату як ключ у вашій базі даних, а лічильник як значення. У цьому випадку ми будемо називати ідентифікатор чату ключом сесії. Ви можете прочитати більше про ключі сесії тут. Фактично, ваш бот буде зберігати колекцію з ідентифікатора чату та деяких власних даних сесії, тобто щось на кшталт цього:
{
"424242": { "pizzaCount": 24 },
"987654": { "pizzaCount": 1729 }
}
2
3
4
Коли ми говоримо “база даних”, ми маємо на увазі будь-яке рішення для зберігання даних. Це включає в себе файли, хмарні сховища чи будь-що інше.
Гаразд, але що таке наразі сесії?
Ми можемо встановити проміжний обробник на бота, який буде надавати дані сесії чату в ctx
для кожного оновлення. Встановлений плагін буде робити щось до і після виклику наших обробників:
- До нашого обробника. Плагін сесії завантажує дані сесії для поточного чату з бази даних. Він зберігає дані про обʼєкт контексту у властивості
ctx
..session - Під час виконання нашого обробника. Ми можемо прочитати
ctx
, щоб отримати доступ к даним, які знаходились в базі даних. Наприклад, якщо буде надіслано повідомлення в чат з ідентифікатором.session 424242
, це будеctx
під час виконання нашого обробника; принаймні, з наведеним вище прикладом стану бази даних. Ми також можемо довільно модифікувати.session = { pizza Count: 24 } ctx
, тобто додавати, видаляти та змінювати поля, як нам заманеться..session - Після нашого обробника. Проміжний обробник сесії гарантує, що дані будуть записані назад до бази даних. Яким би не було значення
ctx
після завершення роботи обробника, воно буде збережено в базі даних..session
У результаті нам більше ніколи не доведеться турбуватися про фактичний звʼязок зі сховищем даних. Ми просто змінюємо дані в ctx
, а плагін подбає про все інше.
Коли використовувати сесії
Пропустіть цей розділ, якщо ви вже знаєте, що хочете використовувати сесії.
Ви можете подумати, що це чудово — “Мені більше ніколи не доведеться турбуватися про бази даних!”. І ви маєте рацію, сесії — ідеальне рішення, але тільки для певних типів даних.
З нашого досвіду, є випадки використання, коли сесії справді блискучі. З іншого боку, є випадки, коли традиційна база даних може підійти краще.
Це порівняння може допомогти вам вирішити, використовувати сесії чи ні.
Сесії | База даних | |
---|---|---|
Доступ | одне ізольоване сховище для кожного чату | доступ до одних і тих самих даних з кількох чатів |
Спільний доступ | дані використовуються тільки ботом | дані використовуються іншими системами: підключеним вебсервером тощо |
Формат | будь-які обʼєкти JavaScript: рядки, числа, масиви тощо | будь-який формат: бінарні або структуровані дані, файли тощо |
Розмір даних | бажано менше близько 3-х МБ для кожного чату | будь-який розмір |
Особливість | вимагається деякими плагінами grammY | підтримує транзакції бази даних |
Це не означає, що все це не може працювати, якщо ви обираєте сесії або бази даних. Наприклад, ви звісно можете зберігати великі бінарні дані в сесії. Однак ваш бот не буде працювати так добре, як міг би, тому ми рекомендуємо використовувати сесії лише там, де вони мають сенс.
Як використовувати сесії
Ви можете додати підтримку сесії до grammY за допомогою вбудованого проміжного обробника сесії.
Приклад використання
Ось приклад бота, який підраховує повідомлення, що містять емодзі піци 🍕:
import { Bot, Context, session, SessionFlavor } from "grammy";
// Визначаємо структуру нашої сесії.
interface SessionData {
pizzaCount: number;
}
// Налаштовуємо тип контексту, щоб він включав сесії.
type MyContext = Context & SessionFlavor<SessionData>;
const bot = new Bot<MyContext>("");
// Встановлюємо проміжний обробник сесії та визначаємо початкове значення сесії.
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("");
// Встановлюємо проміжний обробник сесії та визначаємо початкове значення сесії.
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.27.0/mod.ts";
// Визначаємо структуру нашої сесії.
interface SessionData {
pizzaCount: number;
}
// Налаштовуємо тип контексту, щоб він включав сесії.
type MyContext = Context & SessionFlavor<SessionData>;
const bot = new Bot<MyContext>("");
// Встановлюємо проміжний обробник сесії та визначаємо початкове значення сесії.
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
для проміжного обробника сесії. Передайте функцію, яка генерує новий обʼєкт з початковими даними сесії для нових чатів.
// Створюємо новий обʼєкт, який буде використано як початкові дані сесії.
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 надсилає вебхуки послідовно для кожного чату, тому стандартний обробник ключів сесії є єдиною реалізацією, яка гарантує, що дані не будуть втрачені.
Якщо вам доведеться скористатися цією опцією, що звісно все ще можливо, ви повинні знати, що ви робите. Переконайтеся, що ви розумієте наслідки такої конфігурації, прочитавши цю статтю й особливо цю.
Міграції чату
Якщо ви використовуєте сесії для груп, вам слід знати, що за певних обставин Telegram мігрує звичайні групи в супергрупи, як описано тут.
Ця міграція відбувається лише один раз для кожної групи, але вона може спричинити невідповідності. Це повʼязано з тим, що перенесений чат — це технічно зовсім інший чат, який має інший ідентифікатор, тож його сесія буде ідентифікуватися інакше.
Наразі не існує безпечного рішення цієї проблеми, оскільки повідомлення з двох чатів також ідентифікуються різним чином. Це може призвести до гонок даних. Однак є кілька способів вирішити цю проблему:
Ігнорувати проблему. Дані сесії бота будуть ефективно скинуті під час міграції групи. Проста й надійна поведінка за замовчуванням, але потенційно несподівана одного разу для кожного чату. Наприклад, якщо міграція відбувається під час розмови, яка ведеться за допомогою плагіна розмов, розмова буде скинута.
Зберігати в сесії лише тимчасові або обмежені тайм-аутами дані, а важливі дані, які потрібно перенести під час міграції чату, використовувати базу даних. Потім можна використовувати транзакції та спеціальну логіку для обробки одночасного доступу до даних зі старого та нового чату. Це вимагає багато зусиль і впливає на швидкість роботи, але це єдиний дійсно надійний спосіб вирішити цю проблему.
Теоретично можливо реалізувати обхідний шлях, який відповідає обом чатам без гарантії надійності. Telegram Bot API надсилає оновлення міграції для кожного з двох чатів, щойно міграція була запущена (дивіться властивості
migrate
або_to _chat _id migrate
в документації Telegram API). Проблема в тому, що немає гарантії, що ці повідомлення будуть надіслані до того, як зʼявиться нове повідомлення в супергрупі. Отже, бот може отримати повідомлення від нової супергрупи до того, як він дізнається про міграцію, тож він не зможе зіставити два чати, що призведе до вищезгаданих проблем._from _chat _id Іншим обхідним шляхом може бути обмеження бота лише для супергруп за допомогою фільтрації або обмеження для супергруп функцій, повʼязаних лише з сесією. Однак це перекладає проблему й незручність на користувачів.
Дозвольте користувачам приймати рішення в явному вигляді: “Цей чат мігрував, ви хочете перенести дані бота?”. Набагато надійніше і прозоріше, ніж автоматичні міграції через штучно додану затримку, але гірший користувацький досвід.
Зрештою, розробник сам вирішує, як поводитися з цим крайнім випадком. Залежно від функціоналу бота можна обрати той чи інший спосіб. Якщо дані, про які йде мова, недовговічні: тимчасові або обмежені тайм-аутами, міграція є меншою проблемою. Користувач може відчути міграцію як затримку, якщо вона відбулася невдало, і йому доведеться просто перезапустити функцію.
Ігнорувати проблему, безумовно, найпростіший спосіб, проте важливо знати про таку поведінку. В іншому випадку це може призвести до плутанини і може коштувати годин налагодження.
Зберігання ваших даних
У всіх наведених вище прикладах дані сесії зберігаються в оперативній памʼяті, тому, як тільки бот зупиняється, всі дані втрачаються. Це зручно, коли ви розробляєте бота або запускаєте автоматичні тести, адже не потрібно налаштовувати базу даних, однак це, швидше за все, не бажано у продакшені. У продакшн середовищі ви захочете зберегати свої дані, наприклад, у файлі, базі даних або іншому сховищі.
Вам слід скористатися опцією storage
проміжного обробника сесії, щоб підключити його до вашого сховища даних. Можливо, для 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>;
// Створюємо бота та реєструємо проміжний обробник сесії.
const bot = new Bot<MyContext>("");
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
22
23
24
25
26
27
const { Bot, session } = require("grammy");
const { freeStorage } = require("@grammyjs/storage-free");
// Створюємо бота та реєструємо проміжний обробник сесії.
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.27.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>;
// Створюємо бота та реєструємо проміжний обробник сесії.
const bot = new Bot<MyContext>("");
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
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
, а ці значення були завантажені з різних сховищ даних, і вони також будуть записані назад в різні сховища даних. Звичайно, ви також можете використовувати одне і те ж сховище з різною конфігурацією.
Також можна використовувати різні ключі сесії для кожного фрагмента. Отже, ви можете зберігати частину даних для кожного чату, а частину — для кожного користувача.
Якщо ви використовуєте плагін для конкурентності (runner), переконайтеся, що ви правильно налаштували
sequentialize
, повернувши з функції всі ключі сесії як обмеження.
Ви можете скористатися цією можливістю, передавши type:
до конфігурації сесії. У свою чергу, вам потрібно буде налаштувати кожен фрагмент за допомогою їх власної конфігурації.
bot.use(
session({
type: "multi",
foo: {
// це також типове значення
storage: new MemorySessionStorage(),
initial: () => undefined,
getSessionKey: (ctx) => ctx.chat?.id.toString(),
},
bar: {
initial: () => ({ prop: 0 }),
storage: freeStorage(bot.token),
},
baz: {},
}),
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Зверніть увагу, що ви повинні додати запис конфігурації для кожного фрагмента, який ви хочете використовувати. Якщо ви хочете використовувати конфігурацію за замовчуванням, ви можете вказати порожній обʼєкт, як ми робимо це для baz
у наведеному вище прикладі.
Дані вашої сесії все одно будуть складатися з обʼєкта з декількома властивостями. Ось чому ваш розширювач для контексту не змінюється. У наведеному вище прикладі ось такий інтерфейс може бути використаний для налаштування обʼєкта контексту:
interface SessionData {
foo?: string;
bar: { prop: number };
baz: { width?: number; height?: number };
}
2
3
4
5
Після цього ви можете продовжувати використовувати Session
для вашого обʼєкта контексту.
Ліниві сесії
У цьому розділі описано оптимізацію продуктивності, про яку більшості людей не варто турбуватися.
Ліниві сесії — це альтернативна реалізація сесій, яка може значно зменшити трафік бази даних вашого бота, пропускаючи зайві операції читання та запису.
Припустимо, ваш бот перебуває в груповому чаті, де він не реагує на звичайні текстові повідомлення, а лише на команди. Без сесій станеться наступне:
- Оновлення з новим текстовим повідомленням надіслано вашому боту.
- Обробник не викликається, тому ніяких дій не виконується.
- Проміжний обробник завершує роботу негайно.
Щойно ви встановлюєте стандартні строгі сесії, які безпосередньо надають дані сесії обʼєкту контексту, відбудеться наступне:
- Оновлення з новим текстовим повідомленням надіслано вашому боту.
- Дані сесії завантажуються зі сховища сесії: наприклад, бази даних.
- Обробник не викликається, тому ніяких дій не виконується.
- Ідентичні дані сесії записуються назад до сховища сесії.
- Проміжний обробник завершує роботу, виконавши читання та запис до сховища даних.
Залежно від функціональності вашого бота, це може призвести до великої кількості зайвих зчитувань і записів. Ліниві сесії дозволяють пропустити 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
, доданими до вашого коду.
Тож що потрібно для того, щоб використовувати ліниві сесії замість звичайних строгих? Загалом вам потрібно зробити три дії:
- Замість
Session
використовуйтеFlavor Lazy
у вашому контексті. Вони працюють однаково, простоSession Flavor ctx
загорнуто в.session Promise
для лінивого варіанту. - Використовуйте
lazy
замістьSession session
для реєстрації вашого проміжного обробника сесії. - Завжди використовуйте
await ctx
замість.session ctx
скрізь у ваших проміжних обробниках як для читання, так і для запису. Не хвилюйтеся: ви можете очікувати (.session await
)Promise
з даними вашої сесії скільки завгодно разів, але ви завжди будете посилатися на одне і те ж значення, тому ніколи не буде повторних читань для оновлення.
Зауважте, що у лінивих сесіях ви можете призначати ctx
як обʼєкти, так й обіцянки обʼєктів. Якщо ви встановите 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
є кращим за присвоєння ctx
значення Promise
, але справа в тому, що ви можете це зробити, якщо вам з якихось причин більше подобається такий стиль.
Плагіни, які потребують сесії
Розробники плагінів, які використовують ctx
, повинні завжди дозволяти користувачам передавати Session
— отже, підтримувати обидва режими. У коді плагіна просто постійно очікуйте на 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
Зауважте, що кожен адаптер сховища може приймати параметр типу. Наприклад, для безкоштовних сесій ви можете використати free
замість Memory
. Те саме стосується і всіх інших адаптерів сховищ.
Загальні відомості про плагін
Цей плагін вбудовано в ядро grammY. Вам не потрібно нічого встановлювати, щоб використовувати його. Просто імпортуйте все з самого grammY.
До того ж документація і довідка API цього плагіна уніфіковані з ядром пакета.