Mundarija (23)
- Bu darsda
- 1. Nega bu kerak?
- 2. Ikki xil klaviatura
- 2.1 Reply keyboard
- 2.2 Inline keyboard
- 3. Callback query
- 3.1 Bosishdan javobgacha
- 3.2 grammY'da
- 3.3 "message is not modified"
- 4. callback_data: 64 bayt
- 4.1 Bayt, belgi emas
- 4.2 Nima sig'adi va nima sig'maydi
- 5. Hujumchi nigohi: IDOR tugma orqali
- 6. Menu plagini
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): qaysi klaviatura?
- 2-mashq (o'rta): xavfsiz kodlovchi
- 3-mashq (qiyin): IDOR'siz bajar
- 9. Loyiha qadami: "Bajarildi" tugmalari va sahifalash
- 10. Real ishda
- Xulosa
- Manbalar
Klaviaturalar va callback query: tugmali bot interfeysi
Qisqacha: Telegram botida ikki xil tugma bor. Reply keyboard yozish maydoni o'rnida chiqadi va bosilganda oddiy matn yuboradi. Inline keyboard xabar ostida turadi va bosilganda botga
callback_queryupdate'ini yuboradi. Unga har doimanswerCallbackQuerybilan javob bering.callback_data— 1–64 bayt, belgi emas: kanonda 32 taўo'tdi, 33 tasi rad etildi. Bu ma'lumot mijozdan keladi — egalikni serverda tekshiring.
Bu darsda
- Reply va inline klaviaturani grammY'ning
KeyboardvaInlineKeyboardklasslari bilan yasaysiz. callback_queryqanday kelishini va negaanswerCallbackQuerymajburiyligini tushunasiz.callback_dataning 64 baytli chegarasini o'lchaysiz va ma'lumotni qisqa kodlaysiz.- menu plagini holatli menyuni qanday qurishini kanon chiqishida ko'rasiz.
- Loyiha botiga "Bajarildi" tugmalarini, sahifalashni va IDOR himoyasini qo'shasiz.
Oldin bilishingiz kerak: grammY bilan bot yozish, Matn kodlash: ASCII, Unicode va UTF-8, RBAC va resurs egaligi (28-qismda).
Oldingi darsda bot buyruqlarga matn bilan javob berdi. Foydalanuvchi esa /vazifalar ni yozib, keyin "42-vazifa bajarildi" deb qo'lda yozishi kerak edi. Bugun buni bitta tugma bosishga aylantiramiz.
1. Nega bu kerak?
Telefonda matn yozish noqulay. Foydalanuvchi buyruq nomini eslab qolmaydi, xato yozadi, /vazifalr deydi. Tugmalar uchta muammoni hal qiladi:
- Tanlov aniq. Foydalanuvchi faqat taklif qilingan variantni bosadi.
- Tez. Bitta bosish — bitta harakat.
- Kontekst saqlanadi. Tugma ma'lum xabarga, ma'lum vazifaga bog'langan.
O'xshatish: kafedagi menyu. Ofitsiantga "bir narsa olib keling" deyish o'rniga menyudagi rasmga barmoq bilan ko'rsatasiz. Lekin rasmda faqat taom raqami yozilgan — narxni va mavjudligini oshxona o'zi tekshiradi.
2. Ikki xil klaviatura
2.1 Reply keyboard
Reply keyboard (javob klaviaturasi) — telefon klaviaturasi o'rnida chiqadigan katta tugmalar. Bosilganda ilova shu tugma matnini oddiy xabar sifatida yuboradi. Bot uni message:text sifatida oladi.
import { Keyboard } from "grammy";
await bot.api.sendMessage(1, "Muddat?", {
reply_markup: new Keyboard()
.text("bugun").text("ertaga").row()
.text("yo'q")
.resized().oneTime(),
});Kurs tajribasi mock'ga borgan JSON'ni yozib oldi:
reply_markup: {"keyboard":[[{"text":"bugun"},{"text":"ertaga"}],[{"text":"yo'q"}]],"resize_keyboard":true,"one_time_keyboard":true}keyboard— qatorlar massivi, har qator — tugmalar massivi..row()yangi qator boshlaydi.resize_keyboard(.resized()) — tugmalar balandligi kichrayadi. Busiz ular ekranning yarmini egallaydi.one_time_keyboard(.oneTime()) — bosilgandan keyin klaviatura yashiriladi.
Klaviaturani butunlay olib tashlash uchun reply_markup: { remove_keyboard: true } yuboriladi. Kanon buni ham yubordi.
Reply tugmaning kamchiligi: bot faqat matnni oladi. Foydalanuvchi "bugun" ni qo'lda yozsa ham, tugmani bossa ham — bir xil. Bu ko'p qadamli dialoglarda qulay (Sessiya va ko'p qadamli dialoglar darsida ishlatamiz).
2.2 Inline keyboard
Inline keyboard — xabarning o'ziga yopishgan tugmalar. Ular bir necha turda bo'ladi:
import { InlineKeyboard } from "grammy";
const k = new InlineKeyboard()
.text("Bajarildi", "b:42")
.url("Ilovada ochish", "https://vazifalar.example.uz/v/42")
.row()
.switchInline("Ulashish", "non");
await bot.api.sendMessage(1, "Vazifa #42", { reply_markup: k });Kanon chiqishi:
inline: {"inline_keyboard":[[{"text":"Bajarildi","callback_data":"b:42"},{"text":"Ilovada ochish","url":"https://vazifalar.example.uz/v/42"}],[{"text":"Ulashish","switch_inline_query":"non"}]]}.row() ishladi: JSON'dagi }],[{ — yangi qator chegarasi, "Ulashish" ikkinchi qatorda yolg'iz.
| Tugma | Bosilganda | Botga keladi |
|---|---|---|
.text(yorliq, data) |
hech narsa ochilmaydi | callback_query + data |
.url(yorliq, havola) |
brauzer ochiladi | hech narsa |
.switchInline(yorliq, so'rov) |
chat tanlash, inline rejim | inline_query |
.webApp(yorliq, havola) |
Mini App ochiladi | keyinroq |
switch_inline_query Guruh, kanal botlari va inline rejim darsida, web_app esa Telegram Mini App backendi va Stars darsida.
Tekshirib ko'ring: Foydalanuvchi "Ilovada ochish" tugmasini bosdi. Bot buni qanday biladi?
Javob
Bilmaydi. URL tugmasi bosilganda Telegram havolani brauzerda ochadi va botga hech qanday update yubormaydi. Agar bosilganini bilish kerak bo'lsa — havolaning o'zi sizning serveringizga olib keladi (masalan, /v/42 sahifasi ochilganini serveringiz ko'radi).
3. Callback query
3.1 Bosishdan javobgacha
Inline .text() tugmasi bosilganda nima bo'ladi?
sequenceDiagram
participant F as Foydalanuvchi
participant T as Telegram
participant B as Bot
F->>T: "Bajarildi" ni bosadi
T->>B: callback_query (id, data "b:42", message)
Note over F: tugmada soat aylanadi
B->>B: egalikni tekshiradi, bajaradi
B->>T: answerCallbackQuery (text "Bajarildi")
T->>F: qisqa bildirishnoma
B->>T: editMessageText (yangi ro'yxat)
T->>F: xabar yangilanadiDiagrammaga qarang: bot ikki ish qiladi — so'rovga javob beradi va xabarni tahrirlaydi. Bu ikki alohida metod.
Telegram hujjati aytadi: foydalanuvchi callback tugmasini bosgandan keyin, Telegram ilovalari bot answerCallbackQuery ni chaqirmaguncha progress belgisini ko'rsatib turadi. Javob bermasangiz, foydalanuvchi "bot qotib qoldi" deb o'ylaydi.
answerCallbackQuery ning text maydoni — hujjatga ko'ra 0–200 belgi. show_alert: true bo'lsa — ekran o'rtasida oyna, aks holda yuqorida qisqa yozuv.
3.2 grammY'da
bot.callbackQuery(/^b:(\d{1,18})$/, async (ctx) => {
const id = Number(ctx.match[1]);
await ctx.answerCallbackQuery({ text: "Bajarildi" });
await ctx.editMessageText("Yangi ro'yxat...");
});
bot.on("callback_query:data", (ctx) =>
ctx.answerCallbackQuery({ text: "Eskirgan tugma" }));bot.callbackQuery(regex, f)—dataregex'ga mos kelsa ishlaydi.ctx.match— regex natijasi,ctx.match[1]— birinchi guruh.ctx.editMessageText— tugma turgan xabarni tahrirlaydi. Xabar va chatctx.callbackQuery.messagedan olinadi.- Oxirgi qator — "zaxira" ishlovchi: hech bir regex mos kelmasa ham javob beradi.
Zaxira ishlovchi nega kerak? Bot yangilansa, eski xabarlardagi tugmalarda eski formatdagi data qoladi. Foydalanuvchi bir hafta oldingi xabardagi tugmani bosadi — data hech bir regex'ga mos emas. Zaxira bo'lmasa, soat abadiy aylanadi.
3.3 "message is not modified"
Foydalanuvchi bir tugmani ikki marta tez bossa, bot ikkinchi marta ham xabarni xuddi o'sha matn bilan tahrirlashga urinadi. Telegram buni rad etadi: Bad Request: message is not modified. Bu xato emas — "o'zgarish yo'q" degani. Kanon modul aynan shu xatoni yutadi, boshqalarini esa tashlaydi:
await ctx.editMessageText(matn, {
parse_mode: "HTML", reply_markup: klaviatura,
}).catch((e: unknown) => {
if (!(e instanceof GrammyError &&
e.description.includes("message is not modified"))) throw e;
});Hamma xatoni yutish — xato. Faqat aniq, kutilgan holat yutiladi.
4. callback_data: 64 bayt
4.1 Bayt, belgi emas
Telegram hujjati: callback_data — "1-64 bytes". Kanon mock beshta qiymatni sinadi:
callback_data 64 belgi / 64 bayt: OK
callback_data 65 belgi / 65 bayt: Bad Request: BUTTON_DATA_INVALID
callback_data 32 belgi / 64 bayt: OK
callback_data 33 belgi / 66 bayt: Bad Request: BUTTON_DATA_INVALID
callback_data 57 belgi / 57 bayt: OKUchinchi va to'rtinchi qatorga qarang: 33 ta belgi — 66 bayt. Kirill ў UTF-8 da 2 bayt (Matn kodlash). Lotin harfi — 1 bayt, emoji — 4 bayt. Shuning uchun chegara length bilan emas, bayt bilan tekshiriladi:
const bayt = (s) => new TextEncoder().encode(s).length;
const sina = (d) => {
const n = bayt(d);
const ok = n >= 1 && n <= 64;
return `${d.length} belgi / ${n} bayt: ${ok ? "OK" : "rad"}`;
};
console.log(sina("b:" + "1".repeat(62)));
console.log(sina("b:" + "1".repeat(63)));
console.log(sina("ў".repeat(32)));
console.log(sina("ў".repeat(33)));
const obyekt = {
amal: "bajar", vazifaId: 42, sahifa: 3, filtr: "bugun",
};
console.log(sina(JSON.stringify(obyekt)));
console.log(sina(""));Konsolda:
64 belgi / 64 bayt: OK
65 belgi / 65 bayt: rad
32 belgi / 64 bayt: OK
33 belgi / 66 bayt: rad
57 belgi / 57 bayt: OK
0 belgi / 0 bayt: radNatija kanon mock'iga mos. Oxirgi qator — bo'sh satr ham ruxsat etilmaydi (pastki chegara 1).
4.2 Nima sig'adi va nima sig'maydi
Beshinchi qator — 57 bayt. JSON obyekt sig'di, lekin chegaraga 7 bayt qoldi. filtr: "bugun" o'rniga filtr: "o'tgan hafta" bo'lsa — sig'maydi. Qoida:
- Matn emas, kod.
"Bajarildi: Non olish"emas,"b:42". - Qisqa prefiks. Bir harf — amal turi:
b— bajar,s— sahifa. - Faqat id. Vazifa matni, narx, foydalanuvchi nomi —
datada emas, bazada. - Katta holat — serverda. Filtr, saralash, sahifa birga kerak bo'lsa — holatni bazaga yoki Redis'ga saqlang va
dataga uning qisqa kalitini qo'ying.
Kanon modulida:
/** callback_data — 64 BAYT gacha (belgi emas). Shuning uchun matn
* emas, qisqa kod + id: "b:12345". */
export const CB = {
bajar: (id: number) => `b:${String(id)}`,
sahifa: (n: number) => `s:${String(n)}`,
} as const;Kodlash va ochish bir joyda bo'lsin — ikki joyda yozilgan format albatta ajraladi. Mana JavaScript'da juft funksiya:
const AMALLAR = { b: "bajar", s: "sahifa" };
function kodla(amal, id) {
const harf = Object.keys(AMALLAR).find((k) => AMALLAR[k] === amal);
if (!harf || !Number.isSafeInteger(id) || id < 0) {
throw new TypeError(`noto'g'ri: ${amal} ${id}`);
}
return `${harf}:${id}`;
}
function och(data) {
const m = /^([bs]):(\d{1,15})$/.exec(data);
if (!m) return null;
return { amal: AMALLAR[m[1]], id: Number(m[2]) };
}
console.log(kodla("bajar", 42));
console.log(och("b:42"));
console.log(och("s:3"));
console.log(och("b:42; DROP"));
console.log(och("eski:format"));Konsolda:
b:42
{ amal: 'bajar', id: 42 }
{ amal: 'sahifa', id: 3 }
null
nulloch qat'iy: regex boshidan oxirigacha (^…$) tekshiradi. Ortiqcha belgilar bo'lsa — null, ya'ni "eskirgan tugma".
Tekshirib ko'ring: Nega
ochda raqamlar soni{1,15}bilan cheklangan,\d+emas?
Javob
Number("99999999999999999999") aniq son bermaydi — Number.MAX_SAFE_INTEGER 16 xonali. Cheklovsiz regex juda uzun raqamni o'tkazadi va u boshqa id'ga "yaxlitlanadi". Kanon modulida ham \d{1,18} cheklovi bor. Umuman, mijozdan kelgan har maydon uzunligi bilan cheklanadi.
5. Hujumchi nigohi: IDOR tugma orqali
callback_data ni foydalanuvchi ko'rmaydi — lekin bu uni o'zgartira olmaydi degani emas. Telegram mijozlari ochiq kodli, Bot API'ga o'xshash so'rovni maxsus dastur bilan yuborish mumkin. Bot uchun qoida bitta: callback_data — mijozdan kelgan ma'lumot, xuddi formadagi maydon kabi.
Hujum: Aziz o'z ro'yxatidagi b:10 tugmasi o'rniga b:20 yuboradi. 20 — boshqa foydalanuvchi (Dilnoza)ning vazifasi. Agar bot "id 20 ni bajarildi qil" desa — Aziz begona vazifani o'zgartirdi. Bu IDOR (Insecure Direct Object Reference — himoyasiz to'g'ridan obyekt havolasi) — RBAC va resurs egaligi darsidagi muammo, faqat tugma ichida.
Himoya: id bilan birga kim so'rayotganini ham tekshirish. Kanon modulida manba.bajar(foydalanuvchiId, vazifaId) — vazifa faqat shu foydalanuvchinikidan qidiriladi:
bot.callbackQuery(/^b:(\d{1,18})$/, async (ctx) => {
const fId = await manba.kim(ctx.chat?.id ?? 0);
const id = Number(ctx.match[1]);
if (fId === undefined) {
await ctx.answerCallbackQuery({
text: "Hisob ulanmagan", show_alert: true,
});
return;
}
// egalik manba ichida tekshiriladi (IDOR — callback_data
// mijozdan keladi!)
const ok = await manba.bajar(fId, id);
await ctx.answerCallbackQuery({
text: ok ? "Bajarildi" : "Vazifa topilmadi",
});
// ... ro'yxatni qayta chizish
});Begona vazifa uchun javob — "Vazifa topilmadi", "Ruxsat yo'q" emas. Nega? "Ruxsat yo'q" hujumchiga "bunday vazifa bor" deb aytadi. "Topilmadi" esa mavjud va mavjud bo'lmagan id'ni farqlamaydi.
Kanon testi aynan shu hujumni sinaydi (formatlangan):
test("IDOR: begona vazifa id'si callback'da — 'topilmadi', " +
"Dilnozaniki o'zgarmaydi; eski tugma ham javob oladi",
async () => {
const x = tg.oxirgi(501);
assert.ok(x);
const cb = tg.bosadi(501, x.message_id, "b:20");
await kutib(() => tg.callbackJavob.get(cb));
assert.equal(tg.callbackJavob.get(cb)?.["text"],
"Vazifa topilmadi");
assert.equal((await manba.royxat(2))[0]?.bajarildi, false);
const eski = tg.bosadi(501, x.message_id, "eski:format");
await kutib(() => tg.callbackJavob.get(eski));
assert.equal(tg.callbackJavob.get(eski)?.["text"],
"Eskirgan tugma");
});Ikkinchi qism — eskirgan tugma: eski:format zaxira ishlovchiga tushdi va javob oldi.
Boshqa xavflar:
| Hujum | Himoya |
|---|---|
begona id data da |
egalik serverda (bajar(fId, id)) |
juda uzun / axlat data |
qat'iy regex, uzunlik chegarasi |
| guruhdagi xabar tugmasini begona a'zo bosadi | ctx.from.id bo'yicha tekshirish |
| tugmani 100 marta bosish | idempotent amal: "bajarildi" ikkinchi marta o'zgartirmaydi |
Uchinchi qator muhim: guruhda xabar hammaga ko'rinadi, tugmani istalgan a'zo bosa oladi. ctx.chat.id emas, ctx.from.id — kim bosgani. Kanon bot esa faqat shaxsiy chatda ishlaydi: u yerda chat id va foydalanuvchi id bir xil, shuning uchun yuqoridagi kodda ctx.chat?.id yetarli.
6. Menu plagini
Har tugma uchun callbackQuery yozish va xabarni qo'lda qayta chizish — ko'p kod. menu plagini (@grammyjs/menu) tugmalarni va ularning ishlovchilarini bir joyda e'lon qiladi va bosilganda menyuni o'zi yangilaydi.
import { Menu } from "@grammyjs/menu";
let filtr = "hammasi";
const menyu = new Menu("filtr")
.text(() => `Filtr: ${filtr}`, (ctx) => {
filtr = filtr === "hammasi" ? "bugun" : "hammasi";
ctx.menu.update();
})
.row()
.text("Yopish", (ctx) => ctx.menu.close());
bot.use(menyu);
bot.command("menyu", (ctx) =>
ctx.reply("Sozlama:", { reply_markup: menyu }));Kanon o'lchovi (IlmHamroh, 2026-10; menu 1.5.0):
menu tugmasi: {"text":"Filtr: hammasi","callback_data":"filtr/0/0//hf\u0015�<"} (callback_data 18 bayt — menyu id + tugma joyi + holat xeshi)
bosilgandan keyin: "Filtr: bugun" | editMessage chaqiruvlari: 1Nimani ko'ramiz:
callback_datani plagin o'zi yasadi: menyu nomi (filtr), tugma joyi (0/0— qator va ustun) va oxirida holatning qisqa hash'i.�— terminal ko'rsata olmagan ikkilik belgi; jami 18 bayt.- Bosilganda plagin
editMessageReplyMarkupni o'zi chaqirdi (1 marta) va yorliq "Filtr: bugun" bo'ldi.
Holat hash'i nimaga kerak? Foydalanuvchi eski menyuni bossa, plagin hash mos emasligini ko'radi va menyuni yangilaydi — eskirgan tugma muammosini o'zi hal qiladi.
Kanon mock'i birinchi urinishda editMessageReplyMarkup metodini bilmagan — 404 qaytargan. Metod mock'ka qo'shilgach, plagin ishladi. Bu mock'ning cheklangan ekanini eslatadi: u faqat sinalgan metodlarni biladi.
Bu misolda filtr — global o'zgaruvchi: hamma foydalanuvchi uchun bitta. Haqiqiy botda u har foydalanuvchiga alohida bo'lishi kerak — sessiyada. Buni keyingi darsda qilamiz.
Kanon modul menu plaginini ishlatmadi: "Bajarildi" tugmalari oddiy callbackQuery bilan yetarli. Menu — ko'p darajali sozlamalar menyusi uchun qulay.
7. Ko'p uchraydigan xatolar
| Xato | Natija | To'g'risi |
|---|---|---|
answerCallbackQuery yo'q |
tugmada soat aylanadi | har ishlovchida, zaxirada ham |
data ga matn yozish |
BUTTON_DATA_INVALID |
qisqa kod + id |
length bilan tekshirish |
kirillda 33 belgi o'tadi deb o'ylaysiz | TextEncoder bilan bayt |
data dagi id'ga ishonish |
IDOR | bajar(foydalanuvchiId, id) |
hamma edit xatosini yutish |
haqiqiy xatolar ko'rinmaydi | faqat "not modified" |
guruhda ctx.chat.id bilan ruxsat |
begona a'zo bosadi | ctx.from.id |
8. Mashqlar
Mashqlar kurs/mashqlar/29/21-klaviatura/ da.
1-mashq (oson): qaysi klaviatura?
Har vaziyat uchun reply yoki inline klaviaturani tanlang va sababini ayting:
- Anketada "Jinsingiz?" — ikki variant.
- Har vazifa ostida "Bajarildi" tugmasi.
- Ro'yxatda "Keyingi sahifa".
- "Saytimizda ochish".
Yechim
- Reply — javob matn sifatida dialogga tushadi, keyingi savolga o'tiladi.
- Inline — tugma aniq xabar va vazifaga bog'langan,
datada id. - Inline — xabarni joyida tahrirlash (
editMessageText), chatni to'ldirmasdan. - Inline URL tugmasi — botga update kelmaydi, havola ochiladi.
2-mashq (o'rta): xavfsiz kodlovchi
malumotKodla(qismlar) funksiyasini yozing: satrlar massivini : bilan birlashtiradi. Har qismda : bo'lmasin, natija 1–64 bayt bo'lsin; aks holda RangeError. Uch holatni sinang: ["b", "42"], ["f", "ўзбек"] (natija baytini chiqaring), 40 ta "я" bilan qism. Ishora: bayt — TextEncoder (Matn kodlash).
Yechim
const bayt = (s) => new TextEncoder().encode(s).length;
function malumotKodla(qismlar) {
if (qismlar.some((q) => q.includes(":"))) {
throw new RangeError("qism ichida ':' bor");
}
const d = qismlar.join(":");
const n = bayt(d);
if (n < 1 || n > 64) {
throw new RangeError(`${n} bayt — 1..64 bo'lsin`);
}
return d;
}
console.log(malumotKodla(["b", "42"]));
const f = malumotKodla(["f", "ўзбек"]);
console.log(f, bayt(f));
try {
malumotKodla(["f", "я".repeat(40)]);
} catch (e) {
console.log(e.name, e.message);
}b:42
f:ўзбек 12
RangeError 82 bayt — 1..64 bo'lsinf:ўзбек — 7 belgi, lekin 12 bayt: f va : bittadan, har kirill harfi ikkitadan. 40 ta я — 80 bayt, prefiks bilan 82.
3-mashq (qiyin): IDOR'siz bajar
Xotiradagi ombor yozing: vazifalar — { id, egasi, matn, bajarildi } massivi. bajar(egasi, data) funksiyasi: data ni och (darsdagi) bilan ochadi; null bo'lsa — "Eskirgan tugma"; vazifa faqat egasi nikidan qidiriladi; topilmasa — "Vazifa topilmadi"; allaqachon bajarilgan bo'lsa — "Allaqachon bajarilgan"; aks holda belgilab "Bajarildi". Aziz (1) uchun to'rt bosish: b:10, b:10, b:20 (Dilnozaniki), x:1. Oxirida Dilnozaning vazifasi o'zgarmaganini chiqaring.
Yechim
function och(data) {
const m = /^([bs]):(\d{1,15})$/.exec(data);
if (!m) return null;
const amal = m[1] === "b" ? "bajar" : "sahifa";
return { amal, id: Number(m[2]) };
}
const vazifalar = [
{ id: 10, egasi: 1, matn: "Non olish", bajarildi: false },
{ id: 20, egasi: 2, matn: "Hisobot", bajarildi: false },
];
function bajar(egasi, data) {
const o = och(data);
if (!o || o.amal !== "bajar") return "Eskirgan tugma";
const v = vazifalar.find((x) => x.id === o.id && x.egasi === egasi);
if (!v) return "Vazifa topilmadi";
if (v.bajarildi) return "Allaqachon bajarilgan";
v.bajarildi = true;
return "Bajarildi";
}
for (const d of ["b:10", "b:10", "b:20", "x:1"]) {
console.log(d, "→", bajar(1, d));
}
console.log("Dilnoza:", vazifalar[1].bajarildi);b:10 → Bajarildi
b:10 → Allaqachon bajarilgan
b:20 → Vazifa topilmadi
x:1 → Eskirgan tugma
Dilnoza: falseKalit qator — x.id === o.id && x.egasi === egasi. Faqat x.id === o.id bo'lsa, b:20 Dilnozaning vazifasini o'zgartirardi. Ikkinchi b:10 — idempotentlik: takroriy bosish holatni buzmaydi.
9. Loyiha qadami: "Bajarildi" tugmalari va sahifalash
Oldingi darsdagi bot.ts ga ro'yxat xabari va tugmalar qo'shiladi. royxatXabari — toza funksiya: vazifalar massividan matn va klaviatura yasaydi (sahifada 5 ta):
export function royxatXabari(
v: BotVazifa[], sahifa = 0,
): { matn: string; klaviatura: InlineKeyboard } {
const faol = v.filter((x) => !x.bajarildi);
const k = new InlineKeyboard();
if (faol.length === 0) {
return { matn: "Faol vazifa yo'q. Yangi: /yangi", klaviatura: k };
}
const qism = faol.slice(sahifa * SAHIFA, sahifa * SAHIFA + SAHIFA);
const qatorlar = qism.map((x, i) =>
`${String(sahifa * SAHIFA + i + 1)}. ${html(x.matn)}` +
(x.muddat ? ` — <i>${sanaUz(x.muddat)}</i>` : ""));
for (const x of qism) {
const nom = x.matn.length > 30
? `${x.matn.slice(0, 29)}…`
: x.matn;
k.text(`Bajarildi: ${nom}`, CB.bajar(x.id)).row();
}
if (sahifa > 0) k.text("< Oldingi", CB.sahifa(sahifa - 1));
if ((sahifa + 1) * SAHIFA < faol.length) {
k.text("Keyingi >", CB.sahifa(sahifa + 1));
}
return {
matn: `<b>Faol vazifalar (${String(faol.length)})</b>\n` +
qatorlar.join("\n"),
klaviatura: k,
};
}E'tibor bering:
- Tugma yorlig'ida matn bor (30 belgigacha qisqartirilgan) — yorliq
dataemas, uning chegarasi boshqa.datada esa faqatb:<id>. - Vazifa matni
html()dan o'tadi — foydalanuvchi matnidagi<HTML'ni buzmasin (Media, fayllar va formatlash darsida batafsil). - Toza funksiya oson test qilinadi: kanon testi 12 vazifadan 2-sahifa uchun 6 qator tugma (5 vazifa + navigatsiya) kutadi.
Callback ishlovchilari — «Hujumchi nigohi» bo'limidagi b: ishlovchisi, sahifa ishlovchisi va zaxira:
bot.callbackQuery(/^s:(\d{1,3})$/, async (ctx) => {
const fId = await manba.kim(ctx.chat?.id ?? 0);
await ctx.answerCallbackQuery();
if (fId === undefined) return;
const { matn, klaviatura } = royxatXabari(
await manba.royxat(fId), Number(ctx.match[1]),
);
await ctx.editMessageText(matn, {
parse_mode: "HTML", reply_markup: klaviatura,
});
});
// Eski/noma'lum tugma — baribir javob beramiz, aks holda mijozda
// "soat" aylanib turadi
bot.on("callback_query:data",
(ctx) => ctx.answerCallbackQuery({ text: "Eskirgan tugma" }));Sahifa ishlovchisi answerCallbackQuery() ni birinchi chaqiradi — matnsiz, faqat soatni to'xtatish uchun.
Testlar — test/telegram.test.ts. Biz butun faylni ishga tushirdik (Node 24.21.0); bu darsga tegishli qatorlar:
✔ /vazifalar: HTML ekranlangan, inline tugmalar; bosish → bajarildi, xabar tahrirlandi (62.1289ms)
✔ IDOR: begona vazifa id'si callback'da — 'topilmadi', Dilnozaniki o'zgarmaydi; eski tugma ham javob oladi (31.1902ms)
✔ callback_data 65 bayt va 4097 belgi — 400; HTML xatosi — 400 (7.7922ms)To'liq chiqish — 9/9 o'tdi (grammY bilan bot yozish darsidagi «Loyiha qadami»da). Millisekundlar har safar boshqacha.
Commit xabari:
git commit -m "feat(telegram): Bajarildi tugmalari, sahifalash, IDOR"10. Real ishda
- Do'kon botlarida katalog — inline tugmalar bilan sahifalash; savat —
datada mahsulot id'si, narx esa bazadan. - Bank va to'lov botlarida tasdiqlash tugmalari ("To'lash", "Bekor qilish") har doim serverda qayta tekshiriladi.
- Bot penetratsion testlarida
callback_datani o'zgartirib yuborish — standart tekshiruv. - Intervyuda: "callback_data chegarasi qancha va katta holatni qayerda saqlaysiz?", "Tugma orqali IDOR qanday bo'ladi?"
Xulosa
- Reply keyboard matn yuboradi; inline keyboard
callback_queryyuboradi. answerCallbackQuery— har callback'ga, zaxira ishlovchida ham.callback_data— 1–64 bayt; kirillda 32 harf,TextEncoderbilan o'lchanadi.datada faqat qisqa kod va id; holat — serverda.datamijozdan keladi: egalik serverda, javob "topilmadi".- menu plagini tugma, ishlovchi va yangilashni birlashtiradi; kanon modul oddiy
callbackQuerybilan qoldi.
Keyingi dars: Sessiya va ko'p qadamli dialoglar — bot foydalanuvchi qaysi bosqichda ekanini eslab qoladi va /yangi dialogini quramiz.
Manbalar
- Telegram Bot API:
InlineKeyboardButton(callback_data 1–64 bytes),CallbackQuery,answerCallbackQuery— https://core.telegram.org/bots/api#inlinekeyboardbutton (2026-10-28 da o'qildi) - grammY klaviaturalar: https://grammy.dev/plugins/keyboard
- grammY menu plagini: https://grammy.dev/plugins/menu
- OWASP IDOR: https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html
- Kanon tajribasi (IlmHamroh, 2026-10-27): grammy 1.46.0, @grammyjs/menu 1.5.0, Node 24.21.0, lokal soxta Bot API serveri. Modul testlari — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!