mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-06 07:50:32 +00:00
More docs
This commit is contained in:
parent
3931253a88
commit
f97367b3ee
16 changed files with 415 additions and 100 deletions
|
|
@ -1,6 +1,6 @@
|
|||
Implemented new filter named :code:`MagicData(magic_data)` that helps to filter event by data from middlewares or other filters
|
||||
|
||||
For example you bor is running with argument named :code:`config` that contains the application config then you can filter event by value from this config:
|
||||
For example your bot is running with argument named :code:`config` that contains the application config then you can filter event by value from this config:
|
||||
|
||||
.. code_block: python3
|
||||
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -72,6 +72,7 @@ lint:
|
|||
$(py) black --check --diff $(code_dir)
|
||||
$(py) flake8 $(code_dir)
|
||||
$(py) mypy $(package_dir)
|
||||
# TODO: wemake-python-styleguide
|
||||
|
||||
.PHONY: reformat
|
||||
reformat:
|
||||
|
|
|
|||
|
|
@ -20,28 +20,76 @@ class StorageKey:
|
|||
|
||||
|
||||
class BaseStorage(ABC):
|
||||
"""
|
||||
Base class for all FSM storages
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@asynccontextmanager
|
||||
async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]:
|
||||
"""
|
||||
Isolate events with lock.
|
||||
Will be used as context manager
|
||||
|
||||
:param bot: instance of the current bot
|
||||
:param key: storage key
|
||||
:return: An async generator
|
||||
"""
|
||||
yield None
|
||||
|
||||
@abstractmethod
|
||||
async def set_state(self, bot: Bot, key: StorageKey, state: StateType = None) -> None:
|
||||
"""
|
||||
Set state for specified key
|
||||
|
||||
:param bot: instance of the current bot
|
||||
:param key: storage key
|
||||
:param state: new state
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_state(self, bot: Bot, key: StorageKey) -> Optional[str]:
|
||||
"""
|
||||
Get key state
|
||||
|
||||
:param bot: instance of the current bot
|
||||
:param key: storage key
|
||||
:return: current state
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def set_data(self, bot: Bot, key: StorageKey, data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Write data (replace)
|
||||
|
||||
:param bot: instance of the current bot
|
||||
:param key: storage key
|
||||
:param data: new data
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_data(self, bot: Bot, key: StorageKey) -> Dict[str, Any]:
|
||||
"""
|
||||
Get current data for key
|
||||
|
||||
:param bot: instance of the current bot
|
||||
:param key: storage key
|
||||
:return: current data
|
||||
"""
|
||||
pass
|
||||
|
||||
async def update_data(self, bot: Bot, key: StorageKey, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Update date in the storage for key (like dict.update)
|
||||
|
||||
:param bot: instance of the current bot
|
||||
:param key: storage key
|
||||
:param data: partial data
|
||||
:return: new data
|
||||
"""
|
||||
current_data = await self.get_data(bot=bot, key=key)
|
||||
current_data.update(data)
|
||||
await self.set_data(bot=bot, key=key, data=current_data)
|
||||
|
|
@ -49,4 +97,7 @@ class BaseStorage(ABC):
|
|||
|
||||
@abstractmethod
|
||||
async def close(self) -> None: # pragma: no cover
|
||||
"""
|
||||
Close storage (database connection, file or etc.)
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ class MemoryStorageRecord:
|
|||
|
||||
|
||||
class MemoryStorage(BaseStorage):
|
||||
"""
|
||||
Default FSM storage, stores all data in :class:`dict` and loss everything on shutdown
|
||||
|
||||
.. warning::
|
||||
|
||||
Is not recommended using in production in due to you will lose all data
|
||||
when your bot restarts
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.storage: DefaultDict[StorageKey, MemoryStorageRecord] = defaultdict(
|
||||
MemoryStorageRecord
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@ class KeyBuilder(ABC):
|
|||
|
||||
@abstractmethod
|
||||
def build(self, key: StorageKey, part: Literal["data", "state", "lock"]) -> str:
|
||||
"""
|
||||
This method should be implemented in subclasses
|
||||
|
||||
:param key: contextual key
|
||||
:param part: part of the record
|
||||
:return: key to be used in Redis queries
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -30,9 +37,21 @@ class DefaultKeyBuilder(KeyBuilder):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, prefix: str = "fsm", with_bot_id: bool = False, with_destiny: bool = False
|
||||
self,
|
||||
*,
|
||||
prefix: str = "fsm",
|
||||
separator: str = ":",
|
||||
with_bot_id: bool = False,
|
||||
with_destiny: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
:param prefix: prefix for all records
|
||||
:param separator: separator
|
||||
:param with_bot_id: include Bot id in the key
|
||||
:param with_destiny: include destiny key
|
||||
"""
|
||||
self.prefix = prefix
|
||||
self.separator = separator
|
||||
self.with_bot_id = with_bot_id
|
||||
self.with_destiny = with_destiny
|
||||
|
||||
|
|
@ -44,10 +63,14 @@ class DefaultKeyBuilder(KeyBuilder):
|
|||
if self.with_destiny:
|
||||
parts.append(key.destiny)
|
||||
parts.append(part)
|
||||
return ":".join(parts)
|
||||
return self.separator.join(parts)
|
||||
|
||||
|
||||
class RedisStorage(BaseStorage):
|
||||
"""
|
||||
Redis storage required :code:`aioredis` package installed (:code:`pip install aioredis`)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redis: Redis,
|
||||
|
|
@ -56,6 +79,13 @@ class RedisStorage(BaseStorage):
|
|||
data_ttl: Optional[int] = None,
|
||||
lock_kwargs: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
:param redis: Instance of Redis connection
|
||||
:param key_builder: builder that helps to convert contextual key to string
|
||||
:param state_ttl: TTL for state records
|
||||
:param data_ttl: TTL for data records
|
||||
:param lock_kwargs: Custom arguments for Redis lock
|
||||
"""
|
||||
if key_builder is None:
|
||||
key_builder = DefaultKeyBuilder()
|
||||
if lock_kwargs is None:
|
||||
|
|
@ -70,6 +100,14 @@ class RedisStorage(BaseStorage):
|
|||
def from_url(
|
||||
cls, url: str, connection_kwargs: Optional[Dict[str, Any]] = None, **kwargs: Any
|
||||
) -> "RedisStorage":
|
||||
"""
|
||||
Create an instance of :class:`RedisStorage` with specifying the connection string
|
||||
|
||||
:param url: for example :code:`redis://user:password@host:port/db`
|
||||
:param connection_kwargs: see :code:`aioredis` docs
|
||||
:param kwargs: arguments to be passed to :class:`RedisStorage`
|
||||
:return: an instance of :class:`RedisStorage`
|
||||
"""
|
||||
if connection_kwargs is None:
|
||||
connection_kwargs = {}
|
||||
pool = ConnectionPool.from_url(url, **connection_kwargs)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ MAX_BUTTONS = 100
|
|||
|
||||
|
||||
class KeyboardBuilder(Generic[ButtonType]):
|
||||
"""
|
||||
Generic keyboard builder that helps to adjust your markup with defined shape of lines.
|
||||
|
||||
Works both of InlineKeyboardMarkup and ReplyKeyboardMarkup.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, button_type: Type[ButtonType], markup: Optional[List[List[ButtonType]]] = None
|
||||
) -> None:
|
||||
|
|
@ -257,6 +263,10 @@ def repeat_last(items: Iterable[T]) -> Generator[T, None, None]:
|
|||
|
||||
|
||||
class InlineKeyboardBuilder(KeyboardBuilder[InlineKeyboardButton]):
|
||||
"""
|
||||
Inline keyboard builder inherits all methods from generic builder
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@no_type_check
|
||||
|
|
@ -275,6 +285,7 @@ class InlineKeyboardBuilder(KeyboardBuilder[InlineKeyboardButton]):
|
|||
...
|
||||
|
||||
def as_markup(self, **kwargs: Any) -> InlineKeyboardMarkup:
|
||||
"""Construct an InlineKeyboardMarkup"""
|
||||
...
|
||||
|
||||
def __init__(self, markup: Optional[List[List[InlineKeyboardButton]]] = None) -> None:
|
||||
|
|
@ -290,6 +301,10 @@ class InlineKeyboardBuilder(KeyboardBuilder[InlineKeyboardButton]):
|
|||
|
||||
|
||||
class ReplyKeyboardBuilder(KeyboardBuilder[KeyboardButton]):
|
||||
"""
|
||||
Reply keyboard builder inherits all methods from generic builder
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@no_type_check
|
||||
|
|
|
|||
BIN
docs/_static/fsm_example.png
vendored
Normal file
BIN
docs/_static/fsm_example.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
|
|
@ -19,8 +19,8 @@ Here is list of builtin filters:
|
|||
content_types
|
||||
text
|
||||
exception
|
||||
|
||||
Or you can do :ref:`✨ some magic <magic-filters>`
|
||||
magic_filters
|
||||
magic_data
|
||||
|
||||
Own filters specification
|
||||
=========================
|
||||
|
|
@ -35,7 +35,7 @@ Filters can be:
|
|||
|
||||
- Any awaitable object
|
||||
|
||||
- Subclass of :ref:`BaseFilter <filters-base>`
|
||||
- Subclass of :class:`aiogram.dispatcher.filters.base.BaseFilter`
|
||||
|
||||
- Instances of :ref:`MagicFilter <magic-filters>`
|
||||
|
||||
|
|
|
|||
34
docs/dispatcher/filters/magic_data.rst
Normal file
34
docs/dispatcher/filters/magic_data.rst
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
====
|
||||
MagicData
|
||||
====
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.magic_data.MagicData
|
||||
:members:
|
||||
:member-order: bysource
|
||||
:undoc-members: False
|
||||
|
||||
Can be imported:
|
||||
|
||||
- :code:`from aiogram.dispatcher.filters.magic_data import MagicData`
|
||||
- :code:`from aiogram.dispatcher.filters import MagicData`
|
||||
- :code:`from aiogram.filters import MagicData`
|
||||
|
||||
Or used from filters factory by passing corresponding arguments to handler registration line
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
#. :code:`magic_data=F.event.from_user.id == F.config.admin_id` (Note that :code:`config` should be passed from middleware)
|
||||
|
||||
|
||||
Allowed handlers
|
||||
================
|
||||
|
||||
Allowed update types for this filter:
|
||||
|
||||
- :code:`message`
|
||||
- :code:`edited_message`
|
||||
- :code:`channel_post`
|
||||
- :code:`edited_channel_post`
|
||||
- :code:`inline_query`
|
||||
- :code:`callback_query`
|
||||
|
|
@ -24,7 +24,7 @@ and memorize the attributes chain and the action which should be checked on dema
|
|||
So that's mean you can chain attribute getters, describe simple data validations
|
||||
and then call the resulted object passing single object as argument,
|
||||
for example make attributes chain :code:`F.foo.bar.baz` then add
|
||||
action ':code:`F.foo.bar.baz == 'spam'` and then call the resulted object - :code:`(F.foo.bar.baz == 'spam')(obj)`
|
||||
action ':code:`F.foo.bar.baz == 'spam'` and then call the resulted object - :code:`(F.foo.bar.baz == 'spam').resolve(obj)`
|
||||
|
||||
.. _magic-filter-possible-actions:
|
||||
|
||||
|
|
@ -125,9 +125,9 @@ Can be used only with string attributes.
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
F.text__lower == 'test' # lambda message: message.text.lower() == 'test'
|
||||
F.text__upper.in_('FOO', 'BAR') # lambda message: message.text.upper() in {'FOO', 'BAR'}
|
||||
F.text__len == 5 # lambda message: len(message.text) == 5
|
||||
F.text.lower() == 'test' # lambda message: message.text.lower() == 'test'
|
||||
F.text.upper().in_('FOO', 'BAR') # lambda message: message.text.upper() in {'FOO', 'BAR'}
|
||||
F.text.len() == 5 # lambda message: len(message.text) == 5
|
||||
|
||||
|
||||
Usage in *aiogram*
|
||||
|
|
|
|||
119
docs/dispatcher/finite_state_machine/index.rst
Normal file
119
docs/dispatcher/finite_state_machine/index.rst
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
====================
|
||||
Finite State Machine
|
||||
====================
|
||||
|
||||
A finite-state machine (FSM) or finite-state automaton (FSA, plural: automata), finite automaton,
|
||||
or simply a state machine, is a mathematical model of computation.
|
||||
|
||||
It is an abstract machine that can be in exactly one of a finite number of states at any given time.
|
||||
The FSM can change from one state to another in response to some inputs;
|
||||
the change from one state to another is called a transition.
|
||||
|
||||
An FSM is defined by a list of its states, its initial state,
|
||||
and the inputs that trigger each transition.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<hr>
|
||||
|
||||
Source: `WikiPedia <wiki>`_
|
||||
|
||||
Usage example
|
||||
=============
|
||||
|
||||
Not all functionality of the bot can be implemented as single handler,
|
||||
for example you will need to collect some data from user in separated steps you will need to use FSM.
|
||||
|
||||
|
||||
.. image:: ../../_static/fsm_example.png
|
||||
:alt: FSM Example
|
||||
|
||||
Let's see how to do that step-by-step
|
||||
|
||||
Step by step
|
||||
------------
|
||||
|
||||
Before handle any states you will need to specify what kind of states you want to handle
|
||||
|
||||
.. literalinclude:: ../../../examples/finite_state_machine.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:lineno-start: 15
|
||||
:lines: 15-18
|
||||
|
||||
And then write handler for each state separately from the start of dialog
|
||||
|
||||
Here is dialog can be started only via command :code:`/start`, so lets handle it and make transition user to state :code:`Form.name`
|
||||
|
||||
.. literalinclude:: ../../../examples/finite_state_machine.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:lineno-start: 21
|
||||
:lines: 21-27
|
||||
|
||||
After that you will need to save some data to the storage and make transition to next step.
|
||||
|
||||
.. literalinclude:: ../../../examples/finite_state_machine.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:lineno-start: 48
|
||||
:lines: 48-63
|
||||
|
||||
At the next steps user can make different answers, it can be `yes`, `no` or any other
|
||||
|
||||
Handle :code:`yes` and soon we need to handle :code:`Form.language` state
|
||||
|
||||
.. literalinclude:: ../../../examples/finite_state_machine.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:lineno-start: 77
|
||||
:lines: 77-84
|
||||
|
||||
Handle :code:`no`
|
||||
|
||||
.. literalinclude:: ../../../examples/finite_state_machine.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:lineno-start: 66
|
||||
:lines: 66-74
|
||||
|
||||
And handle any other answers
|
||||
|
||||
.. literalinclude:: ../../../examples/finite_state_machine.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:lineno-start: 87
|
||||
:lines: 87-89
|
||||
|
||||
All possible cases of `like_bots` step was covered, let's implement finally step
|
||||
|
||||
.. literalinclude:: ../../../examples/finite_state_machine.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:lineno-start: 92
|
||||
:lines: 92-102
|
||||
|
||||
And now you have covered all steps from the image, but you can make possibility to cancel conversation, lets do that via command or text
|
||||
|
||||
.. literalinclude:: ../../../examples/finite_state_machine.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:lineno-start: 30
|
||||
:lines: 30-45
|
||||
|
||||
Complete example
|
||||
----------------
|
||||
.. literalinclude:: ../../../examples/finite_state_machine.py
|
||||
:language: python
|
||||
:linenos:
|
||||
|
||||
|
||||
Read more
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
|
||||
storages
|
||||
|
||||
|
||||
.. _wiki: https://en.wikipedia.org/wiki/Finite-state_machine
|
||||
|
|
@ -9,7 +9,7 @@ MemoryStorage
|
|||
-------------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.fsm.storage.memory.MemoryStorage
|
||||
:members: __init__, from_url
|
||||
:members: __init__
|
||||
:member-order: bysource
|
||||
|
||||
RedisStorage
|
||||
|
|
@ -19,5 +19,20 @@ RedisStorage
|
|||
:members: __init__, from_url
|
||||
:member-order: bysource
|
||||
|
||||
Keys inside storage can be customized via key builders:
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.fsm.storage.redis.KeyBuilder
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.fsm.storage.redis.DefaultKeyBuilder
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
Writing own storages
|
||||
====================
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.fsm.storage.base.BaseStorage
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
|
|
|||
|
|
@ -22,5 +22,5 @@ Dispatcher is subclass of router and should be always is root router.
|
|||
dispatcher
|
||||
class_based_handlers/index
|
||||
filters/index
|
||||
filters/magic_filters
|
||||
middlewares
|
||||
finite_state_machine/index
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ Deal with Babel
|
|||
===============
|
||||
|
||||
Step 1: Extract messages
|
||||
-------------------
|
||||
------------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ is template where messages will be extracted and `messages` is translation domai
|
|||
|
||||
|
||||
Step 2: Init language
|
||||
----------------
|
||||
---------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
|
|
|||
|
|
@ -2,23 +2,40 @@
|
|||
Keyboard builder
|
||||
================
|
||||
|
||||
Inline Keyboard
|
||||
===============
|
||||
|
||||
.. autoclass:: aiogram.utils.keyboard.InlineKeyboardBuilder
|
||||
:members: __init__, buttons, copy, export, add, row, adjust, button, as_markup
|
||||
:undoc-members: True
|
||||
|
||||
Reply Keyboard
|
||||
==============
|
||||
|
||||
.. autoclass:: aiogram.utils.keyboard.ReplyKeyboardBuilder
|
||||
:members: __init__, buttons, copy, export, add, row, adjust, button, as_markup
|
||||
:undoc-members: True
|
||||
|
||||
|
||||
Base builder
|
||||
============
|
||||
.. autoclass:: aiogram.utils.keyboard.ReplyKeyboardBuilder
|
||||
:members: __init__, buttons, copy, export, add, row, adjust, button, as_markup
|
||||
:undoc-members: True
|
||||
|
||||
Inline Keyboard
|
||||
===============
|
||||
|
||||
.. autoclass:: aiogram.utils.keyboard.InlineKeyboardBuilder
|
||||
:noindex:
|
||||
|
||||
.. method:: button(text: str, url: Optional[str] = None, login_url: Optional[LoginUrl] = None, callback_data: Optional[Union[str, CallbackData]] = None, switch_inline_query: Optional[str] = None, switch_inline_query_current_chat: Optional[str] = None, callback_game: Optional[CallbackGame] = None, pay: Optional[bool] = None, **kwargs: Any) -> aiogram.utils.keyboard.InlineKeyboardBuilder
|
||||
:noindex:
|
||||
|
||||
Add new inline button to markup
|
||||
|
||||
.. method:: as_markup() -> aiogram.types.inline_keyboard_markup.InlineKeyboardMarkup
|
||||
:noindex:
|
||||
|
||||
Construct an InlineKeyboardMarkup
|
||||
|
||||
Reply Keyboard
|
||||
==============
|
||||
|
||||
.. autoclass:: aiogram.utils.keyboard.ReplyKeyboardBuilder
|
||||
:noindex:
|
||||
|
||||
.. method:: button(text: str, request_contact: Optional[bool] = None, request_location: Optional[bool] = None, request_poll: Optional[KeyboardButtonPollType] = None, **kwargs: Any) -> aiogram.utils.keyboard.ReplyKeyboardBuilder
|
||||
:noindex:
|
||||
|
||||
Add new button to markup
|
||||
|
||||
.. method:: as_markup() -> aiogram.types.reply_keyboard_markup.ReplyKeyboardMarkup
|
||||
:noindex:
|
||||
|
||||
Construct an ReplyKeyboardMarkup
|
||||
|
|
|
|||
|
|
@ -2,40 +2,34 @@ import asyncio
|
|||
import logging
|
||||
import sys
|
||||
from os import getenv
|
||||
from typing import Any, Dict
|
||||
|
||||
from aiogram import Bot, Dispatcher, F
|
||||
from aiogram.dispatcher.filters import Command
|
||||
from aiogram import Bot, Dispatcher, F, Router, html
|
||||
from aiogram.dispatcher.fsm.context import FSMContext
|
||||
from aiogram.dispatcher.fsm.state import State, StatesGroup
|
||||
from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup, ReplyKeyboardRemove
|
||||
from aiogram.utils.keyboard import KeyboardBuilder
|
||||
from aiogram.utils.markdown import hbold
|
||||
|
||||
GENDERS = ["Male", "Female", "Helicopter", "Other"]
|
||||
|
||||
dp = Dispatcher()
|
||||
form_router = Router()
|
||||
|
||||
|
||||
# States
|
||||
class Form(StatesGroup):
|
||||
name = State() # Will be represented in storage as 'Form:name'
|
||||
age = State() # Will be represented in storage as 'Form:age'
|
||||
gender = State() # Will be represented in storage as 'Form:gender'
|
||||
name = State()
|
||||
like_bots = State()
|
||||
language = State()
|
||||
|
||||
|
||||
@dp.message(Command(commands=["start"]))
|
||||
async def cmd_start(message: Message, state: FSMContext):
|
||||
"""
|
||||
Conversation's entry point
|
||||
"""
|
||||
# Set state
|
||||
@form_router.message(commands={"start"})
|
||||
async def command_start(message: Message, state: FSMContext) -> None:
|
||||
await state.set_state(Form.name)
|
||||
await message.answer("Hi there! What's your name?")
|
||||
await message.answer(
|
||||
"Hi there! What's your name?",
|
||||
reply_markup=ReplyKeyboardRemove(),
|
||||
)
|
||||
|
||||
|
||||
@dp.message(Command(commands=["cancel"]))
|
||||
@dp.message(F.text.lower() == "cancel")
|
||||
async def cancel_handler(message: Message, state: FSMContext):
|
||||
@form_router.message(commands={"cancel"})
|
||||
@form_router.message(F.text.casefold() == "cancel")
|
||||
async def cancel_handler(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Allow user to cancel any action
|
||||
"""
|
||||
|
|
@ -44,64 +38,86 @@ async def cancel_handler(message: Message, state: FSMContext):
|
|||
return
|
||||
|
||||
logging.info("Cancelling state %r", current_state)
|
||||
# Cancel state and inform user about it
|
||||
await state.clear()
|
||||
# And remove keyboard (just in case)
|
||||
await message.answer("Cancelled.", reply_markup=ReplyKeyboardRemove())
|
||||
|
||||
|
||||
@dp.message(Form.name)
|
||||
async def process_name(message: Message, state: FSMContext):
|
||||
"""
|
||||
Process user name
|
||||
"""
|
||||
await state.update_data(name=message.text)
|
||||
await state.set_state(Form.age)
|
||||
await message.answer("How old are you?")
|
||||
|
||||
|
||||
# Check age. Age gotta be digit
|
||||
@dp.message(Form.age, ~F.text.isdigit())
|
||||
async def process_age_invalid(message: Message):
|
||||
"""
|
||||
If age is invalid
|
||||
"""
|
||||
return await message.answer("Age gotta be a number.\nHow old are you? (digits only)")
|
||||
|
||||
|
||||
@dp.message(Form.age)
|
||||
async def process_age(message: Message, state: FSMContext):
|
||||
# Update state and data
|
||||
await state.set_state(Form.gender)
|
||||
await state.update_data(age=int(message.text))
|
||||
|
||||
# Configure ReplyKeyboardMarkup
|
||||
constructor = KeyboardBuilder(KeyboardButton)
|
||||
constructor.add(*(KeyboardButton(text=text) for text in GENDERS)).adjust(2)
|
||||
markup = ReplyKeyboardMarkup(
|
||||
resize_keyboard=True, selective=True, keyboard=constructor.export()
|
||||
)
|
||||
await message.reply("What is your gender?", reply_markup=markup)
|
||||
|
||||
|
||||
@dp.message(Form.gender)
|
||||
async def process_gender(message: Message, state: FSMContext):
|
||||
data = await state.update_data(gender=message.text)
|
||||
await state.clear()
|
||||
|
||||
# And send message
|
||||
await message.answer(
|
||||
(
|
||||
f'Hi, nice to meet you, {hbold(data["name"])}\n'
|
||||
f'Age: {hbold(data["age"])}\n'
|
||||
f'Gender: {hbold(data["gender"])}\n'
|
||||
),
|
||||
"Cancelled.",
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@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, 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(),
|
||||
)
|
||||
|
||||
|
||||
@form_router.message(Form.like_bots)
|
||||
async def process_unknown_write_bots(message: Message, state: FSMContext) -> None:
|
||||
await message.reply("I don't understand you :(")
|
||||
|
||||
|
||||
@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()
|
||||
text = (
|
||||
"Thank for all! Python is in my hearth!\nSee you soon."
|
||||
if message.text.casefold() == "python"
|
||||
else "Thank for information!\nSee you soon."
|
||||
)
|
||||
await message.answer(text)
|
||||
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())
|
||||
|
||||
|
||||
async def main():
|
||||
bot = Bot(token=getenv("TELEGRAM_TOKEN"), parse_mode="HTML")
|
||||
dp = Dispatcher()
|
||||
dp.include_router(form_router)
|
||||
|
||||
await dp.start_polling(bot)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue