22 β Majburiy obuna¶
β¬ οΈ Oldingi: 21 β Kanallar bilan ishlash Β· π README Β· Keyingi: 23 β Telegram Web App (Mini App) asoslari β‘οΈ
Bu bobda: "Majburiy obuna" naqshini β foydalanuvchi botdan foydalanishdan oldin bir yoki bir nechta kanalga obuna bo'lishi shartligini β to'liq quramiz. Avval g'oyani (nega marketingda ishlatiladi va qachon zarar keltiradi) tushunamiz, so'ng obuna gate middleware'ni yozamiz: u har xabardan oldin
getChatMemberbilan a'zolikni tekshiradi, a'zo bo'lmasa "Obuna bo'ling" klaviaturasini (kanal havolasi + "Tekshirish" tugmasi) ko'rsatibnext()ni chaqirmaydi, a'zo bo'lsa o'tkazadi.getChatMemberqaytarganstatusqiymatlarini ("creator"/"administrator"/"member"= a'zo;"left"/"kicked"/"restricted"= yo'q) o'rganamiz; bir nechta kanalniPromise.allbilan tekshiramiz; "Tekshirish" callback'ini yozamiz; har xabardagetChatMemberchaqirish sekin va flood xavfli bo'lgani uchun natijani TTL bilan keshlaymiz (Mapyokictx.session); va private kanal uchunchat_join_request->approveChatJoinRequest/declineChatJoinRequesthamdacreateChatInviteLinkbilan join-request havolasini ko'ramiz.Halollik eslatmasi: Bu bobdagi BARCHA mantiq β gate middleware'ning a'zo/a'zo-emas ikki yo'li (
next()chaqirish yoki to'xtatish),statusxaritasi, bir nechta kanal, "Tekshirish" callback (tasdiq +editMessageTextyokishow_alert), kesh TTL ichida/tashqarisidagetChatMemberchaqirilish soni,chat_join_requestapprove/decline vacreateChatInviteLinkβ soxtaUpdatenibot.handleUpdatega uzatib, chiqayotgan API chaqiruvlarini (getChatMemberjavobini mock qilib) transformer bilan ushlab offline ishga tushirib tasdiqlangan (node _verify_22.mjs, 13/13 o'tdi). Jonli kanal a'zoligi, real havola yaratish va join-request'larni Telegram tomonidan yuborilishi token, internet va botning kanalda admin bo'lishini talab qiladi β u "illustrativ" deb belgilanadi.
Majburiy obuna nima?¶
"Majburiy obuna" (yoki "obuna gate", "force subscribe") β bu botning shunday sozlanishi: foydalanuvchi botning asosiy funksiyasidan foydalana olishidan oldin bir yoki bir nechta kanalga (yoki guruhga) obuna bo'lishi shart. Obuna bo'lmaguncha bot har bir xabarga "Avval obuna bo'ling" deb javob beradi va tugmalar ko'rsatadi.
Bu β eng keng tarqalgan Telegram marketing usullaridan biri. Mantiq oddiy: botning qiymatli kontenti (kino, kitob, kod, AI javoblari) "to'lov" sifatida kanal obunasini talab qiladi. Kanal egasi shu yo'l bilan auditoriya yig'adi.
Diqqat β bu qilich ikki tomonlama: Majburiy obuna foydalanuvchini bezovta qiladi. Ko'p kanallar talab qilish, obuna bo'lib darrov chiqib ketish (faqat botdan foydalanish uchun), past sifatli "majburiy" auditoriya β bularning hammasi real muammo. Telegram'ning o'zi ham bunday botlarni vaqti-vaqti bilan cheklaydi. Shuning uchun: kerak bo'lsa 1 ta kanal, foydalanuvchiga aniq tushuntiring, va botning asosiy qiymati obunaga arziydigan bo'lsin. Texnikani o'rganamiz, lekin uni mas'uliyat bilan ishlating.
Texnik jihatdan bu naqsh uchta blokdan iborat:
- Gate middleware β har update'dan oldin a'zolikni tekshiradi (09-bobdagi middleware g'oyasi).
getChatMemberβ Telegram'dan foydalanuvchining kanaldagi statusini so'raydi (21-bobdagi kanal ishi).- Kesh β har xabarda Telegram'ga so'rov yubormaslik uchun natijani vaqtincha saqlaydi (10-bobdagi sessiya/kesh g'oyasi).
Uchchalasini birma-bir quramiz.
Obuna gate middleware¶
Eng muhim qism β gate middleware. U 09-bobdagi (ctx, next) => {} naqshining aniq qo'llanishi: a'zo bo'lsa next() chaqiradi (botning qolgan handlerlari ishlaydi), a'zo bo'lmasa next() ni chaqirmaydi va gate klaviaturasini ko'rsatadi.
import { Bot, InlineKeyboard } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
const KANAL = "@mychannel"; // bot ADMIN bo'lgan kanal username'i
const KANAL_URL = "https://t.me/mychannel"; // ochiq havola (tugma uchun)
const AZO = new Set(["member", "administrator", "creator"]); // "a'zo" statuslari
bot.use(async (ctx, next) => {
// /start kabi ba'zi narsalarni gate'siz o'tkazmoqchi bo'lsangiz, bu yerda istisno qiling
const userId = ctx.from?.id;
if (userId === undefined) return await next(); // foydalanuvchisiz update (kanal posti va h.k.)
const azo = await ctx.api.getChatMember(KANAL, userId);
if (!AZO.has(azo.status)) {
const kb = new InlineKeyboard()
.url("Obuna bo'lish", KANAL_URL).row()
.text("Tekshirish", "obuna:tekshir");
await ctx.reply("Botdan foydalanish uchun avval kanalga obuna bo'ling:", {
reply_markup: kb,
});
return; // next() YO'Q -> pastdagi handlerlar ishlamaydi (gate yopiq)
}
await next(); // a'zo -> davom etamiz, qolgan bot ochiq
});
// Bu handlerlar faqat a'zo foydalanuvchilarga yetib boradi:
bot.command("start", (ctx) => ctx.reply("Salom! Botga xush kelibsiz."));
bot.on("message:text", (ctx) => ctx.reply("Sizning xabaringizni oldim."));
bot.start(); // illustrativ: jonli polling token talab qiladi
Offline sinovda biz ikki holatni ham tekshirdik:
- A'zo foydalanuvchi (
status: "member") β gatenext()ni chaqirdi, handler ishladi, foydalanuvchi"Sizning xabaringizni oldim."oldi (gate matni umuman chiqmadi). - A'zo emas (
status: "left") β gate"Botdan foydalanish uchun..."matnini va klaviaturani ko'rsatdi, handler ishlamadi (next()chaqirilmadi). Klaviaturadaurltugma (kanal havolasi) vacallback_data: "obuna:tekshir"tugma bor edi.
Eslatma β tartib muhim (09-bob): Gate middleware botning eng oldida turishi kerak β boshqa barcha handlerlardan oldin. Aks holda gate'dan oldin yozilgan handler
next()ni chaqirmay zanjirni to'xtatib qo'yadi va gate umuman ishlamaydi.bot.use(gate)ni handlerlardan oldin yozing.Diqqat β
/startni gate'dan tashqarida qoldirish: Ko'pincha/startbuyrug'i gate'ga tushmasligi kerak β chunki yangi foydalanuvchi botni endigina ochganda unga umumiy salom va obuna ko'rsatmasini berish kerak, "Avval obuna bo'ling" jumlasi emas. Buning ikki yo'li bor: (1) gate ichidaif (ctx.message?.text === "/start") return await next();deb istisno qilish, yoki (2)/startni gate'dan oldin ro'yxatdan o'tkazib, qolgan hamma narsani gate ostidagiComposerga joylashtirish (pastda "Gate'ni qisman qo'llash" bo'limida ko'ramiz). Buni offline tasdiqladik.
getChatMember va status tekshirish¶
Gate'ning yuragi β getChatMember. Bu Telegram Bot API metodi foydalanuvchining ma'lum chatdagi (kanal/guruh) holatini qaytaradi. grammY'da uni ikki shaklda chaqirish mumkin:
// 1) ctx.api orqali (chat_id ni o'zingiz berasiz) β gate middleware uchun shu qulay:
const azo = await ctx.api.getChatMember(KANAL, userId);
// 2) ctx alias orqali (HOZIRGI chat uchun, chat_id avtomatik) β guruh ichidagi handlerda:
const azo2 = await ctx.getChatMember(userId); // 21-bobdagi kabi
Qaytgan obyektning eng muhim maydoni β status. U quyidagi qiymatlardan birini oladi:
status |
Ma'nosi | A'zo deb sanaymizmi? |
|---|---|---|
"creator" |
Kanal egasi | Ha |
"administrator" |
Admin | Ha |
"member" |
Oddiy a'zo | Ha |
"restricted" |
Cheklangan (a'zo, lekin huquqlari kesilgan) | Odatda yo'q* |
"left" |
Chiqib ketgan / hech qachon a'zo bo'lmagan | Yo'q |
"kicked" |
Bloklangan (banlangan) | Yo'q |
* "restricted" nozik holat: foydalanuvchi texnik jihatdan kanalda, lekin huquqlari cheklangan. Biz qat'iy yondashib uni a'zo deb sanamaymiz (ko'pchilik botlar shunday qiladi). Xohlasangiz, o'z AZO to'plamingizga "restricted" ni qo'shishingiz mumkin β bu siyosat qarori.
Shuning uchun a'zolikni tekshirishning eng toza yo'li β to'plam:
const AZO = new Set(["member", "administrator", "creator"]);
const azoMi = AZO.has(azo.status); // true yoki false
Bu if (status === "member" || status === "administrator" || ...) dan ancha o'qiluvchan va xatosiz.
Diqqat β eng ko'p uchraydigan xato:
getChatMemberishlashi uchun bot kanalda admin bo'lishi shart. Aks holda TelegramBad Request: member list is inaccessibleyoki shunga o'xshash xato qaytaradi (GrammyError). Demak: botni kanalga admin qiling (kamida "a'zolarni ko'rish" huquqi bilan), keyin gate ishlaydi. Buni botingizbot.catch(08-bob) bilan ushlab, log'lab boring β gate xato berganda butun bot qotmasin.
Bir nechta kanal β hammasiga a'zo bo'lishi shart¶
Agar bir nechta kanalga obunani talab qilsangiz, har biri uchun getChatMember chaqirib, hammasiga a'zo ekanini tekshirasiz. So'rovlarni ketma-ket emas, parallel (Promise.all) yuborgan ma'qul β tezroq:
const KANALLAR = ["@kanal1", "@kanal2"];
async function yetishmaganlar(ctx) {
const userId = ctx.from.id;
const natijalar = await Promise.all(
KANALLAR.map((k) => ctx.api.getChatMember(k, userId))
);
// a'zo BO'LMAGAN kanallar ro'yxati:
return KANALLAR.filter((k, i) => !AZO.has(natijalar[i].status));
}
Gate'da:
bot.use(async (ctx, next) => {
if (ctx.from?.id === undefined) return await next();
const qoldi = await yetishmaganlar(ctx);
if (qoldi.length > 0) {
const kb = new InlineKeyboard();
for (const k of qoldi) {
// har kanal uchun tugma (username'dan havola yasaymiz)
kb.url(`Obuna: ${k}`, `https://t.me/${k.replace("@", "")}`).row();
}
kb.text("Tekshirish", "obuna:tekshir");
await ctx.reply("Quyidagi kanallarga obuna bo'ling:", { reply_markup: kb });
return;
}
await next();
});
Offline sinovda foydalanuvchi @k1 da a'zo, @k2 da emas edi β gate aynan "@k2" ni ko'rsatdi va next() ni chaqirmadi. Faqat yetishmagan kanallar tugma sifatida chiqdi.
"Tekshirish" callback¶
Foydalanuvchi kanalga obuna bo'lgach, "Tekshirish" tugmasini bosadi. Bizning callback handlerimiz qayta getChatMember chaqiradi va natijaga qarab ish ko'radi (07-bobdagi callback bilimi):
bot.callbackQuery("obuna:tekshir", async (ctx) => {
const userId = ctx.from.id;
const azo = await ctx.api.getChatMember(KANAL, userId);
if (AZO.has(azo.status)) {
// Obuna tasdiqlandi -> gate'ni olib tashlaymiz va davom etamiz
await ctx.answerCallbackQuery({ text: "Rahmat! Endi botdan foydalanishingiz mumkin." });
await ctx.editMessageText("Obuna tasdiqlandi. /start ni bosing yoki xabar yuboring.");
} else {
// Hali ham obuna emas -> ogohlantiruvchi oyna (gate qoladi)
await ctx.answerCallbackQuery({
text: "Siz hali obuna bo'lmadingiz. Obuna bo'ling va qaytadan bosing.",
show_alert: true, // foydalanuvchiga katta oyna ko'rsatadi
});
}
});
Offline ikki holatni ham tasdiqladik:
- Endi a'zo (
"member") βanswerCallbackQuerytoast ("Rahmat!...") +editMessageTextbilan gate xabari "Obuna tasdiqlandi" ga almashtirildi. - Hali a'zo emas (
"left") βanswerCallbackQueryshow_alert: truebilan ogohlantirish oynasi chiqdi,editMessageTextchaqirilmadi (gate xabari o'rnida qoldi).
Eslatma β
show_alert:answerCallbackQuery({ text, show_alert: false })ekranning tepasida kichik "toast" ko'rsatadi (bir necha soniya).show_alert: trueesa foydalanuvchi "OK" bosguncha turadigan modal oyna chiqaradi β muhim xabar uchun (masalan "Siz hali obuna emassiz") shu yaxshiroq.Diqqat β
answerCallbackQueryni unutmang: Har bir callback'gaanswerCallbackQuerychaqirilishi shart, aks holda tugmada "soat" aylanaverib qoladi (07-bobda ko'rgan edik). Hatto matnsizctx.answerCallbackQuery()ham yetadi.
Kesh β har xabarda getChatMember chaqirmaslik¶
Gate middleware har bir update'da ishlaydi. Agar har xabarda getChatMember chaqirsangiz, ikki muammo paydo bo'ladi:
- Sekinlik β har xabarga Telegram'ga qo'shimcha so'rov, ya'ni qo'shimcha kechikish.
- Flood / rate-limit β faol bot uchun bu juda ko'p so'rov; Telegram limitlari (
429 Too Many Requests) ga tushib qolishingiz mumkin.
Yechim β a'zolik holatini TTL (yashash muddati) bilan keshlash: bir marta tekshirib, natijani vaqtincha eslab qolasiz va TTL tugamaguncha qayta so'ramaysiz.
Map bilan kesh (eng sodda)¶
const obunaKesh = new Map(); // userId -> { azo: boolean, vaqt: number }
const KESH_TTL = 10 * 60 * 1000; // 10 daqiqa (ms)
async function obunaTekshir(ctx) {
const userId = ctx.from.id;
const yozuv = obunaKesh.get(userId);
const hozir = Date.now();
// Kesh yangimi? -> keshdan qaytar (getChatMember CHAQIRILMAYDI)
if (yozuv && hozir - yozuv.vaqt < KESH_TTL) {
return yozuv.azo;
}
// Kesh yo'q yoki eskirdi -> Telegram'dan so'raymiz va keshga yozamiz
const azo = await ctx.api.getChatMember(KANAL, userId);
const natija = AZO.has(azo.status);
obunaKesh.set(userId, { azo: natija, vaqt: hozir });
return natija;
}
bot.use(async (ctx, next) => {
if (ctx.from?.id === undefined) return await next();
if (await obunaTekshir(ctx)) {
await next();
} else {
const kb = new InlineKeyboard()
.url("Obuna bo'lish", KANAL_URL).row()
.text("Tekshirish", "obuna:tekshir");
await ctx.reply("Avval kanalga obuna bo'ling:", { reply_markup: kb });
}
});
Offline sinovda (vaqtni now o'zgaruvchisi bilan boshqarib):
- Birinchi xabar ->
getChatMember1 marta chaqirildi, natija keshga yozildi. - TTL ichidagi ikkinchi xabar -> keshdan olindi,
getChatMemberchaqirilmadi (jami hali 1 marta). - TTL tugagandan keyingi xabar -> kesh "eskirdi",
getChatMemberqayta chaqirildi (jami 2 marta).
Aynan kutilganidek: TTL ichida 1, TTL tashqarisida qayta.
Diqqat β "Tekshirish" tugmasi keshni yangilashi kerak: Agar callback'da yangi a'zolikni tekshirib, keshni yangilamasangiz, foydalanuvchi obuna bo'lib "Tekshirish" bossa ham, gate middleware eski keshlangan
falseni o'qib uni yana to'xtatishi mumkin. Yechim: "Tekshirish" callback'idaobunaKesh.set(userId, { azo: true, vaqt: Date.now() })deb keshni darrov yangilang (yokiobunaKesh.delete(userId)bilan tozalang, keyingi xabarda qaytadan tekshirilsin).
ctx.session bilan kesh (10-bob)¶
Map xotirada yashaydi β bot qayta ishga tushsa yo'qoladi va cheksiz o'sib xotira "sizdirishi" mumkin (09-bobdagi throttling Map bilan bir muammo). Jiddiyroq variant β a'zolik holatini foydalanuvchining sessiyasida saqlash (10-bob):
import { session } from "grammy";
bot.use(session({ initial: () => ({ obuna: null }) })); // { azo, vaqt } yoki null
bot.use(async (ctx, next) => {
if (ctx.from?.id === undefined) return await next();
const s = ctx.session;
const hozir = Date.now();
let azo;
if (s.obuna && hozir - s.obuna.vaqt < KESH_TTL) {
azo = s.obuna.azo; // keshdan
} else {
const m = await ctx.api.getChatMember(KANAL, ctx.from.id);
azo = AZO.has(m.status);
s.obuna = { azo, vaqt: hozir }; // sessiyaga yoz
}
if (azo) return await next();
const kb = new InlineKeyboard().url("Obuna", KANAL_URL).row().text("Tekshirish", "obuna:tekshir");
await ctx.reply("Avval obuna bo'ling:", { reply_markup: kb });
});
Offline sinovda sessiya keshi ham aynan ishladi: TTL ichidagi ikki xabar uchun getChatMember 1 marta chaqirildi. Sessiyani FileAdapter yoki SQLite (10-bob) bilan doimiy qilsangiz, kesh bot qayta ishga tushganda ham saqlanadi.
Anti-eskirish: Hozir grammY ekotizimida obuna-gate'ni avtomatlashtiradigan rasmiy plagin yo'q β bu naqshni odatda qo'lda quramiz (bobdagi kabi). Internetda turli "force-sub" kutubxonalari uchrashi mumkin, lekin ular grammY'ning rasmiy qismi emas; ishlatishdan oldin manbasini va
getChatMember/kesh mantig'ini tekshiring. Bu yerda men hech qanday tashqi plagin API'sini ixtiro qilmayman β qo'lda yondashuv eng ishonchli va shaffof.
Private kanal: chat_join_request¶
Yuqoridagi gate ochiq (public) kanal uchun ishlaydi: foydalanuvchi havolaga bosib darrov a'zo bo'ladi. Ammo private (yopiq) kanalda ochiq username yo'q, va a'zolik admin tasdig'i orqali bo'ladi. Bu yerda boshqa naqsh ishlatiladi: join-request (qo'shilish so'rovi).
Oqim shunday:
- Siz private kanalga join-request rejimli invite-havola yaratasiz (
createChatInviteLink({ creates_join_request: true })). - Foydalanuvchi havolaga bosadi -> Telegram darrov a'zo qilmaydi, balki so'rov yuboradi.
- Botingizga
chat_join_requestupdate keladi. - Bot so'rovni tasdiqlaydi (
approveChatJoinRequest) yoki rad etadi (declineChatJoinRequest) β masalan to'lov qilganlarni avtomatik tasdiqlash.
const RUXSAT = new Set([111, 222]); // ruxsat etilgan foydalanuvchilar (masalan to'lov qilganlar)
bot.on("chat_join_request", async (ctx) => {
const userId = ctx.from.id; // so'rov yuborgan foydalanuvchi
// ctx.chat β so'rov yuborilgan kanal (21-bobda ko'rgan edik)
if (RUXSAT.has(userId)) {
await ctx.approveChatJoinRequest(userId); // tasdiqlaymiz
// Foydalanuvchiga shaxsiy xabar yuborish mumkin (5 daqiqa oynasi bor):
await ctx.api.sendMessage(userId, "Kanalga qo'shildingiz, xush kelibsiz!");
} else {
await ctx.declineChatJoinRequest(userId); // rad etamiz
}
});
Offline sinovda biz ikki yo'lni ham ko'rdik:
- Ruxsat etilgan foydalanuvchi (
777,RUXSATda bor) ->approveChatJoinRequestuser_id: 777bilan chaqirildi. - Ruxsat etilmagan (
555) ->declineChatJoinRequestuser_id: 555bilan chaqirildi,approveumuman chaqirilmadi.
Eslatma β
ctx.fromvactx.chatjoin-request'da:chat_join_requestupdate'idactx.from= so'rov yuborgan foydalanuvchi,ctx.chat= so'rov yuborilgan kanal. Shuning uchunctx.approveChatJoinRequest(ctx.from.id)to'g'ri ishlaydi β chat_id ham, user_id hamctxdan keladi. Update tuzilishimanage.d.tsda tasdiqlangan.Diqqat β
user_chat_id(5 daqiqalik oyna): Update'dactx.chatJoinRequest.user_chat_idbo'ladi β bu foydalanuvchi bilan shaxsiy chat ID'si. Bot uni so'rov qayta ishlanmaguncha (5 daqiqa) foydalanuvchiga to'g'ridan-to'g'ri xabar yuborish uchun ishlatishi mumkin, foydalanuvchi botni ilgari ishga tushirmagan bo'lsa ham. Ko'p hollardauser_chat_idfoydalanuvchiidsiga teng bo'ladi.
Join-request havolasini yaratish¶
Private kanal uchun invite-havolani bot yaratadi (bot kanalda admin va can_invite_users huquqiga ega bo'lishi shart):
bot.command("havola", async (ctx) => {
// Diqqat: bu komandani private kanal ichida YOKI api'ga chat_id berib chaqiring.
// Bu yerda HOZIRGI chat uchun (ctx alias) yaratamiz:
const link = await ctx.createChatInviteLink({
creates_join_request: true, // havola = darrov a'zo emas, SO'ROV yuboradi
name: "Reklama kampaniyasi", // ixtiyoriy nom (admin paneli uchun)
});
await ctx.reply(`Join-request havola:\n${link.invite_link}`);
});
Offline sinovda createChatInviteLink creates_join_request: true va name: "Reklama" bilan chaqirildi va qaytgan invite_link (https://t.me/+... ko'rinishida) o'qildi.
Illustrativ: Real havola yaratish, foydalanuvchi unga bosishi va
chat_join_requestupdate'ining kelishi jonli kanal, internet va botning kanaldacan_invite_usershuquqli admin bo'lishini talab qiladi. Yuqoridagi kod naqshlari offline tasdiqlangan, lekin "havola ishladi, foydalanuvchi qo'shildi" deyish uchun jonli muhit kerak β bu qism illustrativ.Anti-eskirish β
chat_join_requestni eshitish uchunallowed_updates: Polling/webhook'dachat_join_requestupdate'lari standart keladi (u Telegram'ning standartallowed_updatesro'yxatida). Lekin agar sizbot.start({ allowed_updates: [...] })yokisetWebhook({ allowed_updates: [...] })da ro'yxatni qo'lda chekladingiz va"chat_join_request"ni unutdingiz, update'lar kelmaydi. grammY'ning@grammyjs/typesyordamida to'liq ro'yxatni olishingiz mumkin. Aniq sintaksisni rasmiy hujjatdan oling β bu yerda ixtiro qilmayman.
Gate'ni qisman qo'llash (Composer bilan)¶
Ba'zan butun botni gate ostiga olish noto'g'ri β masalan /start, /help ochiq bo'lishi, qolgan funksiyalar yopiq bo'lishi kerak. 09-bobdagi Composer aynan shu uchun:
import { Composer } from "grammy";
// Ochiq buyruqlar (gate'dan TASHQARIDA):
bot.command("start", (ctx) => ctx.reply("Xush kelibsiz! Botdan foydalanish uchun obuna bo'ling."));
bot.command("help", (ctx) => ctx.reply("Yordam: ..."));
// Qolgan hamma narsa gate ostida:
const himoyalangan = new Composer();
himoyalangan.use(async (ctx, next) => {
const m = await ctx.api.getChatMember(KANAL, ctx.from.id);
if (!AZO.has(m.status)) {
await ctx.reply("Bu funksiya uchun obuna shart.", {
reply_markup: new InlineKeyboard().url("Obuna", KANAL_URL).row().text("Tekshirish", "obuna:tekshir"),
});
return;
}
await next();
});
himoyalangan.on("message:text", (ctx) => ctx.reply("Maxsus kontent (faqat obunachilar uchun)"));
bot.use(himoyalangan);
Offline sinovda /start gate'siz ishladi ("Xush kelibsiz!..."), oddiy matn esa gate'da to'xtadi ("Bu funksiya uchun obuna shart."). Bu β eng moslashuvchan naqsh: ochiq va yopiq qismlarni aniq ajratasiz.
Eslatma β Python ekvivalenti: aiogram'da bu naqsh
Routerva outer-middleware bilan quriladi,getChatMemberesabot.get_chat_member(...)deb chaqiriladi. Mantiq aynan bir xil (a'zolik tekshir -> o'tkaz yoki to'xtat). Solishtirib ko'rishni xohlasangiz, Telegram bot (Python/aiogram) kitobining 22-bobiga qarang.
Tez-tez uchraydigan xatolar¶
| Xato | Sabab | Yechim |
|---|---|---|
getChatMember Bad Request: member list is inaccessible beradi |
Bot kanalda admin emas | Botni kanalga admin qiling (a'zolarni ko'rish huquqi bilan) |
| Gate hech qachon ishlamayapti, hamma o'tib ketyapti | getChatMember xato berib bot.catch da yutilyapti yoki AZO to'plami noto'g'ri |
Xatoni log'lang; status qiymatlarini tekshiring ("member"/"administrator"/"creator") |
Bot juda sekin / 429 Too Many Requests |
Har xabarda getChatMember chaqirilyapti (kesh yo'q) |
Natijani TTL bilan keshlang (Map yoki ctx.session) |
| Foydalanuvchi obuna bo'ldi, lekin gate hali ham to'xtatyapti | Kesh eski false ni saqlab turibdi |
"Tekshirish" callback'ida keshni yangilang yoki delete qiling |
| Tugmadagi "soat" aylanaverib qolyapti | answerCallbackQuery chaqirilmagan |
Har callback oxirida ctx.answerCallbackQuery() ni chaqiring |
| Private kanal havolasi darrov a'zo qilyapti (so'rov emas) | creates_join_request: true berilmagan |
createChatInviteLink({ creates_join_request: true }) ishlating |
chat_join_request update kelmayapti |
allowed_updates da "chat_join_request" yo'q yoki bot admin emas |
allowed_updates ro'yxatiga qo'shing; botni admin qiling (can_invite_users) |
Gate /start ni ham bloklayapti (yangi foydalanuvchi qiynaladi) |
/start gate ostida |
/start ni gate'dan oldin yoki Composer tashqarisida ro'yxatdan o'tkazing |
Mashqlar¶
Quyidagi mashqlarning ko'pi offline tekshiriladi β
bot.handleUpdate(update)ga soxta update uzatib,getChatMemberjavobini transformer'da mock qilasiz (a'zo/a'zo-emas) va chiqayotgan chaqiruvlarni (sendMessage,answerCallbackQuery,approveChatJoinRequest, ...)assertqilasiz. Buyruq update'igaentities:[{type:"bot_command",offset:0,length:N}]qo'shishni unutmang. Transformer naqshi:getChatMemberdapayload.user_id/payload.chat_idga qarab keraklistatusqaytaring.
Oson¶
-
A'zo o'tadi.
status: "member"qaytaradigan mock bilan gate middleware yozing. A'zo foydalanuvchidan oddiy matn uzating va handler ishlaganini (gate matni chiqmaganini) tasdiqlang. -
A'zo emas to'xtaydi.
status: "left"mock bilan gate yozing. Xabar uzating va (a) handler ISHLAMAGANINI, (b) gate matni"...obuna bo'ling"chiqqanini tasdiqlang. -
AZOto'plami.const AZO = new Set(["member","administrator","creator"])yarating.AZO.has("creator"),AZO.has("administrator"),AZO.has("member")true;AZO.has("left"),AZO.has("kicked"),AZO.has("restricted")falseekaniniassertqiling. -
Gate klaviaturasi. A'zo emas holatda gate yuborgan
reply_markupichida birinchi qatordaurltugma, ikkinchi qatordacallback_data: "obuna:tekshir"tugma borligini tasdiqlang.
O'rta¶
-
Bir nechta kanal. Ikki kanal (
@k1,@k2). Mock'da foydalanuvchi faqat@k1da a'zo (@k2da"left"). GatePromise.allbilan tekshirsin va gate matnida faqat@k2ko'rsatilganini tasdiqlang. Handler ishlamaganini ham. -
Hammasiga a'zo. 5-mashqdagi sozlama, lekin mock ikkala kanalda ham
"member"qaytarsin. Handler ISHLAGANINI (gate o'tkazganini) tasdiqlang. -
Tekshirish callback β a'zo.
obuna:tekshircallback handleri yozing. Mock"member"qaytarsin.answerCallbackQuerymatni"Rahmat!..."vaeditMessageTextchaqirilganini tasdiqlang. -
Tekshirish callback β a'zo emas. 7-mashq, lekin mock
"left"qaytarsin.answerCallbackQueryshow_alert: truebilan chaqirilganini vaeditMessageTextCHAQIRILMAGANINI tasdiqlang.
Qiyin¶
-
Kesh TTL ichida.
Mapkesh +obunaTekshir(ctx)yozing (TTL = 10 daqiqa, vaqtninowo'zgaruvchisi bilan boshqaring). Ikki xabarni TTL ichida uzating vagetChatMemberFAQAT 1 marta chaqirilganini tasdiqlang. -
Kesh TTL tugagach. 9-mashq, lekin ikkinchi xabarni
now += TTL + 1dan keyin uzating.getChatMember2 marta chaqirilganini tasdiqlang. -
chat_join_requestapprove/decline.RUXSAT = new Set([777]).chat_join_requesthandler yozing. (a)777join-request uzating ->approveChatJoinRequest(777)chaqirilganini; (b)555uzating ->declineChatJoinRequest(555)chaqirilganini (approve emas) tasdiqlang. -
Join-request havola.
bot.command("havola")yozing:createChatInviteLink({ creates_join_request: true, name: "Reklama" })chaqirsin. Mock havola qaytarsin. Chaqiruv payload'idacreates_join_request === truevaname === "Reklama"ekanini, hamda javobdagiinvite_linkishlatilganini tasdiqlang. -
Kesh + Tekshirish. Gate
Mapkesh ishlatadi. Foydalanuvchi avval"left"(gate to'xtatadi), keyin obuna bo'ladi (mock"member"ga o'tadi). "Tekshirish" callback keshni yangilasin (kesh.set(id, {azo:true, ...})). Keyingi xabarda handler ISHLAGANINI (gate o'tkazganini)getChatMemberqayta chaqirmasdan tasdiqlang.
Yechimlar
Quyidagi yechimlar
_verify_22.mjsdagi naqsh bilan offline ishga tushiriladi.makeBot(statusMap)transformer'igetChatMembernistatusMap[user_id](default"left") bilan mock qiladi;mkText,mkCallback,mkJoinRequestβ soxta update yordamchilari. Qisqartirish uchun bu yordamchilar takrorlanmaydi (bob boshidagi halollik eslatmasiga qarang).AZO,KANAL,KANAL_URLβ bobdagidek.
1-mashq yechimi¶
const { bot, calls } = makeBot({ 777: "member" });
let handlerRan = false;
bot.use(async (ctx, next) => {
const m = await ctx.api.getChatMember(KANAL, ctx.from.id);
if (!AZO.has(m.status)) { await ctx.reply("Avval obuna bo'ling"); return; }
await next();
});
bot.on("message:text", (ctx) => { handlerRan = true; return ctx.reply("Salom, a'zo!"); });
await bot.handleUpdate(mkText("salom"));
assert.equal(handlerRan, true);
const texts = calls.filter((c) => c.method === "sendMessage").map((c) => c.payload.text);
assert.deepEqual(texts, ["Salom, a'zo!"]); // gate matni YO'Q
A'zo ("member") bo'lgani uchun AZO.has true, gate next() chaqirdi va handler ishladi. Gate matni umuman chiqmadi.
2-mashq yechimi¶
const { bot, calls } = makeBot({ 777: "left" });
let handlerRan = false;
bot.use(async (ctx, next) => {
const m = await ctx.api.getChatMember(KANAL, ctx.from.id);
if (!AZO.has(m.status)) { await ctx.reply("Avval obuna bo'ling"); return; }
await next();
});
bot.on("message:text", (ctx) => { handlerRan = true; });
await bot.handleUpdate(mkText("salom"));
assert.equal(handlerRan, false); // handler ishlamadi
const sm = calls.filter((c) => c.method === "sendMessage");
assert.equal(sm[0].payload.text, "Avval obuna bo'ling"); // gate matni chiqdi
"left" -> AZO.has false -> gate matni yuborildi va next() chaqirilmadi, shuning uchun handler false bo'lib qoldi.
3-mashq yechimi¶
const AZO = new Set(["member", "administrator", "creator"]);
assert.equal(AZO.has("creator"), true);
assert.equal(AZO.has("administrator"), true);
assert.equal(AZO.has("member"), true);
assert.equal(AZO.has("left"), false);
assert.equal(AZO.has("kicked"), false);
assert.equal(AZO.has("restricted"), false); // qat'iy: restricted a'zo emas
Set bilan a'zolik tekshiruvi if (status === ... || ...) dan toza. "restricted" ni biz a'zo deb sanamaymiz (siyosat qarori).
4-mashq yechimi¶
const { bot, calls } = makeBot({ 777: "left" });
bot.use(async (ctx, next) => {
const m = await ctx.api.getChatMember(KANAL, ctx.from.id);
if (!AZO.has(m.status)) {
const kb = new InlineKeyboard().url("Obuna bo'lish", KANAL_URL).row().text("Tekshirish", "obuna:tekshir");
await ctx.reply("Avval obuna bo'ling", { reply_markup: kb });
return;
}
await next();
});
bot.on("message:text", (ctx) => {});
await bot.handleUpdate(mkText("salom"));
const rows = calls.find((c) => c.method === "sendMessage").payload.reply_markup.inline_keyboard;
assert.equal(rows[0][0].url, KANAL_URL);
assert.equal(rows[1][0].callback_data, "obuna:tekshir");
InlineKeyboard ikki qatorli klaviatura yasaydi: 1-qatorda url tugma, 2-qatorda callback_data tugma. grammY uni reply_markup.inline_keyboard massiviga aylantiradi.
5-mashq yechimi¶
const KANALLAR = ["@k1", "@k2"];
const { bot, calls } = makeBot();
bot.api.config.use((prev, method, payload) => {
if (method === "getChatMember") {
const member = payload.chat_id === "@k1"; // faqat @k1 da a'zo
return Promise.resolve({ ok: true, result: { status: member ? "member" : "left", user: { id: payload.user_id, is_bot: false, first_name: "U" } } });
}
return prev(method, payload);
});
let handlerRan = false;
bot.use(async (ctx, next) => {
const r = await Promise.all(KANALLAR.map((k) => ctx.api.getChatMember(k, ctx.from.id)));
const qoldi = KANALLAR.filter((k, i) => !AZO.has(r[i].status));
if (qoldi.length) { await ctx.reply(`Obuna bo'ling: ${qoldi.join(", ")}`); return; }
await next();
});
bot.on("message:text", (ctx) => { handlerRan = true; });
await bot.handleUpdate(mkText("salom"));
assert.equal(handlerRan, false);
assert.equal(calls.find((c) => c.method === "sendMessage").payload.text, "Obuna bo'ling: @k2");
Promise.all ikkala kanalni parallel tekshirdi; filter faqat a'zo bo'lmagan @k2 ni qoldirdi va gate o'shani ko'rsatdi.
6-mashq yechimi¶
const KANALLAR = ["@k1", "@k2"];
const { bot } = makeBot();
bot.api.config.use((prev, method, payload) => {
if (method === "getChatMember") {
return Promise.resolve({ ok: true, result: { status: "member", user: { id: payload.user_id, is_bot: false, first_name: "U" } } }); // har ikkisida a'zo
}
return prev(method, payload);
});
let handlerRan = false;
bot.use(async (ctx, next) => {
const r = await Promise.all(KANALLAR.map((k) => ctx.api.getChatMember(k, ctx.from.id)));
if (KANALLAR.some((k, i) => !AZO.has(r[i].status))) { await ctx.reply("Obuna shart"); return; }
await next();
});
bot.on("message:text", (ctx) => { handlerRan = true; });
await bot.handleUpdate(mkText("salom"));
assert.equal(handlerRan, true); // hammasiga a'zo -> o'tdi
Ikkala kanalda ham "member" bo'lgani uchun some(... !AZO.has ...) false β gate next() chaqirdi va handler ishladi.
7-mashq yechimi¶
const { bot, calls } = makeBot({ 777: "member" });
bot.callbackQuery("obuna:tekshir", async (ctx) => {
const m = await ctx.api.getChatMember(KANAL, ctx.from.id);
if (AZO.has(m.status)) {
await ctx.answerCallbackQuery({ text: "Rahmat! Endi botdan foydalanishingiz mumkin." });
await ctx.editMessageText("Obuna tasdiqlandi.");
} else {
await ctx.answerCallbackQuery({ text: "Hali ham obuna emassiz.", show_alert: true });
}
});
await bot.handleUpdate(mkCallback("obuna:tekshir"));
assert.equal(calls.find((c) => c.method === "answerCallbackQuery").payload.text, "Rahmat! Endi botdan foydalanishingiz mumkin.");
assert.ok(calls.find((c) => c.method === "editMessageText"));
A'zo bo'lgani uchun callback tasdiq toast'i va editMessageText (gate xabarini almashtirish) chaqirildi.
8-mashq yechimi¶
const { bot, calls } = makeBot({ 777: "left" });
bot.callbackQuery("obuna:tekshir", async (ctx) => {
const m = await ctx.api.getChatMember(KANAL, ctx.from.id);
if (AZO.has(m.status)) {
await ctx.answerCallbackQuery({ text: "Rahmat!" });
await ctx.editMessageText("Tasdiqlandi");
} else {
await ctx.answerCallbackQuery({ text: "Hali ham obuna emassiz.", show_alert: true });
}
});
await bot.handleUpdate(mkCallback("obuna:tekshir"));
const acq = calls.find((c) => c.method === "answerCallbackQuery");
assert.equal(acq.payload.show_alert, true);
assert.equal(calls.find((c) => c.method === "editMessageText"), undefined); // gate qoladi
A'zo emas -> show_alert: true ogohlantirish oynasi chiqdi, editMessageText chaqirilmadi (gate xabari o'rnida qoldi).
9-mashq yechimi¶
const { bot, calls } = makeBot({ 777: "member" });
const kesh = new Map();
const TTL = 10 * 60 * 1000;
let now = 1000;
async function azoMi(ctx) {
const id = ctx.from.id;
const y = kesh.get(id);
if (y && now - y.vaqt < TTL) return y.azo; // keshdan
const m = await ctx.api.getChatMember(KANAL, id);
const azo = AZO.has(m.status);
kesh.set(id, { azo, vaqt: now });
return azo;
}
bot.use(async (ctx, next) => { if (await azoMi(ctx)) await next(); });
bot.on("message:text", (ctx) => {});
await bot.handleUpdate(mkText("a", 1));
now += 1000; // TTL ICHIDA
await bot.handleUpdate(mkText("b", 2));
assert.equal(calls.filter((c) => c.method === "getChatMember").length, 1); // FAQAT 1 marta
Ikkinchi xabar TTL ichida kelgani uchun natija keshdan olindi β getChatMember qayta chaqirilmadi.
10-mashq yechimi¶
const { bot, calls } = makeBot({ 777: "member" });
const kesh = new Map();
const TTL = 10 * 60 * 1000;
let now = 1000;
async function azoMi(ctx) {
const id = ctx.from.id;
const y = kesh.get(id);
if (y && now - y.vaqt < TTL) return y.azo;
const m = await ctx.api.getChatMember(KANAL, id);
const azo = AZO.has(m.status);
kesh.set(id, { azo, vaqt: now });
return azo;
}
bot.use(async (ctx, next) => { if (await azoMi(ctx)) await next(); });
bot.on("message:text", (ctx) => {});
await bot.handleUpdate(mkText("a", 1));
now += TTL + 1; // TTL TUGADI
await bot.handleUpdate(mkText("b", 2));
assert.equal(calls.filter((c) => c.method === "getChatMember").length, 2); // qayta tekshirildi
Ikkinchi xabar TTL tugagandan keyin kelgani uchun kesh "eskirdi" va getChatMember qayta chaqirildi.
11-mashq yechimi¶
const { bot, calls } = makeBot();
const RUXSAT = new Set([777]);
bot.on("chat_join_request", async (ctx) => {
if (RUXSAT.has(ctx.from.id)) await ctx.approveChatJoinRequest(ctx.from.id);
else await ctx.declineChatJoinRequest(ctx.from.id);
});
await bot.handleUpdate(mkJoinRequest(777));
assert.equal(calls.find((c) => c.method === "approveChatJoinRequest").payload.user_id, 777);
await bot.handleUpdate(mkJoinRequest(555));
assert.equal(calls.find((c) => c.method === "declineChatJoinRequest").payload.user_id, 555);
chat_join_request da ctx.from so'rov yuborgan foydalanuvchi. Ruxsat etilganga approve, qolganiga decline chaqirildi.
12-mashq yechimi¶
const { bot, calls } = makeBot(); // transformer createChatInviteLink uchun {invite_link:"https://t.me/+FAKEINVITE"} qaytaradi
let link = null;
bot.command("havola", async (ctx) => {
const res = await ctx.createChatInviteLink({ creates_join_request: true, name: "Reklama" });
link = res.invite_link;
await ctx.reply(`Havola: ${link}`);
});
await bot.handleUpdate(mkText("/havola"));
const cil = calls.find((c) => c.method === "createChatInviteLink");
assert.equal(cil.payload.creates_join_request, true);
assert.equal(cil.payload.name, "Reklama");
assert.equal(link, "https://t.me/+FAKEINVITE");
createChatInviteLink join-request rejimida havola yaratdi (creates_join_request: true); qaytgan invite_link o'qildi.
13-mashq yechimi¶
const { bot, calls } = makeBot();
let status = "left"; // boshida a'zo emas, keyin obuna bo'ladi
bot.api.config.use((prev, method, payload) => {
if (method === "getChatMember") {
return Promise.resolve({ ok: true, result: { status, user: { id: payload.user_id, is_bot: false, first_name: "U" } } });
}
return prev(method, payload);
});
const kesh = new Map();
const TTL = 600000; let now = 1000;
async function azoMi(ctx) {
const id = ctx.from.id;
const y = kesh.get(id);
if (y && now - y.vaqt < TTL) return y.azo;
const m = await ctx.api.getChatMember(KANAL, id);
const azo = AZO.has(m.status);
kesh.set(id, { azo, vaqt: now });
return azo;
}
bot.use(async (ctx, next) => {
if (ctx.callbackQuery) return await next(); // callback gate'dan o'tsin
if (await azoMi(ctx)) await next(); else await ctx.reply("Obuna shart");
});
bot.callbackQuery("obuna:tekshir", async (ctx) => {
const m = await ctx.api.getChatMember(KANAL, ctx.from.id);
const azo = AZO.has(m.status);
kesh.set(ctx.from.id, { azo, vaqt: now }); // KESHNI YANGILA
await ctx.answerCallbackQuery();
});
let handlerRan = false;
bot.on("message:text", (ctx) => { handlerRan = true; });
await bot.handleUpdate(mkText("a", 1)); // "left" -> gate to'xtatadi (kesh: false)
assert.equal(handlerRan, false);
status = "member"; // foydalanuvchi obuna bo'ldi
await bot.handleUpdate(mkCallback("obuna:tekshir", 2)); // keshni true ga yangilaydi
const oldin = calls.filter((c) => c.method === "getChatMember").length;
await bot.handleUpdate(mkText("b", 3)); // keshdan true -> o'tadi
assert.equal(handlerRan, true);
assert.equal(calls.filter((c) => c.method === "getChatMember").length, oldin); // qayta chaqirilmadi
"Tekshirish" callback keshni true ga yangilagani uchun keyingi xabarda gate getChatMember ni qayta chaqirmasdan keshdan o'qib o'tkazdi. Bu β kesh + callback'ni to'g'ri bog'lashning kaliti (bobdagi "Tekshirish keshni yangilashi kerak" diqqatiga qarang).
β¬ οΈ Oldingi: 21 β Kanallar bilan ishlash Β· π README Β· Keyingi: 23 β Telegram Web App (Mini App) asoslari β‘οΈ