Розмови (conversations
)
З легкістю створюйте потужні розмовні інтерфейси.
Вступ
Розмови дозволяють вам чекати надходження повідомлення. Використовуйте цей плагін, якщо ваш бот має кілька етапів взаємодії з користувачем.
Розмови унікальні тим, що вони представляють нову концепцію, яку ви не знайдете більше ніде у світі. Вони надають елегантне рішення, але вам потрібно буде трохи ознайомитись з тим, як вони працюють, перш ніж ви зрозумієте, що насправді робить ваш код.
Ось простий приклад, щоб ви могли погратися з плагіном, перш ніж ми перейдемо до найцікавішого.
import { Bot, type Context } from "grammy";
import {
type Conversation,
type ConversationFlavor,
conversations,
createConversation,
} from "@grammyjs/conversations";
const bot = new Bot<ConversationFlavor<Context>>(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather)
bot.use(conversations());
/** Визначаємо розмову */
async function hello(conversation: Conversation, ctx: Context) {
await ctx.reply("Привіт! Як тебе звати?");
const { message } = await conversation.waitFor("message:text");
await ctx.reply(`Ласкаво просимо до чату, ${message.text}!`);
}
bot.use(createConversation(hello));
bot.command("enter", async (ctx) => {
// Входимо в оголошену нами функцію "hello".
await ctx.conversation.enter("hello");
});
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 } = require("grammy");
const { conversations, createConversation } = require(
"@grammyjs/conversations",
);
const bot = new Bot(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather)
bot.use(conversations());
/** Визначаємо розмову */
async function hello(conversation, ctx) {
await ctx.reply("Привіт! Як тебе звати?");
const { message } = await conversation.waitFor("message:text");
await ctx.reply(`Ласкаво просимо до чату, ${message.text}!`);
}
bot.use(createConversation(hello));
bot.command("enter", async (ctx) => {
// Входимо в оголошену нами функцію "hello".
await ctx.conversation.enter("hello");
});
bot.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Bot, type Context } from "https://deno.land/x/grammy@v1.34.1/mod.ts";
import {
type Conversation,
type ConversationFlavor,
conversations,
createConversation,
} from "https://deno.land/x/grammy_conversations@v2.0.1/mod.ts";
const bot = new Bot<ConversationFlavor<Context>>(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather)
bot.use(conversations());
/** Визначаємо розмову */
async function hello(conversation: Conversation, ctx: Context) {
await ctx.reply("Привіт! Як тебе звати?");
const { message } = await conversation.waitFor("message:text");
await ctx.reply(`Ласкаво просимо до чату, ${message.text}!`);
}
bot.use(createConversation(hello));
bot.command("enter", async (ctx) => {
// Входимо в оголошену нами функцію "hello".
await ctx.conversation.enter("hello");
});
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
Коли ви увійдете у вищезгадану розмову hello
, бот надішле повідомлення, потім дочекається текстового повідомлення від користувача, після чого надішле ще одне повідомлення. Після цього розмова завершиться.
Тепер перейдемо до найцікавішого.
Як працюють розмови
Погляньте на наступний приклад звичайної обробки повідомлень.
bot.on("message", async (ctx) => {
// обробляємо єдине повідомлення
});
2
3
У звичайних обробниках повідомлень ви завжди маєте лише один обʼєкт контексту.
Порівняйте це з розмовами.
async function hello(conversation: Conversation, ctx0: Context) {
const ctx1 = await conversation.wait();
const ctx2 = await conversation.wait();
// обробляємо три повідомлення
}
2
3
4
5
У цій розмові вам доступні три обʼєкти контексту!
Як і звичайні обробники, плагін розмов отримує лише один обʼєкт контексту від проміжного обробника. А тепер раптом він надає вам три обʼєкти контексту. Як таке можливо?
Функції побудови розмов виконуються не так, як звичайні функції, хоч ми і можемо запрограмувати їх саме так.
Розмови — це механізм для повторного відтворення
Функції побудови розмов виконуються не так, як звичайні функції.
Коли розмова починається, вона виконуватиметься лише до першого виклику очікування. Після цього функція переривається і далі не виконується. Плагін запамʼятовує, що було здійснено виклик очікування, і зберігає цю інформацію.
Коли надійде наступне оновлення, розмова буде виконана з самого початку. Проте цього разу жодні виклики API не виконуються, що дозволяє вашому коду працювати дуже швидко і не мати жодних ефектів. Це називається відтворенням. Як тільки знову буде досягнуто попередній виклик очікування, виконання функції відновиться у звичайному режимі.
async function hello( // |
conversation: Conversation, // |
ctx0: Context, // |
) { // |
await ctx0.reply("Привіт!"); // |
const ctx1 = await conversation.wait(); // A
await ctx1.reply("Привіт ще раз!"); //
const ctx2 = await conversation.wait(); //
await ctx2.reply("Бувай!"); //
} //
2
3
4
5
6
7
8
9
10
async function hello( // .
conversation: Conversation, // .
ctx0: Context, // .
) { // .
await ctx0.reply("Привіт!"); // .
const ctx1 = await conversation.wait(); // A
await ctx1.reply("Привіт ще раз!"); // |
const ctx2 = await conversation.wait(); // B
await ctx2.reply("Бувай!"); //
} //
2
3
4
5
6
7
8
9
10
async function hello( // .
conversation: Conversation, // .
ctx0: Context, // .
) { // .
await ctx0.reply("Привіт!"); // .
const ctx1 = await conversation.wait(); // A
await ctx1.reply("Привіт ще раз!"); // .
const ctx2 = await conversation.wait(); // B
await ctx2.reply("Бувай!"); // |
} // —
2
3
4
5
6
7
8
9
10
- При вході в розмову функція виконуватиметься до мітки
A
. - Коли надійде наступне оновлення, функція буде відтворюватися до мітки
A
, і працюватиме в звичайному режимі від міткиA
до міткиB
. - Коли надійде останнє оновлення, функція буде відтворена до мітки
B
і виконана в нормальному режимі до кінця.
Це означає, що кожен написаний вами рядок коду буде виконано декілька разів: один раз нормально, і ще багато разів під час повторних запусків. Отже, ви повинні переконатися, що ваш код поводитиметься так само під час повторів, як і під час першого виконання.
Якщо ви виконуєте будь-які виклики API через ctx
, включно з ctx
, плагін подбає про них автоматично. На відміну від цього, взаємодія з вашою власною базою даних потребує спеціальної обробки.
Це робиться як наведено нижче.
Золоте правило розмов
Тепер, коли ми знаємо
ЗОЛОТЕ ПРАВИЛО
Код, який виконується по-різному між відтвореннями, слід обгорнути у conversation
.
Ось як застосовувати його:
// ПОГАНО
const response = await accessDatabase();
// ДОБРЕ
const response = await conversation.external(() => accessDatabase());
2
3
4
Обгортання частини вашого коду за допомогою conversation
повідомляє плагіну, що ця частина коду має бути пропущена під час повторного відтворення. Значення, що повертається з обгорнутого коду, зберігається плагіном і повторно використовується під час наступних відтворень. У наведеному вище прикладі це запобігає повторному доступу до бази даних.
ВИКОРИСТОВУЙТЕ conversation
, коли ви …
- читаєте або записуєте до файлів, баз даних/сесій, мережі або глобального стану,
- викликаєте
Math
або.random() Date
,.now() - виконуєте виклики API через
bot
або інші незалежні екземпляри.api Api
.
НЕ ВИКОРИСТОВУЙТЕ conversation
, коли ви …
- викликаєте
ctx
або інші дії контексту,.reply - викликаєте
ctx
або інші методи Bot API через.api .send Message ctx
..api
Плагін розмов надає кілька зручних методів на основі conversation
. Це не тільки спрощує використання Math
і Date
, але й полегшує відлагодження, надаючи можливість приховати логи під час відтворення.
// await conversation.external(() => Math.random());
const rnd = await conversation.random();
// await conversation.external(() => Date.now());
const now = await conversation.now();
// await conversation.external(() => console.log("abc"));
await conversation.log("abc");
2
3
4
5
6
Як conversation
і conversation
можуть відновити початкові значення, коли відбувається повторне відтворення? Плагін повинен якось запамʼятати ці дані, чи не так?
Саме так.
Розмови зберігають стан
У базі даних зберігаються два види даних. Типово використовується легка база даних у памʼяті, яка базується на Map
, але ви можете легко використовувати персистентну базу даних.
- Плагін розмов зберігає всі оновлення.
- Плагін розмов зберігає всі значення, що повертаються
conversation
і результати всіх викликів API..external
Це не є проблемою, якщо ви маєте лише кілька десятків оновлень у розмові. Памʼятайте, що під час тривалого опитування кожен виклик get
також повертає до 100 оновлень.
Однак, якщо ваша розмова ніколи не завершується, ці дані будуть накопичуватися і сповільнювати роботу бота. Уникайте нескінченних циклів.
Обʼєкти контексту розмови
Коли розмова виконується, вона використовує збережені оновлення для створення нових обʼєктів контексту з нуля. Ці обʼєкти контексту відрізняються від обʼєктів контексту, що використовуються в навколишніх проміжних обробниках. Для коду на TypeScript це також означає, що вам тепер потрібно мати два розширювача обʼєктів контексту.
- Зовнішні обʼєкти контексту — це обʼєкти контексту, які ваш бот використовує у проміжних обробниках. Вони надають вам доступ до
ctx
. Для TypeScript вони принаймні матимуть встановлений.conversation .enter Conversation
. Зовнішні обʼєкти контексту також матимуть інші властивості, визначені плагінами, які ви встановили за допомогоюFlavor bot
..use - Внутрішні обʼєкти контексту (також звані обʼєктами контексту розмов) — це обʼєкти контексту, створені плагіном розмов. Вони ніколи не мають доступу до
ctx
, і за замовчуванням вони також не мають доступу до жодного плагіна. Якщо ви хочете мати власні властивості для внутрішніх обʼєктів контексту, прогорніть вниз..conversation .enter
Ви маєте передати як зовнішній, так і внутрішній типи контексту до розмови. Відтак, налаштування TypeScript зазвичай виглядає ось так:
import { Bot, type Context } from "grammy";
import {
type Conversation,
type ConversationFlavor,
} from "@grammyjs/conversations";
// Зовнішні обʼєкти контексту (містять всі плагіни проміжних обробників).
type MyContext = ConversationFlavor<Context>;
// Внутрішні обʼєкти контексту (містять всі плагіни розмов).
type MyConversationContext = Context;
// Використовуйте зовнішній тип контексту для вашого бота.
const bot = new Bot<MyContext>(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather)
// Використовуйте як зовнішній, так і внутрішній тип для розмови.
type MyConversation = Conversation<MyContext, MyConversationContext>;
// Визначте розмову.
async function example(
conversation: MyConversation,
ctx0: MyConversationContext,
) {
// Усі обʼєкти контексту всередині розмови
// мають тип `MyConversationContext`.
const ctx1 = await conversation.wait();
// До обʼєкту зовнішнього контексту можна отримати доступ
// через `conversation.external` і він буде виведений як
// тип `MyContext`.
const session = await conversation.external((ctx) => ctx.session);
}
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
import { Bot, type Context } from "https://deno.land/x/grammy@v1.34.1/mod.ts";
import {
type Conversation,
type ConversationFlavor,
} from "https://deno.land/x/grammy_conversations@v2.0.1/mod.ts";
// Зовнішні обʼєкти контексту (містять всі плагіни проміжних обробників).
type MyContext = ConversationFlavor<Context>;
// Внутрішні обʼєкти контексту (містять всі плагіни розмов).
type MyConversationContext = Context;
// Використовуйте зовнішній тип контексту для вашого бота.
const bot = new Bot<MyContext>(""); // <-- Помістіть токен свого бота між "" (https://t.me/BotFather)
// Використовуйте як зовнішній, так і внутрішній тип для розмови.
type MyConversation = Conversation<MyContext, MyConversationContext>;
// Визначте розмову.
async function example(
conversation: MyConversation,
ctx0: MyConversationContext,
) {
// Усі обʼєкти контексту всередині розмови
// мають тип `MyConversationContext`.
const ctx1 = await conversation.wait();
// До обʼєкту зовнішнього контексту можна отримати доступ
// через `conversation.external` і він буде виведений як
// тип `MyContext`.
const session = await conversation.external((ctx) => ctx.session);
}
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
У наведеному вище прикладі у розмові не встановлено жодного плагіна. Щойно ви почнете встановлювати їх, визначення
My
більше не буде голим типомConversation Context Context
.
Звісно, якщо у вас є декілька розмов і ви хочете, щоб типи контексту відрізнялися між ними, ви можете визначити декілька типів контексту розмови.
Вітаємо! Якщо ви зрозуміли все вищесказане, то найскладніше вже позаду. Решта сторінки присвячена різноманітним можливостям, які надає цей плагін.
Вхід до розмов
До розмов можна увійти зі звичайного обробника.
Типово, розмова має ту ж назву, що і назва функції. За бажанням ви можете перейменувати її під час встановлення у боті.
За бажанням ви можете передавати аргументи до розмови. Зверніть увагу, що аргументи будуть збережені у вигляді JSON-рядка, тому вам потрібно переконатися, що їх можна безпечно передати в JSON
.
До розмов також можна входити з інших розмов за допомогою звичайного виклику функції JavaScript. У цьому випадку вони отримують доступ до потенційного значення, що повертається викликаною розмовою. Це недоступно, коли ви входите до розмови з проміжного обробника.
/**
* Повертає відповідь на питання про життя, всесвіт і все інше.
* Це значення доступне лише тоді, коли розмова
* викликається з іншої розмови.
*/
async function convo(conversation: Conversation, ctx: Context) {
await ctx.reply("Обчислення відповіді");
return 42;
}
/** Приймає два аргументи, які можна серіалізувати у форматі JSON */
async function args(
conversation: Conversation,
ctx: Context,
answer: number,
config: { text: string },
) {
const truth = await convo(conversation, ctx);
if (answer === truth) {
await ctx.reply(config.text);
}
}
bot.use(createConversation(convo, "new-name"));
bot.use(createConversation(args));
bot.command("enter", async (ctx) => {
await ctx.conversation.enter("new-name");
});
bot.command("enter_with_arguments", async (ctx) => {
await ctx.conversation.enter("args", 42, { text: "foo" });
});
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
/**
* Повертає відповідь на питання про життя, всесвіт і все інше.
* Це значення доступне лише тоді, коли розмова
* викликається з іншої розмови.
*/
async function convo(conversation, ctx) {
await ctx.reply("Computing answer");
return 42;
}
/** Приймає два аргументи, які можна серіалізувати у форматі JSON */
async function args(conversation, ctx, answer, config) {
const truth = await convo(conversation, ctx);
if (answer === truth) {
await ctx.reply(config.text);
}
}
bot.use(createConversation(convo, "new-name"));
bot.use(createConversation(args));
bot.command("enter", async (ctx) => {
await ctx.conversation.enter("new-name");
});
bot.command("enter_with_arguments", async (ctx) => {
await ctx.conversation.enter("args", 42, { text: "foo" });
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Відсутність безпеки типів для аргументів
Перевірте, чи ви використовуєте правильні анотації типів для аргументів вашої розмови, і чи передали ви їй відповідні аргументи у виклику enter
. Плагін не може перевіряти типи, крім conversation
та ctx
.
Памʼятайте, що порядок проміжних обробників має значення. Ви можете входити до тих розмов, які було встановлено перед обробником, що викликає enter
.
Очікування на оновлення
Найпростіший тип виклику очікування просто чекає на будь-яке оновлення.
const ctx = await conversation.wait();
Він просто повертає обʼєкт контексту. Всі інші виклики очікування базуються саме на ньому.
Відфільтровані виклики очікування
Якщо ви хочете дочекатися певного типу оновлень, ви можете використовувати виклик очікування з фільтрацією.
// Відповідає запиту фільтрування, як у `bot.on`.
const message = await conversation.waitFor("message");
// Чекаємо на текст, як у випадку з `bot.hears`.
const hears = await conversation.waitForHears(/regex/);
// Чекаємо на команду, як у випадку з `bot.command`.
const start = await conversation.waitForCommand("start");
// тощо
2
3
4
5
6
7
Перегляньте довідку API, щоб побачити [всі доступні способи фільтрації викликів очікування] (/ref/conversations/conversation#wait).
Виклики очікування з фільтрацією гарантовано повертатимуть лише ті оновлення, які відповідають відповідному фільтру. Якщо бот отримає оновлення, яке не відповідає фільтру, воно буде відкинуто. Ви можете передати функцію зворотного виклику, яка буде викликана в цьому випадку.
const message = await conversation.waitFor(":photo", {
otherwise: (ctx) => ctx.reply("Будь ласка, надішліть фото!"),
});
2
3
Усі виклики очікування з фільтрацією можна обʼєднати в ланцюжок для фільтрації за кількома параметрами одночасно.
// Чекаємо на фото з конкретним підписом.
let photoWithCaption = await conversation.waitFor(":photo")
.andForHears("XY");
// Для кожного випадку використовуємо окрему функцію для незадовільних оновлень:
photoWithCaption = await conversation
.waitFor(":photo", { otherwise: (ctx) => ctx.reply("Не фото") })
.andForHears("XY", { otherwise: (ctx) => ctx.reply("Не той підпис") });
2
3
4
5
6
7
Якщо в одному з ланцюжкових викликів очікування вказати лише otherwise
, то він буде викликаний лише тоді, коли цей конкретний фільтр відкине оновлення.
Перевірка обʼєктів контексту
Дуже поширеною практикою є деструктуризація отриманих обʼєктів контексту. Після цього можна виконувати подальші перевірки отриманих даних.
const { message } = await conversation.waitFor("message");
if (message.photo) {
// Обробляємо повідомлення з фото
}
2
3
4
Розмови також є ідеальним місцем для використання has
-перевірок.
Вихід з розмов
Найпростіший спосіб завершити розмову — це вийти (return
) з неї. Викидання помилки також завершує розмову.
Якщо цього недостатньо, ви можете зупинити розмову вручну в будь-який момент.
async function convo(conversation: Conversation, ctx: Context) {
// Усі гілки завершають розмову:
if (ctx.message?.text === "return") {
return;
} else if (ctx.message?.text === "error") {
throw new Error("boom");
} else {
await conversation.halt(); // ніколи не повертає значення
}
}
2
3
4
5
6
7
8
9
10
Ви також можете завершити розмову у проміжному обробнику.
bot.use(conversations());
bot.command("clean", async (ctx) => {
await ctx.conversation.exit("convo");
});
2
3
4
Ви можете зробити це ще до того, як цільова розмова буде встановлена у вашій системі проміжних обробників. Достатньо мати встановленим сам плагін розмов.
Це просто JavaScript
Якщо відкинути побічні ефекти, то розмови — це звичайні функції JavaScript. Вони можуть виконуватися дивним чином, але при розробці бота про це зазвичай можна забути. Весь звичайний синтаксис JavaScript буде працювати.
Більшість речей у цьому розділі очевидні, якщо ви використовували розмови протягом деякого часу. Однак, якщо ви новачок, деякі з цих речей можуть вас здивувати.
Змінні, розгалуження та цикли
Ви можете використовувати звичайні змінні для зберігання стану між оновленнями. Ви можете використовувати розгалуження за допомогою if
або switch
. Цикли for
і while
також працюють.
await ctx.reply("Надішліть мені свої улюблені числа, відокремлені комами!");
const { message } = await conversation.waitFor("message:text");
const numbers = message.text.split(",");
let sum = 0;
for (const str of numbers) {
const n = parseInt(str.trim(), 10);
if (!isNaN(n)) {
sum += n;
}
}
await ctx.reply("Сума цих чисел становить: " + sum);
2
3
4
5
6
7
8
9
10
11
Це просто JavaScript
Функції та рекурсія
Ви можете розбити розмову на кілька функцій. Вони можуть викликати одна одну і навіть застосовувати рекурсію. Насправді, плагін навіть не знає, що ви використовували окремі функції.
Ось той самий код, що і вище, перероблений з використанням функцій.
/** Розмова для складання чисел */
async function sumConvo(conversation: Conversation, ctx: Context) {
await ctx.reply("Надішліть мені свої улюблені числа, відокремлені комами!");
const { message } = await conversation.waitFor("message:text");
const numbers = message.text.split(",");
await ctx.reply("Сума цих чисел становить: " + sumStrings(numbers));
}
/** Перетворює всі задані рядки у числа та складає їх */
function sumStrings(numbers: string[]): number {
let sum = 0;
for (const str of numbers) {
const n = parseInt(str.trim(), 10);
if (!isNaN(n)) {
sum += n;
}
}
return sum;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** Розмова для складання чисел */
async function sumConvo(conversation, ctx) {
await ctx.reply("Надішліть мені свої улюблені числа, відокремлені комами!");
const { message } = await conversation.waitFor("message:text");
const numbers = message.text.split(",");
await ctx.reply("Сума цих чисел становить: " + sumStrings(numbers));
}
/** Перетворює всі задані рядки у числа та складає їх */
function sumStrings(numbers) {
let sum = 0;
for (const str of numbers) {
const n = parseInt(str.trim(), 10);
if (!isNaN(n)) {
sum += n;
}
}
return sum;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Це просто JavaScript
Модулі та класи
У JavaScript є функції вищого порядку, класи та інші способи структурування коду в модулі. Звісно, всі вони можуть бути перетворені на розмови.
Отже, ось наведений вище код ще раз перероблений, але вже в модуль за допомогою простої інʼєкції залежностей.
/**
* Модуль, який може запитувати у користувача числа, і який
* надає спосіб складання чисел, надісланих користувачем.
*
* Потребує передачі дескриптора розмови.
*/
function sumModule(conversation: Conversation) {
/** Перетворює всі задані рядки у числа та складає їх */
function sumStrings(numbers) {
let sum = 0;
for (const str of numbers) {
const n = parseInt(str.trim(), 10);
if (!isNaN(n)) {
sum += n;
}
}
return sum;
}
/** Запитує у користувача числа */
async function askForNumbers(ctx: Context) {
await ctx.reply("Надішліть мені свої улюблені числа, відокремлені комами!");
}
/** Чекає, поки користувач надішле числа, та надсилає їхню суму */
async function sumUserNumbers() {
const ctx = await conversation.waitFor(":text");
const sum = sumStrings(ctx.msg.text);
await ctx.reply("Сума цих чисел становить: " + sum);
}
return { askForNumbers, sumUserNumbers };
}
/** Розмова для складання чисел */
async function sumConvo(conversation: Conversation, ctx: Context) {
const mod = sumModule(conversation);
await mod.askForNumbers(ctx);
await mod.sumUserNumbers();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Модуль, який може запитувати у користувача числа, і який
* надає спосіб складання чисел, надісланих користувачем.
*
* Потребує передачі дескриптора розмови.
*/
function sumModule(conversation: Conversation) {
/** Перетворює всі задані рядки у числа та складає їх */
function sumStrings(numbers) {
let sum = 0;
for (const str of numbers) {
const n = parseInt(str.trim(), 10);
if (!isNaN(n)) {
sum += n;
}
}
return sum;
}
/** Запитує у користувача числа */
async function askForNumbers(ctx: Context) {
await ctx.reply("Надішліть мені свої улюблені числа, відокремлені комами!");
}
/** Чекає, поки користувач надішле числа, та надсилає їхню суму */
async function sumUserNumbers() {
const ctx = await conversation.waitFor(":text");
const sum = sumStrings(ctx.msg.text);
await ctx.reply("Сума цих чисел становить: " + sum);
}
return { askForNumbers, sumUserNumbers };
}
/** Розмова для складання чисел */
async function sumConvo(conversation: Conversation, ctx: Context) {
const mod = sumModule(conversation);
await mod.askForNumbers(ctx);
await mod.sumUserNumbers();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Це явно перебір для такого простого завдання, як складання кількох чисел. Однак це демонструє ширшу ідею.
Ви вже здогадалися: Це просто JavaScript.
Персистентні розмови
Типово, всі дані, що зберігаються плагіном розмов, зберігаються в памʼяті. Це означає, що коли ваш процес завершується, всі розмови будуть видалені і їх потрібно буде перезапустити.
Якщо ви хочете зберегти дані після перезавантажень сервера, вам потрібно підключити плагін розмов до бази даних. Ми створили багато різних адаптерів сховищ, щоб спростити цю задачу. Це ті самі адаптери, які використовує плагін сесій.
Припустимо, ви хочете зберігати дані на диску у каталозі з назвою convo
. Це означає, що вам потрібен File
.
import { FileAdapter } from "@grammyjs/storage-file";
bot.use(conversations({
storage: new FileAdapter({ dirName: "convo-data" }),
}));
2
3
4
5
import { FileAdapter } from "https://deno.land/x/grammy_storages@v2.4.2/file/src/mod.ts";
bot.use(conversations({
storage: new FileAdapter({ dirName: "convo-data" }),
}));
2
3
4
5
Готово!
Ви можете використовувати будь-який адаптер сховища, який може зберігати дані типу Versioned
з Conversation
. Обидва типи можна імпортувати з плагіна розмов. Інакше кажучи, якщо ви хочете витягти сховище у змінну, ви можете використати таку анотацію типу.
const storage = new FileAdapter<VersionedState<ConversationData>>({
dirName: "convo-data",
});
2
3
Аналогічні типи можна використовувати з будь-якими іншими адаптерами сховищ.
Версіонування даних
Якщо ви збережете стан розмови в базі даних, а потім оновите вихідний код, виникне невідповідність між збереженими даними і функцією побудови розмови. Це є різновидом пошкодження даних, що призведе до неможливості відтворення.
Ви можете запобігти цьому, вказавши версію вашого коду. Щоразу, коли ви змінюєте розмову, ви можете збільшувати версію. Тоді плагін розмов виявить невідповідність версій і автоматично оновить всі дані.
bot.use(conversations({
storage: {
type: "key",
version: 42, // може бути числом або рядком
adapter: storageAdapter,
},
}));
2
3
4
5
6
7
Якщо ви не вкажете версію, буде використано значення 0
.
Забули змінити версію? Не хвилюйтеся!
Плагін розмов вже має хороші засоби захисту, які повинні перехоплювати більшість випадків пошкодження даних. У разі виявлення, десь всередині розмови виникне помилка, яка призведе до аварійного завершення розмови. При умові, що ви не перехопите і не подавите цю помилку, розмова видалить пошкоджені дані і перезапуститься коректно.
Проте, цей захист не покриває 100% випадків, тому вам варто обовʼязково оновлювати номер версії в майбутньому.
Дані, які неможливо серіалізувати
Памʼятайте, що всі дані, повернуті з conversation
, будуть збережені. Це означає, що всі дані, повернуті з conversation
, мають підлягати серіалізації.
Якщо ви хочете повернути дані, які не можна серіалізувати, наприклад, класи або Big
, ви можете надати власний серіалізатор, щоб виправити це.
const largeNumber = await conversation.external({
// Викликаємо API, який повертає BigInt, який не можна серіалізувати в JSON.
task: () => 1000n ** 1000n,
// Перетворюємо bigint у рядок для зберігання.
beforeStore: (n) => String(n),
// Перетворюємо рядок назад у тип bigint для використання.
afterLoad: (str) => BigInt(str),
});
2
3
4
5
6
7
8
Якщо ви хочете викинути помилку з функції, ви можете вказати додаткові функції серіалізації обʼєктів помилок. Перевірте External
у довіднику API.
Ключі сховища
Типово, дані розмов зберігаються для кожного чату. Це відповідає роботі плагіна сесій.
Отже, розмова не може обробляти оновлення з декількох чатів. За бажанням, ви можете визначити власну функцію ключа зберігання. Як і у випадку з сесіями, не рекомендується використовувати цю опцію у безсерверних середовищах через потенційні стани гонитви.
Також, як і у випадку з сесіями, ви можете зберігати дані розмов у певному просторі імен за допомогою параметра prefix
. Це особливо корисно, якщо ви хочете використовувати один і той самий адаптер для зберігання даних сесій і даних розмов. Зберігання даних у просторах імен запобігатиме їхньому змішуванню.
Ви можете вказати обидві опції ось так.
bot.use(conversations({
storage: {
type: "key",
adapter: storageAdapter,
getStorageKey: (ctx) => ctx.from?.id.toString(),
prefix: "convo-",
},
}));
2
3
4
5
6
7
8
Якщо до розмови увійшов користувач з ідентифікатором 424242
, ключем зберігання буде convo
.
Ознайомтеся з довідкою API для Conversation
, щоб дізнатися більше про зберігання даних за допомогою плагіна розмов. Серед іншого, там пояснюється, як зберігати дані взагалі без функції ключа зберігання, використовуючи type:
.
Використання плагінів всередині розмов
Зауважте, що обʼєкти контексту всередині розмов не залежать від обʼєктів контексту у навколишніх проміжномих обробниках. Це означає, що вони не будуть мати встановлених плагінів, навіть якщо плагіни встановлені у вашому боті.
На щастя, усі плагіни grammY окрім сесій сумісні з розмовами. Наприклад, ось як ви можете встановити плагін гідратації для розмови.
// Встановлюємо ззовні лише плагін розмов.
type MyContext = ConversationFlavor<Context>;
// Встановлюємо всередині лише плагін гідратації.
type MyConversationContext = HydrateFlavor<Context>;
bot.use(conversations());
// Передаємо зовнішній та внутрішній обʼєкт контексту.
type MyConversation = Conversation<MyContext, MyConversationContext>;
async function convo(conversation: MyConversation, ctx: MyConversationContext) {
// Плагін гідратації встановлений на `ctx`.
const other = await conversation.wait();
// Плагін гідратації також встановлений на контексті `other`.
}
bot.use(createConversation(convo, { plugins: [hydrate()] }));
bot.command("enter", async (ctx) => {
// Плагін гідратації НЕ встановлений на `ctx` тут.
await ctx.conversation.enter("convo");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bot.use(conversations());
async function convo(conversation, ctx) {
// Плагін гідратації встановлений на `ctx`.
const other = await conversation.wait();
// Плагін гідратації також встановлений на контексті `other`.
}
bot.use(createConversation(convo, { plugins: [hydrate()] }));
bot.command("enter", async (ctx) => {
// Плагін гідратації НЕ встановлений на `ctx` тут.
await ctx.conversation.enter("convo");
});
2
3
4
5
6
7
8
9
10
11
12
13
У звичайному проміжному обробнику плагіни виконують певний код на поточному обʼєкті контексту, потім викликають next
, щоб дочекатися наступного проміжного обробника, а потім знову виконують певний код.
Розмови не є проміжними обробниками, і у цьому контексті плагіни працюватимуть дещо інакше. Коли розмова створює обʼєкт контексту, він буде переданий плагінам, які оброблять його у звичайному режимі. Для плагінів це виглядає так, ніби встановлені лише плагіни і не існує жодних наступних обробників. Після обробки всіма плагінами обʼєкт контексту стає доступним для розмови.
У підсумку, будь-яка робота з очищення, виконана плагінами, виконується до того, як буде запущено функцію побудови розмови. З цим добре працюють усі плагіни, окрім сесій. Якщо ви хочете використовувати сесії, прогорність вниз.
Типові плагіни
Якщо у вас багато розмов, які потребують однакового набору плагінів, ви можете визначити типові плагіни. Тепер вам більше не потрібно передавати hydrate
до create
.
// TypeScript потребує допомоги з двома типами контексту,
// тому вам часто доведеться вказувати їх для використання плагінів.
bot.use(conversations<MyContext, MyConversationContext>({
plugins: [hydrate()],
}));
// У цій розмові буде встановлено плагін гідратації.
bot.use(createConversation(convo));
2
3
4
5
6
7
bot.use(conversations({
plugins: [hydrate()],
}));
// У цій розмові буде встановлено плагін гідратації.
bot.use(createConversation(convo));
2
3
4
5
Переконайтеся, що ви встановили розширювачі контексту всіх типових плагінів на внутрішні типи контексту всіх розмов.
Використання плагінів-перетворювачі у розмовах
Якщо ви встановлюєте плагін через bot
, ви не можете передати його безпосередньо до масиву plugins
. Замість цього ви повинні встановити його в екземпляр Api
кожного обʼєкту контексту. Це легко зробити зсередини звичайного проміжного обробника плагіна.
bot.use(createConversation(convo, {
plugins: [async (ctx, next) => {
ctx.api.config.use(transformer);
await next();
}],
}));
2
3
4
5
6
Замініть transformer
на будь-який плагін, який ви хочете встановити. Ви можете встановити декілька перетворювачів в одному виклику ctx
.
Доступ до сесій всередині розмов
Через те, як плагіни працюють всередині розмов, плагін сесії не може бути встановлений всередині розмови таким самим чином, як інші плагіни. Ви не можете передати його до масиву plugins
, оскільки він:
- Зчитає дані.
- Викличе
next
, який негайно завершить виконання. - Запише назад ті самі дані.
- Передасть обʼєкт контекст розмові.
Зверніть увагу на те, що сесія зберігається перед тим, як ви її зміните. Це означає, що всі зміни даних сесії буде втрачено.
Замість цього ви можете використовувати conversation
для отримання доступу до зовнішнього обʼєкта контексту. Саме в ньому встановлено плагін сесії.
// Зчитуємо дані сесії всередині розмови.
const session = await conversation.external((ctx) => ctx.session);
// Змінюємо дані сесії всередині розмови.
session.count += 1;
// Зберігаємо дані сесії всередині розмови.
await conversation.external((ctx) => {
ctx.session = session;
});
2
3
4
5
6
7
8
9
10
У певному сенсі, використання плагіна сесій можна вважати виконанням побічних ефектів. Зрештою, сесії отримують доступ до бази даних. Враховуючи, що ми повинні дотримуватися золотого правила, цілком логічно, що доступ до сесії має бути загорнутий у conversation
.
Розмовні меню
Ви можете визначити меню за допомогою плагіна меню поза межами розмови, а потім передати його до масиву plugins
, як і будь
Однак це означає, що меню не матиме доступу до дескриптора розмови conversation
у своїх обробниках кнопок. Отже, ви не можете чекати на оновлення зсередини меню.
В ідеалі, після натискання кнопки має бути можливість дочекатися повідомлення від користувача, а потім виконати навігацію по меню, коли користувач відповість. Це можна зробити за допомогою conversation
. Він дозволяє визначати розмовні меню.
let email = "";
const emailMenu = conversation.menu()
.text("Отримати поточний email", (ctx) => ctx.reply(email || "порожньо"))
.text(() => email ? "Змінити email" : "Встановити email", async (ctx) => {
await ctx.reply("Який ваш email?");
const response = await conversation.waitFor(":text");
email = response.msg.text;
await ctx.reply(`Ваш email: ${email}!`);
ctx.menu.update();
})
.row()
.url("Довідка", "https://grammy.dev");
const otherMenu = conversation.menu()
.submenu("Перейти до меню emailʼів", emailMenu, async (ctx) => {
await ctx.reply("Навігування");
});
await ctx.reply("Ось ваше меню", {
reply_markup: otherMenu,
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
conversation
повертає меню, яке можна створити, додаючи кнопки так само, як це робить плагін меню. Насправді, якщо ви подивитеся на Conversation
у довіднику API, ви побачите, що він дуже схожий на Menu
з плагіна меню.
Розмовні меню залишаються активними лише доти, доки активна розмова. Перед виходом з розмови вам необхідно викликати ctx
для всіх меню.
Якщо ви хочете запобігти завершенню розмови, ви можете просто використати наступний фрагмент коду наприкінці розмови. Утім, майте на увазі, що це погана ідея — залишати розмову жити вічно.
// Очікувати вічно.
await conversation.waitUntil(() => false, {
otherwise: (ctx) =>
ctx.reply("Будь ласка, скористайтеся наведеним вище меню!"),
});
2
3
4
5
Нарешті, зверніть увагу, що розмовні меню гарантовано ніколи не перетинаються із зовнішніми меню. Це означає, що зовнішнє меню ніколи не буде обробляти оновлення меню всередині розмови, і навпаки.
Сумісність з плагіном меню
Коли ви визначаєте меню поза розмовою і використовуєте його для входу в розмову, ви можете визначити розмовне меню, яке буде діяти доти, доки розмова активна. Коли розмова завершиться, зовнішнє меню відновить керування.
Спершу ви маєте надати однаковий ідентифікатор обом меню.
// Ззовні розмови (плагін меню):
const menu = new Menu("my-menu");
// Всередині розмови (розмовне меню):
const menu = conversation.menu("my-menu");
2
3
4
Для того, щоб це працювало, ви повинні переконатися, що обидва меню мають однакову структуру, коли ви передаєте керування у розмову або з розмови. Інакше при натисканні кнопки меню буде визначено як застаріле, і обробник кнопки не буде викликано.
Структура базується на двох факторах:
- Форма меню: кількість рядків або кількість кнопок у кожному рядку.
- Напис на кнопці.
Рекомендується спочатку відредагувати меню до вигляду, який відповідає потребам розмови, щойно ви в неї входите. Після цього розмова може визначити відповідне меню, яке відразу стане активним.
Аналогічно, якщо розмова залишає після себе якісь меню (не закриваючи їх), зовнішні меню зможуть перейняти контроль над ними. Знову ж таки, структура меню повинна збігатися.
Приклад такої сумісності можна знайти у репозиторії прикладів ботів.
Розмовні форми
Часто розмови використовуються для побудови форм в інтерфейсі чату.
Усі виклики очікування повертають обʼєкти контексту. Однак, коли ви чекаєте на текстове повідомлення, ви можете захотіти отримати лише текст повідомлення і не взаємодіяти з рештою елементів обʼєкта контексту.
Форми розмов дають вам можливість поєднати валідацію оновлень з отриманням даних з обʼєкта контексту. Це схоже на поле у формі. Розглянемо наступний приклад.
await ctx.reply("Будь ласка, надішліть мені фото, щоб я зменшив його розмір!");
const photo = await conversation.form.photo();
await ctx.reply("Якою має бути нова ширина фото?");
const width = await conversation.form.int();
await ctx.reply("Якою має бути нова висота фото?");
const height = await conversation.form.int();
await ctx.reply(`Зменшення розміру фото до ${width}x${height} ...`);
const scaled = await scaleImage(photo, width, height);
await ctx.replyWithPhoto(scaled);
2
3
4
5
6
7
8
9
Існує набагато більше доступних полів форми. Перегляньте Conversation
у довіднику API.
Всі поля форми приймають функцію otherwise
, яка буде виконана, коли буде отримано невідповідне оновлення. Крім того, всі вони приймають функцію action
, яка буде виконана, коли поле форми буде заповнено правильно.
// Чекаємо на основну операцію обчислення.
const op = await conversation.form.select(["+", "-", "*", "/"], {
action: (ctx) => ctx.deleteMessage(),
otherwise: (ctx) => ctx.reply("Очікується +, -, *, або /!"),
});
2
3
4
5
Розмовні форми навіть дозволяють створювати власні поля за допомогою conversation
.
Тайм-аути очікування
Кожного разу, коли ви чекаєте на оновлення, ви можете передати значення тайм-ауту.
// Чекаємо лише одну годину, перш ніж вийти з розмови.
const oneHourInMilliseconds = 60 * 60 * 1000;
await conversation.wait({ maxMilliseconds: oneHourInMilliseconds });
2
3
Коли виконується виклик очікування, викликається conversation
.
Як тільки надходить наступне оновлення, знову викликається conversation
. Якщо отримання оновлення зайняло більше max
, розмову буде перервано, а оновлення буде повернуто системі проміжних обробників. Отже, буде запущено будь-який наступне проміжний обробник.
Це створить враження, що на момент отримання оновлення розмова вже не була активною.
Зверніть увагу, що це не призведе до запуску коду через точно вказаний час. Натомість код буде запущено, як тільки надійде наступне оновлення.
Ви можете вказати типове значення тайм-ауту для всіх викликів очікування всередині розмови.
// Завжди чекаємо лише одну годину.
const oneHourInMilliseconds = 60 * 60 * 1000;
bot.use(createConversation(convo, {
maxMillisecondsToWait: oneHourInMilliseconds,
}));
2
3
4
5
Передача значення безпосередньо виклику очікування замінить типове значення.
Події входу та виходу
Ви можете вказати функцію зворотного виклику, яка буде виконана щоразу, коли ви входите до розмови. Аналогічно, ви можете вказати функцію зворотного виклику, яка буде виконана при завершенні розмови.
bot.use(conversations({
onEnter(id, ctx) {
// `id` розмови, до якої увійшли.
},
onExit(id, ctx) {
// `id` розмови, з якої вийшли.
},
}));
2
3
4
5
6
7
8
Кожна функція зворотного виклику отримує два значення. Перше значення — це ідентифікатор розмови, до якої увійшли або з якої вийшли. Друге значення — це поточний обʼєкт контексту навколишнього проміжного обробника.
Зауважте, що зворотні виклики викликаються лише при вході або виході з розмови через ctx
. Зворотний виклик on
також викликається, коли розмова завершується за допомогою conversation
або коли вичерпується час очікування.
Одночасні виклики очікування
Ви можете використовувати комбінований Promise
для одночасного очікування декількох подій. Коли надійде нове оновлення, буде виконано лише перший відповідний виклик очікування.
await ctx.reply("Надішліть фото та підпис!");
const [textContext, photoContext] = await Promise.all([
conversation.waitFor(":text"),
conversation.waitFor(":photo"),
]);
await ctx.replyWithPhoto(photoContext.msg.photo.at(-1).file_id, {
caption: textContext.msg.text,
});
2
3
4
5
6
7
8
У наведеному вище прикладі не має значення, що користувач відправить першим — фото чи текст. Обидва Promise
и будуть виконані в тому порядку, в якому користувач відправить два повідомлення, на які очікує код. Promise
працює у звичайному режимі; він виконається лише тоді, коли виконаються всі передані Promise
и.
Це також може бути використано для очікування не повʼязаних між собою подій. Наприклад, ось як ви можете встановити глобальний обробник завершення розмови всередині неї.
conversation.waitForCommand("exit") // немає `await`!
.then(() => conversation.halt());
2
Щойно розмова завершиться будь
async function convo(conversation: Conversation, ctx: Context) {
const _promise = conversation.wait() // немає `await`!
.then(() => ctx.reply("Мене ніколи не надішлють!"));
// Розмова завершується одразу після входу.
}
2
3
4
5
6
async function convo(conversation, ctx) {
const _promise = conversation.wait() // немає `await`!
.then(() => ctx.reply("Мене ніколи не надішлють!"));
// Розмова завершується одразу після входу.
}
2
3
4
5
6
Коли декілька викликів очікування надходять одночасно, плагін розмов буде відстежувати список викликів очікування. Щойно надійде наступне оновлення, він відтворить функцію побудови розмови один раз для кожного виклику очікування, доки один з них не прийме оновлення. Лише якщо жоден з очікуваних викликів не прийме оновлення, оновлення буде відхилено.
Контрольні точки та повернення в минуле
Плагін розмов відстежує виконання ваших функцій побудови розмов.
Це дозволяє створювати контрольні точки протягом виконання. Контрольна точка містить інформацію про те, як далеко функція пройшла на даний момент. Вона може бути використана для того, щоб пізніше повернутися до цієї точки.
При цьому всі дії, виконані за цей час, не будуть скасовані. Зокрема, повернення до контрольної точки не призведе до магічного скасування надсилання будь-яких повідомлень.
const checkpoint = conversation.checkpoint();
// Пізніше:
if (ctx.hasCommand("reset")) {
await conversation.rewind(checkpoint); // ніколи не повертає результат
}
2
3
4
5
6
Контрольні точки можуть бути дуже корисними для “повернення назад”. Однак, подібно до break
та continue
у JavaScript з мітками, стрибки можуть зробити код менш читабельним. Переконайтеся, що ви не зловживаєте цією можливістю.
За лаштунками, перемотування розмови перериває виконання розмови так само, як і при виклику очікування. Потім функція відтворюється лише до того місця, де було створено контрольну точку. Перемотування розмови не виконує функції в буквальному сенсі у зворотному порядку, хоч це і виглядає саме так.
Паралельні розмови
Розмови в неповʼязаних чатах повністю незалежні і завжди можуть вестися паралельно.
Однак, у кожному чаті типово може бути лише одна активна розмова. Якщо ви спробуєте увійти до розмови, коли розмова вже активна, виклик enter
призведе до помилки.
Ви можете змінити цю поведінку, позначивши розмову як таку, що може виконуватися паралельно.
bot.use(createConversation(convo, { parallel: true }));
Це змінює дві речі.
По-перше, тепер ви можете увійти до цієї розмови, навіть якщо та сама або інша розмова вже активна. Наприклад, якщо у вас є розмови captcha
і settings
, ви можете активувати captcha
пʼять разів, а settings
— дванадцять разів, і все це в одному і тому ж чаті.
По-друге, коли розмова не приймає оновлення, оновлення більше не відхиляється. Замість цього, управління передається до системи проміжних обробників.
Всі встановлені розмови отримають можливість обробляти вхідне оновлення, доки якась з них не прийме його. Однак, лише одна розмова зможе фактично обробити оновлення.
Якщо одночасно активними є кілька різних розмов, порядок у системі проміжних обробників визначатиме, яка розмова отримає оновлення першою. Якщо одна розмова активна кілька разів, найстаріша розмова (та, до якої увійшли першою) отримає можливість обробити оновлення першою.
Це найкраще пояснити на прикладі.
async function captcha(conversation: Conversation, ctx: Context) {
const user = ctx.from!.id;
await ctx.reply(
"Ласкаво просимо до чату! Який фреймворк для розробки ботів найкращий?",
);
const answer = await conversation.waitFor(":text").andFrom(user);
if (answer.msg.text === "grammY") {
await ctx.reply("Правильно! У вас світле майбутнє!");
} else {
await ctx.banAuthor();
}
}
async function settings(conversation: Conversation, ctx: Context) {
const user = ctx.from!.id;
const main = conversation.checkpoint();
const options = ["Налаштування чату", "Довідка", "Приватність"];
await ctx.reply("Ласкаво просимо до налаштувань!", {
reply_markup: Keyboard.from(options
.map((btn) => [Keyboard.text(btn)])),
});
const option = await conversation.waitFor(":text")
.andFrom(user)
.and((ctx) => options.includes(ctx.msg.text), {
otherwise: (ctx) => ctx.reply("Будь ласка, використовуйте кнопки!"),
});
await openSettingsMenu(option, main);
}
bot.use(createConversation(captcha));
bot.use(createConversation(settings));
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
async function captcha(conversation, ctx) {
const user = ctx.from.id;
await ctx.reply(
"Ласкаво просимо до чату! Який фреймворк для розробки ботів найкращий?",
);
const answer = await conversation.waitFor(":text").andFrom(user);
if (answer.msg.text === "grammY") {
await ctx.reply("Правильно! У вас світле майбутнє!");
} else {
await ctx.banAuthor();
}
}
async function settings(conversation, ctx) {
const user = ctx.from.id;
const main = conversation.checkpoint();
const options = ["Налаштування чату", "Довідка", "Приватність"];
await ctx.reply("Ласкаво просимо до налаштувань!", {
reply_markup: Keyboard.from(options
.map((btn) => [Keyboard.text(btn)])),
});
const option = await conversation.waitFor(":text")
.andFrom(user)
.and((ctx) => options.includes(ctx.msg.text), {
otherwise: (ctx) => ctx.reply("Будь ласка, використовуйте кнопки!"),
});
await openSettingsMenu(option, main);
}
bot.use(createConversation(captcha));
bot.use(createConversation(settings));
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
Наведений вище код працює в групових чатах. Він надає дві розмови. Розмова captcha
використовується для того, щоб переконатися, що до чату приєднуються тільки хороші розробники (безсоромний прикол grammY, хах). Розмова settings
використовується для реалізації меню налаштувань у груповому чаті.
Зверніть увагу, що всі виклики очікування фільтруються, серед іншого, за ідентифікатором користувача.
Припустимо, що вже відбулося наступне.
- Викликано
ctx
для входу до розмови.conversation .enter("captcha") captcha
під час обробки оновлення від користувача з ідентифікаторомctx
..from .id = == 42 - Викликано
ctx
для входу до розмови.conversation .enter("settings") settings
під час обробки оновлення від користувача з ідентифікаторомctx
..from .id = == 3 - Викликано
ctx
для входу до розмови.conversation .enter("captcha") captcha
під час обробки оновлення від користувача з ідентифікаторомctx
..from .id = == 43
Це означає, що у цьому груповому чаті зараз активні три розмови: captcha
активна двічі, а settings
активна один раз.
Зауважте, що
ctx
надає різні способи для виходу з конкретних розмов, навіть якщо увімкнено паралельні розмови..conversation
Далі відбувається наступне.
- Користувач
3
надсилає повідомлення з текстом"About"
. - Приходить оновлення з текстовим повідомленням.
- Відтворюється перший екземпляр розмови
captcha
. - Виклик
wait
приймає оновлення, але доданий фільтрFor(": text") and
відхиляє оновлення.From(42) - Відтворюється другий екземпляр розмови
captcha
. - Виклик
wait
приймає оновлення, але доданий фільтрFor(": text") and
відхиляє оновлення.From(43) - Всі екземпляри
captcha
відхилили оновлення, тому управління передається системі проміжних обробників. - Відтворюється екземпляр розмови
settings
. - Виклик очікування завершується, і
option
буде містити обʼєкт контексту для оновлення текстового повідомлення. - Викликається функція
open
. Вона може надіслати користувачеві інформаційне повідомлення та відмотати розмову назад доSettings Menu main
, перезапустивши меню.
Зверніть увагу, що хоча дві розмови чекали, поки користувачі 42
і 43
завершать введення капчі, бот коректно відповів користувачеві 3
, який запустив меню налаштувань. Виклики очікування з фільтрацією можуть визначати, які оновлення є релевантними для поточної розмови. Відхилені оновлення пропускаються і можуть бути оброблені в інших розмовах.
У наведеному вище прикладі використовується груповий чат, щоб проілюструвати, як розмови можуть обробляти декілька користувачів паралельно в одному чаті. Насправді паралельні розмови працюють у всіх чатах. Це дозволяє вам чекати на різні події в чаті з одним користувачем.
Ви можете комбінувати паралельні розмови з тайм
Перевірка активних розмов
У проміжному обробнику ви можете перевірити, яка розмова наразі активна.
bot.command("stats", (ctx) => {
const convo = ctx.conversation.active("convo");
console.log(convo); // 0 або 1
const isActive = convo > 0;
console.log(isActive); // false або true
});
2
3
4
5
6
Коли ви передаєте ідентифікатор розмови до ctx
, вона поверне 1
, якщо ця розмова активна, і 0
в іншому випадку.
Якщо для розмови увімкнено паралельність, функція поверне кількість активних на даний момент екземплярів розмови.
Викличте ctx
без аргументів, щоб отримати обʼєкт, який містить ідентифікатори усіх активних розмов як ключі. Відповідні значення описують, скільки екземплярів кожної розмови є активними.
Якщо розмова captcha
активна двічі, а розмова settings
активна один раз, ctx
буде працювати ось так.
bot.command("stats", (ctx) => {
const stats = ctx.conversation.active();
console.log(stats); // { captcha: 2, settings: 1 }
});
2
3
4
Перехід з версії 1.x на 2.x
Conversations 2.0 — це повне переписування з нуля.
Незважаючи на те, що основні концепції зовнішнього вигляду API залишилися незмінними, обидві реалізації кардинально відрізняються в тому, як вони працюють під капотом. У двох словах, міграція з версії 1.x на 2.x призводить до дуже незначних змін у вашому коді, але вимагає від вас видалення всіх збережених даних. Тобто, всі розмови будуть перезапущені.
Міграція даних з версії 1.x на 2.x
Під час оновлення з версії 1.x до 2.x немає можливості зберегти поточний стан розмов.
Вам слід просто видалити відповідні дані з ваших сесій. Подумайте про використання для цього міграції сесій.
Збереження даних про поточні розмови у версії 2.x можна зробити, як описано тут.
Зміни у типах між версією 1.x та 2.x
У версії 1.x тип контексту всередині розмови був тим самим типом контексту, який використовувався у навколишньому проміжному обробнику.
Починаючи з версії 2.x, ви повинні завжди оголошувати два типи контексту — тип зовнішнього контексту і тип внутрішнього контексту. Ці типи ніколи не можуть бути однаковими, а якщо вони збігаються, то у вашому коді закралася помилка. Це повʼязано з тим, що тип зовнішнього контексту завжди повинен мати Conversation
, тоді як тип внутрішнього контексту ніколи не повинен мати його встановленим.
Крім того, тепер ви можете встановити незалежний набір плагінів для кожної розмови.
Зміни у доступі до сесії між версією 1.x та 2.x
Ви більше не можете використовувати conversation
. Замість цього ви повинні використовувати conversation
.
// Зчитуємо дані сесії.
const session = await conversation.session;
const session = await conversation.external((ctx) => ctx.session);
// Записуємо дані сесії.
conversation.session = newSession;
await conversation.external((ctx) => {
ctx.session = newSession;
});
2
3
4
5
6
7
8
9
Доступ до
ctx
був можливий у версії 1.x, але він завжди був некоректним..session ctx
більше не доступний у версії 2.x..session
Зміни у сумісності плагінів між версією 1.x та 2.x
Розмови версії 1.x були майже не сумісні з жодним плагіном. Хоча деякої сумісності можна було досягти за допомогою conversation
.
У версії 2.x цю можливість було вилучено. Замість цього ви можете передавати плагіни до масиву plugins
, як описано тут. Сесії потребують особливого використання. Сумісність меню покращилася з впровадженням розмовних меню.
Зміни у паралельних розмовах між версією 1.x та 2.x
Паралельні розмови працюють однаково для 1.x і 2.x.
Однак, ця можливість була поширеним джерелом плутанини при ненавмисному використанні. У версії 2.x вам потрібно спеціально увімкнути цю можливість, вказавши { parallel:
, як описано тут.
Єдиною суттєвою зміною у цій функціональності є те, що оновлення більше не передаються до системи проміжних обробників за замовчуванням. Замість цього, це робиться тільки тоді, коли розмова позначена як паралельна.
Зауважте, що всі методи очікування і поля форм надають параметр next
для заміни типової поведінки. Цей параметр було перейменовано з drop
у версії 1.x, а семантику прапорця було відповідно змінено.
Зміни у формах між версією 1.x та 2.x
У 1.x форми були справді зламані. Наприклад, conversation
повертала текстові повідомлення навіть для оновлень edited
старих повідомлень. Багато з цих дивацтв було виправлено у версії 2.x.
Виправлення помилок технічно не вважається зміною, що порушує роботу системи, але це все одно суттєва зміна поведінки.