Maqola · 2026-08-16 · ~9 daqiqa o‘qiladi · o'rta
aiogram FSM: ko'p bosqichli dialog-formalarni to'g'ri qurish
Mundarija
Avvalgi qismda erkin matnli AI-yo'lni ko'rdik. Endi "klassik yo'l" — qadam-baqadam forma. Humo botida bunday dialoglar bir nechta: login (telefon → SMS kod), tarif hisoblash (kompaniya → transport → o'lchamlar), murojaat qoldirish. Hammasining tagida bitta tushuncha yotadi: holatlar mashinasi (FSM — Finite State Machine).
Muammo: HTTP'da yo'q edi, botda bor#
Web-formada hamma maydon bitta sahifada keladi. Botda esa dialog vaqtga cho'zilgan: telefonni so'radik — foydalanuvchi ertaga javob berishi mumkin. Shu orada bot uchun u "qaysi qadamda turgani"ni eslab turish kerak. Bu holat (state) va unga yig'ilgan ma'lumotlar (data) — FSM'ning ikki yarmi:
[boshlang'ich] --/login--> [telefon kutilmoqda] --raqam keldi--> [kod kutilmoqda]
|
[kabinetda] <--kod to'g'ri keldi--+
1-qadam: holatlar va storage#
bot/
├── states/
│ └── login.py ← holatlar deklaratsiyasi
├── handlers/
│ ├── login.py ← dialog qadamlari
│ └── nav_reset.py ← "qotgan"larni qutqarish (oxirida)
├── validators.py ← sof tekshiruv funksiyalari (testlanadi)
└── app.py ← dispatcher + storage
# bot/states/login.py
from aiogram.fsm.state import State, StatesGroup
class Login(StatesGroup):
# har State — dialogning bitta "to'xtash bekati"
waiting_phone = State()
waiting_code = State()
# bot/app.py
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
# Holatlar Redis'da: bot qayta ishga tushsa ham dialog joyidan davom etadi.
# MemoryStorage faqat o'yin uchun — pastdagi ogohlantirishga qarang
storage = RedisStorage.from_url("redis://127.0.0.1:6379/1")
dp = Dispatcher(storage=storage)
MemoryStorage bilan production'ga chiqmang
Har deploy'da bot qayta ishga tushadi — MemoryStorage'dagi barcha holatlar o'chadi. SMS kod kutayotgan yuz foydalanuvchi "javobsiz bot"ga yozib qoladi: bot ularning xabarini hech qaysi holatga tegishli emas deb e'tiborsiz qoldiradi. Redis bilan esa restart foydalanuvchiga umuman sezilmaydi.
2-qadam: validatsiya — handler'dan tashqarida#
Har qadamda foydalanuvchi istalgan narsani yozishi mumkin. Tekshiruvlarni sof funksiya qilib ajratamiz — ular aiogram'siz testlanadi:
# bot/validators.py
import re
PHONE_RE = re.compile(r"^\+?998\d{9}$")
def normalize_phone(raw: str) -> str | None:
"""'+998 90 123-45-67' -> '+998901234567'; yaroqsiz bo'lsa None."""
digits = re.sub(r"[^\d+]", "", raw)
if not digits.startswith("+"):
digits = "+" + digits
return digits if PHONE_RE.match(digits) else None
def is_sms_code(raw: str) -> bool:
return raw.strip().isdigit() and len(raw.strip()) == 6
3-qadam: dialog qadamlari#
# bot/handlers/login.py
from aiogram import F, Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from bot.api import auth_api # kompaniya API'si (SMS yuboradi/tekshiradi)
from bot.states.login import Login
from bot.validators import is_sms_code, normalize_phone
router = Router()
@router.message(Command("login"))
async def login_start(message: types.Message, state: FSMContext):
await state.set_state(Login.waiting_phone) # bekatga keldik
await message.answer("Telefon raqamingizni yuboring: +998XXXXXXXXX")
@router.message(Login.waiting_phone, F.text)
async def got_phone(message: types.Message, state: FSMContext):
phone = normalize_phone(message.text)
if phone is None:
# holatni O'ZGARTIRMAYMIZ — foydalanuvchi shu bekatda qoladi
# va qayta urinadi. Xato = dialogning oxiri emas
await message.answer("Raqam formati noto'g'ri. Namuna: +998901234567")
return
await auth_api.send_sms(phone)
# keyingi qadamga KERAK bo'ladigan ma'lumotni holat datasiga yozamiz
await state.update_data(phone=phone)
await state.set_state(Login.waiting_code)
await message.answer("SMS'dagi 6 xonali kodni kiriting.")
@router.message(Login.waiting_code, F.text)
async def got_code(message: types.Message, state: FSMContext):
if not is_sms_code(message.text):
await message.answer("Kod 6 ta raqamdan iborat bo'ladi. Qayta kiriting.")
return
data = await state.get_data() # avvalgi qadamda yozilgan phone
token = await auth_api.verify(data["phone"], message.text.strip())
if token is None:
await message.answer("Kod noto'g'ri yoki eskirgan. Qayta kiriting yoki /login.")
return
await state.clear() # dialog tugadi — holat tozalanadi
await message.answer("Xush kelibsiz! Kabinet: /menu")
Uch qoida shu kodda ko'rinib turibdi:
- Xato holatni buzmaydi — yaroqsiz kiritishda
set_statechaqirilmaydi, foydalanuvchi joyida qayta urinadi; - Qadamlararo ma'lumot
update_datada — global o'zgaruvchida emas (bir vaqtda yuz odam dialogda bo'ladi!); - Dialog oxiri —
state.clear()— tozalanmagan holat keyingi suhbatlarni "yeydi".
4-qadam: «qotib qolgan» foydalanuvchini qutqarish#
Eng ko'p uchraydigan FSM dardi: foydalanuvchi kod kutish bekatida turib
/menu yozadi — lekin bu xabarni Login.waiting_code handler'i "kod emas"
deb rad etaveradi. Foydalanuvchi nazarida bot qotib qolgan.
Humo'dagi yechim — global navigatsiya-reset: asosiy buyruqlar har qanday holatda ishlaydi va holatni tozalab yuboradi:
# bot/handlers/nav_reset.py
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
router = Router()
# Bu router dispatcher'ga BIRINCHI ulanadi — FSM handler'lardan oldin ishlaydi
NAV_COMMANDS = ["start", "menu", "help", "cancel"]
@router.message(Command(*NAV_COMMANDS))
async def nav_reset(message: types.Message, state: FSMContext):
"""Global navigatsiya-reset: foydalanuvchini FSM holatidan chiqaradi."""
if await state.get_state() is not None:
await state.clear()
# shu yerdan odatiy menyu/started oqimiga o'tib ketiladi
await message.answer("Bosh menyu: /calc — hisoblash, /login — kabinet")
# bot/app.py (davomi)
from bot.handlers import login, nav_reset
dp.include_router(nav_reset.router) # 1) avval qutqaruvchi
dp.include_router(login.router) # 2) keyin dialoglar
Router tartibi — bu prioritet
aiogram xabarni router'larga ulangan tartibda taklif qiladi. Qutqaruvchi
router oxirida tursa, /menu avval FSM handler'iga tushadi va "kod
noto'g'ri" javobini oladi. Tartib — kodda ko'rinmaydigan, lekin xatti-
harakatni butunlay o'zgartiradigan sozlama; izoh yozib qo'ying.
5-qadam: test — validatorlar va oqim#
# tests/test_validators.py
from bot.validators import is_sms_code, normalize_phone
def test_phone_normalization():
assert normalize_phone("+998 90 123-45-67") == "+998901234567"
assert normalize_phone("998901234567") == "+998901234567"
assert normalize_phone("90 123 45 67") is None # kod yo'q
assert normalize_phone("+7 900 000 00 00") is None # boshqa davlat
def test_sms_code_shape():
assert is_sms_code(" 123456 ")
assert not is_sms_code("12345")
assert not is_sms_code("kod123")
Dialog oqimining o'zini ham tekshirsa bo'ladi — aiogram holat obyektlari oddiy
Python, FSMContextni test storage bilan yaratib, handler funksiyalarini
to'g'ridan-to'g'ri chaqirasiz. Lekin eng katta foyda — validatorlarning sof
funksiya ekani: mantiqning eng sinuvchan qismi framework'siz testlanadi.
Xulosa#
- Bot dialogi — holatlar mashinasi: har qadam alohida
State, ma'lumotupdate_datada, yakunstate.clear(); - Storage — Redis: restart foydalanuvchi dialogini o'ldirmasin;
- Yaroqsiz kiritish holatni o'zgartirmaydi — foydalanuvchi bekatida qoladi;
- Global nav-reset router'i birinchi ulanadi — "qotgan" foydalanuvchi har
doim
/menubilan qutuladi; - Validatsiya — sof funksiyalarda: test oson, mantiq bitta joyda.
Keyingi qismda botga jadval bo'yicha ishlaydigan vazifalarni qo'shamiz: haftalik hisobotlar, kunlik anomaliya tekshiruvi va adminlarga avtomatik ogohlantirishlar.