05 β Xabar yuborish, formatlash va media¶
β¬ οΈ Oldingi: 04 β Filtrlar va buyruqlar Β· π README Β· Keyingi: 06 β Klaviaturalar: reply va inline β‘οΈ
Bu bobda: botning eng asosiy ishini β javob qaytarishni β chuqur o'rganamiz.
ctx.replyning barcha imkoniyatlari (matnga javob qilish,reply_parameters), matnni chiroyli formatlash (qalin, qiya, kod, havola) ikki uslub orqali βparse_mode: "HTML"vaMarkdownV2β va nega HTML'ni tavsiya qilishimiz. So'ng media: rasm, hujjat, video, audio, ovoz, sticker yuborish;InputFileorqali fayl manbalari (lokal yo'l,Buffer, URL,file_id); bir nechta rasmni bitta albom qilib yuborish (replyWithMediaGroup); vafile_idnega tejamkor ekani. Yo'l-yo'lakay tez-tez uchraydigan xatolarni jadval qilamiz.Halollik eslatmasi: bu bobdagi handler kodi β
ctx.replyningparse_modevareply_parametersqabul qilishi,replyWithPhoto/replyWithDocument/replyWithVideo/replyWithAudio/replyWithVoice,replyWithMediaGroupqaysi Telegram metodiga (sendPhoto,sendMediaGroup...) va qanday payload bilan ketishi,InputFileva URL/file_idstring'ning farqi, MarkdownV2 escaping natijasi β offline ishga tushirib tekshirilgan: soxtaUpdatenibot.handleUpdatega uzatib va chiqayotgan API chaqiruvlarini transformer bilan ushlab (10/10 PASS, grammY 1.43.0). Jonli yuborish β haqiqiy rasm/fayl Telegram'ga ketishi,file_idning haqiqiy qiymati β token va internet talab qiladi; bunday joylar matnda Illustrativ deb belgilangan.
1. ctx.reply β eng ko'p ishlatadigan metod¶
02-bobda ctx.reply("Salom!") bilan tanishdik. Endi uning to'liq imkoniyatlarini ochamiz. ctx.reply ostida Telegram'ning sendMessage metodi turadi va u shunchaki ctx.api.sendMessage(ctx.chat.id, text, options) ning qisqartmasi: chat_id ni grammY joriy chatdan avtomatik oladi.
import { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
bot.command("start", (ctx) => ctx.reply("Assalomu alaykum!"));
bot.on("message:text", (ctx) => ctx.reply("Siz yozdingiz: " + ctx.message.text));
bot.start();
ctx.reply(text, options) ikkinchi argument sifatida options obyekti oladi. Eng muhim variantlar: parse_mode (formatlash), reply_parameters (xabarga javob), reply_markup (klaviatura β 06-bob), link_preview_options (havola oldindan ko'rinishi), disable_notification (jimgina yuborish).
Eslatma:
ctx.replydoim Promise qaytaradi. Agar javob xabariningmessage_idsi kerak bo'lsa (keyin tahrirlash yoki o'chirish uchun),awaitqiling:const sent = await ctx.reply("...")β keyinsent.message_id.
ctx.reply va ctx.api.sendMessage farqi¶
ctx.reply faqat joriy chatga javob beradi. Boshqa chatga (masalan, admin'ga xabar yuborish) kerak bo'lsa, chat_id ni o'zingiz beradigan ctx.api.sendMessage dan foydalaning:
bot.command("xabar", (ctx) => {
// Joriy chatga β qisqa yo'l:
ctx.reply("Sizga javob.");
// Boshqa (admin) chatga β chat_id o'zimiz beramiz:
ctx.api.sendMessage(123456789, "Yangi foydalanuvchi keldi: " + ctx.from.first_name);
});
Eslatma:
ctx.apiβ botning to'liq Telegram Bot API'siga ochiq darvozasi. Handler'dan tashqarida ham (masalan, rejali vazifada β 15-bob)bot.api.sendMessage(...)ishlaydi, chunkictxyo'q.ctx.apivabot.apiβ bir xil narsa.
2. Xabarga javob qilish β reply_parameters¶
Telegram'da xabarni "iqtibos qilib" javob berish mumkin (chat'da javob strelkasi bilan ko'rinadi). Buni reply_parameters: { message_id } bilan qilamiz:
bot.on("message:text", (ctx) => {
ctx.reply("Bu sizning xabaringizga javob.", {
reply_parameters: { message_id: ctx.msg.message_id },
});
});
ctx.msg β joriy xabar (yangilanish message, edited_message, channel_post bo'lishidan qat'i nazar mavjud bo'lgan xabar). ctx.msg.message_id β o'sha xabarning ID si. Shu ID ni reply_parameters.message_id ga bersak, javobimiz aynan o'sha xabarga ulanadi.
Diqqat: eski grammY/Telegraf misollarida
reply_to_message_idni ko'rasiz. Telegram uni eskirtirgan (deprecated). grammY 1.43.0'da to'g'ri yo'l βreply_parameters: { message_id }. Yangireply_parametersboshqa chatga javob qilish (chat_id), iqtibos qismi (quote) kabi imkoniyatlarni ham beradi.
Boshqa chatdagi xabarga javob qilish (kamdan-kam, lekin mumkin):
Men buni offline tekshirdim: reply_parameters: { message_id: 42 } bilan yuborilgan ctx.reply ning payload'ida reply_parameters.message_id === 42 chiqdi.
3. Matnni formatlash: parse_mode¶
Telegram xabar matnini chiroyli qiladi: qalin, qiya, kod, havolalar. Buning ikki yo'li bor va siz parse_mode orqali qaysi birini ishlatishni aytasiz: "HTML" yoki "MarkdownV2".
3.1. HTML uslubi (tavsiya etamiz)¶
bot.command("html", (ctx) => {
ctx.reply(
"<b>Qalin</b>, <i>qiya</i>, <u>tagchiziq</u>, <s>chizilgan</s>\n" +
"<code>inline kod</code>\n" +
'<a href="https://grammy.dev">grammY hujjati</a>',
{ parse_mode: "HTML" }
);
});
HTML'da quyidagi teglar ishlaydi:
| Teg | Natija |
|---|---|
<b>...</b> yoki <strong> |
qalin |
<i>...</i> yoki <em> |
qiya |
<u>...</u> |
tagchiziq |
<s>...</s> yoki <del> |
chizilgan |
<code>...</code> |
bir qatorli kod |
<pre>...</pre> |
ko'p qatorli kod bloki |
<pre><code class="language-js">...</code></pre> |
tilga ko'ra ranglangan blok |
<a href="URL">matn</a> |
havola |
<tg-spoiler>...</tg-spoiler> |
yashirin (spoiler) matn |
<blockquote>...</blockquote> |
iqtibos |
HTML'da faqat 3 ta belgi qochiriladi β chunki ular HTML teg sintaksisi bilan to'qnashadi: < -> <, > -> >, & -> &. Ana shuning uchun HTML'ni boshlovchiga tavsiya qilamiz: dinamik matn (foydalanuvchi ismi, narx, nuqtali son) deyarli hech qachon muammo bermaydi.
Foydalanuvchidan kelgan matnni HTML ichiga qo'yganda uni qochirish kichik funksiya bilan:
function escapeHtml(s) {
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
bot.on("message:text", (ctx) => {
const xavfsiz = escapeHtml(ctx.message.text);
ctx.reply(`Siz yozdingiz: <b>${xavfsiz}</b>`, { parse_mode: "HTML" });
});
Diqqat:
&ni birinchi qochiring, aks holda keyin qo'ygan<dagi&ham qayta qochirilib&lt;bo'lib qoladi. Yuqoridagi tartib (&-> keyin<,>) to'g'ri.
3.2. MarkdownV2 uslubi (ehtiyot bilan)¶
bot.command("md", (ctx) => {
ctx.reply("*Qalin*, _qiya_, `kod`, [havola](https://grammy.dev)", {
parse_mode: "MarkdownV2",
});
});
MarkdownV2 belgilari: *qalin*, _qiya_, __tagchiziq__, ~chizilgan~, `kod`, ```blok```, [matn](url), ||spoiler||.
Muammo shundaki, MarkdownV2'da juda ko'p belgi maxsus va matn ichida uchrasa, \ bilan qochirilishi shart:
Ya'ni oddiy Narx: 3.50 (chegirma!) jumlasidagi nuqta, qavslar va undov β barchasi qochirilishi kerak. Qo'lda funksiya:
function escapeMarkdownV2(s) {
return s.replace(/[_*\[\]()~`>#+\-=|{}.!\\]/g, (c) => "\\" + c);
}
bot.command("narx", (ctx) => {
const matn = escapeMarkdownV2("Narx: 3.50 (chegirma!)");
// -> "Narx: 3\.50 \(chegirma\!\)"
ctx.reply(matn, { parse_mode: "MarkdownV2" });
});
Men buni offline ishlatib ko'rdim: kirish "Narx: 3.50 (chegirma!)", chiqish "Narx: 3\.50 \(chegirma\!\)" β nuqta, qavs va undov to'g'ri qochirilgan, parse_mode payload'da "MarkdownV2" bo'ldi.
Diqqat: agar maxsus belgini unutib qochirmasangiz, Telegram butun xabarni rad etadi:
Bad Request: can't parse entities. Bu bobning eng ko'p uchraydigan xatosi. Shu sababli β dinamik matn ko'p bo'lsa, HTML'ni tanlang.
3.3. @grammyjs/parse-mode plagini (qisqacha eslatma)¶
Qo'lda escaping zerikarli. grammY'ning rasmiy @grammyjs/parse-mode plagini fmt shablon-funksiyasini beradi: u o'rnatilgan matnni avtomatik qochiradi va siz format va matnni aralashtirasiz:
// Illustrativ β bu alohida paket: npm i @grammyjs/parse-mode
import { bold, fmt } from "@grammyjs/parse-mode";
bot.command("fmt", async (ctx) => {
const ism = ctx.from.first_name; // ehtimol maxsus belgili
await ctx.reply(...fmt`Salom, ${bold(ism)}! Narx: 3.50 (chegirma!)`);
});
Illustrativ:
@grammyjs/parse-modealohida paket β bu kitobning asosiy oqimida o'rnatmaymiz, shu bois yuqoridagi blokni faqat eslatma sifatida ko'rsatdik. Boshlovchi uchunparse_mode: "HTML"+ kichikescapeHtmlyetarli va ishonchli. Plagin kerak bo'lsa, hujjati: https://grammy.dev/plugins/parse-mode .
4. Media yuborish: rasm, hujjat, video, audio, ovoz¶
Matndan keyin β fayllar. grammY har media turi uchun qulay replyWithX metodini beradi. Hammasi bitta naqshga bo'ysunadi: ctx.replyWithX(manba, options), bu yerda manba β fayl (InputFile, URL yoki file_id), options β caption, parse_mode va h.k.
| Tur | Metod | Telegram metodi | Caption? |
|---|---|---|---|
| Rasm | ctx.replyWithPhoto |
sendPhoto |
Ha |
| Hujjat | ctx.replyWithDocument |
sendDocument |
Ha |
| Video | ctx.replyWithVideo |
sendVideo |
Ha |
| Audio (musiqa) | ctx.replyWithAudio |
sendAudio |
Ha |
| Ovozli xabar | ctx.replyWithVoice |
sendVoice |
Ha |
| Sticker | ctx.replyWithSticker |
sendSticker |
Yo'q |
replyWithPhoto rasmni siqadi (chat'da rasm bo'lib ko'rinadi). Asl sifat saqlanishi kerak bo'lsa (masalan PDF, ZIP, yoki katta PNG), replyWithDocument ishlating β u faylni qayta ishlamasdan jo'natadi.
Men offline tekshirganimda replyWithDocument, replyWithVideo, replyWithAudio, replyWithVoice mos ravishda sendDocument, sendVideo, sendAudio, sendVoice metodlariga ketdi va caption payload'da saqlandi.
4.1. InputFile β fayl manbasi¶
Lokal fayl yoki xotiradagi ma'lumotni yuborish uchun grammY'ning InputFile klassini import qilamiz:
Manbalarning to'rt turi:
// 1) Lokal fayl yo'li β disk'dan o'qiladi va yuklab beriladi
bot.command("rasm", (ctx) =>
ctx.replyWithPhoto(new InputFile("rasmlar/logo.png"), { caption: "Bizning logo" })
);
// 2) Buffer (xotirada yaratilgan ma'lumot β masalan QR yoki grafik)
import { Buffer } from "node:buffer";
bot.command("buf", (ctx) => {
const buf = Buffer.from("Salom, bu matnli fayl!", "utf-8");
return ctx.replyWithDocument(new InputFile(buf, "salom.txt"));
});
// 3) URL string β to'g'ridan-to'g'ri, InputFile YO'Q (Telegram o'zi yuklab oladi)
bot.command("url", (ctx) =>
ctx.replyWithPhoto("https://grammy.dev/images/grammY.png", { caption: "Internetdan" })
);
// 4) file_id string β eng tejamkor (4.3-bo'limda)
bot.command("qayta", (ctx) => ctx.replyWithPhoto("AgACAgIAAxk...")); // saqlangan file_id
Diqqat: URL va
file_idβ oddiy string sifatida beriladi,new InputFile(...)ichiga o'ralmaydi.new InputFile("https://...")ni Telegram URL emas, fayl nomi deb tushunadi va xato beradi. Eng tez-tez uchraydigan boshlovchi xatosi:ctx.replyWithPhoto("rasm.jpg")deb yozish β bu lokal fayl emas, Telegram unifile_iddeb o'qiydi va topolmaydi. Lokal fayl uchun albattanew InputFile("rasm.jpg").
Men offline tekshirdim: new InputFile("rasm.jpg") bilan yuborilgan replyWithPhoto payload'ining photo maydoni haqiqatan InputFile instansiyasi bo'ldi; URL string esa photo ga string sifatida o'tdi.
4.2. Caption va parse_mode media bilan¶
Sticker'dan tashqari har media caption (izoh) qabul qiladi va caption ham parse_mode bilan formatlanadi:
bot.command("mahsulot", (ctx) => {
ctx.replyWithPhoto(new InputFile("rasmlar/mahsulot.jpg"), {
caption: "<b>Telefon</b>\nNarx: <i>5 000 000 so'm</i>",
parse_mode: "HTML",
});
});
Diqqat: caption uzunligi 1024 belgi bilan cheklangan (oddiy xabar 4096 belgi). Uzun matnni rasm ostiga sig'dirib bo'lmaydi β bunday holda rasmni alohida, matnni alohida yuboring, yoki matnni qisqartiring. Bu bobning yana bir tez-tez uchraydigan xatosi.
4.3. file_id β bir marta yuklang, ko'p marta yuboring¶
Bu media bilan ishlashning eng muhim tejamkorlik usuli. Siz biror faylni Telegram'ga birinchi marta yuborganingizda, Telegram uni o'z serverida saqlaydi va javobda unga file_id beradi. Keyingi safar o'sha faylni yuborganda, butun faylni qayta yuklash o'rniga shunchaki shu file_id string'ni bersangiz bas β tez va trafik tejaydi.
let logoFileId = null; // amalda: DB yoki sessiyada saqlang (10-bob)
bot.command("yukla", async (ctx) => {
const sent = await ctx.replyWithPhoto(new InputFile("rasmlar/logo.png"));
// Javobda Telegram bir nechta o'lcham qaytaradi; eng kattasini olamiz:
logoFileId = sent.photo.at(-1).file_id;
await ctx.reply("Logo yuklandi, file_id saqlandi.");
});
bot.command("logo", (ctx) => {
if (!logoFileId) return ctx.reply("Avval /yukla bilan yuklang.");
// Qayta yuklash YO'Q β string file_id ni beramiz:
return ctx.replyWithPhoto(logoFileId, { caption: "Saqlangan logo (tez!)" });
});
Men buni offline tekshirdim: replyWithPhoto(new InputFile(...)) javobidagi photo.at(-1).file_id ni oldim va keyingi replyWithPhoto(file_id) chaqirig'ida u photo ga string sifatida o'tdi.
Eslatma: rasm uchun
result.photoβ bir nechta o'lchamli massiv (thumbdan to to'liqgacha), shuning uchunsent.photo.at(-1)(eng katta) ni olamiz. Hujjat uchunsent.document.file_id, video uchunsent.video.file_id, ovoz uchunsent.voice.file_id. Har media turining o'z maydoni bor.Anti-eskirish:
file_idfaqat shu bot doirasida amal qiladi β boshqa botga uni bersangiz ishlamaydi. Shuningdekfile_idvaqt o'tib o'zgarishi mumkin (kamdan-kam, lekin mumkin), shuning uchun "abadiy" deb tayanmang; asl faylni ham saqlab qo'ying.
5. Albom (media guruh) β bir nechta rasmni birga yuborish¶
Bir nechta rasm yoki videoni bitta "albom" (gallereya) qilib yuborish uchun replyWithMediaGroup ishlatamiz. U InputMediaPhoto/InputMediaVideo obyektlari massivini (2 dan 10 tagacha) oladi:
bot.command("galereya", (ctx) => {
ctx.replyWithMediaGroup([
{ type: "photo", media: new InputFile("rasmlar/1.jpg"), caption: "Birinchi rasm" },
{ type: "photo", media: new InputFile("rasmlar/2.jpg") },
{ type: "photo", media: "https://grammy.dev/images/grammY.png" }, // URL ham mumkin
]);
});
Har element β oddiy obyekt: { type, media, caption?, parse_mode? }. type β "photo", "video", "document" yoki "audio". media β InputFile, URL yoki file_id (xuddi yakka media kabi).
Men offline tekshirdim: replyWithMediaGroup([...]) sendMediaGroup metodiga ketdi, payload.media ikki elementli massiv bo'ldi, birinchisining caption si saqlandi, ikkinchisining media si URL string bo'lib qoldi.
Diqqat: albomda caption odatda faqat birinchi element ostida ko'rinadi (Telegram albomning tagidagi yagona izoh sifatida ko'rsatadi). Albomni
captionparametri bilan emas, birinchi element ichidagicaptionbilan beriladi.Eslatma: rasm va videoni bitta albomda aralashtirish mumkin, lekin hujjatlar (
document) va audiolar faqat bir xil turdagilar bilan birga turishi kerak β masalan, hammasidocumentyoki hammasiaudio.
6. Havola oldindan ko'rinishi va jimgina yuborish¶
Ikki foydali kichik variant:
bot.command("havola", (ctx) => {
ctx.reply("grammY: https://grammy.dev", {
link_preview_options: { is_disabled: true }, // havola "kartochkasi" ko'rsatilmaydi
});
});
bot.command("jim", (ctx) => {
ctx.reply("Bu xabar ovozsiz keladi.", { disable_notification: true });
});
Anti-eskirish: eski kodda
disable_web_page_preview: trueni ko'rasiz β Telegram unilink_preview_options: { is_disabled: true }ga almashtirgan. grammY 1.43.0'da yangi nomdan foydalaning.
7. Tez-tez uchraydigan xatolar¶
| Xato | Sabab | Yechim |
|---|---|---|
Bad Request: can't parse entities |
MarkdownV2'da maxsus belgi (., (, !...) qochirilmagan |
escapeMarkdownV2 ishlating yoki HTML'ga o'ting |
| HTML formatlanmadi, teglar matn bo'lib chiqdi | parse_mode: "HTML" berilmagan |
ctx.reply(text, { parse_mode: "HTML" }) |
< o'rniga &lt; chiqdi |
& dan keyin < qochirilgan (noto'g'ri tartib) |
Avval &, keyin <, > qochiring |
| Lokal rasm jo'nalmadi, "file not found" / wrong file_id | ctx.replyWithPhoto("rasm.jpg") β string yo'l berilgan |
new InputFile("rasm.jpg") ishlating |
URL InputFile ichida ishlamadi |
new InputFile("https://...") β URL fayl nomi deb o'qildi |
URL'ni to'g'ridan-to'g'ri string bering |
| Caption kesilib qoldi yoki xato | Caption 1024 belgidan oshgan | Matnni qisqartiring yoki alohida xabar yuboring |
| Albom yuborilmadi | 1 ta yoki 10 tadan ko'p element berilgan | 2..10 element bering |
file_id boshqa botda ishlamadi |
file_id botga bog'langan |
Har bot o'z faylini yuklasin / o'z file_id sini saqlasin |
reply_to_message_id ishlamadi/ogohlantirish |
Eskirgan maydon | reply_parameters: { message_id } ishlating |
Mashqlar¶
Quyidagi mashqlarning ko'pini 02-bobdagi offline tekshirish naqshi bilan sinashingiz mumkin (token va internetsiz): soxta bot, bot.api.config.use(transformer) bilan chiqayotgan API'ni calls massiviga yig'ish, so'ng bot.handleUpdate(...). Buyruq uchun entities: [{ type: "bot_command", offset: 0, length: N }] qo'shishni unutmang.
Oson¶
- Qalin salom.
/salombuyrug'iga botning ismini qalin qilib qaytaradigan handler yozing (parse_mode: "HTML"). Tekshiring: payloadtextichida<b>bor vaparse_mode === "HTML". - Javob qilish. Har qanday matnga, o'sha xabarning o'ziga "iqtibos" qilib javob bering (
reply_parameters: { message_id: ctx.msg.message_id }). - URL rasm.
/rasmbuyrug'iga internetdagi rasm URL'ini yuboring (replyWithPhoto(url)). Tekshiring: metodsendPhoto,photoβ string. - Kod bloki.
/helpbuyrug'iga<pre>ichida kichik kod namunasini HTML bilan yuboring.
O'rta¶
- Echo xavfsiz HTML. Foydalanuvchi matnini qalin qilib qaytaring, lekin avval
escapeHtmlbilan qochiring.<b>yozgan foydalanuvchi<b>ko'rishini tekshiring. - MarkdownV2 narx.
escapeMarkdownV2yozing va"Chegirma: -20% (bugun!)"ni MarkdownV2 bilan yuboring. Chiqishda\-20\%... emas (%maxsus emas), lekin-,(,),!qochirilganini tekshiring. - Hujjat va izoh.
Bufferdan matnli fayl yasab (InputFile(buf, "hisobot.txt")),captionbilan hujjat sifatida yuboring. MetodsendDocumentekanini tekshiring. - Media tanlash.
/fotorasm,/faylhujjat yuboradigan ikki handler yozing; ikkalasi mossendPhoto/sendDocumentga ketishini bitta testda tekshiring. - Admin'ga xabar. Har yangi
message:textga foydalanuvchigactx.reply("Qabul qilindi"), admin'gactx.api.sendMessage(ADMIN_ID, ...)yuboring. Admin payload'iningchat_idsi to'g'ri ekanini tekshiring.
Qiyin¶
- Albom.
/galereyaga 3 ta rasmli albom yuboring; birinchisida caption bo'lsin.sendMediaGroupga 3 elementli massiv ketishini va birinchi element caption'ini tekshiring. - file_id qayta ishlatish.
/yukladaInputFileyuborib, javobdanfile_idni o'zgaruvchiga saqlang;/qaytada o'shafile_id(string) bilan yuboring. Ikkinchi chaqiruv payload'i stringphotoekanini tekshiring. - Mahsulot kartochkasi.
captionni HTML bilan formatlab (nomi qalin, narxi qiya), rasm bilan yuboradigan funksiya yozing; caption vaparse_modepayload'da birga ketishini tekshiring. - Universal escaping. Bitta funksiya yozing:
formatlash(text, mode)βmode === "HTML"bo'lsaescapeHtml,"MarkdownV2"bo'lsaescapeMarkdownV2qo'llasin va mosparse_modebilanctx.replyqilsin. Ikki rejimni ikki testda tekshiring.
Yechimlar
Quyidagi yechimlar 02-bobdagi offline naqshga tayanadi. Eslatma uchun qisqa skelet (har yechimda qayta ishlatiladi):
import { Bot, InputFile } from "grammy";
import assert from "node:assert/strict";
function makeBot() {
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: false, can_connect_to_business: false, has_main_web_app: false };
const calls = [];
bot.api.config.use((prev, method, payload) => {
calls.push({ method, payload });
if (method === "sendMessage")
return Promise.resolve({ ok: true, result: { message_id: 1, date: 0,
chat: { id: payload.chat_id, type: "private" }, text: payload.text } });
if (method.startsWith("send"))
return Promise.resolve({ ok: true, result: { message_id: 2, date: 0,
chat: { id: payload.chat_id, type: "private" },
photo: [{ file_id: "FID_123", file_unique_id: "u", width: 90, height: 90 }] } });
return Promise.resolve({ ok: true, result: true });
});
return { bot, calls };
}
function mkUpdate(text, id = 1) {
const message = { message_id: id, date: 0, text,
chat: { id: 777, type: "private" }, from: { id: 777, is_bot: false, first_name: "Ali" } };
if (text.startsWith("/"))
message.entities = [{ type: "bot_command", offset: 0, length: text.split(/\s/)[0].length }];
return { update_id: id, message };
}
1-mashq yechimi¶
bot.command("salom", (ctx) =>
ctx.reply(`Salom, <b>${ctx.from.first_name}</b>!`, { parse_mode: "HTML" }));
// Tekshirish:
const { bot, calls } = makeBot();
// ... handlerni ulang ...
await bot.handleUpdate(mkUpdate("/salom"));
assert.match(calls[0].payload.text, /<b>Ali<\/b>/);
assert.equal(calls[0].payload.parse_mode, "HTML");
parse_mode: "HTML" payload'ga qo'shiladi va Telegram <b> ni qalin qiladi.
2-mashq yechimi¶
bot.on("message:text", (ctx) =>
ctx.reply("Javob: " + ctx.message.text, {
reply_parameters: { message_id: ctx.msg.message_id },
}));
// calls[0].payload.reply_parameters.message_id === o'sha xabar ID si
ctx.msg.message_id joriy xabarning ID si; reply_parameters javobni o'sha xabarga ulaydi.
3-mashq yechimi¶
bot.command("rasm", (ctx) =>
ctx.replyWithPhoto("https://grammy.dev/images/grammY.png"));
// calls[0].method === "sendPhoto"; typeof calls[0].payload.photo === "string"
URL β string bo'lib o'tadi, Telegram o'zi yuklab oladi. InputFile ga o'ramaymiz.
4-mashq yechimi¶
bot.command("help", (ctx) =>
ctx.reply('<pre><code class="language-js">console.log("salom")</code></pre>',
{ parse_mode: "HTML" }));
<pre><code class="language-..."> tilga ko'ra ranglangan kod bloki beradi.
5-mashq yechimi¶
function escapeHtml(s) {
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
bot.on("message:text", (ctx) =>
ctx.reply(`Siz: <b>${escapeHtml(ctx.message.text)}</b>`, { parse_mode: "HTML" }));
// "<b>" yozgan foydalanuvchi:
await bot.handleUpdate(mkUpdate("<b>"));
assert.match(calls[0].payload.text, /<b>/); // qochirilgan
Foydalanuvchi yozgan <b> formatlanmaydi, balki <b> matn bo'lib ko'rinadi. & birinchi qochirilgani uchun ikki marta qochirish bo'lmaydi.
6-mashq yechimi¶
function escapeMarkdownV2(s) {
return s.replace(/[_*\[\]()~`>#+\-=|{}.!\\]/g, (c) => "\\" + c);
}
bot.command("narx", (ctx) =>
ctx.reply(escapeMarkdownV2("Chegirma: -20% (bugun!)"), { parse_mode: "MarkdownV2" }));
await bot.handleUpdate(mkUpdate("/narx"));
assert.equal(calls[0].payload.text, "Chegirma: \\-20% \\(bugun\\!\\)");
-, (, ), ! qochirildi; % va : maxsus belgi emas, qochirilmaydi.
7-mashq yechimi¶
import { Buffer } from "node:buffer";
bot.command("hisobot", (ctx) => {
const buf = Buffer.from("Sotuvlar: 120 dona\n", "utf-8");
return ctx.replyWithDocument(new InputFile(buf, "hisobot.txt"),
{ caption: "Kunlik hisobot" });
});
await bot.handleUpdate(mkUpdate("/hisobot"));
assert.equal(calls[0].method, "sendDocument");
assert.equal(calls[0].payload.caption, "Kunlik hisobot");
Buffer dan InputFile(buf, "nom") bilan fayl yasaymiz; ikkinchi argument β Telegram'da ko'rinadigan fayl nomi.
8-mashq yechimi¶
bot.command("foto", (ctx) => ctx.replyWithPhoto(new InputFile("x.jpg")));
bot.command("fayl", (ctx) => ctx.replyWithDocument(new InputFile("x.pdf")));
await bot.handleUpdate(mkUpdate("/foto"));
await bot.handleUpdate(mkUpdate("/fayl"));
assert.deepEqual(calls.map((c) => c.method), ["sendPhoto", "sendDocument"]);
Rasm siqiladi (sendPhoto), hujjat asl sifatda ketadi (sendDocument).
9-mashq yechimi¶
const ADMIN_ID = 999;
bot.on("message:text", async (ctx) => {
await ctx.reply("Qabul qilindi");
await ctx.api.sendMessage(ADMIN_ID, "Yangi: " + ctx.message.text);
});
await bot.handleUpdate(mkUpdate("salom"));
const adminCall = calls.find((c) => c.payload.chat_id === ADMIN_ID);
assert.ok(adminCall);
assert.match(adminCall.payload.text, /Yangi:/);
ctx.reply joriy chatga, ctx.api.sendMessage(ADMIN_ID, ...) boshqa chatga β chat_id ni o'zimiz beramiz.
10-mashq yechimi¶
bot.command("galereya", (ctx) =>
ctx.replyWithMediaGroup([
{ type: "photo", media: new InputFile("1.jpg"), caption: "Birinchi" },
{ type: "photo", media: new InputFile("2.jpg") },
{ type: "photo", media: "https://example.com/3.jpg" },
]));
await bot.handleUpdate(mkUpdate("/galereya"));
assert.equal(calls[0].method, "sendMediaGroup");
assert.equal(calls[0].payload.media.length, 3);
assert.equal(calls[0].payload.media[0].caption, "Birinchi");
Albom 2..10 element oladi; caption odatda birinchi element ostida ko'rinadi.
11-mashq yechimi¶
let saqlangan = null;
bot.command("yukla", async (ctx) => {
const m = await ctx.replyWithPhoto(new InputFile("logo.png"));
saqlangan = m.photo.at(-1).file_id;
});
bot.command("qayta", (ctx) => ctx.replyWithPhoto(saqlangan));
await bot.handleUpdate(mkUpdate("/yukla"));
await bot.handleUpdate(mkUpdate("/qayta"));
assert.equal(saqlangan, "FID_123");
assert.equal(calls[1].payload.photo, "FID_123"); // string, qayta yuklash YO'Q
Birinchi yuborishda file_id keladi; ikkinchisida shu string bilan tez qayta yuboramiz.
12-mashq yechimi¶
function mahsulotKartochkasi(ctx, nomi, narxi) {
return ctx.replyWithPhoto(new InputFile("mahsulot.jpg"), {
caption: `<b>${nomi}</b>\nNarx: <i>${narxi}</i>`,
parse_mode: "HTML",
});
}
bot.command("mahsulot", (ctx) => mahsulotKartochkasi(ctx, "Telefon", "5 000 000 so'm"));
await bot.handleUpdate(mkUpdate("/mahsulot"));
assert.match(calls[0].payload.caption, /<b>Telefon<\/b>/);
assert.equal(calls[0].payload.parse_mode, "HTML");
Caption ham parse_mode bilan formatlanadi β rasm ostidagi izoh qalin/qiya bo'ladi.
13-mashq yechimi¶
function escapeHtml(s) {
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function escapeMarkdownV2(s) {
return s.replace(/[_*\[\]()~`>#+\-=|{}.!\\]/g, (c) => "\\" + c);
}
function formatlash(ctx, text, mode) {
const xavfsiz = mode === "HTML" ? escapeHtml(text) : escapeMarkdownV2(text);
return ctx.reply(xavfsiz, { parse_mode: mode });
}
bot.command("h", (ctx) => formatlash(ctx, "a < b & c", "HTML"));
bot.command("m", (ctx) => formatlash(ctx, "narx 3.5!", "MarkdownV2"));
await bot.handleUpdate(mkUpdate("/h"));
await bot.handleUpdate(mkUpdate("/m"));
assert.equal(calls[0].payload.text, "a < b & c");
assert.equal(calls[0].payload.parse_mode, "HTML");
assert.equal(calls[1].payload.text, "narx 3\\.5\\!");
assert.equal(calls[1].payload.parse_mode, "MarkdownV2");
Bitta funksiya rejimga qarab to'g'ri escaping va parse_mode ni qo'llaydi β kodni qayta ishlatamiz.
Endi bot nafaqat matn, balki chiroyli formatlangan xabar va media yubora oladi. Keyingi bobda foydalanuvchiga tugmalar beramiz: reply va inline klaviaturalar, callback tugmalar β bot bilan muloqotni bir necha barobar qulay qiladi. Python/aiogram ekvivalentini ko'rish qiziq bo'lsa: tgbot-python kitobi. Media faylni yuborishning teskarisi β foydalanuvchi yuborgan faylni yuklab olish (getFile/download) β 12-bobda ko'ramiz.
β¬ οΈ Oldingi: 04 β Filtrlar va buyruqlar Β· π README Β· Keyingi: 06 β Klaviaturalar: reply va inline β‘οΈ