Maqolalar 12 daqiqa 37 ko‘rish
Ichma-ich tugmalar va «Ortga»: botda adashtirmaydigan navigatsiya
Kategoriya ichiga kirdingiz, tovarni ochdingiz — endi ortga qanday qaytasiz? Bot navigatsiyasining butun siri callback_data'ni to'g'ri loyihalashda. Ekran tushunchasi, 64 baytlik chegara va «xabarni tahrirlash» hiylasi — Aladdin botidagi ishlagan yechim.
Mundarija
Avvalgi qismda katalogni backend'dan oladigan bo'ldik. Endi uni foydalanuvchiga ko'rsatish kerak: bosh menyudan kategoriyaga kiradi, ro'yxatdan tovarni ochadi, ko'rib bo'lgach ortga qaytadi. Sayt bo'lsa brauzerning o'zi "Back" tugmasini berardi. Botda esa bunday tugma yo'q — uni o'zimiz qurishimiz kerak, va aynan shu joyda ko'p botlar chalkashib ketadi: "Ortga" bosilsa boshqa joyga olib boradi, yoki umuman ishlamaydi.
Asosiy g'oya: har tugma o'zi bilan manzil olib yuradi#
Telegram'da inline tugma bosilganda botga callback_data degan qisqa satr
keladi. Navigatsiyaning butun siri shu satrni to'g'ri loyihalashda. Qoida
sodda: callback ichida foydalanuvchi qayerga borishi VA qayerdan kelgani
yozilgan bo'lsin. Shunda "Ortga" tugmasi hech qachon adashmaydi, chunki
manzil tugmaning o'zida turibdi:
Bosh menyu → callback: "menu"
Kategoriya 5 → callback: "cat:5"
Tovar 12 (5-katdan) → callback: "prod:12:5" ← qayerdan kelgani ham ichida
Tovar ekranidagi "Ortga" tugmasi cat:5 deb yoziladi — ya'ni u shunchaki
"ortga" emas, "5-kategoriyaga qaytish" tugmasi. Foydalanuvchi qaysi yo'l
bilan kelgan bo'lsa, o'sha yo'ldan qaytadi.
Nega holatda saqlamaymiz?
Yo'lni FSM'da yoki bazada saqlash ham mumkin edi. Lekin callback'ning o'zida saqlash bitta katta afzallik beradi: xabar eskirsa ham ishlaydi. Foydalanuvchi bir hafta oldingi xabardagi tugmani bossa, bot hech qanday holatga qaramay to'g'ri ekranni ochadi. Holatga bog'langan navigatsiya esa "eski tugma bosildi — bot adashdi" degan muammoni olib keladi.
Ekran tushunchasi: yangi xabar emas, tahrirlash#
Ikkinchi muhim qaror — har bosishda yangi xabar yubormaslik. Yangi xabar yuborilsa, chat uch daqiqada menyularning qabristoniga aylanadi. To'g'ri yo'l: bitta xabarni ekran deb qarab, tugma bosilganda o'sha xabarning o'zini tahrirlash. Foydalanuvchi go'yo kichik ilova ichida yurgandek his qiladi.
bot/
├── keyboards/
│ └── catalog.py ← tugma quruvchilar
├── handlers/
│ └── navigation.py ← ekranlar orasida yurish
├── utils/
│ ├── callbacks.py ← callback satrlarini ajratish
│ └── screens.py ← xavfsiz tahrirlash yordamchisi
└── app.py ← botni ishga tushirish
# bot/keyboards/catalog.py
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
def categories_kb(categories: list[dict]) -> InlineKeyboardMarkup:
rows = [
[InlineKeyboardButton(text=c["name"], callback_data=f"cat:{c['id']}")] for c in categories
]
return InlineKeyboardMarkup(inline_keyboard=rows)
def products_kb(products: list[dict], category_id: int) -> InlineKeyboardMarkup:
rows = [
# tovar tugmasi qayerdan ochilganini ham eslab qoladi
[InlineKeyboardButton(text=p["name"], callback_data=f"prod:{p['id']}:{category_id}")]
for p in products
]
# "Ortga" — bu yerda bosh menyuga
rows.append([InlineKeyboardButton(text="← Ortga", callback_data="menu")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def product_card_kb(category_id: int) -> InlineKeyboardMarkup:
# tovardan ortga — aynan o'sha kategoriyaga
return InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton(text="← Ortga", callback_data=f"cat:{category_id}")]]
)
Handler'lar: uch ekran, uch marshrut#
navigation.py 1-qismdagi handlers/products.py ning o'rnini egallaydi —
cat: tugmasini endi shu yerdagi screen_category ushlaydi, shuning uchun
ikkalasini bir vaqtda ulamang. parse_product_cb esa pastdagi
test bo'limida yoziladi.
# bot/handlers/navigation.py
import logging
from aiogram import F, Router, html, types
from aiogram.filters import CommandStart, ExceptionTypeFilter
from bot.api.catalog import CatalogUnavailable, catalog
from bot.keyboards.catalog import categories_kb, product_card_kb, products_kb
from bot.utils.callbacks import parse_product_cb
from bot.utils.screens import show_screen
router = Router()
logger = logging.getLogger(__name__)
@router.message(CommandStart())
async def start(message: types.Message):
# birinchi "ekran" — keyingi hamma bosishlar shu xabarni tahrirlaydi
categories = await catalog.categories()
await message.answer("Bo'limni tanlang:", reply_markup=categories_kb(categories))
@router.callback_query(F.data == "menu")
async def screen_menu(callback: types.CallbackQuery):
categories = await catalog.categories()
await show_screen(callback, "Bo'limni tanlang:", categories_kb(categories))
@router.callback_query(F.data.startswith("cat:"))
async def screen_category(callback: types.CallbackQuery):
category_id = int(callback.data.split(":")[1])
products = await catalog.products(category_id)
await show_screen(callback, "Tovarlar:", products_kb(products, category_id))
@router.callback_query(F.data.startswith("prod:"))
async def screen_product(callback: types.CallbackQuery):
product_id, category_id = parse_product_cb(callback.data)
product = await catalog.product(product_id)
# backend'dan kelgan matn HTML ichiga ekranlab qo'yiladi: nomdagi "&" yoki "<"
# Telegram'ning HTML tahlilchisini sindirmasin
text = (
f"{html.bold(html.quote(product['name']))}\nNarxi: {html.quote(str(product['price']))} so'm"
)
await show_screen(callback, text, product_card_kb(category_id))
@router.error(ExceptionTypeFilter(CatalogUnavailable))
async def catalog_down(event: types.ErrorEvent):
# 1-qismdagi try/except'ni har handler'da takrorlamaymiz: shu router'dagi
# istalgan ekran backend yotganda shu yerga tushadi
logger.error("Katalog API javob bermadi: %s", event.exception)
text = "Katalog vaqtincha ishlamayapti, birozdan keyin urinib ko'ring."
if event.update.callback_query:
# alert ko'rsatadi va tugmadagi "yuklanmoqda" belgisini ham yopadi
await event.update.callback_query.answer(text, show_alert=True)
elif event.update.message:
await event.update.message.answer(text)
E'tibor bering, har handler o'z ekranini butunlay o'zi chizadi va boshqa ekranlar haqida hech narsa bilmaydi. Navigatsiya mantiqi tugmalarning ichida ketgani uchun handler'lar bir-biriga bog'lanib qolmaydi.
Uchta qator alohida izohga arziydi:
-
/start— birinchi ekranni yuboradi. Qolgan hamma ekranlar callback orqali shu xabarni tahrirlaydi; bu handler bo'lmasa, bot foydalanuvchiga umuman hech narsa ko'rsatmaydi; -
html.quote— tovar nomi backend'dan keladi vaparse_mode="HTML"bilan yuboriladi. Telegram HTML'da teg yoki entity bo'lmagan har bir<,>va&ekranlanishini talab qiladi, aks holda xabarni rad etadi. "Changyutgich& Max" kabi nom ekranni sindirmasligi uchun matn <Pro> &ko'rinishida yuboriladi; -
@router.error— 1-qismdagi "backend yotsa" javobini har handler'datry/exceptbilan takrorlamaymiz. Shu router'dagi istalgan ekrandaCatalogUnavailablechiqsa, xato shu yerga tushadi: tugma bosilgan bo'lsa, foydalanuvchi alert ko'radi va tugmadagi "yuklanmoqda" belgisi yopiladi,/startbo'lsa, oddiy xabar oladi.
Router'ni ishga tushiradigan fayl:
# bot/app.py
import asyncio
import logging
import os
from aiogram import Bot, Dispatcher
from bot.handlers import navigation
async def main() -> None:
logging.basicConfig(level=logging.INFO)
bot = Bot(os.environ["BOT_TOKEN"])
dp = Dispatcher()
dp.include_router(navigation.router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Loyiha ildizidan (bot/ papka turgan joydan) paket sifatida ishga
tushiriladi:
BOT_TOKEN=... python -m bot.app
python bot/app.py deb ishga tushirsangiz, Python bot/ ichidan boshlaydi
va from bot.handlers ... qatori ModuleNotFoundError: No module named 'bot'
bilan yiqiladi.
Xavfsiz tahrirlash: mayda, lekin muhim ikki hiyla#
edit_textni to'g'ridan-to'g'ri chaqirsangiz, ikkita mayda muammoga
duch kelasiz. Ikkalasini bitta yordamchiga yig'ib qo'yamiz:
# bot/utils/screens.py
from aiogram import types
from aiogram.exceptions import TelegramBadRequest
async def show_screen(callback: types.CallbackQuery, text: str, keyboard) -> None:
# 1-hiyla: answer() bo'lmasa, tugma ustida "soat" aylanaveradi —
# Telegram bot javob berganini bilishi kerak
await callback.answer()
try:
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
except TelegramBadRequest as exc:
# 2-hiyla: foydalanuvchi bir tugmani ikki marta bossa, Telegram
# "message is not modified" xatosini beradi. Bu haqiqiy xato emas —
# ekran allaqachon kerakli holatda. Jimgina o'tkazib yuboramiz
if "message is not modified" not in str(exc):
raise
64 bayt — callback_data chegarasi
Telegram callback_dataga ko'pi bilan 64 bayt sig'diradi. prod:12:5
bemalol sig'adi, lekin yo'l chuqurlashib prod:12:cat:5:page:3:sort:price
ga aylansa, chegaraga urilasiz — va Telegram tugmani jo'natishda xato
beradi. Yo'l uzayib ketsa, qisqartma yozuv o'ylang (masalan p:12:5:3)
yoki chuqur holatni bazada saqlab, callback'da faqat kalitini yuboring.
Rasmli ekranlar: tahrirlashning bitta istisnosi#
Tovar kartasida rasm bo'lsa, ish biroz o'zgaradi: matnli xabarni rasmli
xabarga edit_text bilan aylantirib bo'lmaydi, chunki Telegram uchun bular
har xil turdagi xabarlar. Bunda ikki yo'l bor: rasm o'rniga havolali matn
ishlatish (eng sodda) yoki edit_media bilan rasmni almashtirib borish:
from aiogram.types import InputMediaPhoto
# rasmli ekranlar orasida yurish: matn emas, media tahrirlanadi
await callback.message.edit_media(
InputMediaPhoto(media=product["image_url"], caption=text, parse_mode="HTML"),
reply_markup=product_card_kb(category_id),
)
Aladdin'da har tovarning rasmi bor, shuning uchun katalog ekranlari boshidanoq
rasmli xabar qilib qurilgan — shunda edit_media butun yo'l davomida ishlayveradi.
Test: marshrut satrlarini tekshiramiz#
Navigatsiyada eng ko'p sinadigan narsa — callback satrlarini yig'ish va
ajratish. Shuning uchun ajratish alohida funksiyada turadi — screen_product
aynan shu funksiyadan foydalanadi, test esa uni tekshiradi:
# bot/utils/callbacks.py
def parse_product_cb(data: str) -> tuple[int, int]:
"""'prod:12:5' -> (12, 5); format buzuq bo'lsa ValueError."""
prefix, product_id, category_id = data.split(":")
if prefix != "prod":
raise ValueError(data)
return int(product_id), int(category_id)
# tests/test_callbacks.py
import pytest
from bot.utils.callbacks import parse_product_cb
def test_product_callback_roundtrip():
assert parse_product_cb("prod:12:5") == (12, 5)
def test_broken_callback_raises():
with pytest.raises(ValueError):
parse_product_cb("cat:5")
python -m pytest tests -q
Oddiy pytest emas, python -m pytest: bu shakl joriy papkani import
yo'liga qo'shadi va test bot paketini topadi. Busiz
ModuleNotFoundError: No module named 'bot' chiqadi.
Xulosa#
Botda "Ortga" tugmasi sehr emas, oddiy intizom: har tugma o'zining to'liq
manzilini callback ichida olib yuradi, ekranlar yangi xabar o'rniga mavjudini
tahrirlaydi, answer() esa hech qachon unutilmaydi. Shu uch odat bilan
katalog qancha chuqur bo'lsa ham foydalanuvchi adashmaydi — Aladdin'da bu
yechim besh yildan beri minglab odamga xizmat qilyapti.
Keyingi qismda botni uch tilda gapirtiramiz: tarjima fayllari, til tanlash va "kontent bir tilda, interfeys boshqa tilda" muammosining yechimi.