07 β Callback query va inline rejim¶
β¬ οΈ Oldingi: 06 β Klaviaturalar: reply va inline Β· π README Β· Keyingi: 08 β Conversations β suhbatlar β‘οΈ
Bu bobda: 06-bobda inline klaviatura (
InlineKeyboard) yasashni o'rgandik, lekin tugma bosilganda nima sodir bo'lishini chala qoldirdik. Endi to'liq yopamiz. Inline tugma bosilsa botcallback_querydegan maxsus yangilanish (update) oladi β unibot.callbackQuery("data", handler)yokibot.callbackQuery(/regex/, handler)(regex guruhictx.match[1]da) bilan ushlaymiz. So'ngctx.answerCallbackQuery()orqali "soat aylanishini" to'xtatish va toast/alert ko'rsatishni;ctx.editMessageText/ctx.editMessageReplyMarkupbilan mavjud xabarni o'rnida tahrirlashni (ovoz hisoblagichi, menyu o'tishlari); shu asosda sahifalash (pagination) qurishni;callback_datani xavfsiz dizayn qilishni (kalit:qiymat, <64 bayt) va nihoyat istalgan chatda@bot ...deb yozib ishlatiladigan inline rejimni (bot.inlineQuery) o'rganamiz.Halollik eslatmasi: Bobdagi butun handler mantig'i β
bot.callbackQuery(satr va regex),ctx.answerCallbackQuery({ text, show_alert }),ctx.editMessageText/ctx.editMessageReplyMarkup,callback_datakodlash/parslash, 64-bayt cheklovi, pagination chegaralari,"message is not modified"xatosini ushlash,bot.inlineQuery+ctx.answerInlineQueryβ tokensiz, OFFLINEbot.handleUpdatega soxtacallback_query/inline_queryupdate'ini berib va chiqayotgan API chaqiruvlarini transformer bilan ushlab haqiqatan ishga tushirib tekshirildi (13/13 test o'tdi). Jonli natija β tugma bosilganda telefonda toast/alert chiqishi, xabar o'rnida yangilanishi,@botqidiruvi β@BotFathertoken + internet talab qiladi va matnda "illustrativ" deb belgilangan. Hech qayerda soxta "ishladi / xabar yetib bordi" yozilmagan.
Callback query nima?¶
06-bobda inline tugma yasadik: new InlineKeyboard().text("Ha", "yes"). Reply tugmadan farqi shu β inline tugma bosilganda chatga matn yubormaydi. Uning o'rniga Telegram botingizga callback_query degan maxsus yangilanish jo'natadi. Bu yangilanish ichida tugmaga oldindan biriktirilgan callback_data satri keladi (bizning misolda "yes"). Handler shu satrga qarab "qaysi tugma bosildi" deb tushunadi.
Oqim quyidagicha:
Uchta asosiy narsa bor, hammasini ketma-ket o'rganamiz:
callback_dataβ tugmaga yashirin yozib qo'yiladigan satr (1..64 bayt). Masalan"prod:view:42".bot.callbackQuery(...)β bu satrga (yoki regexga) mos handler.ctx.answerCallbackQuery()β Telegram'ga "qabul qildim" deb javob berish (MAJBURIY).
JS eslatma: Bu kitob siz JavaScript/Node asoslarini bilasiz deb faraz qiladi (
async/await,Promise, ESMimport, massiv/obyekt metodlari). Agar bular yangi bo'lsa, avval JavaScript β 0 dan Expertgacha ni o'qing. Biz Telegram/grammY'ga xos narsalarni to'liq tushuntiramiz.
Eng oddiy callback handler¶
Avval callback_data ni oddiy satr sifatida ishlatamiz. 06-bobdagidek inline klaviatura yasaymiz, lekin endi tugma bosilishini ushlaymiz:
// bot.js β eng oddiy callback misoli
import { Bot, InlineKeyboard } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN); // .env dan, kodga YOZILMAYDI
bot.command("start", (ctx) => {
const kb = new InlineKeyboard()
.text("Salom ber", "say_hi")
.text("Yopish", "close");
return ctx.reply("Tugmani bosing:", { reply_markup: kb });
});
// callback_data === "say_hi" bo'lgan tugma bosilganda ishlaydi
bot.callbackQuery("say_hi", async (ctx) => {
// Avval answer() β Telegram'ga "qabul qildim" deymiz (pastda batafsil)
await ctx.answerCallbackQuery({ text: "Salom!" });
// Endi xabarni o'rnida yangilaymiz
await ctx.editMessageText("Botdan salom! π");
});
bot.callbackQuery("close", async (ctx) => {
await ctx.answerCallbackQuery();
// Faqat tugmalarni olib tashlaymiz, matn qoladi
await ctx.editMessageReplyMarkup(); // markup'siz -> tugmalar yo'qoladi
});
bot.start(); // jonli polling β token+internet kerak (illustrativ)
Yangi narsalar:
bot.callbackQuery("say_hi", handler)βmessageemas,callback_queryyangilanishini ushlaydi. Argument β bosilgan tugmaningcallback_datasiga aniq mos kelishi kerak bo'lgan satr.ctxβ bu yerda ichidactx.callbackQuerybor (message:textdactx.messagebo'lgani kabi).ctx.callbackQuery.data(satr"say_hi"),ctx.from(kim bosgan),ctx.callbackQuery.message(qaysi xabardagi tugma bosilgan).ctx.msgham shu xabarga ishora qiladi.ctx.answerCallbackQuery(...)β Telegram'ga javob. Bunisiz tugmada soat aylanaverib qoladi (pastda).ctx.editMessageText(...)β xabarni o'rnida yangilaydi (yangi xabar yubormaydi).message_idni grammYctx'dan avtomatik oladi β siz bermaysiz.
Telefonda bu shunday ko'rinadi (illustrativ β token+internet kerak): foydalanuvchi "Salom ber" tugmasini bosadi, ekran tepasida qisqa "Salom!" toast paydo bo'ladi, xabar matni "Botdan salom! π" ga aylanadi.
Anti-eskirish: Internetda ko'p misol Telegraf yoki node-telegram-bot-api uchun. U yerda
bot.action("say_hi", ctx => ctx.answerCbQuery())(Telegraf) yokibot.on("callback_query", ...)(node-telegram-bot-api) ko'rasiz β bular grammY EMAS. grammY'dabot.callbackQuery(...)vactx.answerCallbackQuery(...)ishlatiladi. Aralashtirmang.
Bu xatti-harakat OFFLINE tasdiqlandi: say_hi callback'ida answerCallbackQuery chaqiruvi callback_query_id bilan ketdi, editMessageText esa yangi matn bilan message_id: 50 (ctx'dagi xabar) ga yo'naltirildi.
ctx.answerCallbackQuery() β nega MAJBURIY?¶
Foydalanuvchi inline tugmani bosganda Telegram klientida tugmada kichik soat (loading) aylanishni boshlaydi. Bot answerCallbackQuery jo'natmaguncha bu soat ~30 soniya aylanaveradi va foydalanuvchiga "bot javob bermayapti" degan taassurot beradi. Shuning uchun har callback handlerida ctx.answerCallbackQuery() chaqirish shart β hatto ko'rsatadigan matn bo'lmasa ham (bo'sh chaqiruv).
answerCallbackQuery ning ikki ko'rinishi bor:
// 1) TOAST β ekran tepasida qisqa paydo bo'lib yo'qoladi
await ctx.answerCallbackQuery({ text: "Saqlandi!" });
// 2) ALERT β markazda "OK" tugmali oyna, foydalanuvchi yopguncha turadi
await ctx.answerCallbackQuery({
text: "Diqqat: bu amalni ortga qaytarib bo'lmaydi!",
show_alert: true,
});
// 3) Bo'sh β hech narsa ko'rsatmaydi, faqat soatni to'xtatadi
await ctx.answerCallbackQuery();
textβ ko'rsatiladigan matn (HTML emas, oddiy matn; ~200 belgigacha).show_alert: trueβ toast emas, modal oyna (foydalanuvchi diqqatini jalb qiladi).url/cache_timeβ kamdan-kam kerak (urlβ o'yin yoki maxsus deep-link uchun,cache_timeβ Telegram javobni necha soniya keshlasin).
Qoida: handlerda imkon qadar tezroq
answerCallbackQuery()chaqiring, og'ir ish (DB so'rovi, fayl) keyin bo'lsin. Aks holda soat uzoq aylanadi. Ko'pincha birinchi qatorawait ctx.answerCallbackQuery()bo'ladi.
{ text, show_alert: true } payload'i OFFLINE tasdiqlandi β chiqayotgan answerCallbackQuery da text va show_alert: true to'g'ri ketdi.
bot.callbackQuery filtri: satr, regex va bot.on("callback_query:data")¶
Tugmalar ko'paygach, har biriga alohida aniq satr yozish noqulay. grammY uch xil yo'l beradi:
// 1) Aniq satr mosligi
bot.callbackQuery("menu:home", (ctx) => ctx.answerCallbackQuery());
// 2) Regex β guruhlar ctx.match'da. ctx.match[1] = birinchi guruh (STRING!)
bot.callbackQuery(/^page:(\d+)$/, async (ctx) => {
const page = Number(ctx.match[1]); // "3" -> 3 (qo'lda Number())
await ctx.answerCallbackQuery();
// ...
});
// 3) Massiv β bir nechta aniq satr
bot.callbackQuery(["yes", "no"], (ctx) => ctx.answerCallbackQuery());
// 4) Hamma callback_data'li tugma (filtrsiz)
bot.on("callback_query:data", async (ctx) => {
const data = ctx.callbackQuery.data; // satr
await ctx.answerCallbackQuery();
});
Muhim nuans (OFFLINE tasdiqlandi): regex guruhi ctx.match[1] satr qaytaradi ("3", son emas). Raqamga aylantirish kerak bo'lsa, o'zingiz Number(ctx.match[1]) qiling. Bu β 04-bobdagi bot.hears(/echo (.+)/) dagi ctx.match bilan bir xil mantiq.
Eslatma:
bot.on("callback_query:data")β bu 04-bobdagi filter query (bot.on("message:text")kabi).callback_query:data"callback_query'dadatamaydoni bor" degani (o'yin tugmalarininggame_short_namei emas). Aksariyat botlar uchun aynan shu kerak.
callback_data dizayni: kalit:qiymat va 64-bayt cheklovi¶
Oddiy satr ("say_hi", "close") kichik botlarda yetadi. Lekin tugmaga bir nechta parametr yozish kerak bo'lsa-chi? Masalan "42-mahsulotni ko'rish"? Eng keng tarqalgan yondashuv β kalit:qiymat formati: ma'lumotni : bilan ajratib bitta satrga yig'ish, handlerda esa split(":") bilan parslash.
// Tugma yasashda β kodlash
const productId = 42;
const kb = new InlineKeyboard()
.text("Ko'rish", `prod:view:${productId}`)
.text("Sotib olish", `prod:buy:${productId}`);
// Handlerda β parslash
bot.on("callback_query:data", async (ctx) => {
const [ns, action, idStr] = ctx.callbackQuery.data.split(":");
if (ns !== "prod") return; // bizning emas
const id = Number(idStr); // STRING -> NUMBER, o'zimiz aylantiramiz
await ctx.answerCallbackQuery();
await ctx.editMessageText(`${id}-mahsulot, amal: ${action}`);
});
Eng muhim cheklov: callback_data 64 BAYTdan oshmasligi kerak (Telegram qoidasi). Oshsa, Telegram tugmani jimgina rad etadi β kodingiz xato bermaydi, lekin tugma "ishlamaydi" (klaviatura yuborishda 400 xatosi keladi). Shuning uchun callback'ga uzun matn solmang, balki id soling. Tekshirish uchun kichik yordamchi yozish foydali (OFFLINE tasdiqlandi):
function encodeCb(prefix, ...parts) {
const data = [prefix, ...parts].join(":");
if (Buffer.byteLength(data, "utf8") > 64) {
throw new Error("callback_data 64 baytdan oshdi: " + data);
}
return data;
}
encodeCb("prod", "view", 42); // "prod:view:42" (OK)
encodeCb("x", "y".repeat(70)); // Error: 64 baytdan oshdi
Diqqat β bayt, belgi emas:
Buffer.byteLength(data, "utf8")baytlarni hisoblaydi. Lotin harf 1 bayt, emoji 4 baytgacha bo'lishi mumkin. Shu sababdata.length(belgilar soni) emas, baytlarni tekshiring.Illustrativ β
@grammyjs/callback-data: Murakkab loyihalardacallback_datani qo'lda kodlash/parslash zerikarli bo'lib qoladi. grammY ekotizimida buni soddalashtirib, sxema bilan yasash uchun@grammyjs/callback-dataplagini bor (alohida paket). Bu yerda biz uni o'rnatmadik va sinamadik β faqat nomini eslatib o'tamiz; rasmiy hujjatdan o'rganing: grammy.dev. Bizning kitobda hamma joyda qo'ldakalit:qiymatyondashuvi yetarli.
Xabarni tahrirlash: editMessageText vs editMessageReplyMarkup¶
Inline tugmalar bilan ishlashda eng ko'p ishlatiladigan amallar β mavjud xabarni o'rnida o'zgartirish (yangi xabar yubormay). Ikki asosiy metod:
| Metod | Nimani o'zgartiradi | Qachon |
|---|---|---|
ctx.editMessageText(text, { reply_markup }) |
Xabar matni (va ixtiyoriy tugmalar) | Sahifalash, menyu o'tishlari |
ctx.editMessageReplyMarkup({ reply_markup }) |
Faqat tugmalar (matn tegmaydi) | Tugma holatini yangilash (like/ovoz sanog'i) |
// Matnni ham, tugmalarni ham yangilash
await ctx.editMessageText("Yangi sahifa matni", { reply_markup: newKb });
// Faqat tugmalarni yangilash (matn o'sha-o'sha)
await ctx.editMessageReplyMarkup({ reply_markup: newKb });
// Tugmalarni butunlay olib tashlash (markup'siz chaqiring)
await ctx.editMessageReplyMarkup();
grammY message_id ni ctx dan avtomatik oladi (OFFLINE tasdiqlandi: editMessageText chiqayotgan payload'da message_id: 50 β ya'ni callback kelgan xabar β avtomatik turdi). Siz faqat yangi matn/markup berasiz.
Stateless ovoz hisoblagichi (tekshirilgan)¶
callback_data ichiga joriy qiymatni yozib, har bosishda uni +1 qilib tugmani yangilash β bot hech qayerda holat saqlamasligini bildiradi (stateless). Kichik holatlar uchun ajoyib:
// vote.js β ovoz hisoblagichi (handler offline tekshirilgan)
import { Bot, InlineKeyboard } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
function voteKb(count) {
return new InlineKeyboard().text(`π ${count}`, `vote:${count}`);
}
bot.command("vote", (ctx) =>
ctx.reply("Ovoz bering:", { reply_markup: voteKb(0) }),
);
bot.callbackQuery(/^vote:(\d+)$/, async (ctx) => {
const next = Number(ctx.match[1]) + 1;
await ctx.editMessageReplyMarkup({ reply_markup: voteKb(next) });
await ctx.answerCallbackQuery({ text: `Ovoz: ${next}` });
});
bot.start();
OFFLINE natija (tugma vote:5 bosildi): chiqayotgan editMessageReplyMarkup payload'ida yangi tugma matni "π 6", callback_data esa "vote:6" bo'ldi; answerCallbackQuery toast matni "Ovoz: 6" ketdi.
Diqqat β "joriy hisob" qayerda? Bot hech qayerda joriy sonni saqlamayapti β qiymat har tugmaning
callback_datasiga yozilgan va keyingi bosishda undan o'qiladi. Bu juda yengil, lekin cheklangan: bir necha foydalanuvchi bir xabarga ovoz bersa, har biri o'z bosgan tugmasidagi sondan +1 qiladi (umumiy sanoq emas). Haqiqiy umumiy hisob uchun holatni serverda saqlash kerak β buni 10-bobda sessiya va DB bilan to'g'rilaymiz.
Gotcha: "message is not modified"¶
Telegram bir xil matn/markup bilan tahrirlashni rad etadi va 400: Bad Request: message is not modified qaytaradi. Masalan foydalanuvchi allaqachon ochiq sahifaning tugmasini qayta bossa. grammY'da bu GrammyError bo'lib otiladi (bot.catch ga boradi yoki try/catch bilan ushlanadi). Buni nazokat bilan yutish kerak:
import { GrammyError } from "grammy";
bot.callbackQuery(/^page:(\d+)$/, async (ctx) => {
const { text, kb } = renderPage(Number(ctx.match[1]));
try {
await ctx.editMessageText(text, { reply_markup: kb });
} catch (e) {
// "message is not modified" β bu xato emas, e'tiborsiz qoldiramiz
if (
e instanceof GrammyError &&
e.description.includes("message is not modified")
) {
// jim turamiz
} else {
throw e; // boshqa xatoni yashirmaymiz
}
}
await ctx.answerCallbackQuery();
});
OFFLINE tasdiqlandi: transformer editMessageText ga 400 + "message is not modified" qaytarganda, try/catch uni ushladi va yutdi (qayta otmadi), keyin answerCallbackQuery baribir chaqirildi.
Eslatma:
ctx.callbackQuery.messagejuda eski bo'lsa (Telegram'da ~48 soatdan oshgan), uni tahrirlab bo'lmaydi. Bunday holdactx.reply(...)bilan yangi xabar yuboring.
Sahifalash (pagination) inline tugmalar bilan¶
Bu β callback'larning eng amaliy qo'llanilishi. Ko'p elementli ro'yxat bor (mahsulotlar, postlar) β uni bir xabarga sig'dirib bo'lmaydi. Yechim: bir vaqtda bitta sahifa ko'rsatib, << / >> tugmalari bilan ulardan o'tish. Tugma bosilganda bitta xabar o'rnida yangilanadi.
Asosiy g'oya: har navigatsiya tugmasining callback_data siga maqsad sahifa raqami yoziladi (page:1, page:2, ...). Tugma bosilsa, handler shu raqamga mos sahifani chizadi va editMessageText qiladi.
Quyidagi pagination logikasi (chegaralar bilan: birinchi sahifada << yo'q, oxirgisida >> yo'q) OFFLINE tekshirildi:
// pagination.js β ro'yxatni sahifalash (logika offline tekshirilgan)
import { Bot, InlineKeyboard, GrammyError } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
// Namuna ma'lumot (real loyihada bu DB'dan keladi)
const ITEMS = Array.from({ length: 23 }, (_, i) => `Mahsulot ${i + 1}`);
const PER_PAGE = 5;
const totalPages = () => Math.ceil(ITEMS.length / PER_PAGE); // 5
function renderPage(page) {
const pages = totalPages();
page = Math.max(0, Math.min(page, pages - 1)); // chegaradan chiqmaslik
const start = page * PER_PAGE;
const chunk = ITEMS.slice(start, start + PER_PAGE);
const text =
`<b>Mahsulotlar</b> (sahifa ${page + 1}/${pages})\n\n` +
chunk.map((name) => `β’ ${name}`).join("\n");
const kb = new InlineKeyboard();
if (page > 0) kb.text("<<", `page:${page - 1}`);
kb.text(`${page + 1}/${pages}`, "noop"); // joriy sahifa (hech narsa qilmaydi)
if (page < pages - 1) kb.text(">>", `page:${page + 1}`);
return { text, kb };
}
bot.command("list", (ctx) => {
const { text, kb } = renderPage(0);
return ctx.reply(text, { parse_mode: "HTML", reply_markup: kb });
});
bot.callbackQuery(/^page:(\d+)$/, async (ctx) => {
const { text, kb } = renderPage(Number(ctx.match[1]));
try {
await ctx.editMessageText(text, { parse_mode: "HTML", reply_markup: kb });
} catch (e) {
if (
!(e instanceof GrammyError && e.description.includes("message is not modified"))
) {
throw e;
}
}
await ctx.answerCallbackQuery();
});
// o'rta tugma β faqat soatni to'xtatadi
bot.callbackQuery("noop", (ctx) => ctx.answerCallbackQuery());
bot.start();
OFFLINE tekshiruv natijalari (klaviatura quruvchidan o'qib olingan tugma matnlari):
totalPages()= 5 (23 element / 5 = yuqoriga yaxlitlab 5 sahifa).- Sahifa 0: tugmalar
["1/5", ">>"]β<<yo'q (to'g'ri, birinchi sahifa). - Sahifa 2:
["<<", "3/5", ">>"]β ikkala yo'nalish ham bor. - Sahifa 4 (oxirgi):
["<<", "5/5"]β>>yo'q; mahsulotlarMahsulot 21..23(oxirgi sahifada 3 ta). - Handler tekshiruvi:
page:1callback'i kelganda chiqayotganeditMessageTextmatnida"sahifa 2/5"va"Mahsulot 6"bor edi, so'nganswerCallbackQuerychaqirildi.nooptugma esa faqatanswerCallbackQueryqildi.
Maslahat: Real loyihada
ITEMSo'rniga DB'danLIMIT/OFFSETbilan faqat kerakli sahifani o'qing β butun ro'yxatni xotiraga yuklamang. SQL/baza bilan ishlash uchun ../sql/README.md ga qarang; biz buni 10-bobda bot ichida qilamiz.Python bilan solishtirish: aiogram (Python) kitobida xuddi shu sahifalash
CallbackDatafactory (pack()/unpack()) bilan tiplangan tarzda yasaladi. grammY'da tayyor factory o'rniga soddapage:${n}satri vaNumber(ctx.match[1])ishlatamiz β yengilroq, lekin tipni o'zingiz tiklaysiz. Taqqoslash: ../tgbot-python/README.md.
Inline rejim (bot.inlineQuery) β kirish¶
Hozirgacha bot bilan uning chatida ishladik. Inline rejim butunlay boshqacha tajriba beradi: foydalanuvchi istalgan chatda (do'sti bilan suhbatda, guruhda) @botingiz pizza deb yozadi va bot taklif qilgan natijalardan birini tanlab, o'sha chatga yuboradi β bot o'sha guruhga a'zo bo'lishi shart emas. Mashhur misol β @gif, @vid, @wiki botlari.
Foydalanuvchi @bot ... deb yozganda Telegram botga inline_query yangilanishini yuboradi. Bot unga natijalar ro'yxati bilan javob beradi.
Birinchi β yoqish. Inline rejim sukut bo'yicha o'chiq. @BotFather ga boring, /setinline ni tanlang, botingizni ko'rsating va placeholder matn bering (masalan "pizza qidiring..."). Busiz @bot ga hech qanday inline_query kelmaydi (jonli qadam β illustrativ).
Endi handler:
// inline.js β inline rejim qidiruvi (handler offline tekshirilgan)
import { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
const PRODUCTS = [
"Pepperoni pizza",
"Margarita pizza",
"Tort pizza",
"Lavash",
"Burger",
"Hot-dog",
];
// /.*/ -> har qanday so'rov (bo'sh ham). Odatda bitta umumiy handler bo'ladi.
bot.inlineQuery(/.*/, async (ctx) => {
const query = ctx.inlineQuery.query.toLowerCase().trim();
const found = query
? PRODUCTS.filter((p) => p.toLowerCase().includes(query))
: PRODUCTS;
// Telegram bir martada 50 tagacha natija qabul qiladi
const results = found.slice(0, 50).map((name, i) => ({
type: "article",
id: String(i), // natijaning NOYOB id si
title: name, // ro'yxatda ko'rinadigan sarlavha
description: `${name} buyurtma qilish`, // ostidagi kichik matn
input_message_content: {
message_text: `Men <b>${name}</b> tanladim!`,
parse_mode: "HTML",
},
}));
await ctx.answerInlineQuery(results, {
cache_time: 10, // Telegram natijani 10 soniya keshlaydi
is_personal: true, // kesh har foydalanuvchi uchun alohida
});
});
bot.start();
Tushuntirish:
bot.inlineQuery(/.*/, handler)βmessage/callback_queryemas,inline_queryyangilanishini ushlaydi. Argument β so'rov matniga mos regex (/.*/= hammasi). Regex guruhlarictx.matchda bo'ladi (bot.hearsdagidek).ctx.inlineQuery.queryβ@botdan keyingi matn (foydalanuvchi nima qidirayotgani).ctx.inlineQuery.offsetβ sahifalash uchun (uzun ro'yxatlarda; pastdagi mashqda).- natija obyekti β bu yerda
type: "article"(matnli maqola).id(noyob bo'lishi shart),title, ixtiyoriydescription, va majburiyinput_message_content(tanlanganda chatga nima yuborilishi). Boshqa turlari ham bor:type: "photo",type: "document"va h.k. input_message_content: { message_text }β tanlangan natija chatga matn sifatida yuboriladi.ctx.answerInlineQuery(results, { cache_time, is_personal })β natijalarni Telegram'ga qaytaradi.
OFFLINE tasdiqlandi: query="pizza" bilan handler 2 ta natija yasadi (Pepperoni pizza, Margarita pizza), chiqayotgan answerInlineQuery payload'ida results.length === 2, birinchi natija type: "article", title: "Pepperoni pizza", input_message_content.message_text: "Men Pepperoni pizza tanladim!" (HTML tagi sof matn sifatida saqlanadi, parse_mode bilan), va cache_time: 10, is_personal: true to'g'ri ketdi.
Regex guruhlari bilan ham ishlaydi (OFFLINE tasdiqlandi) β masalan kalkulyator:
bot.inlineQuery(/^son (\d+)$/, async (ctx) => {
const n = Number(ctx.match[1]); // "7" -> 7
await ctx.answerInlineQuery([
{
type: "article",
id: "1",
title: `Kvadrat: ${n * n}`,
input_message_content: { message_text: `${n}^2 = ${n * n}` },
},
]);
});
// mos kelmagan so'rovga ham javob shart -> bo'sh ro'yxat
bot.on("inline_query", (ctx) => ctx.answerInlineQuery([]));
Diqqat β har
inline_queryga javob bering: Agar regex mos kelmasa va boshqa handler ushlamasa, Telegram javobsiz qoladi. Shu sabab oxiridabot.on("inline_query", ...)bilan bo'sh ro'yxat qaytaradigan fallback qo'yish yaxshi odat. OFFLINE:son 7-> "Kvadrat: 49",salom-> bo'sh ro'yxat (fallback).Diqqat β soxta natija yo'q: Bu yerda biz handler funksiyasi to'g'ri ishlashini (natija obyektlari yasalishi,
answerInlineQuerychaqirilishi, payload tarkibi) offline tasdiqladik. Telefonda@bot pizzaqidiruvi haqiqatan ishlashi@BotFatherda/setinline+ jonli Telegram talab qiladi β buni kitobda "ishladi" deb yozmaymiz, o'z botingizda sinab ko'rasiz.
Hammasini ulash: kichik to'liq bot¶
Quyida callback_data kalit:qiymat, editMessageText, answerCallbackQuery(alert) va inline rejim bir joyda. Handler qismi offline tekshirilgan idiomlarga tayanadi; bot.start() (polling) β jonli, illustrativ.
// app.js β to'liq misol (handlerlar offline-tasdiqlangan idiom; polling jonli)
import { Bot, InlineKeyboard, GrammyError } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
const MENU = ["Profil", "Sozlamalar", "Yordam"];
function menuKb() {
const kb = new InlineKeyboard();
MENU.forEach((name) => kb.text(name, `menu:${name}`).row());
kb.text("O'chirish", "del");
return kb;
}
bot.command("start", (ctx) =>
ctx.reply("<b>Asosiy menyu</b>", { parse_mode: "HTML", reply_markup: menuKb() }),
);
// menu:<bo'lim> β bo'lim tafsiloti + "Orqaga"
bot.callbackQuery(/^menu:(.+)$/, async (ctx) => {
const item = ctx.match[1];
await ctx.answerCallbackQuery();
const back = new InlineKeyboard().text("β¬
οΈ Orqaga", "back");
try {
await ctx.editMessageText(`Siz <b>${item}</b> bo'limini tanladingiz.`, {
parse_mode: "HTML",
reply_markup: back,
});
} catch (e) {
if (!(e instanceof GrammyError && e.description.includes("message is not modified"))) {
throw e;
}
}
});
bot.callbackQuery("back", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.editMessageText("<b>Asosiy menyu</b>", {
parse_mode: "HTML",
reply_markup: menuKb(),
});
});
bot.callbackQuery("del", async (ctx) => {
await ctx.answerCallbackQuery({ text: "Xabar o'chirildi", show_alert: true });
await ctx.deleteMessage();
});
// Inline rejim β menyu bo'limlarini qidirish
bot.inlineQuery(/.*/, async (ctx) => {
const q = ctx.inlineQuery.query.toLowerCase().trim();
const items = q ? MENU.filter((m) => m.toLowerCase().includes(q)) : MENU;
const results = items.map((name, i) => ({
type: "article",
id: String(i),
title: name,
input_message_content: { message_text: `Bo'lim: ${name}` },
}));
await ctx.answerInlineQuery(results, { cache_time: 5, is_personal: true });
});
bot.start();
Eslatma β handler tartibi: grammY handlerlarni ro'yxatdan o'tgan tartibda tekshiradi (03-bobdagi Composer mantig'i). Aniqroq filtr (
bot.callbackQuery("back")) umumiyroq filtrdan (bot.callbackQuery(/^menu:(.+)$/)yokibot.on("callback_query:data")) oldin turishi kerak, aks holda umumiy handler avval ushlab oladi.
OFFLINE testni o'zingiz yozish¶
Spec'imizning oltin qoidasi: handlerni jonli Telegram'siz sinash. grammY'da bu juda toza β bot.handleUpdate(update) ga soxta callback_query update'ini beramiz va bot.api.config.use(transformer) bilan chiqayotgan API chaqiruvlarini ushlaymiz. (Bu naqshni 16-bobda Vitest bilan rasmiylashtiramiz.)
// verify.mjs β node verify.mjs bilan ishga tushadi (token+internet kerak emas)
import { Bot, InlineKeyboard } from "grammy";
import assert from "node:assert/strict";
const bot = new Bot("12345:FAKE-OFFLINE-TOKEN");
// init() tarmoqqa chiqmasligi uchun botInfo ni qo'lda beramiz:
bot.botInfo = {
id: 12345, is_bot: true, first_name: "T", username: "t_bot",
can_join_groups: true, can_read_all_group_messages: false,
supports_inline_queries: true, can_connect_to_business: false,
has_main_web_app: false,
};
const calls = [];
bot.api.config.use((prev, method, payload) => {
calls.push({ method, payload }); // chiqayotgan API'ni yozib olamiz
return Promise.resolve({ ok: true, result: true }); // soxta natija
});
// SINALADIGAN handler:
bot.callbackQuery(/^vote:(\d+)$/, async (ctx) => {
const next = Number(ctx.match[1]) + 1;
await ctx.editMessageReplyMarkup({
reply_markup: new InlineKeyboard().text(`π ${next}`, `vote:${next}`),
});
await ctx.answerCallbackQuery({ text: `Ovoz: ${next}` });
});
// soxta callback_query update (tugma bosilgan):
await bot.handleUpdate({
update_id: 1,
callback_query: {
id: "cbq1",
from: { id: 777, is_bot: false, first_name: "Ali" },
chat_instance: "ci",
data: "vote:5",
message: {
message_id: 50, date: 0,
chat: { id: 777, type: "private" },
from: { id: 12345, is_bot: true, first_name: "T", username: "t_bot" },
text: "Ovoz bering:",
},
},
});
assert.equal(calls[0].method, "editMessageReplyMarkup");
assert.equal(calls[0].payload.reply_markup.inline_keyboard[0][0].callback_data, "vote:6");
assert.equal(calls[1].method, "answerCallbackQuery");
assert.equal(calls[1].payload.text, "Ovoz: 6");
console.log("O'TDI: vote:5 -> tugma 'vote:6', toast 'Ovoz: 6'");
Pattern kaliti:
bot.botInfo = {...}βbot.init()nigetMeuchun tarmoqqa chiqishdan to'xtatadi (soxta token bilan ulanmaymiz).bot.api.config.use(transformer)β chiqayotgan har bir API chaqiruvini ushlaydi, biz qaysi metod va payload borligini tekshiramiz.callback_queryupdate βmessage:texto'rnigacallback_queryobyekti;datamaydoni tugmaningcallback_datasiga teng. (Buyruq update'idan farqi: bu yerdabot_commandentity kerak emas β u faqat/startkabi buyruqlar uchun edi.)
Bu bobdagi 13 ta tekshiruv aynan shu naqsh bilan yozilib, 13/13 o'tdi.
Tez-tez uchraydigan xatolar¶
| Xato | Sabab | Yechim |
|---|---|---|
| Tugmada soat aylanaveradi | ctx.answerCallbackQuery() chaqirilmagan |
HAR callback handlerda chaqiring (bo'sh bo'lsa ham) |
| Tugma "ishlamaydi", xato yo'q | callback_data 64 baytdan oshgan |
Buffer.byteLength(data,"utf8") <= 64 ni ta'minlang; uzun matn emas, id soling |
400: message is not modified |
bir xil matn/markup bilan editMessageText |
try/catch + GrammyError da description.includes("message is not modified") ni yuting |
ctx.match[1] raqam kutilgan, lekin satr |
regex guruhi har doim string qaytaradi | Number(ctx.match[1]) bilan aylantiring |
@bot qidiruvi ishlamaydi |
inline rejim yoqilmagan | @BotFather da /setinline bilan yoqing |
| Inline natija chiqmaydi / Telegram rad etadi | id takrorlangan yoki input_message_content yo'q |
har natijaga noyob id; article da input_message_content MAJBURIY |
inline_query javobsiz qoladi |
regex mos kelmadi, fallback yo'q | oxirida bot.on("inline_query", c => c.answerInlineQuery([])) qo'shing |
| Telegraf/node-telegram-bot-api idiomlari | boshqa kutubxona misoli | grammY: bot.callbackQuery, ctx.answerCallbackQuery, bot.inlineQuery |
Mashqlar¶
Quyidagi mashqlarning aksariyati OFFLINE tekshiriladi β yuqoridagi "OFFLINE testni o'zingiz yozish" naqshidan foydalaning (bot.botInfo + transformer + bot.handleUpdate(callback_query)).
Oson¶
- Ikkita tugmali (
callback_data"yes"va"no") klaviatura yasab, "yes" bosilganda toast"Tasdiqlandi", "no" bosilganda alert"Bekor qilindi"(show_alert: true) chiqaradigan handlerlar yozing. - Bitta tugmali "like" yozing: bosilganda
editMessageReplyMarkupbilan matnini"π N"dan"π N+1"ga o'zgartiradi (callback_dataichida sanoq yuradi, masalanlike:N). ctx.from.first_namedan foydalanib, tugma bosilganda"Salom, <ism>!"debeditMessageTextqiladigan handler yozing.callback_datani"clr:red"formatida yasang; handlerdasplit(":")bilan rangni ajratib oling vactx.answerCallbackQuery({ text: "Tanlangan rang: " + rang })qiling. OFFLINE: toast matni"Tanlangan rang: red"ekanini tasdiqlang.encodeCbyordamchisini yozing (yuqoridagidek):"prod:view:42"ni qaytarsin, 70 belgilik qiymatdaErrortashlasin.assertbilan ikkala holatni tekshiring.
O'rta¶
- 30 elementli ro'yxatni 6 tadan sahifalang (5 sahifa).
renderPage(2)qaysi elementlarni qaytarishini (Element 13..18) va navigatsiya tugmalari matnini (["<<", "3/5", ">>"])assertbilan tekshiring. - Pagination handleriga
"message is not modified"ni ushlovchitry/catch(+GrammyError) qo'shing. OFFLINE: transformereditMessageTextga 400 qaytarsa ham handler yiqilmasligini vaanswerCallbackQuerybaribir chaqirilishini tasdiqlang. callback_datani"item:5"va"item:5:3"(id va ixtiyoriy qty) formatida parslang:split(":")natijasiga qarabqtybor-yo'qligini aniqlang (qtybo'lmasanull). Ikkala satr uchun parslangan obyektniassertbilan tekshiring.- Inline qidiruv handlerini yozing: 10 ta shahar ro'yxatidan
ctx.inlineQuery.queryga mos kelganlarinitype: "article"natijalar qilib qaytaring (bo'sh so'rovda hammasini). OFFLINE:query="tosh"bilanToshkentqaytishini tasdiqlang. - Ovoz hisoblagichiga "Reset" tugmasini qo'shing β bosilganda
editMessageReplyMarkupbilan qiymatni0ga qaytaradi vashow_alert: truebilan alert ko'rsatadi. Diqqat: alert matnini lotin alifbosida ("Nollandi") yozing, kirillda emas. OFFLINE: yangi tugmacallback_datasi"vote:0"ekanini tasdiqlang.
Qiyin¶
- Inline rejimda
offsetbilan sahifalashni amalga oshiring: 100 elementli ro'yxatni 50 tadan ikki "sahifa" qilib,ctx.answerInlineQuery(results, { next_offset })bilan qaytaring. Birinchi javobdanext_offset: "50", ikkinchisidanext_offset: ""(boshqa yo'q). OFFLINE: bo'shoffsetda 50 ta natija vanext_offset === "50",offset="50"da 50 ta natija vanext_offset === ""ekanini tasdiqlang. - "Savat" botini yozing:
callback_data"cart:add:<id>"/"cart:remove:<id>". Tugmalar savatga qo'shadi/oladi vaeditMessageTextbilan joriy savat tarkibini ko'rsatadi. Savatni xotiradaMap(userId -> { productId: qty }) da saqlang (vaqtinchalik β 10-bobda DB bilan to'g'rilanadi). OFFLINE: bir foydalanuvchicart:add:1ni ikki marta bossa, savatda 1-mahsulotx2bo'lishini tasdiqlang. - To'liq OFFLINE test yozing: 6-mashqdagi pagination handleringizni
bot.handleUpdatebilan sinang βpage:0xabaridanpage:1ga o'tilganda chiqayotganeditMessageTextmatnida"sahifa 2/5"borligini va so'nganswerCallbackQuerychaqirilishiniassertbilan tasdiqlang.
Yechimlar
1-mashq yechimi¶
import { Bot, InlineKeyboard } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
bot.command("confirm", (ctx) =>
ctx.reply("Tasdiqlaysizmi?", {
reply_markup: new InlineKeyboard().text("Ha", "yes").text("Yo'q", "no"),
}),
);
bot.callbackQuery("yes", (ctx) => ctx.answerCallbackQuery({ text: "Tasdiqlandi" })); // toast
bot.callbackQuery("no", (ctx) =>
ctx.answerCallbackQuery({ text: "Bekor qilindi", show_alert: true }),
); // alert
Toast β show_alert bermasdan; alert β show_alert: true.
2-mashq yechimi¶
function likeKb(count) {
return new InlineKeyboard().text(`π ${count}`, `like:${count}`);
}
bot.command("like", (ctx) => ctx.reply("Yoqdimi?", { reply_markup: likeKb(0) }));
bot.callbackQuery(/^like:(\d+)$/, async (ctx) => {
const next = Number(ctx.match[1]) + 1;
await ctx.editMessageReplyMarkup({ reply_markup: likeKb(next) });
await ctx.answerCallbackQuery();
});
callback_data ichida joriy sanoq yuradi; regex guruhdan o'qib, +1 qilamiz va editMessageReplyMarkup bilan tugmani yangilaymiz.
3-mashq yechimi¶
bot.callbackQuery("greet", async (ctx) => {
const name = ctx.from.first_name; // tugmani BOSGAN foydalanuvchi
await ctx.answerCallbackQuery();
await ctx.editMessageText(`Salom, ${name}!`);
});
ctx.from β tugmani bosgan foydalanuvchi (xabar egasi β botning o'zi emas).
4-mashq yechimi¶
bot.on("callback_query:data", async (ctx) => {
const [ns, value] = ctx.callbackQuery.data.split(":");
if (ns !== "clr") return;
await ctx.answerCallbackQuery({ text: "Tanlangan rang: " + value });
});
// Tugma: new InlineKeyboard().text("Qizil", "clr:red")
OFFLINE tekshirilganda clr:red callback'i kelsa, answerCallbackQuery payload'ida text === "Tanlangan rang: red" bo'ladi.
5-mashq yechimi¶
import assert from "node:assert/strict";
function encodeCb(prefix, ...parts) {
const data = [prefix, ...parts].join(":");
if (Buffer.byteLength(data, "utf8") > 64) {
throw new Error("callback_data 64 baytdan oshdi: " + data);
}
return data;
}
assert.equal(encodeCb("prod", "view", 42), "prod:view:42");
assert.throws(() => encodeCb("x", "y".repeat(70)), /64 baytdan oshdi/);
console.log("O'TDI");
Buffer.byteLength(...,"utf8") baytlarni hisoblaydi (belgi emas) β emoji yoki uzun matn solinsa, oldindan ushlaymiz.
6-mashq yechimi¶
import assert from "node:assert/strict";
import { InlineKeyboard } from "grammy";
const ITEMS = Array.from({ length: 30 }, (_, i) => `Element ${i + 1}`);
const PER_PAGE = 6;
const totalPages = () => Math.ceil(ITEMS.length / PER_PAGE); // 5
function renderPage(page) {
const pages = totalPages();
page = Math.max(0, Math.min(page, pages - 1));
const start = page * PER_PAGE;
const chunk = ITEMS.slice(start, start + PER_PAGE);
const kb = new InlineKeyboard();
if (page > 0) kb.text("<<", `page:${page - 1}`);
kb.text(`${page + 1}/${pages}`, "noop");
if (page < pages - 1) kb.text(">>", `page:${page + 1}`);
return { chunk, kb };
}
const labels = (kb) => kb.inline_keyboard[0].map((b) => b.text);
const p2 = renderPage(2);
assert.deepEqual(p2.chunk, ["Element 13", "Element 14", "Element 15", "Element 16", "Element 17", "Element 18"]);
assert.deepEqual(labels(p2.kb), ["<<", "3/5", ">>"]);
console.log("O'TDI");
2-sahifa (0-indeksli) = elementlar 13..18. O'rta sahifada ikkala navigatsiya tugmasi ham bor.
7-mashq yechimi¶
import { GrammyError } from "grammy";
bot.callbackQuery(/^page:(\d+)$/, async (ctx) => {
const { text, kb } = renderPage(Number(ctx.match[1]));
try {
await ctx.editMessageText(text, { reply_markup: kb });
} catch (e) {
if (!(e instanceof GrammyError && e.description.includes("message is not modified"))) {
throw e; // boshqa xatoni yashirmaymiz
}
}
await ctx.answerCallbackQuery();
});
OFFLINE: transformer'da editMessageText uchun { ok:false, error_code:400, description:"Bad Request: message is not modified" } qaytaring. Handler bu xatoni yutadi (qayta otmaydi) va answerCallbackQuery baribir chaqiriladi. Foydalanuvchi joriy ochiq sahifani qayta bosgan holatda aynan shu yuz beradi β bu xato emas.
8-mashq yechimi¶
import assert from "node:assert/strict";
function parseItem(data) {
const parts = data.split(":"); // ["item","5"] yoki ["item","5","3"]
return {
id: Number(parts[1]),
qty: parts[2] !== undefined ? Number(parts[2]) : null,
};
}
assert.deepEqual(parseItem("item:5"), { id: 5, qty: null });
assert.deepEqual(parseItem("item:5:3"), { id: 5, qty: 3 });
console.log("O'TDI");
split(":") natijasi uzunligiga qarab qty ixtiyoriy. Bu β Python aiogram'dagi CallbackData Optional maydoniga teng yondashuv, lekin qo'lda.
9-mashq yechimi¶
import { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
const CITIES = ["Toshkent", "Samarqand", "Buxoro", "Xiva", "Andijon",
"Namangan", "Farg'ona", "Nukus", "Qarshi", "Termiz"];
bot.inlineQuery(/.*/, async (ctx) => {
const q = ctx.inlineQuery.query.toLowerCase().trim();
const found = q ? CITIES.filter((c) => c.toLowerCase().includes(q)) : CITIES;
const results = found.map((city, i) => ({
type: "article",
id: String(i),
title: city,
input_message_content: { message_text: `Shahar: ${city}` },
}));
await ctx.answerInlineQuery(results, { cache_time: 10, is_personal: true });
});
OFFLINE: query="tosh" bilan natijalar massivida title: "Toshkent" bo'ladi (bo'sh so'rovda hamma 10 shahar).
10-mashq yechimi¶
import { InlineKeyboard } from "grammy";
function voteKb(count) {
return new InlineKeyboard()
.text(`π ${count}`, `vote:${count}`)
.text("π Reset", "vote:reset");
}
bot.callbackQuery("vote:reset", async (ctx) => {
await ctx.editMessageReplyMarkup({ reply_markup: voteKb(0) });
await ctx.answerCallbackQuery({ text: "Nollandi", show_alert: true });
});
Diqqat:
"vote:reset"handleri/^vote:(\d+)$/regexidan oldin turishi kerak (yoki regex faqat raqam ushlaganidan reset baribir tushib qoladi β\d+"reset" ga mos kelmaydi, shuning uchun bu yerda tartib muhim emas, lekin odat sifatida aniqni oldinga qo'ying). Alert matni lotin alifbosida β"Nollandi". OFFLINE: yangi tugmacallback_datasi"vote:0"bo'ladi.
11-mashq yechimi¶
import { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
const BIG = Array.from({ length: 100 }, (_, i) => `Element ${i + 1}`);
const PAGE = 50;
bot.inlineQuery(/.*/, async (ctx) => {
const offset = ctx.inlineQuery.offset ? Number(ctx.inlineQuery.offset) : 0;
const chunk = BIG.slice(offset, offset + PAGE);
const results = chunk.map((name, i) => ({
type: "article",
id: String(offset + i),
title: name,
input_message_content: { message_text: name },
}));
const next_offset = offset + PAGE < BIG.length ? String(offset + PAGE) : "";
await ctx.answerInlineQuery(results, { cache_time: 10, is_personal: true, next_offset });
});
Foydalanuvchi natijalar ro'yxatini pastga skroll qilganda Telegram avval bergan next_offset qiymati bilan yangi inline_query yuboradi. next_offset: "" (bo'sh) β "boshqa natija yo'q" degani. OFFLINE: bo'sh offset -> 50 natija + next_offset: "50"; offset="50" -> 50 natija + next_offset: "".
12-mashq yechimi¶
import { Bot, InlineKeyboard, GrammyError } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
const PRODUCTS = { 1: "Olma", 2: "Banan", 3: "Uzum" };
// Vaqtinchalik savat: userId -> { productId: qty }. 10-bobda DB bilan to'g'rilanadi.
const CARTS = new Map();
function cartText(userId) {
const cart = CARTS.get(userId) ?? {};
const entries = Object.entries(cart);
if (entries.length === 0) return "<b>Savat bo'sh.</b>";
const lines = entries.map(([pid, qty]) => `β’ ${PRODUCTS[pid]} x${qty}`);
return "<b>Savat:</b>\n" + lines.join("\n");
}
function cartKb() {
const kb = new InlineKeyboard();
for (const [pid, name] of Object.entries(PRODUCTS)) {
kb.text(`β ${name}`, `cart:add:${pid}`).text(`β ${name}`, `cart:remove:${pid}`).row();
}
return kb;
}
bot.command("cart", (ctx) =>
ctx.reply(cartText(ctx.from.id), { parse_mode: "HTML", reply_markup: cartKb() }),
);
bot.callbackQuery(/^cart:(add|remove):(\d+)$/, async (ctx) => {
const action = ctx.match[1];
const pid = ctx.match[2]; // string kalit (obyekt kaliti)
const uid = ctx.from.id;
const cart = CARTS.get(uid) ?? {};
if (action === "add") {
cart[pid] = (cart[pid] ?? 0) + 1;
} else {
if (cart[pid]) {
cart[pid] -= 1;
if (cart[pid] <= 0) delete cart[pid];
}
}
CARTS.set(uid, cart);
await ctx.answerCallbackQuery();
try {
await ctx.editMessageText(cartText(uid), { parse_mode: "HTML", reply_markup: cartKb() });
} catch (e) {
if (!(e instanceof GrammyError && e.description.includes("message is not modified"))) throw e;
}
});
OFFLINE: cart:add:1 ni ikki marta yuborsangiz, CARTS.get(uid) { "1": 2 } bo'ladi va cartText "β’ Olma x2" chiqaradi. Savat xotirada β bot qayta ishga tushsa yo'qoladi (10-bobda DB bilan to'g'rilanadi).
13-mashq yechimi¶
// test_pagination.mjs β node test_pagination.mjs
import { Bot, InlineKeyboard } from "grammy";
import assert from "node:assert/strict";
const ITEMS = Array.from({ length: 30 }, (_, i) => `Element ${i + 1}`);
const PER_PAGE = 6;
const totalPages = () => Math.ceil(ITEMS.length / PER_PAGE);
function renderPage(page) {
const pages = totalPages();
page = Math.max(0, Math.min(page, pages - 1));
const chunk = ITEMS.slice(page * PER_PAGE, (page + 1) * PER_PAGE);
const text = `Sahifa ${page + 1}/${pages}: ` + chunk.join(", ");
const kb = new InlineKeyboard();
if (page > 0) kb.text("<<", `page:${page - 1}`);
if (page < pages - 1) kb.text(">>", `page:${page + 1}`);
return { text, kb };
}
const bot = new Bot("12345:FAKE");
bot.botInfo = {
id: 12345, is_bot: true, first_name: "T", username: "t_bot",
can_join_groups: true, can_read_all_group_messages: false,
supports_inline_queries: true, can_connect_to_business: false, has_main_web_app: false,
};
const calls = [];
bot.api.config.use((prev, method, payload) => {
calls.push({ method, payload });
return Promise.resolve({ ok: true, result: true });
});
bot.callbackQuery(/^page:(\d+)$/, async (ctx) => {
const { text, kb } = renderPage(Number(ctx.match[1]));
await ctx.editMessageText(text, { reply_markup: kb });
await ctx.answerCallbackQuery();
});
await bot.handleUpdate({
update_id: 1,
callback_query: {
id: "q", from: { id: 2, is_bot: false, first_name: "A" }, chat_instance: "ci",
data: "page:1",
message: { message_id: 1, date: 0, chat: { id: 1, type: "private" },
from: { id: 12345, is_bot: true, first_name: "T", username: "t_bot" }, text: "Sahifa 1/5: ..." },
},
});
assert.equal(calls[0].method, "editMessageText");
assert.ok(calls[0].payload.text.includes("Sahifa 2/5"));
assert.equal(calls[1].method, "answerCallbackQuery");
console.log("O'TDI: page:1 -> editMessageText(Sahifa 2/5) + answerCallbackQuery");
Transformer API chaqiruvlarini yozadi; biz editMessageText (sahifa o'tdi, matnda "Sahifa 2/5") va answerCallbackQuery (soat to'xtatildi) chaqirilganini tasdiqlaymiz β jonli Telegram'siz.
β¬ οΈ Oldingi: 06 β Klaviaturalar: reply va inline Β· π README Β· Keyingi: 08 β Conversations β suhbatlar β‘οΈ