Mundarija (23)
- Bu darsda
- 1. Nega bu kerak?
- 2. Sessiya
- 2.1 session() middleware
- 2.2 Xotira yoki Redis: kanon o'lchovi
- 2.3 Sessiya kimniki: getSessionKey
- 3. Dialog — holatlar mashinasi
- 3.1 Qo'lda: bosqich sessiyada
- 3.2 conversations plagini
- 4. Replay: dialog boshidan qayta o'ynaladi
- 4.1 Mexanizm
- 4.2 Replay qoidalari
- 5. Bekor qilish va timeout
- 6. Muddatni tahlil qilish
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): qaysi storage?
- 2-mashq (o'rta): timeout'li holatlar mashinasi
- 3-mashq (qiyin): replay taqlidi
- 9. Loyiha qadami: /yangi dialogi
- 10. Real ishda
- Xulosa
- Manbalar
Sessiya va ko'p qadamli dialoglar: bot nimani eslab qoladi
Qisqacha: Har update botga alohida keladi — bot "oldingi xabarda nima so'ragan edim?" ni o'zi eslamaydi. Sessiya — har chat (yoki foydalanuvchi) uchun saqlanadigan kichik obyekt. Xotirada saqlansa, bot qayta ishga tushganda yo'qoladi: kanonda hisoblagich 2 → 2 bo'ldi, Redis bilan 2 → 4. Ko'p qadamli savol-javob uchun grammY'da conversations plagini bor: dialog funksiyasi har yangi xabarda boshidan qayta o'ynaladi (replay), shuning uchun yon ta'sirlar
conversation.externalichida yoziladi.
Bu darsda
- Sessiya nima uchun kerakligini va grammY
session()qanday ishlashini ko'rasiz. - Xotira va Redis storage farqini kanon o'lchovi bilan tushuntirasiz.
getSessionKeybilan sessiyani chat yoki foydalanuvchi bo'yicha ajratasiz.- Holatlar mashinasini qo'lda yozasiz, keyin uni conversations plagini bilan solishtirasiz.
- Replay mexanizmi,
external,now(), bekor qilish va timeout'ni tushunasiz. - Loyiha botiga
/yangidialogini qo'shasiz.
Oldin bilishingiz kerak: Klaviaturalar va callback query, String, TTL va kalit nomlash (Redis), Backend'da sana va vaqt.
Oldingi darsdagi menu misolida filtr bitta global o'zgaruvchida edi — hamma foydalanuvchi uchun bitta. Bugun har foydalanuvchiga o'z holatini beramiz.
1. Nega bu kerak?
Foydalanuvchi /yangi yozdi. Bot: "Vazifa matnini yozing". Foydalanuvchi: "Non olish". Endi bot "Muddat?" deb so'rashi kerak. Keyin "ertaga" keladi.
Muammo: "ertaga" degan xabar botga alohida update bo'lib keladi. Unda "bu — non olish vazifasining muddati" degan ma'lumot yo'q. Bot qaysi savolga javob kelganini o'zi eslab qolishi kerak.
REST prinsiplari va resurslar darsida holatsizlik (statelessness) haqida gapirgansiz: har HTTP so'rov o'zi yetarli. Bot dialogi — aksincha: suhbatning bosqichi bor. Uni qayerdadir saqlash kerak.
O'xshatish: telefon orqali pizza buyurtmasi. Operator "Qanday pizza?" deb so'raydi, siz javob berasiz, u qog'ozga yozadi. Keyin "Manzil?" deydi. Qog'oz — sessiya. Operator almashsa va qog'oz yo'qolsa, siz boshidan aytishingiz kerak.
2. Sessiya
2.1 session() middleware
grammY'ning session plagini (yadroga kiradi) har update uchun ctx.session obyektini beradi: ishlovchidan oldin o'qiydi, keyin saqlaydi.
import { Bot, session } from "grammy";
bot.use(session({ initial: () => ({ soni: 0 }) }));
bot.command("bos", (ctx) => {
ctx.session.soni++;
return ctx.reply(String(ctx.session.soni));
});initial— yangi chat uchun boshlang'ich qiymat. Funksiya bo'lishi shart: har chatga yangi obyekt. Obyektning o'zini bersangiz, hamma chat bitta obyektni bo'lishadi.ctx.sessionga yozilgan o'zgarish ishlovchi tugagach storage'ga saqlanadi.
TypeScript'da ctx.session ning tipi SessionFlavor<T> bilan qo'shiladi: type Ctx = Context & SessionFlavor<{ soni: number }>.
2.2 Xotira yoki Redis: kanon o'lchovi
Sukutda sessiya xotirada (Map) saqlanadi. Kurs tajribasi shunday sinadi: botni ishga tushiradi, foydalanuvchi ikki marta /bos yozadi, bot to'xtaydi (deploy taqlidi), qayta ishga tushadi va yana ikki marta /bos. Xuddi shu Redis storage bilan:
import { RedisAdapter } from "@grammyjs/storage-redis";
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
const storage = new RedisAdapter({ instance: redis, ttl: 3600 });
bot.use(session({
initial: () => ({ soni: 0 }),
storage,
prefix: "t21:",
}));Natija (kanon, @grammyjs/storage-redis 2.6.0, ioredis 6.0.0, Redis 8.10.2):
xotira, 1-ishga tushirish: hisoblagich 2
xotira, 2-ishga tushirish: hisoblagich 2
Redis, 1-ishga tushirish: hisoblagich 2
Redis, 2-ishga tushirish: hisoblagich 4
Redis kalitlari: t21:3 | TTL: 3600- Xotira — ikkinchi ishga tushishda hisoblagich yana 0 dan boshlandi va 2 bo'ldi. Oldingi 2 yo'qoldi.
- Redis — 2 dan davom etdi va 4 bo'ldi.
- Kalit
t21:3—prefix+ chat id (3). TTL 3600 soniya — bir soat harakatsiz qolsa sessiya o'chadi (String, TTL va kalit nomlash).
Bot har deploy'da, har xatoda, har server qayta yuklanishida qayta ishga tushadi. Xotiradagi sessiya — faqat tajriba uchun. Prodakshnda storage — Redis yoki baza. grammY'da Postgres, Mongo va boshqa storage adapterlari ham bor (hujjat bo'yicha, kanonda sinalmagan).
Ikkinchi sabab — bir nechta nusxa. Bot ikki serverda ishlasa, xotira har birida alohida. Bir xabar birinchi serverga, keyingisi ikkinchisiga tushsa — sessiya "yo'qoladi". Redis ikkalasi uchun umumiy.
2.3 Sessiya kimniki: getSessionKey
grammY hujjatiga ko'ra sessiya sukutda chat bo'yicha saqlanadi. Shaxsiy chatda chat = foydalanuvchi, farqi yo'q. Guruhda esa hamma a'zo bitta sessiyani bo'lishadi.
Biz buni o'zimiz sinadik: guruhda (-100500) foydalanuvchilar 1, 2, 1 navbat bilan /bos yozdi. Ikki sozlama:
bot.use(session({
initial: () => ({ soni: 0 }),
// sukut: ctx.chat?.id.toString()
getSessionKey: (ctx) => ctx.from?.id.toString(),
}));Natija (biz ishga tushirdik, grammY 1.46.0, lokal soxta Bot API; format: foydalanuvchi → hisoblagich):
sukut (chat): 1 → 1, 2 → 2, 1 → 3
foydalanuvchi: 1 → 1, 2 → 1, 1 → 2Sukutda 2-foydalanuvchi 1-ning hisoblagichini davom ettirdi. getSessionKey foydalanuvchi bo'yicha bo'lganda — har kim o'ziniki.
Qaysi biri to'g'ri? Mavzuga bog'liq. Guruh sozlamasi (masalan, "guruh tili") — chat bo'yicha. Anketa yoki dialog — foydalanuvchi bo'yicha (yoki foydalanuvchi + chat). Aks holda guruhda ikki kishi bir vaqtda anketa to'ldirsa, javoblar aralashadi.
Tekshirib ko'ring:
initial: () => ({ soni: 0 })o'rnigaconst bosh = { soni: 0 }; ... initial: () => boshyozildi. Xotira storage bilan ikki chat/bosqilsa nima bo'ladi?
Javob
Ikkala chat bitta obyektni oladi va bitta hisoblagichni oshiradi: birinchi chat 1, ikkinchisi 2 ko'radi. initial har chaqiruvda yangi obyekt qaytarishi kerak. Redis'da bu xato ko'rinmay qolishi mumkin (har safar JSON'dan yangi obyekt o'qiladi) — shuning uchun yanada xavfli.
3. Dialog — holatlar mashinasi
3.1 Qo'lda: bosqich sessiyada
Eng oddiy dialog — sessiyada bosqich maydoni. Har xabarda bot bosqichga qarab qaror qiladi. Bu holatlar mashinasi (state machine): bir nechta holat va ular orasidagi o'tishlar.
function qadam(sessiya, matn) {
if (matn === "/bekor") {
sessiya.bosqich = null;
return "Bekor qilindi.";
}
switch (sessiya.bosqich) {
case null:
if (matn !== "/yangi") return "Buyruqlar: /yangi";
sessiya.bosqich = "matn";
return "Vazifa matni?";
case "matn":
if (matn.length < 1 || matn.length > 100) return "1–100 belgi";
sessiya.matn = matn;
sessiya.bosqich = "muddat";
return "Muddat?";
case "muddat":
sessiya.bosqich = null;
return `Qo'shildi: ${sessiya.matn} — ${matn}`;
}
}
const s = { bosqich: null };
for (const m of ["salom", "/yangi", "Non olish", "ertaga"]) {
console.log(`${m} → ${qadam(s, m)}`);
}
for (const m of ["/yangi", "/bekor", "ertaga"]) {
console.log(`${m} → ${qadam(s, m)}`);
}Konsolda:
salom → Buyruqlar: /yangi
/yangi → Vazifa matni?
Non olish → Muddat?
ertaga → Qo'shildi: Non olish — ertaga
/yangi → Vazifa matni?
/bekor → Bekor qilindi.
ertaga → Buyruqlar: /yangisessiya — Redis'dagi obyekt bo'lsa, bu dialog bot qayta ishga tushsa ham davom etadi. Telegraf'dagi scenes va wizard shu g'oyaga asoslangan: har sahna — holat, ctx.wizard.next() — keyingi qadam (hujjat bo'yicha; kanon Telegraf'ni o'rnatmadi).
Bu usulning kamchiligi: 2 qadamda ham switch o'sib boryapti. 6 qadamli anketa, har birida tekshiruv va "qayta so'rash" — kod tarqalib ketadi. Dialog mantig'i bir joyda o'qilmaydi.
stateDiagram-v2
[*] --> Bosh
Bosh --> Matn: /yangi
Matn --> Matn: noto'g'ri matn
Matn --> Muddat: to'g'ri matn
Muddat --> Muddat: tushunarsiz sana
Muddat --> Bosh: saqlandi
Matn --> Bosh: /bekor yoki timeout
Muddat --> Bosh: /bekor yoki timeoutDiagrammaga qarang: loyihadagi /yangi dialogi aynan shunday — ikki savol, har birida qayta so'rash halqasi va har joydan chiqish (/bekor, timeout).
3.2 conversations plagini
@grammyjs/conversations holatlar mashinasini oddiy async funksiya ko'rinishida yozishga imkon beradi:
import {
conversations, createConversation,
} from "@grammyjs/conversations";
bot.use(conversations());
bot.use(createConversation(async (conv, ctx) => {
await ctx.reply("Ism?");
const ism = (await conv.waitFor("message:text")).message.text;
await ctx.reply("Yosh?");
const yosh = (await conv.waitFor("message:text")).message.text;
await ctx.reply(`Saqlandi: ${ism}, ${yosh}`);
}, "anketa"));
bot.command("anketa", (ctx) => ctx.conversation.enter("anketa"));conv.waitFor("message:text") — "keyingi matnli xabarni kut". Kod chiziqli o'qiladi: savol — javob — savol — javob. Bosqich maydoni, switch yo'q.
Lekin await conv.waitFor(...) haqiqatan bir soat "kutib" turolmaydi — bot qayta ishga tushishi mumkin, javob ertaga kelishi mumkin. Plagin buni qanday qiladi?
4. Replay: dialog boshidan qayta o'ynaladi
4.1 Mexanizm
grammY hujjati tushuntiradi: dialog funksiyasi wait ga yetganda to'xtaydi va shu paytgacha nima bo'lganini storage'ga yozadi. Keyingi update kelganda funksiya boshidan qayta bajariladi, lekin bu safar oldingi wait lar saqlangan javoblarni darhol qaytaradi va API chaqiruvlari qayta yuborilmaydi. Bu replay (qayta o'ynash). Oxirgi wait ga yetgach funksiya odatdagidek davom etadi.
O'xshatish: video o'yindagi saqlash nuqtasi emas, balki o'yinni har safar boshidan, lekin tez aylantirib, oxirgi joygacha o'ynash. Ekranga hech narsa chiqmaydi, faqat oxirgi joydan boshlab ko'rsatiladi.
Natija: dialog funksiyasi bir necha marta chaqiriladi. Kurs tajribasi buni sanadi. Dialog o'rtasida (ism yozilgandan keyin) bot to'xtatildi va qayta ishga tushirildi — xotira va Redis storage bilan:
xotira: oxirgi javob "(dialogdan tashqari) 25"; dialog funksiyasi 2 marta chaqirildi (replay), external 1 marta
Redis: oxirgi javob "Saqlandi: Aziz, 25"; dialog funksiyasi 3 marta chaqirildi (replay), external 1 martaIkki qatorni solishtiring:
- Xotira. Qayta ishga tushgandan keyin dialog yo'q edi. "25" javobi dialogga emas, oddiy matn ishlovchisiga tushdi:
(dialogdan tashqari) 25. Foydalanuvchi uchun bot "yosh?" deb so'rab, javobni tushunmadi. - Redis. Dialog davom etdi:
Saqlandi: Aziz, 25. Funksiya 3 marta chaqirildi: boshida, "Aziz" kelganda (replay + davom), "25" kelganda (replay + davom). external— ikkalasida 1 marta. Bu keyingi bo'limning asosiy g'oyasi.
4.2 Replay qoidalari
Funksiya bir necha marta bajarilsa, unda har safar bir xil natija beradigan kod bo'lishi kerak. Muammoli joylar:
| Kod | Muammo replay'da | Yechim |
|---|---|---|
| bazaga yozish | har replay'da yana yoziladi | conversation.external(() => ...) |
fetch tashqi API'ga |
har replay'da yana so'rov | external |
Math.random() |
har safar boshqa qiymat | conversation.random() |
Date.now(), new Date() |
har safar boshqa vaqt | conversation.now() |
ctx.reply |
plagin o'zi boshqaradi | oddiy yozing |
conversation.external(f) — f ni birinchi marta bajaradi va natijasini saqlaydi; replay'da f chaqirilmaydi, saqlangan natija qaytadi. Kanonda external ichidagi hisoblagich 1 marta oshdi, dialog funksiyasi esa 2–3 marta chaqirildi.
Diqqat:
externalsiz bazaga yozish — replay'da takroriy yozuv. Kanon modulidamanba.yarat(...)vamanba.kim(...)ikkalasi hamexternalichida.
Tekshirib ko'ring: Dialog ichida
const id = crypto.randomUUID()yozildi va keyingi xabarda shuidbilan bazaga yozildi. Nima xato?
Javob
Replay'da randomUUID() yangi qiymat beradi. Birinchi o'tishda foydalanuvchiga bir id ko'rsatilgan bo'lsa, bazaga boshqasi yoziladi. To'g'risi: await conversation.external(() => crypto.randomUUID()) — qiymat bir marta yaratiladi va replay'da saqlangan qaytadi.
5. Bekor qilish va timeout
Foydalanuvchi dialogni yarmida tashlab ketishi mumkin. Ikki chiqish yo'li kerak:
- Bekor qilish.
/bekorbuyrug'i:ctx.conversation.exitAll()— barcha faol dialoglarni tugatadi. Dialog ichida ham/bekormatnini tekshirish kerak:waitFor("message:text")buyruqni ham matn sifatida oladi. - Timeout.
waitFor(..., { maxMilliseconds })— javob shu vaqtdan kech kelsa, dialog tugaydi. Busiz foydalanuvchi bir haftadan keyin "ertaga" yozsa, bot uni bir hafta oldingi savolga javob deb oladi.
otherwise — kutilgan update kelmasa (masalan, matn o'rniga rasm), nima qilish: otherwise: (c) => c.reply("Iltimos, matn yuboring.").
Dialog storage'i ham Redis'da bo'lishi kerak — aks holda deploy dialogni uzadi (kanondagi "xotira" qatori). Kanon modul storage ni parametr sifatida oladi.
6. Muddatni tahlil qilish
Dialogning ikkinchi savoli — muddat. Foydalanuvchi 28.10 18:00, ertaga yoki yo'q yozadi. Kanon muddatTahlil funksiyasi bularni Date, null yoki undefined (tushunmadim) ga aylantiradi. Vaqt — Toshkent (UTC+5, yozgi vaqt yo'q), hozir parametr sifatida keladi — test va replay uchun (Vaqt va tasodifni boshqarish).
Mana kanon funksiyasining JavaScript nusxasi (brauzerda ishlaydi):
function muddatTahlil(s, hozir = new Date()) {
const t = s.trim().toLowerCase();
if (["yo'q", "yoq", "-", "kerak emas"].includes(t)) return null;
const toshkentKun = new Date(hozir.getTime() + 5 * 3600_000);
if (t === "bugun" || t === "ertaga") {
return new Date(Date.UTC(
toshkentKun.getUTCFullYear(), toshkentKun.getUTCMonth(),
toshkentKun.getUTCDate() + (t === "ertaga" ? 1 : 0), 18 - 5,
));
}
const m = /^(\d{1,2})\.(\d{1,2})(?:\s+(\d{1,2}):(\d{2}))?$/.exec(t);
if (!m) return undefined;
const [, kun, oy, soat = "9", daq = "00"] = m;
const d = new Date(Date.UTC(
toshkentKun.getUTCFullYear(), Number(oy) - 1, Number(kun),
Number(soat) - 5, Number(daq),
));
if (
d.getUTCMonth() !== Number(oy) - 1 ||
Number(soat) > 23 || Number(daq) > 59
) return undefined;
if (d.getTime() < hozir.getTime()) {
d.setUTCFullYear(d.getUTCFullYear() + 1);
}
return d;
}
const hozir = new Date("2026-10-27T10:00:00Z"); // Toshkent 15:00
const iso = (x) => x instanceof Date ? x.toISOString() : String(x);
for (const s of ["28.10 18:00", "ertaga", "yo'q", "31.02",
"01.01", "25:00", "28.10 25:00"]) {
console.log(s.padEnd(12), iso(muddatTahlil(s, hozir)));
}Konsolda:
28.10 18:00 2026-10-28T13:00:00.000Z
ertaga 2026-10-28T13:00:00.000Z
yo'q null
31.02 undefined
01.01 2027-01-01T04:00:00.000Z
25:00 undefined
28.10 25:00 undefined28.10 18:00Toshkentda — UTC'da 13:00. Bazada UTC saqlanadi (Backend'da sana va vaqt).ertaga— ertangi kun 18:00.31.02—Date.UTCuni 3-mart deb "to'g'rilaydi"; oy mos kelmagani uchunundefined.01.01— vaqtsiz sana 09:00 bo'ladi; 1-yanvar o'tib ketgani uchun keyingi yil.28.10 25:00— soat 25 rad etiladi.
Natijalar kanon testidagi qiymatlarga mos (2026-10-28T13:00:00.000Z, 2027-01-01T04:00:00.000Z).
7. Ko'p uchraydigan xatolar
| Xato | Natija | To'g'risi |
|---|---|---|
| sessiya xotirada (prod) | deploy'da hamma dialog uziladi | Redis / baza storage |
initial — obyekt, funksiya emas |
chatlar bitta obyektni bo'lishadi | () => ({...}) |
| guruhda sukut kalit | ikki kishining anketasi aralashadi | getSessionKey foydalanuvchi bo'yicha |
dialogda bazaga external siz yozish |
takroriy yozuv | conversation.external |
dialogda new Date() |
replay'da boshqa vaqt | conversation.now() |
| timeout yo'q | haftalik eski savolga javob | maxMilliseconds |
dialogda /bekor tekshirilmaydi |
buyruq matn bo'lib saqlanadi | dialog ichida ham tekshirish |
8. Mashqlar
Mashqlar kurs/mashqlar/29/22-sessiya/ da.
1-mashq (oson): qaysi storage?
Uch vaziyat: (a) botni o'z kompyuteringizda sinayapsiz; (b) bot bitta serverda, kuniga bir marta deploy; (c) bot uch serverda yuk taqsimlagich (load balancer) orqasida. Har biri uchun sessiya storage'ini tanlang.
Yechim
(a) Xotira — yetarli, qayta ishga tushganda yo'qolishi muammo emas. (b) Redis yoki baza — har deploy xotirani tozalaydi, dialoglar uziladi (kanon: 2 → 2). (c) Faqat umumiy storage (Redis/baza) — xotira har serverda alohida, xabarlar turli serverga tushadi.
2-mashq (o'rta): timeout'li holatlar mashinasi
Darsdagi qadam ga vaqt qo'shing: qadam(sessiya, matn, hozirMs). Har javobda sessiya.oxirgi = hozirMs yozilsin. Dialog o'rtasida (bosqich null emas) oxirgi xabardan 10 daqiqadan (600 000 ms) ko'p o'tgan bo'lsa — bosqichni null qiling va "Vaqt tugadi. Qaytadan: /yangi" qaytaring. Sinang: /yangi (0 ms), Non (60 000), /yangi (100 000), ertaga (800 000).
Yechim
const TIMEOUT = 600_000;
function qadam(sessiya, matn, hozirMs) {
const eski = hozirMs - (sessiya.oxirgi ?? hozirMs);
sessiya.oxirgi = hozirMs;
if (sessiya.bosqich && eski > TIMEOUT) {
sessiya.bosqich = null;
return "Vaqt tugadi. Qaytadan: /yangi";
}
if (matn === "/bekor") {
sessiya.bosqich = null;
return "Bekor qilindi.";
}
switch (sessiya.bosqich) {
case null:
if (matn !== "/yangi") return "Buyruqlar: /yangi";
sessiya.bosqich = "matn";
return "Vazifa matni?";
case "matn":
sessiya.matn = matn;
sessiya.bosqich = "muddat";
return "Muddat?";
case "muddat":
sessiya.bosqich = null;
return `Qo'shildi: ${sessiya.matn} — ${matn}`;
}
}
const s = { bosqich: null };
const qadamlar = [
["/yangi", 0], ["Non", 60_000], ["/yangi", 100_000],
["ertaga", 800_000],
];
for (const [m, t] of qadamlar) {
console.log(`${t} ms ${m} → ${qadam(s, m, t)}`);
}0 ms /yangi → Vazifa matni?
60000 ms Non → Muddat?
100000 ms /yangi → Qo'shildi: Non — /yangi
800000 ms ertaga → Buyruqlar: /yangiUchinchi qatorga qarang: /yangi muddat sifatida saqlandi — bu darsdagi "dialogda buyruq tekshirilmaydi" xatosi. To'rtinchi qatorda dialog allaqachon tugagan edi, shuning uchun timeout ishlamadi. Kanon dialogi bu xatoga yo'l qo'ymaydi: / bilan boshlangan matn vazifa matni sifatida qabul qilinmaydi, muddat esa muddatTahlil dan o'tadi.
3-mashq (qiyin): replay taqlidi
Replay g'oyasini tekshiring. dialog(kut, tashqi) funksiyasi "Ism?" ni chiqaradi va kut() dan ism oladi. Keyin tashqi(() => ...) ichida bazagaYozildi hisoblagichini oshiradi. So'ng "Yosh?" ni chiqaradi, kut() dan yosh oladi va Saqlandi: ... ni chiqaradi. oyna(javoblar, eslab) uni boshidan bajaradi: kut() javoblar dan navbatdagini qaytaradi, tugasa TUGADI xatosini tashlaydi; tashqi eslab massivida natija bo'lsa uni qaytaradi, bo'lmasa f() ni chaqirib yozadi. Ikki update simulyatsiya qiling: ["Aziz"], keyin ["Aziz", "25"]. Replay'da ekranga chiqish faqat yangi qismda bo'lsin (ishora: kut() chaqiruvlari sonini oldingi javoblar soni bilan solishtiring).
Yechim
let bazagaYozildi = 0;
let chaqiruv = 0;
async function dialog(kut, tashqi, chiqar) {
chaqiruv++;
chiqar("Ism?");
const ism = await kut();
await tashqi(() => ++bazagaYozildi);
chiqar("Yosh?");
const yosh = await kut();
chiqar(`Saqlandi: ${ism}, ${yosh}`);
}
async function oyna(javoblar, eslab, eski) {
let i = 0;
let j = 0;
const kut = async () => {
if (i >= javoblar.length) throw new Error("TUGADI");
return javoblar[i++];
};
const tashqi = async (f) => {
if (j < eslab.length) return eslab[j++];
const r = f();
eslab.push(r);
j++;
return r;
};
const chiqar = (m) => { if (i >= eski) console.log(m); };
try {
await dialog(kut, tashqi, chiqar);
} catch (e) {
if (e.message !== "TUGADI") throw e;
}
}
const eslab = [];
await oyna([], eslab, 0);
await oyna(["Aziz"], eslab, 1);
await oyna(["Aziz", "25"], eslab, 2);
console.log({ chaqiruv, bazagaYozildi });Ism?
Yosh?
Saqlandi: Aziz, 25
{ chaqiruv: 3, bazagaYozildi: 1 }Dialog 3 marta chaqirildi, "bazaga yozish" — 1 marta. Kanonning Redis qatoridagi raqamlar xuddi shunday: funksiya 3 marta, external 1 marta. Haqiqiy plagin bundan murakkab (API chaqiruvlarini ham saqlaydi), lekin g'oya shu.
9. Loyiha qadami: /yangi dialogi
Kanon bot.ts ga conversations plagini va /yangi dialogini qo'shdi (@grammyjs/conversations 2.1.1). Formatlangan nusxa, ma'nosi kanondagidek:
bot.use(conversations<BotCtx, IchkiCtx>(
o.storage ? { storage: o.storage } : {},
));
async function yangi(
conversation: Conversation<BotCtx>, ctx: IchkiCtx,
) {
const fId = await conversation.external(
() => manba.kim(ctx.chat?.id ?? 0),
);
if (fId === undefined) {
await ctx.reply(
"Avval hisobingizni ulang: ilovadagi \"Telegram'ni ulash\" " +
"tugmasi.",
);
return;
}
await ctx.reply(
"Vazifa matnini yozing (100 belgigacha). Bekor qilish: /bekor",
);
const kutish = {
maxMilliseconds: o.dialogTimeoutMs ?? 10 * 60_000,
};
let matn = "";
for (;;) {
const m = await conversation.waitFor("message:text", {
...kutish,
otherwise: (c) => c.reply("Iltimos, matn yuboring."),
});
matn = m.message.text.trim();
if (matn === "/bekor") {
await m.reply("Bekor qilindi.");
return;
}
if (
matn.length >= 1 && matn.length <= 100 &&
!matn.startsWith("/")
) break;
await m.reply(
`Matn 1–100 belgi bo'lsin (hozir ${String(matn.length)}).`,
);
}
await ctx.reply(
"Muddat? Masalan: <code>28.10 18:00</code>, " +
"<code>ertaga</code> yoki <code>yo'q</code>",
{ parse_mode: "HTML" },
);
let muddat: Date | null | undefined;
for (;;) {
const m = await conversation.waitFor("message:text", kutish);
if (m.message.text.trim() === "/bekor") {
await m.reply("Bekor qilindi.");
return;
}
// replay'da bir xil qiymat (deterministik)
const hozir = new Date(await conversation.now());
muddat = muddatTahlil(m.message.text, hozir);
if (muddat !== undefined) break;
await m.reply(
"Tushunmadim. Format: KK.OO SS:DD, masalan 28.10 18:00",
);
}
const yaratilgan = await conversation.external(
() => manba.yarat(fId, matn, muddat ?? null),
);
await ctx.reply(
`Qo'shildi: <b>${html(matn)}</b>` +
(muddat ? ` — ${sanaUz(muddat)}` : "") +
` (#${String(yaratilgan.id)})`,
{ parse_mode: "HTML" },
);
}
bot.use(createConversation<BotCtx, IchkiCtx>(yangi, "yangi"));
bot.command("yangi", async (ctx) => {
await ctx.conversation.enter("yangi");
});
bot.command("bekor", async (ctx) => {
await ctx.conversation.exitAll();
await ctx.reply("Hech narsa davom etmayapti.");
});Darsdagi har qoida kodda bor:
manba.kimvamanba.yarat—externalichida (replay'da qayta chaqirilmaydi).conversation.now()— replay'da bir xil "hozir".- Ikki halqa — "qayta so'rash" holatlari (diagrammadagi o'z-o'ziga strelkalar).
/bekor— harwaitFordan keyin tekshiriladi.maxMilliseconds— sukut 10 daqiqa; test uni 60 s qiladi./bilan boshlangan matn vazifa bo'lmaydi — 2-mashqdagi xato yo'q.storage— parametr: test xotira bilan, prodakshn Redis bilan (loyihaga ulanganda).
Kanon testi (formatlangan):
test("/yangi dialog: ...", async () => {
let n = soni(501);
tg.yozadi(501, "/yangi");
assert.match((await javob(501, n)).text ?? "", /matnini yozing/);
n = soni(501); tg.yozadi(501, "x".repeat(101));
assert.match((await javob(501, n)).text ?? "", /1–100 belgi/);
n = soni(501); tg.yozadi(501, "Dorixonaga borish <tez>");
assert.match((await javob(501, n)).text ?? "", /Muddat\?/);
n = soni(501); tg.yozadi(501, "ertaga emas");
assert.match((await javob(501, n)).text ?? "", /Tushunmadim/);
n = soni(501); tg.yozadi(501, "yo'q");
assert.match((await javob(501, n)).text ?? "",
/Qo'shildi: <b>Dorixonaga borish <tez><\/b>/);
// ... /yangi → /bekor → "Bekor qilindi."
});Biz butun test faylini ishga tushirdik (Node 24.21.0, 9/9 o'tdi); bu darsga tegishli qatorlar:
✔ /yangi dialog: noto'g'ri matn → qayta so'raydi, noto'g'ri muddat → qayta, yaratiladi; /bekor (138.3034ms)
✔ ulanmagan chat /yangi — dialog boshlanmaydi (15.1066ms)Test dialogi xotira storage bilan; Redis bilan davom etishni kurs tajribasi ko'rsatdi.
Commit xabari:
git commit -m "feat(telegram): /yangi dialogi — conversations, muddat"10. Real ishda
- Ro'yxatdan o'tish, anketa, buyurtma rasmiylashtirish (manzil → telefon → tasdiq) — ko'p qadamli dialoglar.
- Prodakshn botlarda sessiya Redis'da, TTL bilan: tashlab ketilgan dialoglar o'zi o'chadi.
- Telegraf loyihalarida scenes/wizard'ni ko'p uchratasiz — g'oya bir xil: holat serverda.
- Intervyuda: "Bot qayta ishga tushganda dialog holati qayerda saqlanadi?", "Replay nima va qanday tuzoqlari bor?"
Xulosa
- Har update alohida; dialog holati sessiyada saqlanadi.
- Xotira sessiyasi deploy'da yo'qoladi (kanon: 2 → 2); Redis bilan davom etadi (2 → 4).
- Sukut kalit — chat; guruhda dialog uchun foydalanuvchi bo'yicha kalit.
- conversations — dialog oddiy
asyncfunksiya; har update'da replay. - Yon ta'sirlar
externalda, vaqtconversation.now()da;/bekorva timeout har savolda.
Keyingi dars: Media, fayllar va formatlash — rasm va hujjat yuborish, file_id ni qayta ishlatish, 20 va 50 MB chegaralari va HTML ekranlash.
Manbalar
- grammY sessiyalar: https://grammy.dev/plugins/session (
getSessionKey— sukut chat bo'yicha) - grammY conversations (replay,
external): https://grammy.dev/plugins/conversations — 2026-10-28 da o'qildi - @grammyjs/storage-redis: https://github.com/grammyjs/storages/tree/main/packages/redis
- Telegraf scenes (hujjat, sinalmagan): https://telegraf.js.org/
- Kanon tajribasi (IlmHamroh, 2026-10-27): grammy 1.46.0, @grammyjs/conversations 2.1.1, @grammyjs/storage-redis 2.6.0, ioredis 6.0.0, Redis 8.10.2, Node 24.21.0.
getSessionKeytajribasi va modul testlari — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!