mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Docs + example
This commit is contained in:
parent
d4248c5672
commit
f1ca0e5271
4 changed files with 695 additions and 1 deletions
|
|
@ -212,12 +212,19 @@ class HandlerContainer:
|
|||
@dataclass()
|
||||
class SceneConfig:
|
||||
state: Optional[str]
|
||||
"""Scene state"""
|
||||
filters: Dict[str, List[CallbackType]]
|
||||
"""Global scene filters"""
|
||||
handlers: List[HandlerContainer]
|
||||
"""Scene handlers"""
|
||||
actions: Dict[SceneAction, Dict[str, CallableObject]]
|
||||
"""Scene actions"""
|
||||
reset_data_on_enter: Optional[bool] = None
|
||||
"""Reset scene data on enter"""
|
||||
reset_history_on_enter: Optional[bool] = None
|
||||
"""Reset scene history on enter"""
|
||||
callback_query_without_state: Optional[bool] = None
|
||||
"""Allow callback query without state"""
|
||||
|
||||
|
||||
async def _empty_handler(*args: Any, **kwargs: Any) -> None:
|
||||
|
|
@ -424,6 +431,19 @@ class Scene:
|
|||
|
||||
|
||||
class SceneWizard:
|
||||
"""
|
||||
A class that represents a wizard for managing scenes in a Telegram bot.
|
||||
|
||||
Instance of this class is passed to each scene as a parameter.
|
||||
So, you can use it to transition between scenes, get and set data, etc.
|
||||
|
||||
.. note::
|
||||
|
||||
This class is not meant to be used directly. Instead, it should be used
|
||||
as a parameter in the scene constructor.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scene_config: SceneConfig,
|
||||
|
|
@ -433,6 +453,16 @@ class SceneWizard:
|
|||
event: TelegramObject,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
"""
|
||||
A class that represents a wizard for managing scenes in a Telegram bot.
|
||||
|
||||
:param scene_config: The configuration of the scene.
|
||||
:param manager: The scene manager.
|
||||
:param state: The FSMContext object for storing the state of the scene.
|
||||
:param update_type: The type of the update event.
|
||||
:param event: The TelegramObject represents the event.
|
||||
:param data: Additional data for the scene.
|
||||
"""
|
||||
self.scene_config = scene_config
|
||||
self.manager = manager
|
||||
self.state = state
|
||||
|
|
@ -443,6 +473,14 @@ class SceneWizard:
|
|||
self.scene: Optional[Scene] = None
|
||||
|
||||
async def enter(self, **kwargs: Any) -> None:
|
||||
"""
|
||||
Enter method is used to transition into a scene in the SceneWizard class.
|
||||
It sets the state, clears data and history if specified,
|
||||
and triggers entering event of the scene.
|
||||
|
||||
:param kwargs: Additional keyword arguments.
|
||||
:return: None
|
||||
"""
|
||||
loggers.scene.debug("Entering scene %r", self.scene_config.state)
|
||||
if self.scene_config.reset_data_on_enter:
|
||||
await self.state.set_data({})
|
||||
|
|
@ -452,27 +490,65 @@ class SceneWizard:
|
|||
await self._on_action(SceneAction.enter, **kwargs)
|
||||
|
||||
async def leave(self, _with_history: bool = True, **kwargs: Any) -> None:
|
||||
"""
|
||||
Leaves the current scene.
|
||||
This method is used to exit a scene and transition to the next scene.
|
||||
|
||||
:param _with_history: Whether to include history in the snapshot. Defaults to True.
|
||||
:param kwargs: Additional keyword arguments.
|
||||
:return: None
|
||||
|
||||
"""
|
||||
loggers.scene.debug("Leaving scene %r", self.scene_config.state)
|
||||
if _with_history:
|
||||
await self.manager.history.snapshot()
|
||||
await self._on_action(SceneAction.leave, **kwargs)
|
||||
|
||||
async def exit(self, **kwargs: Any) -> None:
|
||||
"""
|
||||
Exit the current scene and enter the default scene/state.
|
||||
|
||||
:param kwargs: Additional keyword arguments.
|
||||
:return: None
|
||||
"""
|
||||
loggers.scene.debug("Exiting scene %r", self.scene_config.state)
|
||||
await self.manager.history.clear()
|
||||
await self._on_action(SceneAction.exit, **kwargs)
|
||||
await self.manager.enter(None, _check_active=False, **kwargs)
|
||||
|
||||
async def back(self, **kwargs: Any) -> None:
|
||||
"""
|
||||
This method is used to go back to the previous scene.
|
||||
|
||||
:param kwargs: Keyword arguments that can be passed to the method.
|
||||
:return: None
|
||||
"""
|
||||
loggers.scene.debug("Back to previous scene from scene %s", self.scene_config.state)
|
||||
await self.leave(_with_history=False, **kwargs)
|
||||
new_scene = await self.manager.history.rollback()
|
||||
await self.manager.enter(new_scene, _check_active=False, **kwargs)
|
||||
|
||||
async def retake(self, **kwargs: Any) -> None:
|
||||
"""
|
||||
This method allows to re-enter the current scene.
|
||||
|
||||
:param kwargs: Additional keyword arguments to pass to the scene.
|
||||
:return: None
|
||||
"""
|
||||
await self.goto(self.scene_config.state, **kwargs)
|
||||
|
||||
async def goto(self, scene: Union[Type[Scene], str], **kwargs: Any) -> None:
|
||||
"""
|
||||
The `goto` method transitions to a new scene.
|
||||
It first calls the `leave` method to perform any necessary cleanup
|
||||
in the current scene, then calls the `enter` event to enter the specified scene.
|
||||
|
||||
:param scene: The scene to transition to. Can be either a `Scene` instance
|
||||
or a string representing the scene.
|
||||
:param kwargs: Additional keyword arguments to pass to the `enter`
|
||||
method of the scene manager.
|
||||
:return: None
|
||||
"""
|
||||
await self.leave(**kwargs)
|
||||
await self.manager.enter(scene, _check_active=False, **kwargs)
|
||||
|
||||
|
|
@ -502,23 +578,51 @@ class SceneWizard:
|
|||
return True
|
||||
|
||||
async def set_data(self, data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Sets custom data in the current state.
|
||||
|
||||
:param data: A dictionary containing the custom data to be set in the current state.
|
||||
:return: None
|
||||
"""
|
||||
await self.state.set_data(data=data)
|
||||
|
||||
async def get_data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
This method returns the data stored in the current state.
|
||||
|
||||
:return: A dictionary containing the data stored in the scene state.
|
||||
"""
|
||||
return await self.state.get_data()
|
||||
|
||||
async def update_data(
|
||||
self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
This method updates the data stored in the current state
|
||||
|
||||
:param data: Optional dictionary of data to update.
|
||||
:param kwargs: Additional key-value pairs of data to update.
|
||||
:return: Dictionary of updated data
|
||||
"""
|
||||
if data:
|
||||
kwargs.update(data)
|
||||
return await self.state.update_data(data=kwargs)
|
||||
|
||||
async def clear_data(self) -> None:
|
||||
"""
|
||||
Clears the data.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
await self.set_data({})
|
||||
|
||||
|
||||
class ScenesManager:
|
||||
"""
|
||||
The ScenesManager class is responsible for managing scenes in an application.
|
||||
It provides methods for entering and exiting scenes, as well as retrieving the active scene.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
registry: SceneRegistry,
|
||||
|
|
@ -561,6 +665,15 @@ class ScenesManager:
|
|||
_check_active: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Enters the specified scene.
|
||||
|
||||
:param scene_type: Optional Type[Scene] or str representing the scene type to enter.
|
||||
:param _check_active: Optional bool indicating whether to check if
|
||||
there is an active scene to exit before entering the new scene. Defaults to True.
|
||||
:param kwargs: Additional keyword arguments to pass to the scene's wizard.enter() method.
|
||||
:return: None
|
||||
"""
|
||||
if _check_active:
|
||||
active_scene = await self._get_active_scene()
|
||||
if active_scene is not None:
|
||||
|
|
@ -576,6 +689,12 @@ class ScenesManager:
|
|||
await scene.wizard.enter(**kwargs)
|
||||
|
||||
async def close(self, **kwargs: Any) -> None:
|
||||
"""
|
||||
Close method is used to exit the currently active scene in the ScenesManager.
|
||||
|
||||
:param kwargs: Additional keyword arguments passed to the scene's exit method.
|
||||
:return: None
|
||||
"""
|
||||
scene = await self._get_active_scene()
|
||||
if not scene:
|
||||
return
|
||||
|
|
@ -583,7 +702,16 @@ class ScenesManager:
|
|||
|
||||
|
||||
class SceneRegistry:
|
||||
"""
|
||||
A class that represents a registry for scenes in a Telegram bot.
|
||||
"""
|
||||
|
||||
def __init__(self, router: Router) -> None:
|
||||
"""
|
||||
Initialize a new instance of the SceneRegistry class.
|
||||
|
||||
:param router: The router instance used for scene registration.
|
||||
"""
|
||||
self.router = router
|
||||
self._scenes: Dict[Optional[str], Type[Scene]] = {}
|
||||
|
||||
|
|
@ -635,6 +763,22 @@ class SceneRegistry:
|
|||
return await handler(event, data)
|
||||
|
||||
def add(self, *scenes: Type[Scene], router: Optional[Router] = None) -> None:
|
||||
"""
|
||||
This method adds the specified scenes to the router.
|
||||
If a router is not provided, it uses the default router stored
|
||||
in the SceneRegistry instance.
|
||||
The scenes are included in the router by calling the `as_router()`
|
||||
method on each scene and passing the router as a parameter to this method.
|
||||
|
||||
If a scene with the same state already exists in the registry, a SceneException is raised.
|
||||
|
||||
:param scenes: A variable length parameter that accepts one or more types of scenes.
|
||||
These scenes are instances of the Scene class.
|
||||
:param router: An optional parameter that specifies the router
|
||||
to which the scenes should be added. If not provided, the scenes will be
|
||||
added to the default router stored in the SceneRegistry instance.
|
||||
:return: None
|
||||
"""
|
||||
if router is None:
|
||||
router = self.router
|
||||
|
||||
|
|
@ -649,6 +793,20 @@ class SceneRegistry:
|
|||
router.include_router(scene.as_router())
|
||||
|
||||
def get(self, scene: Optional[Union[Type[Scene], str]]) -> Type[Scene]:
|
||||
"""
|
||||
This method returns the registered Scene object for the specified scene.
|
||||
The scene parameter can be either a Scene object or a string representing
|
||||
the name of the scene. If a Scene object is provided, the state attribute
|
||||
of the SceneConfig object associated with the Scene object will be used as the scene name.
|
||||
If None or an invalid type is provided, a SceneException will be raised.
|
||||
|
||||
If the specified scene is not registered in the SceneRegistry object,
|
||||
a SceneException will be raised.
|
||||
|
||||
:param scene: A Scene object or a string representing the name of the scene.
|
||||
:return: The registered Scene object corresponding to the given scene parameter.
|
||||
|
||||
"""
|
||||
if inspect.isclass(scene) and issubclass(scene, Scene):
|
||||
scene = scene.__scene_config__.state
|
||||
if scene is not None and not isinstance(scene, str):
|
||||
|
|
@ -708,7 +866,8 @@ class ObserverMarker:
|
|||
|
||||
class OnMarker:
|
||||
"""
|
||||
The `_On` class is used as a marker class to define different types of events in the Scenes.
|
||||
The `OnMarker` class is used as a marker class to define different
|
||||
types of events in the Scenes.
|
||||
|
||||
Attributes:
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ Read more
|
|||
.. toctree::
|
||||
|
||||
storages
|
||||
scene
|
||||
|
||||
|
||||
.. _wiki: https://en.wikipedia.org/wiki/Finite-state_machine
|
||||
|
|
|
|||
241
docs/dispatcher/finite_state_machine/scene.rst
Normal file
241
docs/dispatcher/finite_state_machine/scene.rst
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
=============
|
||||
Scenes Wizard
|
||||
=============
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
.. warning::
|
||||
|
||||
This feature is experimental and may be changed in future versions.
|
||||
|
||||
**aiogram's** basics API is easy to use and powerful,
|
||||
allowing the implementation of simple interactions such as triggering a command or message
|
||||
for a response.
|
||||
However, certain tasks require a dialogue between the user and the bot.
|
||||
This is where Scenes come into play.
|
||||
|
||||
Understanding Scenes
|
||||
====================
|
||||
|
||||
A Scene in **aiogram** is like an abstract, isolated namespace or room that a user can be
|
||||
ushered into via the code. When a user is inside a Scene, all other global commands or
|
||||
message handlers are isolated, and they stop responding to user actions.
|
||||
Scenes provide a structure for more complex interactions,
|
||||
effectively isolating and managing contexts for different stages of the conversation.
|
||||
They allow you to control and manage the flow of the conversation in a more organized manner.
|
||||
|
||||
Scene Lifecycle
|
||||
---------------
|
||||
|
||||
Each Scene can be "entered", "left" of "exited", allowing for clear transitions between different
|
||||
stages of the conversation.
|
||||
For instance, in a multi-step form filling interaction, each step could be a Scene -
|
||||
the bot guides the user from one Scene to the next as they provide the required information.
|
||||
|
||||
Scene Listeners
|
||||
---------------
|
||||
|
||||
Scenes have their own hooks which are command or message listeners that only act while
|
||||
the user is within the Scene.
|
||||
These hooks react to user actions while the user is 'inside' the Scene,
|
||||
providing the responses or actions appropriate for that context.
|
||||
When the user is ushered from one Scene to another, the actions and responses change
|
||||
accordingly as the user is now interacting with the set of listeners inside the new Scene.
|
||||
These 'Scene-specific' hooks or listeners, detached from the global listening context,
|
||||
allow for more streamlined and organized bot-user interactions.
|
||||
|
||||
|
||||
Scene Interactions
|
||||
------------------
|
||||
|
||||
Each Scene is like a self-contained world, with interactions defined within the scope of that Scene.
|
||||
As such, only the handlers defined within the specific Scene will react to user's input during
|
||||
the lifecycle of that Scene.
|
||||
|
||||
|
||||
Scene Benefits
|
||||
--------------
|
||||
|
||||
Scenes can help manage more complex interaction workflows and enable more interactive and dynamic
|
||||
dialogs between the user and the bot.
|
||||
This offers great flexibility in handling multi-step interactions or conversations with the users.
|
||||
|
||||
How to use Scenes
|
||||
=================
|
||||
|
||||
For example we have a quiz bot, which asks the user a series of questions and then displays the results.
|
||||
|
||||
Lets start with the data models, in this example simple data models are used to represent
|
||||
the questions and answers, in real life you would probably use a database to store the data.
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:lines: 18-94
|
||||
:caption: Questions list
|
||||
|
||||
Then, we need to create a Scene class that will represent the quiz game scene:
|
||||
|
||||
.. note::
|
||||
|
||||
Keyword argument passed into class definition describes the scene name - is the same as state of the scene.
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:pyobject: QuizScene
|
||||
:emphasize-lines: 1
|
||||
:lines: -7
|
||||
:caption: Quiz Scene
|
||||
|
||||
|
||||
Also we need to define a handler that helps to start the quiz game:
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:caption: Start command handler
|
||||
:lines: 251-253
|
||||
|
||||
Once the scene is defined, we need to register it in the SceneRegistry:
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:pyobject: create_dispatcher
|
||||
:caption: Registering the scene
|
||||
|
||||
So, now we can implement the quiz game logic, each question is sent to the user one by one,
|
||||
and the user's answer is checked at the end of all questions.
|
||||
|
||||
Now we need to write an entry point for the question handler:
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:caption: Question handler entry point
|
||||
:pyobject: QuizScene.on_enter
|
||||
|
||||
|
||||
Once scene is entered, we should expect the user's answer, so we need to write a handler for it,
|
||||
this handler should expect the text message, save the answer and retake
|
||||
the question handler for the next question:
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:caption: Answer handler
|
||||
:pyobject: QuizScene.answer
|
||||
|
||||
When user answer with unknown message, we should expect the text message again:
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:caption: Unknown message handler
|
||||
:pyobject: QuizScene.unknown_message
|
||||
|
||||
When all questions are answered, we should show the results to the user, as you can see in the code below,
|
||||
we use `await self.wizard.exit()` to exit from the scene when questions list is over in the `QuizScene.on_enter` handler.
|
||||
|
||||
Thats means that we need to write an exit handler to show the results to the user:
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:caption: Show results handler
|
||||
:pyobject: QuizScene.on_exit
|
||||
|
||||
Also we can implement a actions to exit from the quiz game or go back to the previous question:
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:caption: Exit handler
|
||||
:pyobject: QuizScene.exit
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:caption: Back handler
|
||||
:pyobject: QuizScene.back
|
||||
|
||||
Now we can run the bot and test the quiz game:
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:caption: Run the bot
|
||||
:lines: 282-
|
||||
|
||||
Complete them all
|
||||
|
||||
.. literalinclude:: ../../../examples/quiz_scene.py
|
||||
:language: python
|
||||
:caption: Quiz Example
|
||||
|
||||
|
||||
Components
|
||||
==========
|
||||
|
||||
- :class:`aiogram.fsm.scene.Scene` - represents a scene, contains handlers
|
||||
- :class:`aiogram.fsm.scene.SceneRegistry` - container for all scenes in the bot, used to register scenes and resolve them by name
|
||||
- :class:`aiogram.fsm.scene.ScenesManager` - manages scenes for each user, used to enter, leave and resolve current scene for user
|
||||
- :class:`aiogram.fsm.scene.SceneConfig` - scene configuration, used to configure scene
|
||||
- :class:`aiogram.fsm.scene.SceneWizard` - scene wizard, used to interact with user in scene from active scene handler
|
||||
- Markers - marker for scene handlers, used to mark scene handlers
|
||||
|
||||
|
||||
.. autoclass:: aiogram.fsm.scene.Scene
|
||||
:members:
|
||||
|
||||
.. autoclass:: aiogram.fsm.scene.SceneRegistry
|
||||
:members:
|
||||
|
||||
.. autoclass:: aiogram.fsm.scene.ScenesManager
|
||||
:members:
|
||||
|
||||
.. autoclass:: aiogram.fsm.scene.SceneConfig
|
||||
:members:
|
||||
|
||||
.. autoclass:: aiogram.fsm.scene.SceneWizard
|
||||
:members:
|
||||
|
||||
Markers
|
||||
-------
|
||||
|
||||
Markers are similar to the Router event registering mechanism,
|
||||
but they are used to mark scene handlers in the Scene class.
|
||||
|
||||
It can be imported from :code:`from aiogram.fsm.scene import on` and should be used as decorator.
|
||||
|
||||
Allowed event types:
|
||||
|
||||
- message
|
||||
- edited_message
|
||||
- channel_post
|
||||
- edited_channel_post
|
||||
- inline_query
|
||||
- chosen_inline_result
|
||||
- callback_query
|
||||
- shipping_query
|
||||
- pre_checkout_query
|
||||
- poll
|
||||
- poll_answer
|
||||
- my_chat_member
|
||||
- chat_member
|
||||
- chat_join_request
|
||||
|
||||
Each event type can be filtered in the same way as in the Router.
|
||||
|
||||
Also each event type can be marked as scene entry point, exit point or leave point.
|
||||
|
||||
If you want to mark the scene can be entered from message or inline query,
|
||||
you should use :code:`on.message` or :code:`on.inline_query` marker:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyScene(Scene, name="my_scene"):
|
||||
@on.message.enter()
|
||||
async def on_enter(self, message: types.Message):
|
||||
pass
|
||||
|
||||
@on.callback_query.enter()
|
||||
async def on_enter(self, callback_query: types.CallbackQuery):
|
||||
pass
|
||||
|
||||
|
||||
Scene has only tree points for transitions:
|
||||
|
||||
- enter point - when user enters to the scene
|
||||
- leave point - when user leaves the scene and the enter another scene
|
||||
- exit point - when user exits from the scene
|
||||
293
examples/quiz_scene.py
Normal file
293
examples/quiz_scene.py
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from os import getenv
|
||||
from typing import Any
|
||||
|
||||
from aiogram import Router, html, F, Dispatcher, Bot
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.scene import Scene, on, SceneRegistry, ScenesManager
|
||||
from aiogram.fsm.storage.memory import SimpleEventIsolation
|
||||
from aiogram.types import Message, KeyboardButton, ReplyKeyboardRemove
|
||||
from aiogram.utils.formatting import as_section, as_numbered_list, Bold, as_list, as_key_value
|
||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
||||
|
||||
TOKEN = getenv("BOT_TOKEN")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Answer:
|
||||
"""
|
||||
Represents an answer to a question.
|
||||
"""
|
||||
|
||||
text: str
|
||||
"""The answer text"""
|
||||
is_correct: bool = False
|
||||
"""Indicates if the answer is correct"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Question:
|
||||
"""
|
||||
Class representing a quiz with a question and a list of answers.
|
||||
"""
|
||||
|
||||
text: str
|
||||
"""The question text"""
|
||||
answers: list[Answer]
|
||||
"""List of answers"""
|
||||
|
||||
correct_answer: str = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.correct_answer = next(answer.text for answer in self.answers if answer.is_correct)
|
||||
|
||||
|
||||
# Fake data, in real application you should use a database or something else
|
||||
QUESTIONS = [
|
||||
Question(
|
||||
text="What is the capital of France?",
|
||||
answers=[
|
||||
Answer("Paris", is_correct=True),
|
||||
Answer("London"),
|
||||
Answer("Berlin"),
|
||||
Answer("Madrid"),
|
||||
],
|
||||
),
|
||||
Question(
|
||||
text="What is the capital of Spain?",
|
||||
answers=[
|
||||
Answer("Paris"),
|
||||
Answer("London"),
|
||||
Answer("Berlin"),
|
||||
Answer("Madrid", is_correct=True),
|
||||
],
|
||||
),
|
||||
Question(
|
||||
text="What is the capital of Germany?",
|
||||
answers=[
|
||||
Answer("Paris"),
|
||||
Answer("London"),
|
||||
Answer("Berlin", is_correct=True),
|
||||
Answer("Madrid"),
|
||||
],
|
||||
),
|
||||
Question(
|
||||
text="What is the capital of England?",
|
||||
answers=[
|
||||
Answer("Paris"),
|
||||
Answer("London", is_correct=True),
|
||||
Answer("Berlin"),
|
||||
Answer("Madrid"),
|
||||
],
|
||||
),
|
||||
Question(
|
||||
text="What is the capital of Italy?",
|
||||
answers=[
|
||||
Answer("Paris"),
|
||||
Answer("London"),
|
||||
Answer("Berlin"),
|
||||
Answer("Rome", is_correct=True),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class QuizScene(Scene, state="quiz"):
|
||||
"""
|
||||
This class represents a scene for a quiz game.
|
||||
|
||||
It inherits from Scene class and is associated with the state "quiz".
|
||||
It handles the logic and flow of the quiz game.
|
||||
"""
|
||||
|
||||
@on.message.enter()
|
||||
async def on_enter(self, message: Message, state: FSMContext, step: int | None = 0) -> Any:
|
||||
"""
|
||||
Method triggered when the user enters the quiz scene.
|
||||
|
||||
It displays the current question and answer options to the user.
|
||||
|
||||
:param message:
|
||||
:param state:
|
||||
:param step: Scene argument, can be passed to the scene using the wizard
|
||||
:return:
|
||||
"""
|
||||
if not step:
|
||||
# This is the first step, so we should greet the user
|
||||
await message.answer("Welcome to the quiz!")
|
||||
|
||||
try:
|
||||
quiz = QUESTIONS[step]
|
||||
except IndexError:
|
||||
# This error means that the question's list is over
|
||||
return await self.wizard.exit()
|
||||
|
||||
markup = ReplyKeyboardBuilder()
|
||||
markup.add(*[KeyboardButton(text=answer.text) for answer in quiz.answers])
|
||||
|
||||
if step > 0:
|
||||
markup.button(text="🔙 Back")
|
||||
markup.button(text="🚫 Exit")
|
||||
|
||||
await state.update_data(step=step)
|
||||
return await message.answer(
|
||||
text=QUESTIONS[step].text,
|
||||
reply_markup=markup.adjust(2).as_markup(resize_keyboard=True),
|
||||
)
|
||||
|
||||
@on.message.exit()
|
||||
async def on_exit(self, message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Method triggered when the user exits the quiz scene.
|
||||
|
||||
It calculates the user's answers, displays the summary, and clears the stored answers.
|
||||
|
||||
:param message:
|
||||
:param state:
|
||||
:return:
|
||||
"""
|
||||
data = await state.get_data()
|
||||
answers = data.get("answers", {})
|
||||
|
||||
correct = 0
|
||||
incorrect = 0
|
||||
user_answers = []
|
||||
for step, quiz in enumerate(QUESTIONS):
|
||||
answer = answers.get(step)
|
||||
is_correct = answer == quiz.correct_answer
|
||||
if is_correct:
|
||||
correct += 1
|
||||
else:
|
||||
incorrect += 1
|
||||
if answer is None:
|
||||
answer = "no answer"
|
||||
user_answers.append(f"{quiz.text} ({'✅' if is_correct else '❌'} {html.quote(answer)})")
|
||||
|
||||
content = as_list(
|
||||
as_section(
|
||||
Bold("Your answers:"),
|
||||
as_numbered_list(*user_answers),
|
||||
),
|
||||
"",
|
||||
as_section(
|
||||
Bold("Summary:"),
|
||||
as_list(
|
||||
as_key_value("Correct", correct),
|
||||
as_key_value("Incorrect", incorrect),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
await message.answer(**content.as_kwargs(), reply_markup=ReplyKeyboardRemove())
|
||||
await state.set_data({})
|
||||
|
||||
@on.message(F.text == "🔙 Back")
|
||||
async def back(self, message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Method triggered when the user selects the "Back" button.
|
||||
|
||||
It allows the user to go back to the previous question.
|
||||
|
||||
:param message:
|
||||
:param state:
|
||||
:return:
|
||||
"""
|
||||
data = await state.get_data()
|
||||
step = data["step"]
|
||||
|
||||
previous_step = step - 1
|
||||
if previous_step < 0:
|
||||
# In case when the user tries to go back from the first question,
|
||||
# we just exit the quiz
|
||||
return await self.wizard.exit()
|
||||
return await self.wizard.back(step=previous_step)
|
||||
|
||||
@on.message(F.text == "🚫 Exit")
|
||||
async def exit(self, message: Message) -> None:
|
||||
"""
|
||||
Method triggered when the user selects the "Exit" button.
|
||||
|
||||
It exits the quiz.
|
||||
|
||||
:param message:
|
||||
:return:
|
||||
"""
|
||||
await self.wizard.exit()
|
||||
|
||||
@on.message(F.text)
|
||||
async def answer(self, message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Method triggered when the user selects an answer.
|
||||
|
||||
It stores the answer and proceeds to the next question.
|
||||
|
||||
:param message:
|
||||
:param state:
|
||||
:return:
|
||||
"""
|
||||
data = await state.get_data()
|
||||
step = data["step"]
|
||||
answers = data.get("answers", {})
|
||||
answers[step] = message.text
|
||||
await state.update_data(answers=answers)
|
||||
|
||||
await self.wizard.retake(step=step + 1)
|
||||
|
||||
@on.message()
|
||||
async def unknown_message(self, message: Message) -> None:
|
||||
"""
|
||||
Method triggered when the user sends a message that is not a command or an answer.
|
||||
|
||||
It asks the user to select an answer.
|
||||
|
||||
:param message: The message received from the user.
|
||||
:return: None
|
||||
"""
|
||||
await message.answer("Please select an answer.")
|
||||
|
||||
|
||||
quiz_router = Router(name=__name__)
|
||||
# Add handler that initializes the scene
|
||||
quiz_router.message.register(QuizScene.as_handler(), Command("quiz"))
|
||||
|
||||
|
||||
@quiz_router.message(Command("start"))
|
||||
async def command_start(message: Message, scenes: ScenesManager):
|
||||
await scenes.close()
|
||||
await message.answer(
|
||||
"Hi! This is a quiz bot. To start the quiz, use the /quiz command.",
|
||||
reply_markup=ReplyKeyboardRemove(),
|
||||
)
|
||||
|
||||
|
||||
def create_dispatcher():
|
||||
# Event isolation is needed to correctly handle fast user responses
|
||||
dispatcher = Dispatcher(
|
||||
events_isolation=SimpleEventIsolation(),
|
||||
)
|
||||
dispatcher.include_router(quiz_router)
|
||||
|
||||
# To use scenes, you should create a SceneRegistry and register your scenes there
|
||||
scene_registry = SceneRegistry(dispatcher)
|
||||
# ... and then register a scene in the registry
|
||||
# by default, Scene will be mounted to the router that passed to the SceneRegistry,
|
||||
# but you can specify the router explicitly using the `router` argument
|
||||
scene_registry.add(QuizScene, router=quiz_router)
|
||||
|
||||
return dispatcher
|
||||
|
||||
|
||||
async def main():
|
||||
dispatcher = create_dispatcher()
|
||||
bot = Bot(TOKEN)
|
||||
await dispatcher.start_polling(bot)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(main())
|
||||
# Alternatively, you can use aiogram-cli:
|
||||
# `aiogram run polling quiz_scene:create_dispatcher --log-level info --token BOT_TOKEN`
|
||||
Loading…
Add table
Add a link
Reference in a new issue