aiogram/examples/scene.py
Andrew df7b16d5b3
Some checks failed
Tests / tests (macos-latest, 3.10) (push) Has been cancelled
Tests / tests (macos-latest, 3.11) (push) Has been cancelled
Tests / tests (macos-latest, 3.12) (push) Has been cancelled
Tests / tests (macos-latest, 3.13) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.11) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.12) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
Tests / tests (windows-latest, 3.10) (push) Has been cancelled
Tests / tests (windows-latest, 3.11) (push) Has been cancelled
Tests / tests (windows-latest, 3.12) (push) Has been cancelled
Tests / tests (windows-latest, 3.13) (push) Has been cancelled
Tests / pypy-tests (macos-latest, pypy3.10) (push) Has been cancelled
Tests / pypy-tests (macos-latest, pypy3.11) (push) Has been cancelled
Tests / pypy-tests (ubuntu-latest, pypy3.10) (push) Has been cancelled
Tests / pypy-tests (ubuntu-latest, pypy3.11) (push) Has been cancelled
EOL of Py3.9 (#1726)
* Drop py3.9 and pypy3.9

Add pypy3.11 (testing) into `tests.yml`

Remove py3.9 from matrix in `tests.yml`

Refactor not auto-gen code to be compatible with py3.10+, droping ugly 3.9 annotation.

Replace some `from typing` imports to `from collections.abc`, due to deprecation

Add `from __future__ import annotations` and `if TYPE_CHECKING:` where possible

Add some `noqa` to calm down Ruff in some places, if Ruff will be used as default linting+formatting tool in future

Replace some relative imports to absolute

Sort `__all__` tuples in `__init__.py` and some other `.py` files

Sort `__slots__` tuples in classes

Split raises into `msg` and `raise` (`EM101`, `EM102`) to not duplicate error message in the traceback

Add `Self` from `typing_extenstion` where possible

Resolve typing problem in `aiogram/filters/command.py:18`

Concatenate nested `if` statements

Convert `HandlerContainer` into a dataclass in `aiogram/fsm/scene.py`

Bump tests docker-compose.yml `redis:6-alpine` -> `redis:8-alpine`

Bump tests docker-compose.yml `mongo:7.0.6` -> `mongo:8.0.14`

Bump pre-commit-config `black==24.4.2` -> `black==25.9.0`

Bump pre-commit-config `ruff==0.5.1` -> `ruff==0.13.3`

Update Makefile lint for ruff to show fixes

Add `make outdated` into Makefile

Use `pathlib` instead of `os.path`

Bump `redis[hiredis]>=5.0.1,<5.3.0` -> `redis[hiredis]>=6.2.0,<7`

Bump `cryptography>=43.0.0` -> `cryptography>=46.0.0` due to security reasons

Bump `pytz~=2023.3` -> `pytz~=2025.2`

Bump `pycryptodomex~=3.19.0` -> `pycryptodomex~=3.23.0` due to security reasons

Bump linting and formatting tools

* Add `1726.removal.rst`

* Update aiogram/utils/dataclass.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update aiogram/filters/callback_data.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update 1726.removal.rst

* Remove `outdated` from Makefile

* Add `__slots__` to `HandlerContainer`

* Remove unused imports

* Add `@dataclass` with `slots=True` to `HandlerContainer`

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 19:19:23 +03:00

209 lines
7 KiB
Python

from __future__ import annotations
from os import getenv
from typing import TypedDict
from aiogram import Bot, Dispatcher, F, html
from aiogram.filters import Command
from aiogram.fsm.scene import After, Scene, SceneRegistry, on
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
KeyboardButton,
Message,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
)
TOKEN = getenv("BOT_TOKEN")
BUTTON_CANCEL = KeyboardButton(text="❌ Cancel")
BUTTON_BACK = KeyboardButton(text="🔙 Back")
class FSMData(TypedDict, total=False):
name: str
language: str
class CancellableScene(Scene):
"""
This scene is used to handle cancel and back buttons,
can be used as a base class for other scenes that needs to support cancel and back buttons.
"""
@on.message(F.text.casefold() == BUTTON_CANCEL.text.casefold(), after=After.exit())
async def handle_cancel(self, message: Message) -> None:
await message.answer("Cancelled.", reply_markup=ReplyKeyboardRemove())
@on.message(F.text.casefold() == BUTTON_BACK.text.casefold(), after=After.back())
async def handle_back(self, message: Message) -> None:
await message.answer("Back.")
class LanguageScene(CancellableScene, state="language"):
"""
This scene is used to ask user what language he prefers.
"""
@on.message.enter()
async def on_enter(self, message: Message) -> None:
await message.answer(
"What language do you prefer?",
reply_markup=ReplyKeyboardMarkup(
keyboard=[[BUTTON_BACK, BUTTON_CANCEL]],
resize_keyboard=True,
),
)
@on.message(F.text.casefold() == "python", after=After.exit())
async def process_python(self, message: Message) -> None:
await message.answer(
"Python, you say? That's the language that makes my circuits light up! 😉",
)
await self.input_language(message)
@on.message(after=After.exit())
async def input_language(self, message: Message) -> None:
data: FSMData = await self.wizard.get_data()
await self.show_results(message, language=message.text, **data)
async def show_results(self, message: Message, name: str, language: str) -> None:
await message.answer(
text=f"I'll keep in mind that, {html.quote(name)}, "
f"you like to write bots with {html.quote(language)}.",
reply_markup=ReplyKeyboardRemove(),
)
class LikeBotsScene(CancellableScene, state="like_bots"):
"""
This scene is used to ask user if he likes to write bots.
"""
@on.message.enter()
async def on_enter(self, message: Message) -> None:
await message.answer(
"Did you like to write bots?",
reply_markup=ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text="Yes"), KeyboardButton(text="No")],
[BUTTON_BACK, BUTTON_CANCEL],
],
resize_keyboard=True,
),
)
@on.message(F.text.casefold() == "yes", after=After.goto(LanguageScene))
async def process_like_write_bots(self, message: Message) -> None:
await message.reply("Cool! I'm too!")
@on.message(F.text.casefold() == "no", after=After.exit())
async def process_dont_like_write_bots(self, message: Message) -> None:
await message.answer(
"Not bad not terrible.\nSee you soon.",
reply_markup=ReplyKeyboardRemove(),
)
@on.message()
async def input_like_bots(self, message: Message) -> None:
await message.answer("I don't understand you :(")
class NameScene(CancellableScene, state="name"):
"""
This scene is used to ask user's name.
"""
@on.message.enter() # Marker for handler that should be called when a user enters the scene.
async def on_enter(self, message: Message) -> None:
await message.answer(
"Hi there! What's your name?",
reply_markup=ReplyKeyboardMarkup(keyboard=[[BUTTON_CANCEL]], resize_keyboard=True),
)
@on.callback_query.enter() # different types of updates that start the scene also supported.
async def on_enter_callback(self, callback_query: CallbackQuery) -> None:
await callback_query.answer()
await self.on_enter(callback_query.message)
@on.message.leave() # Marker for handler that should be called when a user leaves the scene.
async def on_leave(self, message: Message) -> None:
data: FSMData = await self.wizard.get_data()
name = data.get("name", "Anonymous")
await message.answer(f"Nice to meet you, {html.quote(name)}!")
@on.message(after=After.goto(LikeBotsScene))
async def input_name(self, message: Message) -> None:
await self.wizard.update_data(name=message.text)
class DefaultScene(
Scene,
reset_data_on_enter=True, # Reset state data
reset_history_on_enter=True, # Reset history
callback_query_without_state=True, # Handle callback queries even if user in any scene
):
"""
Default scene for the bot.
This scene is used to handle all messages that are not handled by other scenes.
"""
start_demo = on.message(F.text.casefold() == "demo", after=After.goto(NameScene))
@on.message(Command("demo"))
async def demo(self, message: Message) -> None:
await message.answer(
"Demo started",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton(text="Go to form", callback_data="start")]],
),
)
@on.callback_query(F.data == "start", after=After.goto(NameScene))
async def demo_callback(self, callback_query: CallbackQuery) -> None:
await callback_query.answer(cache_time=0)
await callback_query.message.delete_reply_markup()
@on.message.enter() # Mark that this handler should be called when a user enters the scene.
@on.message()
async def default_handler(self, message: Message) -> None:
await message.answer(
"Start demo?\nYou can also start demo via command /demo",
reply_markup=ReplyKeyboardMarkup(
keyboard=[[KeyboardButton(text="Demo")]],
resize_keyboard=True,
),
)
def create_dispatcher() -> Dispatcher:
dispatcher = Dispatcher()
# Scene registry should be the only one instance in your application for proper work.
# It stores all available scenes.
# You can use any router for scenes, not only `Dispatcher`.
registry = SceneRegistry(dispatcher)
# All scenes at register time converts to Routers and includes into specified router.
registry.add(
DefaultScene,
NameScene,
LikeBotsScene,
LanguageScene,
)
return dispatcher
def main() -> None:
dp = create_dispatcher()
bot = Bot(token=TOKEN)
dp.run_polling(bot)
if __name__ == "__main__":
# Recommended to use CLI instead of this snippet.
# `aiogram run polling scene_example:create_dispatcher --token BOT_TOKEN --log-level info`
main()