Кінцевий автомат (FSM)¶
Кінцевий автомат, кінцевий автоматон, або машина станів (FSM, FSA, finite automaton, state machine) - це математична модель обчислень.
Це абстрактна машина, яка може перебувати в одному зі скінченної кількості станів у будь-який момент часу. Кінцевий автомат може переходити з одного стану в інший у відповідь на деякі вхідні дані; перехід з одного стану в інший називається переходом.
Кінцевий автомат визначається списком його станів, початковим станом і вхідними даними, які запускають кожен перехід.
Джерело: WikiPedia
Приклад використання¶
Не всі функції бота можна реалізувати як єдиний обробник (handler), наприклад, якщо Вам потрібно буде збирати деякі дані від користувача в окремих кроках, вам потрібно буде використовувати FSM.

Гайда, подивимось як реалізувати це крок за кроком
Крок за кроком¶
Перед обробкою будь-яких станів Вам потрібно буде вказати тип станів, які Ви хочете обробляти
class Form(StatesGroup):
name = State()
like_bots = State()
language = State()
А потім напишіть обробник (handler) для кожного стану окремо від початку діалогу
Тут діалог можна почати лише за допомогою команди /start
, тому давайте обробимо її та зробимо перехід користувача до стану Form.name
@form_router.message(CommandStart())
async def command_start(message: Message, state: FSMContext) -> None:
await state.set_state(Form.name)
await message.answer(
"Hi there! What's your name?",
reply_markup=ReplyKeyboardRemove(),
)
Після цього Вам потрібно буде зберегти деякі дані в пам’яті та перейти до наступного кроку.
@form_router.message(Form.name)
async def process_name(message: Message, state: FSMContext) -> None:
await state.update_data(name=message.text)
await state.set_state(Form.like_bots)
await message.answer(
f"Nice to meet you, {html.quote(message.text)}!\nDid you like to write bots?",
reply_markup=ReplyKeyboardMarkup(
keyboard=[
[
KeyboardButton(text="Yes"),
KeyboardButton(text="No"),
]
],
resize_keyboard=True,
),
)
На наступних кроках користувач може дати різні відповіді, це може бути «так», «ні» або будь-що інше
Обробка yes
і скоро нам потрібно буде обробити стан Form.language
@form_router.message(Form.like_bots, F.text.casefold() == "yes")
async def process_like_write_bots(message: Message, state: FSMContext) -> None:
await state.set_state(Form.language)
await message.reply(
"Cool! I'm too!\nWhat programming language did you use for it?",
reply_markup=ReplyKeyboardRemove(),
)
Обробка no
@form_router.message(Form.like_bots, F.text.casefold() == "no")
async def process_dont_like_write_bots(message: Message, state: FSMContext) -> None:
data = await state.get_data()
await state.clear()
await message.answer(
"Not bad not terrible.\nSee you soon.",
reply_markup=ReplyKeyboardRemove(),
)
await show_summary(message=message, data=data, positive=False)
І обробка будь-яких інших відповідей
@form_router.message(Form.like_bots)
async def process_unknown_write_bots(message: Message) -> None:
await message.reply("I don't understand you :(")
Всі можливі випадки кроку like_bots було розглянуто, нумо реалізуємо останній крок
@form_router.message(Form.language)
async def process_language(message: Message, state: FSMContext) -> None:
data = await state.update_data(language=message.text)
await state.clear()
if message.text.casefold() == "python":
await message.reply(
"Python, you say? That's the language that makes my circuits light up! 😉"
)
await show_summary(message=message, data=data)
async def show_summary(message: Message, data: Dict[str, Any], positive: bool = True) -> None:
name = data["name"]
language = data.get("language", "<something unexpected>")
text = f"I'll keep in mind that, {html.quote(name)}, "
text += (
f"you like to write bots with {html.quote(language)}."
if positive
else "you don't like to write bots, so sad..."
)
await message.answer(text=text, reply_markup=ReplyKeyboardRemove())
І тепер Ви виконали всі кроки на зображенні, але ви можете зробити можливість скасувати діалог, давайте зробимо це за допомогою команди або тексту
@form_router.message(Command("cancel"))
@form_router.message(F.text.casefold() == "cancel")
async def cancel_handler(message: Message, state: FSMContext) -> None:
"""
Allow user to cancel any action
"""
current_state = await state.get_state()
if current_state is None:
return
logging.info("Cancelling state %r", current_state)
await state.clear()
await message.answer(
"Cancelled.",
reply_markup=ReplyKeyboardRemove(),
)
Повний приклад¶
1import asyncio
2import logging
3import sys
4from os import getenv
5from typing import Any, Dict
6
7from aiogram import Bot, Dispatcher, F, Router, html
8from aiogram.client.default import DefaultBotProperties
9from aiogram.enums import ParseMode
10from aiogram.filters import Command, CommandStart
11from aiogram.fsm.context import FSMContext
12from aiogram.fsm.state import State, StatesGroup
13from aiogram.types import (
14 KeyboardButton,
15 Message,
16 ReplyKeyboardMarkup,
17 ReplyKeyboardRemove,
18)
19
20TOKEN = getenv("BOT_TOKEN")
21
22form_router = Router()
23
24
25class Form(StatesGroup):
26 name = State()
27 like_bots = State()
28 language = State()
29
30
31@form_router.message(CommandStart())
32async def command_start(message: Message, state: FSMContext) -> None:
33 await state.set_state(Form.name)
34 await message.answer(
35 "Hi there! What's your name?",
36 reply_markup=ReplyKeyboardRemove(),
37 )
38
39
40@form_router.message(Command("cancel"))
41@form_router.message(F.text.casefold() == "cancel")
42async def cancel_handler(message: Message, state: FSMContext) -> None:
43 """
44 Allow user to cancel any action
45 """
46 current_state = await state.get_state()
47 if current_state is None:
48 return
49
50 logging.info("Cancelling state %r", current_state)
51 await state.clear()
52 await message.answer(
53 "Cancelled.",
54 reply_markup=ReplyKeyboardRemove(),
55 )
56
57
58@form_router.message(Form.name)
59async def process_name(message: Message, state: FSMContext) -> None:
60 await state.update_data(name=message.text)
61 await state.set_state(Form.like_bots)
62 await message.answer(
63 f"Nice to meet you, {html.quote(message.text)}!\nDid you like to write bots?",
64 reply_markup=ReplyKeyboardMarkup(
65 keyboard=[
66 [
67 KeyboardButton(text="Yes"),
68 KeyboardButton(text="No"),
69 ]
70 ],
71 resize_keyboard=True,
72 ),
73 )
74
75
76@form_router.message(Form.like_bots, F.text.casefold() == "no")
77async def process_dont_like_write_bots(message: Message, state: FSMContext) -> None:
78 data = await state.get_data()
79 await state.clear()
80 await message.answer(
81 "Not bad not terrible.\nSee you soon.",
82 reply_markup=ReplyKeyboardRemove(),
83 )
84 await show_summary(message=message, data=data, positive=False)
85
86
87@form_router.message(Form.like_bots, F.text.casefold() == "yes")
88async def process_like_write_bots(message: Message, state: FSMContext) -> None:
89 await state.set_state(Form.language)
90
91 await message.reply(
92 "Cool! I'm too!\nWhat programming language did you use for it?",
93 reply_markup=ReplyKeyboardRemove(),
94 )
95
96
97@form_router.message(Form.like_bots)
98async def process_unknown_write_bots(message: Message) -> None:
99 await message.reply("I don't understand you :(")
100
101
102@form_router.message(Form.language)
103async def process_language(message: Message, state: FSMContext) -> None:
104 data = await state.update_data(language=message.text)
105 await state.clear()
106
107 if message.text.casefold() == "python":
108 await message.reply(
109 "Python, you say? That's the language that makes my circuits light up! 😉"
110 )
111
112 await show_summary(message=message, data=data)
113
114
115async def show_summary(message: Message, data: Dict[str, Any], positive: bool = True) -> None:
116 name = data["name"]
117 language = data.get("language", "<something unexpected>")
118 text = f"I'll keep in mind that, {html.quote(name)}, "
119 text += (
120 f"you like to write bots with {html.quote(language)}."
121 if positive
122 else "you don't like to write bots, so sad..."
123 )
124 await message.answer(text=text, reply_markup=ReplyKeyboardRemove())
125
126
127async def main():
128 # Initialize Bot instance with default bot properties which will be passed to all API calls
129 bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
130
131 dp = Dispatcher()
132
133 dp.include_router(form_router)
134
135 # Start event dispatching
136 await dp.start_polling(bot)
137
138
139if __name__ == "__main__":
140 logging.basicConfig(level=logging.INFO, stream=sys.stdout)
141 asyncio.run(main())
Changing state for another user¶
In some cases, you might need to change the state for a user other than the one who triggered the current handler. For example, you might want to change the state of a user based on an admin’s command.
To do this, you can use the get_context
method of the FSM middleware through the dispatcher:
@example_router.message(Command("example"))
async def command_example(message: Message, dispatcher: Dispatcher, bot: Bot):
user_id = ... # Get the user ID in the way that you need
state = await dispatcher.fsm.get_context(
bot=bot,
chat_id=user_id,
user_id=user_id,
)
# Now you can use the state context to change the state for the specified user
await state.set_state(YourState.some_state)
# Or store data in the state
await state.update_data(some_key="some_value")
# Or clear the state
await state.clear()
This allows you to manage the state of any user in your bot, not just the one who triggered the current handler.