22 β Majburiy obuna¶
β¬ οΈ Oldingi: 21 β Kanallar bilan ishlash Β· π README Β· Keyingi: 23 β Telegram Web App (Mini App) asoslari β‘οΈ
Bu bobda: "Majburiy obuna" (forced subscription) β foydalanuvchini bot xizmatidan foydalanishdan oldin bir yoki bir nechta kanalga obuna bo'lishga majburlash. Bu O'zbekistonda eng ko'p so'raladigan amaliy funksiya. Biz quyidagilarni o'rganamiz:
bot.get_chat_member(channel, user_id)bilan obunani aniqlash (statusleft/kicked= obuna emas;member/administrator/creator= obuna;restrictedesais_memberga qarab); obuna middleware (./09 ga tayanib β har bir handlerdan oldin tekshiradi, obuna bo'lmasa "gate" ko'rsatib handlerni to'xtatadi); "Obuna bo'ling" inline gate klaviaturasi (kanal URL tugmasi + "β Tekshirish" callback); bir nechta majburiy kanal; private kanal (invite link +@router.chat_join_requestorqali avtomatik approve); va obunani qisqa vaqt keshlash (qayta-qaytaget_chat_memberchaqirmaslik).Halol eslatma: bu bobdagi obuna mantiqi β
is_subscribedtekshiruvi, middleware'ning gate/handler tarmoqlari, gate klaviaturasini qurish, "Tekshirish" callback, kesh vachat_join_requestapprove oqimi βfeed_update+get_chat_member/approve_chat_join_requestni mock (AsyncMock) qilib, offline va tokensiz haqiqatan ishga tushirilib tekshirildi. Natijalar matnda ko'rsatilgan. Jonli qism (real foydalanuvchining kanalga obuna bo'lishi, bot kanalda admin bo'lishi, jonli approve) BotFather tokeni, internet va kanal-admin huquqini talab qiladi β bunday joylar "illustrativ" deb halol belgilangan.
1. Majburiy obuna nima va qanday ishlaydi¶
Tasavvur qiling: sizda foydali bot bor (masalan, kino qidiruvchi yoki kursga ro'yxatdan o'tkazuvchi). Siz xohlaysizki, undan foydalanadigan har bir kishi avval sizning kanalingizga obuna bo'lsin. Bu β majburiy obuna.
Mantiq sodda va u butunlay bitta API metodga tayanadi: bot.get_chat_member(channel, user_id). Bu metod "falon kanalda falon foydalanuvchi qanday a'zo?" degan savolga javob beradi β ChatMember obyektini qaytaradi, undagi .status esa foydalanuvchining holatini bildiradi.
Oqim quyidagicha: foydalanuvchi botga xabar yozadi β obuna middleware uni ushlab, get_chat_member orqali obunani tekshiradi β agar obuna bo'lsa, handler ishlaydi; aks holda foydalanuvchiga "Obuna bo'ling" gate (darvoza) ko'rsatiladi va handler umuman ishlamaydi.
Bu bob to'liq 09 β Middleware ga tayanadi. Agar
BaseMiddleware,__call__(self, handler, event, data),await handler(event, data)ni chaqirmaslik orqali handlerni to'xtatish vaoutervsinnerfarqi sizga notanish bo'lsa, avval o'sha bobni qayta ko'rib chiqing. Kanal ID, bot kanalda admin bo'lishi va kanal turlari uchun 21 β Kanallar bilan ishlash foydali.
Muhim shart. get_chat_member ishlashi uchun bot o'sha kanalda admin bo'lishi shart (yoki kamida a'zolarni ko'ra olishi kerak). Aks holda Telegram xato qaytaradi. Bu jonli talab β offline testda biz metodni mock qilamiz, lekin real botda kanalga botni qo'shib, admin qilib qo'yishni unutmang.
2. Obunani aniqlash: get_chat_member va status¶
bot.get_chat_member(chat_id, user_id) qaytargan ChatMember ning .status maydoni aiogram.enums.ChatMemberStatus qiymatlaridan biri bo'ladi. Ularning ma'nosi:
status |
Ma'nosi | Obuna bormi? |
|---|---|---|
creator |
Kanal egasi | β Ha |
administrator |
Admin | β Ha |
member |
Oddiy a'zo | β Ha |
restricted |
Cheklangan | β οΈ Faqat is_member=True bo'lsa |
left |
Chiqib ketgan / hech qachon a'zo bo'lmagan | β Yo'q |
kicked |
Ban qilingan | β Yo'q |
Demak qoida: a'zo = status creator, administrator yoki member (yoki restricted bo'lib is_member=True). left va kicked β obuna emas.
Nega
left"hech qachon a'zo bo'lmagan" ni ham bildiradi? Telegram obuna bo'lmagan odam uchun hamleftqaytaradi β ya'ni "hozir kanalda yo'q" degani. Shuning uchunleftni "obuna emas" deb hisoblash to'g'ri.
Mana obunani aniqlovchi sof funksiya. U ChatMember obyektini olib, bool qaytaradi:
from aiogram.enums import ChatMemberStatus
OK_STATUSES = {
ChatMemberStatus.CREATOR,
ChatMemberStatus.ADMINISTRATOR,
ChatMemberStatus.MEMBER,
}
def is_subscribed_status(member) -> bool:
"""ChatMember -> obuna bormi?"""
if member.status in OK_STATUSES:
return True
# restricted bo'lsa, faqat is_member=True bo'lsa hali a'zo hisoblanadi
if member.status == ChatMemberStatus.RESTRICTED:
return bool(getattr(member, "is_member", False))
return False
Biz buni har bir status uchun offline ishga tushirib tekshirdik (har xil ChatMember* obyektlari yasab). Haqiqiy natija:
creator -> True
administrator -> True
member -> True
left -> False
kicked -> False
restricted is_member=True -> True
restricted is_member=False -> False
Bitta kanal uchun bitta foydalanuvchini tekshiruvchi yordamchi:
async def is_subscribed(bot, channel_id: int, user_id: int) -> bool:
member = await bot.get_chat_member(channel_id, user_id)
return is_subscribed_status(member)
Statusni hardkod string bilan solishtirmang.
if member.status == "member"ham ishlaydi, lekinChatMemberStatusenum'idan foydalanish β xato yozish ehtimolini kamaytiradi va kod o'qimishliroq bo'ladi. Enum qiymatlari aynan"creator","administrator","member","restricted","left","kicked".
3. "Obuna bo'ling" gate klaviaturasi¶
Foydalanuvchi obuna bo'lmagan bo'lsa, unga ikki narsa kerak: (1) kanalga o'tish tugmasi (URL tugma) va (2) obunani qayta tekshirish tugmasi (callback). Buni 06 β Klaviaturalar dagi InlineKeyboardBuilder bilan quramiz:
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
def build_gate_keyboard(channels) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder()
for ch in channels:
# URL tugma: foydalanuvchini to'g'ridan-to'g'ri kanalga olib boradi
kb.button(text=f"π’ {ch['title']}", url=ch["url"])
# Callback tugma: obunani qayta tekshirish
kb.button(text="β
Tekshirish", callback_data="check_sub")
kb.adjust(1) # har bir tugma alohida qatorda
return kb.as_markup()
Bu yerda channels β har bir kanal uchun title va url (ochiq kanallar uchun https://t.me/...) saqlovchi lug'atlar ro'yxati. Biz klaviaturani offline qurib, tugmalarni tekshirdik. Haqiqiy natija (bitta kanal bilan):
qatorlar soni: 2
1-tugma: text='π’ Mening kanalim' url='https://t.me/mychannel'
2-tugma: text='β
Tekshirish' callback_data='check_sub'
Birinchi qator β URL tugmasi (kanalga o'tish), ikkinchi qator β callback_data="check_sub" bo'lgan tekshirish tugmasi. adjust(1) har tugmani o'z qatoriga joylaydi β tartibli ko'rinish uchun.
URL tugma uchun kanal
usernamebo'lishi kerak.https://t.me/usernameko'rinishidagi havola faqat ochiq (public) kanalga ishlaydi. Private kanal uchun esa invite link kerak β buni 7-bo'limda ko'ramiz.
4. Obuna middleware β gate'ni har handlerdan oldin qo'yish¶
Endi eng muhim qism. Biz obuna tekshiruvini har bir handler ichida takrorlashni xohlamaymiz β bu 09-bobdagi "30 ta handler" muammosi. O'rniga middleware yozamiz: u har bir kelgan xabar/callback handler'ga yetishidan oldin ishlab, obunani tekshiradi. Obuna bo'lmasa, gate ko'rsatadi va await handler(...) ni chaqirmaydi β shu bilan handler bloklanadi.
Avval bir nechta kanalni hammasini tekshiruvchi yordamchini yozamiz. U obuna bo'lmagan kanallar ro'yxatini qaytaradi (gate'da faqat o'shalarni ko'rsatish uchun):
async def check_all_subscriptions(bot, user_id, channels):
not_subscribed = []
for ch in channels:
member = await bot.get_chat_member(ch["id"], user_id)
if not is_subscribed_status(member):
not_subscribed.append(ch)
ok = len(not_subscribed) == 0
return ok, not_subscribed
Endi middleware. Diqqat: uni message va callback_query darajasida ro'yxatga olamiz (dp.update darajasida emas). Sababi β dp.update.outer_middleware da event argumenti butun Update bo'ladi, biz esa gate javobini yuborish uchun aniq Message/CallbackQuery turini bilishimiz kerak. (Buni biz offline test paytida isinstance(event, Message) ishlamay qolgani orqali aniqladik β Update darajasida event Message emas, Update bo'lar ekan.)
from typing import Any, Dict
from aiogram import BaseMiddleware, Bot
from aiogram.types import Message, CallbackQuery
class SubscriptionMiddleware(BaseMiddleware):
def __init__(self, channels):
self.channels = channels
async def __call__(self, handler, event, data: Dict[str, Any]) -> Any:
user = data.get("event_from_user")
if user is None:
# foydalanuvchi yo'q (masalan, kanal posti) β tekshirmaymiz
return await handler(event, data)
bot: Bot = data["bot"]
ok, missing = await check_all_subscriptions(bot, user.id, self.channels)
if ok:
return await handler(event, data) # obuna bor -> davom
# obuna EMAS -> gate ko'rsatamiz, handlerni chaqirmaymiz
kb = build_gate_keyboard(missing)
text = "Botdan foydalanish uchun quyidagi kanal(lar)ga obuna bo'ling:"
if isinstance(event, Message):
await event.answer(text, reply_markup=kb)
elif isinstance(event, CallbackQuery):
# callback'da to'g'ridan-to'g'ri javob: alert chiqaramiz
await event.answer("Avval obuna bo'ling!", show_alert=True)
return # <- handler chaqirilmaydi (gate qo'yildi)
Ro'yxatga olish β message va callback_query darajasida, outer (filtrlashdan oldin, hamma uchun):
channels = [
{"id": -1001234567890, "url": "https://t.me/mychannel", "title": "Mening kanalim"},
]
dp.message.outer_middleware(SubscriptionMiddleware(channels))
dp.callback_query.outer_middleware(SubscriptionMiddleware(channels))
data["event_from_user"]β bu 09-bobda ko'rganimizdek, aiogram avtomatik soladigan ishonchli kalit;MessagevaCallbackQueryda bir xil ishlaydi.data["bot"]esa joriyBotobyekti.
Offline tekshiruv β ikki tarmoq¶
Endi eng muhim qism: ikki tarmoqni (obuna bor / obuna yo'q) haqiqatan tekshirdik. bot.get_chat_member ni AsyncMock bilan almashtirib, bir holatda left (obuna yo'q), boshqasida member (obuna bor) qaytardik, so'ng /start ni feed_update bilan yubordik.
Obuna YO'Q (get_chat_member -> left) tarmog'i β handler ishlamasligi kerak:
import asyncio
from datetime import datetime
from unittest.mock import AsyncMock
from aiogram import Bot, Dispatcher, Router
from aiogram.filters import CommandStart
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import (
Update, Message, Chat, User, ChatMemberLeft, ChatMemberMember,
)
FAKE = "123456:AAH-FakeTest_abc" # soxta token β offline uchun
router = Router()
@router.message(CommandStart())
async def start(message: Message):
# jonli botda: await message.answer("Xush kelibsiz!")
print("[HANDLER] /start ishladi, user:", message.from_user.id)
def make_start(uid):
msg = Message(message_id=1, date=datetime.now(),
chat=Chat(id=uid, type="private"),
from_user=User(id=uid, is_bot=False, first_name="Ali"),
text="/start")
return Update(update_id=1, message=msg)
async def main():
u = User(id=1, is_bot=False, first_name="Ali")
# --- obuna YO'Q: left qaytaramiz ---
bot = Bot(token=FAKE)
bot.get_chat_member = AsyncMock(return_value=ChatMemberLeft(status="left", user=u))
dp = Dispatcher(storage=MemoryStorage())
dp.message.outer_middleware(SubscriptionMiddleware(channels))
dp.include_router(router)
print("obuna YO'Q:")
await dp.feed_update(bot, make_start(1)) # handler ishlamasligi kerak
await bot.session.close()
asyncio.run(main())
Bu testni biz haqiqatan ishga tushirdik. Haqiqiy natija (gate'ning event.answer qatorini offline'da log bilan almashtirib):
=== middleware: obuna YO'Q (status=left) ===
log = [('gate-message', 1, 1)]
-> handler ISHLAMADI, gate ko'rsatildi (to'g'ri)
Va obuna BOR (get_chat_member -> member) tarmog'i β handler ishlashi kerak:
Demak middleware to'g'ri ishlaydi: obuna bo'lmaganda handler umuman chaqirilmadi (faqat gate logi bor), obuna bo'lganda esa handler ishladi. Bu aynan 09-bobdagi "short-circuit" naqshi β await handler(...) ni chaqirmaslik orqali update'ni to'xtatish.
event.answerjonli qism. Yuqoridagiawait event.answer(...)vaawait event.answer(..., show_alert=True)chaqiruvlari jonli botda foydalanuvchiga xabar/alert yuboradi β bu token+internet talab qiladi. Offline testda biz bu qatorlarni log yozish bilan almashtirib, middleware'ning qaror mantiqini (qaysi tarmoqqa ketishini) tekshirdik. Kod aiogram 3.x uchun to'g'ri.
5. "β Tekshirish" callback β gate'ni yopish¶
Foydalanuvchi kanalga obuna bo'lgach, "β
Tekshirish" tugmasini bosadi. Bu callback_data="check_sub" ni yuboradi. Endi shu callback'ni qayta ishlaymiz: obunani yangidan tekshiramiz (chunki foydalanuvchi endi obuna bo'lgan bo'lishi mumkin) va natijaga qarab javob beramiz.
Diagrammada ko'rinib turibdi: gate'da URL tugmalari + "β Tekshirish" bor; foydalanuvchi tekshirishni bosganda callback handler obunani qayta tekshiradi β obuna bo'lsa "Xush kelibsiz" (gate yopiladi), bo'lmasa alert chiqib gate qoladi (aylana).
from aiogram import F
from aiogram.types import CallbackQuery
@router.callback_query(F.data == "check_sub")
async def on_check(callback: CallbackQuery, bot: Bot):
ok, missing = await check_all_subscriptions(bot, callback.from_user.id, channels)
if ok:
# hammasiga obuna bo'ldi -> gate'ni yopamiz, ichkariga kiritamiz
await callback.message.edit_text("Rahmat! Endi botdan foydalanishingiz mumkin. /start")
await callback.answer()
else:
# hali hammasiga obuna emas -> alert chiqaramiz, gate qoladi
await callback.answer("Hali hamma kanalga obuna bo'lmadingiz!", show_alert=True)
Diqqat β
check_subcallback va middleware. "Tekshirish" tugmasi callback yuboradi, lekin foydalanuvchi hali obuna bo'lmagan bo'lishi mumkin β shunda yuqoridagiSubscriptionMiddleware(callback_query darajasida ham ulangan) bu callback'ni ham bloklaydi vaon_checkumuman ishlamaydi! Buni hal qilish uchun ikki yo'l bor: (1)check_subcallback'ni middleware'da istisno qiling (if isinstance(event, CallbackQuery) and event.data == "check_sub": return await handler(event, data)β middleware boshida), yoki (2) tekshirish mantiqini middleware'ning callback-tarmog'iga (isinstance(event, CallbackQuery)bo'lganda obunani qayta tekshirib, o'tgan bo'lsahandlerni chaqirish) joylang. Sodda botlar uchun (1) β istisno qilish β eng oson.
Quyida (1) variantli to'liqroq middleware (callback check_sub ni o'tkazib yuboradi):
class SubscriptionMiddleware(BaseMiddleware):
def __init__(self, channels):
self.channels = channels
async def __call__(self, handler, event, data):
# "Tekshirish" callback'ini ALBATTA o'tkazamiz β aks holda
# obuna bo'lmagan user uni hech qachon bosa olmaydi
if isinstance(event, CallbackQuery) and event.data == "check_sub":
return await handler(event, data)
user = data.get("event_from_user")
if user is None:
return await handler(event, data)
bot = data["bot"]
ok, missing = await check_all_subscriptions(bot, user.id, self.channels)
if ok:
return await handler(event, data)
kb = build_gate_keyboard(missing)
if isinstance(event, Message):
await event.answer(
"Botdan foydalanish uchun quyidagi kanal(lar)ga obuna bo'ling:",
reply_markup=kb,
)
elif isinstance(event, CallbackQuery):
await event.answer("Avval obuna bo'ling!", show_alert=True)
return
Callback mantiqining ikki tarmog'ini offline tekshirdik. Avval get_chat_member -> left (hali obuna yo'q), keyin -> member (obuna bo'ldi). Haqiqiy natija (callback.answer/edit_text ni log bilan almashtirib):
=== 'Tekshirish' callback ===
obuna yo'q -> ('still-not-subscribed', 2)
obuna bor -> ('welcome',)
-> to'g'ri
Birinchi marta (obuna yo'q) β 2 ta kanalga hali obuna emas, alert chiqdi; ikkinchi marta (obuna bo'ldi) β "welcome", gate yopildi.
6. Bir nechta majburiy kanal¶
Ko'pincha bitta emas, bir nechta kanalga obunani talab qilishadi. Biz yuqorida buni allaqachon ko'zda tutdik: channels β bu ro'yxat, check_all_subscriptions esa har bir kanalni aylanib chiqib, obuna bo'lmaganlarini to'playdi. Gate'da esa faqat o'sha "qolgan" kanallar ko'rsatiladi (foydalanuvchi 3 tadan 2 tasiga obuna bo'lsa, faqat 1 tasi ko'rinadi).
channels = [
{"id": -1001111111111, "url": "https://t.me/ch1", "title": "Kanal 1"},
{"id": -1002222222222, "url": "https://t.me/ch2", "title": "Kanal 2"},
]
Biz buni offline tekshirdik: get_chat_member ni shunday mock qildikki, 1-kanalga member (obuna), 2-kanalga left (obuna emas) qaytarsin. Haqiqiy natija:
=== bir nechta kanal: birortasiga obuna emas ===
obuna bo'lmagan kanallar: ['Kanal 2']
hammasiga obuna bo'lganda: []
-> to'g'ri
Demak foydalanuvchi faqat "Kanal 2" ga obuna bo'lmagani aniqlandi β gate'da faqat shu kanal tugmasi chiqadi. Hammasiga obuna bo'lganda esa ro'yxat bo'sh ([]) β gate ko'rsatilmaydi.
Ko'p kanal = ko'p API chaqiruv. Har bir kanal uchun alohida
get_chat_memberchaqiruvi ketadi. 5 kanal + 1000 faol foydalanuvchi = juda ko'p so'rov. Aynan shu sababli keyingi bo'limdagi kesh muhim. Bundan tashqari,asyncio.gatherbilan kanallarni parallel tekshirish ham mumkin (lekin Telegram limitiga ehtiyot bo'ling β 15 β Broadcast va flood control ga qarang).
7. Private kanal β invite link va chat_join_request¶
Yuqoridagi https://t.me/username URL tugmasi faqat ochiq kanalga ishlaydi. Private (yopiq) kanalda username yo'q β faqat invite link orqali kirish mumkin. Bundan tashqari, private kanalga kirishni tasdiqlash (join request) bilan sozlash mumkin: foydalanuvchi linkni bosadi, "Join" so'rovi botga keladi, bot esa shartlarni tekshirib approve yoki decline qiladi.
Invite link yaratish¶
Bot kanalda admin bo'lib, "invite users" huquqiga ega bo'lsa, creates_join_request=True bilan link yaratadi β bunda har kirish bot tasdig'ini talab qiladi:
async def make_join_link(bot, channel_id: int) -> str:
link = await bot.create_chat_invite_link(
chat_id=channel_id,
creates_join_request=True, # har kirish approve talab qiladi
name="Bot orqali kirish",
)
return link.invite_link # https://t.me/+AbCdEf... ko'rinishida
Bu linkni gate klaviaturasidagi URL tugmasiga qo'yamiz (public kanaldagi https://t.me/username o'rniga).
Bu chaqiruv jonli β bot kanalda admin bo'lishi va token kerak (illustrativ).
create_chat_invite_linkaiogram 3.x'da mavjud metod; uning natijasiChatInviteLinkobyekti,invite_linkmaydoni esa to'liq URL.
chat_join_request handler β approve / decline¶
Foydalanuvchi join so'rovi yuborganda, botga chat_join_request update keladi. Uni @router.chat_join_request() bilan ushlaymiz:
from aiogram.types import ChatJoinRequest
@router.chat_join_request()
async def on_join_request(request: ChatJoinRequest, bot: Bot):
user_id = request.from_user.id
chat_id = request.chat.id
# bu yerda istalgan shartni tekshirish mumkin (masalan, boshqa kanalga obuna)
shartlar_bajarildi = True
if shartlar_bajarildi:
# qabul qilish β ikki usul bir xil ishlaydi:
await bot.approve_chat_join_request(chat_id, user_id)
# yoki qisqa shortcut: await request.approve()
else:
await bot.decline_chat_join_request(chat_id, user_id)
# yoki: await request.decline()
ChatJoinRequest ning .approve() / .decline() shortcut'lari ham bor (ular ichkarida xuddi shu metodlarni chaqiradi). Offline test uchun biz bot.approve_chat_join_request ni AsyncMock qilib, chat_join_request update'ini feed_update bilan yubordik. Haqiqiy natija:
=== chat_join_request approve (private kanal) ===
join_log = [('request', 7, -1001), ('approved', 7)]
approve_chat_join_request chaqirildi: 1 marta
-> chat_join_request tutildi va approve() chaqirildi (to'g'ri)
Demak handler join so'rovini ushladi (('request', 7, -1001)) va approve_chat_join_request ni aniq 1 marta chaqirdi. request.approve() shortcut'i jonli Telegram serveriga so'rov yuboradi, shuning uchun offline testda biz bot.approve_chat_join_request(...) ko'rinishini mock qilib ishlatdik β natija bir xil.
Private kanalda obunani "tekshirish" o'rniga "approve". Public kanalda foydalanuvchi obuna bo'ladi, biz
get_chat_memberbilan tekshiramiz. Private + join-request modelida esa boshqacha: foydalanuvchi so'rov yuboradi, biz unichat_join_requestda darhol approve qilamiz. Bu ikkala modelni birga ham ishlatish mumkin (masalan: avval boshqa public kanalga obunani tekshirib, keyin private kanalga approve berish).
8. Obunani keshlash β qayta-qayta API chaqirmaslik¶
Har bir xabarda har bir kanal uchun get_chat_member chaqirish β sekin va Telegram limitiga urilishi mumkin. Yechim: tekshiruv natijasini qisqa vaqtga (masalan, 5 daqiqa) eslab qolish β kesh. Foydalanuvchi obuna deb topilsa, keyingi xabarlarida API'ni qayta chaqirmaymiz.
Oddiy TTL (vaqt bilan eskirib o'chadigan) keshni time.monotonic() bilan yozamiz (09-bobdagi throttling'dagi kabi monotonic ishlatamiz):
import time
from typing import Dict
class SubCache:
def __init__(self, ttl: float = 300.0): # 5 daqiqa
self.ttl = ttl
self._store: Dict[int, tuple[bool, float]] = {}
def get(self, user_id: int):
item = self._store.get(user_id)
if item is None:
return None
ok, ts = item
if time.monotonic() - ts > self.ttl: # eskirgan -> o'chir
del self._store[user_id]
return None
return ok
def set(self, user_id: int, ok: bool):
self._store[user_id] = (ok, time.monotonic())
def invalidate(self, user_id: int):
self._store.pop(user_id, None) # "Tekshirish" da tozalash uchun
check_all_subscriptions ga keshni ulaymiz: avval keshga qaraymiz, bo'lmasa API'ni chaqirib, natijani keshlaymiz:
async def check_all_subscriptions(bot, user_id, channels, cache=None):
if cache is not None:
cached = cache.get(user_id)
if cached is not None:
return cached, [] # keshdan β API'ga bormaymiz
not_subscribed = []
for ch in channels:
member = await bot.get_chat_member(ch["id"], user_id)
if not is_subscribed_status(member):
not_subscribed.append(ch)
ok = len(not_subscribed) == 0
if cache is not None:
cache.set(user_id, ok)
return ok, not_subscribed
Keshni offline tekshirdik: bitta foydalanuvchi uchun check_all_subscriptions ni ikki marta chaqirdik va get_chat_member necha marta chaqirilganini sanadik. Haqiqiy natija:
=== kesh ishlashi ===
ok1=True ok2=True, get_chat_member chaqiruvlari soni: 1
-> ikkinchi tekshiruv keshdan keldi (API 1 marta chaqirildi)
Demak ikkinchi tekshiruv API'ga umuman bormadi β natija keshdan keldi. 1000 foydalanuvchili botda bu juda katta tejamkorlik.
Muhim β keshni "Tekshirish" da tozalang. Agar siz
ok=False(obuna emas) ni ham keshlasangiz, foydalanuvchi obuna bo'lib "β Tekshirish" ni bosganda, kesh hali "obuna emas" deb tursa, gate yopilmaydi! Shuning uchunon_checkcallback'ida obunani tekshirishdan oldincache.invalidate(user_id)chaqiring (yoki faqatok=Trueni keshlang,ok=Falseni keshlamang). Ishlab chiqarish (production) botidacachetools.TTLCacheyoki Redis ishlatish β kesh xotirasi cheksiz o'smasligi uchun (10 β Ma'lumotlar bazasi va 17 β Production deploy foydali).
9. To'liq tasvir β barchasi birga¶
Yuqoridagi qismlarni jamlasak, majburiy obunali bot quyidagi qismlardan iborat:
is_subscribed_status(member)β statusdan obunani aniqlovchi sof funksiya (2-bo'lim).check_all_subscriptions(bot, user_id, channels, cache)β barcha kanallarni (kesh bilan) tekshiruvchi (6, 8-bo'lim).build_gate_keyboard(channels)β URL + "Tekshirish" tugmali klaviatura (3-bo'lim).SubscriptionMiddlewareβ har handler/callback'dan oldin gate qo'yuvchi,check_subni istisno qiluvchi (4, 5-bo'lim).on_checkcallback β "Tekshirish" bosilganda obunani qayta tekshirib gate'ni yopuvchi (5-bo'lim).- (private uchun)
make_join_link+@router.chat_join_requestβ invite link va approve (7-bo'lim).
Ro'yxatga olish tartibi:
cache = SubCache(ttl=300.0)
dp.message.outer_middleware(SubscriptionMiddleware(channels, cache))
dp.callback_query.outer_middleware(SubscriptionMiddleware(channels, cache))
dp.include_router(router) # router ichida start, on_check, on_join_request
Anti-misol β β ESKIRGAN 2.x usuli. Eski qo'llanmalarda
@dp.message_handler(...)+dp.middleware.setup(...)+raise CancelHandler()ko'rishingiz mumkin. Bu aiogram 2.x sintaksisi va 3.x'da ishlamaydi. 3.x'da:BaseMiddleware+__call__+await handler(...)ni chaqirmaslik orqali to'xtatish (CancelHandler shart emas). Eski kodni ko'rsangiz, uni 3.x ga o'tkazing.Etika va Telegram qoidalari. Majburiy obuna kuchli vosita, lekin Telegram qoidalariga zid agressiv ishlatish (masalan, foydalanuvchini aldab obuna qildirib, keyin darrov chiqishga undash) hisobingizga cheklov keltirishi mumkin. Faqat o'z kanallaringizga, halol tarzda ishlating.
Mashqlar¶
Oson¶
get_chat_memberqaytarganstatusning qaysi qiymatlari "obuna bor" deb hisoblanadi, qaysilari "obuna emas"?restrictedqachon "a'zo" hisoblanadi?is_subscribed_statusfunksiyasini yozing:ChatMemberobyektini olib, obuna bor-yo'qliginiboolqaytarsin.creator,administrator,member->True;left,kicked->False.- Obuna middleware nima uchun
dp.updatedarajasida emas,dp.message/dp.callback_querydarajasida ro'yxatga olinadi? (Eslatma:eventturi bilan bog'liq.) - Gate klaviaturasida ikki turdagi tugma bor. Ularning maqsadi nima va
InlineKeyboardBuilderda qaysi parametr bilan quriladi (url=vscallback_data=)? - Middleware obuna bo'lmaganini aniqlasa, handler ishlashi uchun nimani qilmasligi kerak? Bu qaysi 09-bob naqshiga asoslanadi?
- Public kanal va private kanal uchun foydalanuvchini kanalga yo'naltirish nimasi bilan farq qiladi (URL turi bo'yicha)?
O'rta¶
build_gate_keyboard(channels)ni yozing va 2 ta kanal bilan offline chaqirib, klaviaturada nechta qator chiqishini oldindan ayting, keyin tekshiring (har kanal alohida qatorda + "Tekshirish" qatori).check_all_subscriptions(bot, user_id, channels)ni yozing.bot.get_chat_memberniAsyncMockbilan shunday almashtiringki, 1-kanalgamember, 2-kanalgaleftqaytarsin. Qaysi kanal "obuna emas" ro'yxatiga tushishini tekshiring.SubscriptionMiddlewareni yozing vafeed_updatebilan ikki tarmoqni tekshiring:get_chat_member->leftbo'lganda/starthandler ishlamasligini,-> memberbo'lganda ishlashini isbotlang (handler ichidaprint/log bilan).- "β
Tekshirish" callback'i obuna bo'lmagan foydalanuvchida ham bosilishi uchun middleware'da
check_subni istisno qiling. Buni offline tekshiring: obuna yo'q usercheck_subcallback yuborgandaon_checkhaqiqatan ishlasin. SubCache(TTL kesh) yozing. Bitta foydalanuvchini ikki marta tekshirib,get_chat_memberfaqat 1 marta chaqirilishini (ikkinchisi keshdan)AsyncMock.await_countbilan tasdiqlang.@router.chat_join_request()handler yozing: join so'rovini ushlab,bot.approve_chat_join_request(chat_id, user_id)chaqirsin.feed_update+ mock bilan approve aniq 1 marta chaqirilishini tekshiring.
Qiyin¶
- Keshli to'liq oqimni yozing: foydalanuvchi obuna emas (
left) -> gate ko'rinadi -> (mock'nimemberga o'zgartiring) ->on_checkcallback'dacache.invalidate+ qayta tekshiruv -> gate yopiladi. Kesh tozalanmasa gate yopilmasligini ham ko'rsating. asyncio.gatherbilan barcha kanallarni parallel tekshiruvchicheck_all_subscriptionsvariantini yozing. 3 ta kanalni mock qilib, natija ketma-ket versiya bilan bir xil chiqishini tasdiqlang. Parallel tekshiruvning afzalligi va xatari (Telegram limit) nimada?- Ikki bosqichli gate yozing: foydalanuvchi avval public kanalga obuna bo'lishi (
get_chat_memberbilan tekshiriladi), so'ng private kanalga join so'rovi yuborishi kerak.chat_join_requestda public kanalga obunani tekshirib, obuna bo'lsa approve, bo'lmasa decline qiling. Offline ikki tarmoqni tekshiring. restrictedholatini to'liq qamrang:is_member=True(a'zo) vais_member=False(chiqib ketgan, lekin cheklov muddati tugamagan) uchunis_subscribed_statusto'g'riTrue/FalseqaytarishiniChatMemberRestrictedobyekti yasab offline tasdiqlang.
Yechimlar
1. creator, administrator, member -> obuna bor. left, kicked -> obuna emas. restricted -> faqat is_member=True bo'lsa a'zo (cheklangan, lekin hali kanalda); is_member=False bo'lsa β emas.
2.
from aiogram.enums import ChatMemberStatus
OK = {ChatMemberStatus.CREATOR, ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.MEMBER}
def is_subscribed_status(member) -> bool:
if member.status in OK:
return True
if member.status == ChatMemberStatus.RESTRICTED:
return bool(getattr(member, "is_member", False))
return False
3. dp.update.outer_middleware da event argumenti butun Update bo'ladi β undan isinstance(event, Message) False qaytaradi, shuning uchun gate javobini (message.answer / callback.answer) yubora olmaymiz. dp.message / dp.callback_query darajasida esa event aniq Message / CallbackQuery bo'ladi β gate'ni to'g'ri yuborish mumkin. (Buni biz offline test paytida isinstance ishlamay qolgani orqali aniqladik.)
4. Ikki tugma: (1) URL tugma (url=) β foydalanuvchini to'g'ridan-to'g'ri kanalga olib boradi; (2) callback tugma (callback_data="check_sub") β obunani qayta tekshirish uchun botga signal yuboradi. InlineKeyboardBuilder da kb.button(text=..., url=...) va kb.button(text=..., callback_data=...).
5. Middleware await handler(event, data) ni chaqirmasligi kerak β oddiygina return qiladi. Bu 09-bobdagi "short-circuit" (zanjirni uzish) naqshi: handler chaqirilmasa, update tashlanadi.
6. Public kanal: https://t.me/username (kanal username'i bor). Private kanal: username yo'q, faqat invite link (https://t.me/+AbCdEf...), uni create_chat_invite_link bilan yaratiladi.
7. 2 kanal -> 3 qator (har kanal alohida + 1 "Tekshirish").
from aiogram.utils.keyboard import InlineKeyboardBuilder
def build_gate_keyboard(channels):
kb = InlineKeyboardBuilder()
for ch in channels:
kb.button(text=f"π’ {ch['title']}", url=ch["url"])
kb.button(text="β
Tekshirish", callback_data="check_sub")
kb.adjust(1)
return kb.as_markup()
channels = [
{"url": "https://t.me/ch1", "title": "Kanal 1"},
{"url": "https://t.me/ch2", "title": "Kanal 2"},
]
kb = build_gate_keyboard(channels)
print(len(kb.inline_keyboard)) # -> 3
assert len(kb.inline_keyboard) == 3
8.
import asyncio
from unittest.mock import AsyncMock
from aiogram import Bot
from aiogram.types import User, ChatMemberMember, ChatMemberLeft
from aiogram.enums import ChatMemberStatus
OK = {ChatMemberStatus.CREATOR, ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.MEMBER}
async def check_all(bot, uid, channels):
miss = []
for ch in channels:
m = await bot.get_chat_member(ch["id"], uid)
if m.status not in OK:
miss.append(ch)
return len(miss) == 0, miss
async def main():
u = User(id=1, is_bot=False, first_name="A")
bot = Bot("123456:AAH-FakeTest_abc")
async def side(chat_id, user_id):
if chat_id == -1001:
return ChatMemberMember(status="member", user=u)
return ChatMemberLeft(status="left", user=u)
bot.get_chat_member = AsyncMock(side_effect=side)
channels = [{"id": -1001, "title": "1"}, {"id": -1002, "title": "2"}]
ok, miss = await check_all(bot, 1, channels)
print(ok, [c["id"] for c in miss]) # -> False [-1002]
assert not ok and miss[0]["id"] == -1002
await bot.session.close()
asyncio.run(main())
9. 4-bo'limdagi SubscriptionMiddleware va offline test kodidagi kabi: dp.message.outer_middleware(SubscriptionMiddleware(channels)), bot.get_chat_member = AsyncMock(return_value=ChatMemberLeft(...)) -> handler ishlamaydi (faqat gate); ... = ChatMemberMember(...) -> handler ishlaydi. Handler ichida print/global log bilan ishlagan-ishlamaganini ko'rsating. (Matnda chiqqan natija: left -> log=[('gate-message',...)], member -> log=[('handler',...)].)
10. Middleware'ning eng boshiga:
from aiogram.types import CallbackQuery
async def __call__(self, handler, event, data):
if isinstance(event, CallbackQuery) and event.data == "check_sub":
return await handler(event, data) # ALBATTA o'tkaz
...
left) user check_sub callback yuborsa ham on_check ishlaydi β chunki middleware uni istisno qildi. (Aks holda gate'ni hech qachon yopolmaydi.)
11.
import asyncio, time
from unittest.mock import AsyncMock
from aiogram import Bot
from aiogram.types import User, ChatMemberMember
class SubCache:
def __init__(self, ttl=300.0):
self.ttl = ttl; self._s = {}
def get(self, uid):
it = self._s.get(uid)
if it is None: return None
ok, ts = it
if time.monotonic() - ts > self.ttl:
del self._s[uid]; return None
return ok
def set(self, uid, ok): self._s[uid] = (ok, time.monotonic())
async def check(bot, uid, channels, cache):
c = cache.get(uid)
if c is not None: return c
for ch in channels:
await bot.get_chat_member(ch["id"], uid)
cache.set(uid, True); return True
async def main():
u = User(id=1, is_bot=False, first_name="A")
bot = Bot("123456:AAH-FakeTest_abc")
mock = AsyncMock(return_value=ChatMemberMember(status="member", user=u))
bot.get_chat_member = mock
cache = SubCache()
await check(bot, 5, [{"id": -1001}], cache)
await check(bot, 5, [{"id": -1001}], cache)
print(mock.await_count) # -> 1
assert mock.await_count == 1
await bot.session.close()
asyncio.run(main())
12.
import asyncio
from datetime import datetime
from unittest.mock import AsyncMock
from aiogram import Bot, Dispatcher, Router
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Update, Chat, User, ChatJoinRequest
router = Router()
@router.chat_join_request()
async def on_jr(request: ChatJoinRequest, bot: Bot):
await bot.approve_chat_join_request(request.chat.id, request.from_user.id)
async def main():
bot = Bot("123456:AAH-FakeTest_abc")
bot.approve_chat_join_request = AsyncMock(return_value=True)
dp = Dispatcher(storage=MemoryStorage())
dp.include_router(router)
jr = ChatJoinRequest(
chat=Chat(id=-1001, type="channel", title="P"),
from_user=User(id=7, is_bot=False, first_name="A"),
user_chat_id=7, date=datetime.now(),
)
await dp.feed_update(bot, Update(update_id=1, chat_join_request=jr))
print(bot.approve_chat_join_request.await_count) # -> 1
assert bot.approve_chat_join_request.await_count == 1
await bot.session.close()
asyncio.run(main())
13. Asosiy fikr: cache.invalidate(user_id) ni on_check da, tekshirishdan oldin chaqirish.
@router.callback_query(F.data == "check_sub")
async def on_check(callback, bot, cache):
cache.invalidate(callback.from_user.id) # eski "obuna yo'q" ni o'chir
ok, miss = await check_all_subscriptions(bot, callback.from_user.id, channels, cache)
if ok:
# gate yopiladi (edit_text + answer)
...
else:
# alert, gate qoladi
...
invalidate chaqirilmasa va ok=False keshlangan bo'lsa: foydalanuvchi obuna bo'lib check_sub bossa ham, check_all_subscriptions keshdan eski False ni qaytaradi -> gate yopilmaydi. (Mock'ni left dan member ga o'zgartirib, invalidate siz keshdan False, invalidate bilan yangi True kelishini offline ko'rsating.) Alternativa: ok=False ni umuman keshlamaslik.
14.
import asyncio
async def check_parallel(bot, uid, channels):
async def one(ch):
m = await bot.get_chat_member(ch["id"], uid)
return ch, (m.status in OK)
results = await asyncio.gather(*[one(ch) for ch in channels])
miss = [ch for ch, ok in results if not ok]
return len(miss) == 0, miss
get_chat_member ketma-ket emas, bir vaqtda ketadi -> kechikish kamayadi (3x emas, ~1x). Xatar: bir foydalanuvchi uchun bir zumda 3 ta API so'rovi -> ko'p kanal + ko'p user'da Telegram flood limit ga urilish ehtimoli oshadi. Sekin-asto (ketma-ket) yoki keshli yondashuv xavfsizroq. Natija ketma-ket versiya bilan bir xil bo'lishini bir xil mock bilan assert qiling.
15.
@router.chat_join_request()
async def on_jr(request: ChatJoinRequest, bot: Bot):
PUBLIC_CH = -1009999999999
m = await bot.get_chat_member(PUBLIC_CH, request.from_user.id)
if m.status in OK: # public kanalga obuna
await bot.approve_chat_join_request(request.chat.id, request.from_user.id)
else:
await bot.decline_chat_join_request(request.chat.id, request.from_user.id)
get_chat_member -> member bo'lsa approve chaqirilishini, -> left bo'lsa decline chaqirilishini ikki alohida mock bilan await_count orqali tasdiqlang.
16.
from aiogram.types import User, ChatMemberRestricted
u = User(id=1, is_bot=False, first_name="A")
def restricted(is_member):
return ChatMemberRestricted(
status="restricted", user=u, is_member=is_member,
can_send_messages=True, can_send_audios=True, can_send_documents=True,
can_send_photos=True, can_send_videos=True, can_send_video_notes=True,
can_send_voice_notes=True, can_send_polls=True,
can_send_other_messages=True, can_add_web_page_previews=True,
can_change_info=False, can_invite_users=False, can_pin_messages=False,
can_manage_topics=False, until_date=0,
can_react_to_messages=True, can_edit_tag=False,
)
assert is_subscribed_status(restricted(True)) is True
assert is_subscribed_status(restricted(False)) is False
print("restricted OK")
ChatMemberRestricted aiogram 3.28'da yuqoridagi barcha maydonlarni talab qiladi β biz buni offline tekshirdik. Asosiy mantiq: is_member qiymati a'zolikni hal qiladi.)
β¬ οΈ Oldingi: 21 β Kanallar bilan ishlash Β· π README Β· Keyingi: 23 β Telegram Web App (Mini App) asoslari β‘οΈ