Fix parsing phone and add redis storage for FSM in telegram bot

This commit is contained in:
2026-01-17 20:03:33 +03:00
parent a065c6b7be
commit 781527f2de
14 changed files with 153 additions and 27 deletions

View File

@@ -1,7 +1,9 @@
import asyncio
import pathlib
import sys
import typer
from loguru import logger
from .notifications import get_cli as get_notifications_cli
from .repositories import RepositoriesContainer
@@ -29,6 +31,8 @@ def callback(
settings = ctx.obj["settings"]
ctx.obj["repositories_container"] = RepositoriesContainer(settings=settings.repository)
logger.remove()
logger.add(sys.stdout, level=settings.logging.level, format=settings.logging.format)
def run(ctx: typer.Context):

View File

@@ -1,3 +1,4 @@
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
from .notifications import NotificationsSettings
@@ -6,6 +7,11 @@ from .telegram_bot.polling import TelegramBotPollingSettings
from .telegram_bot.webhook import TelegramBotWebhookSettings
class LoggingSettings(BaseModel):
level: str = "INFO"
format: str = "{time}|{level:>8}|{message}"
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="BIRTHDAY_POOL_BOT__",
@@ -13,6 +19,7 @@ class Settings(BaseSettings):
extra="ignore",
)
logging: LoggingSettings = LoggingSettings()
repository: RepositorySettings = RepositorySettings()
telegram_bot: (
TelegramBotPollingSettings |

View File

@@ -1,9 +1,9 @@
import aiogram
import facet
from aiogram.fsm.storage.memory import MemoryStorage
from birthday_pool_bot.repositories import RepositoriesContainer
from .settings import TelegramBotSettings
from .storage import get_telegram_bot_fsm_storage
from .ui import setup_dispatcher
@@ -15,7 +15,9 @@ class BaseTelegramBotService(facet.AsyncioServiceMixin):
):
self._settings = settings
self._repositories_container = repositories_container
self._dispatcher = aiogram.Dispatcher(storage=MemoryStorage())
self._dispatcher = aiogram.Dispatcher(
storage=get_telegram_bot_fsm_storage(settings=settings.storage),
)
self._bot = aiogram.Bot(token=settings.token)
setup_dispatcher(

View File

@@ -1,8 +1,13 @@
from pydantic import BaseModel
from .enums import TelegramBotMethodEnum
from .storage import MemoryStorageSettings, RedisStorageSettings
class TelegramBotSettings(BaseModel):
method: TelegramBotMethodEnum = TelegramBotMethodEnum.POLLING
token: str = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
token: str = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
storage: (
MemoryStorageSettings |
RedisStorageSettings
) = MemoryStorageSettings()

View File

@@ -0,0 +1,16 @@
from .fabric import get_telegram_bot_fsm_storage
from .settings import (
MemoryStorageSettings,
RedisStorageSettings,
StorageSettings,
)
__all__ = (
# fabric
"get_telegram_bot_fsm_storage",
# settings
"MemoryStorageSettings",
"RedisStorageSettings",
"StorageSettings",
)

View File

@@ -0,0 +1,6 @@
import enum
class StorageTypeEnum(str, enum.Enum):
MEMORY = "memory"
REDIS = "redis"

View File

@@ -0,0 +1,20 @@
import redis
from aiogram.fsm.storage.base import BaseStorage
from .enums import StorageTypeEnum
from .settings import StorageSettings
def get_telegram_bot_fsm_storage(settings: StorageSettings) -> BaseStorage:
match settings.type:
case StorageTypeEnum.MEMORY.value:
return MemoryStorage()
case StorageTypeEnum.REDIS.value:
redis_client = redis.Redis(
host=settings.host,
port=settings.port,
db=settings.database,
)
return RedisStorage(redis=redis_client)
case _:
raise ValueError(f"Unsupported telegram bot storage type: {settings.type}")

View File

@@ -0,0 +1,16 @@
from typing import Literal
class StorageSettings(BaseModel):
type: StorageTypeEnum
class MemoryStorageSettings(StorageSettings):
type: Literal[StorageType.MEMORY.value] = StorageTypeEnum.MEMORY.value
class RedisStorageSettings(StorageSettings):
type: Litreal[StorageType.REDIS.value] = StorageTypeEnum.REDIS.value
host: str = "localhost"
port: int = 6379
database: int = 0

View File

@@ -1,8 +1,6 @@
from aiogram import types
from aiogram.enums import MessageOriginType, ParseMode
from aiogram.fsm.context import FSMContext
from pydantic_core import PydanticCustomError
from pydantic_extra_types.phone_numbers import PhoneNumber
from birthday_pool_bot.dto import (
PaymentData,
@@ -27,7 +25,7 @@ from .states import (
SetProfileNameState,
SetProfilePhoneState,
)
from .utils import get_telegram_user_full_name, parse_date
from .utils import get_telegram_user_full_name, parse_date, parse_phone
async def fallback_message_handler(
@@ -114,12 +112,10 @@ async def set_profile_phone_message_handler(
await state.set_state(SetProfilePhoneState.WAITING_FOR_PHONE)
return
phone_number = message.contact.phone_number
try:
PhoneNumber._validate(phone_number, None)
except PydanticCustomError:
phone_number = parse_phone(text=message.contact.phone_number)
if phone_number is None:
await message.reply(
text="Некорректный номер телефон, попробуйте ещё раз",
text="Некорректный номер телефона, попробуйте ещё раз",
)
await state.set_state(SetProfilePhoneState.WAITING_FOR_PHONE)
return
@@ -146,7 +142,7 @@ async def set_profile_birthdate_message_handler(
user: User,
users_repository: UsersRepository,
):
birthday = parse_date(message=message)
birthday = parse_date(text=message.text)
if birthday is None:
await logic.ask_profile_birthdate(message=message)
await state.set_state(SetProfileBirthdayState.WAITING_FOR_DATE)
@@ -182,9 +178,8 @@ async def set_profile_gift_payment_phone_message_handler(
await state.set_state(SetProfileGiftPaymentDataState.WAITING_FOR_PHONE)
return
try:
PhoneNumber._validate(phone_number, None)
except PydanticCustomError:
phone_number = parse_phone(text=phone_number)
if phone_number is None:
await message.reply(
text="Некорректный номер телефон, попробуйте ещё раз",
)
@@ -371,10 +366,8 @@ async def set_new_subscription_user_message_handler(
telegram_id=user_telegram_id,
)
elif message.text:
user_phone = message.text
try:
PhoneNumber._validate(user_phone, None)
except PydanticCustomError:
user_phone = parse_phone(text=message.text)
if user_phone is None:
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
@@ -437,7 +430,7 @@ async def set_new_subscription_user_birthday_message_handler(
state: FSMContext,
users_repository: UsersRepository,
):
birthday = parse_date(message=message)
birthday = parse_date(text=message.text)
if birthday is None:
await logic.ask_new_subscription_user_birthday(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_DATE)
@@ -798,9 +791,8 @@ async def set_new_subscription_pool_payment_phone_message_handler(
await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
return
try:
PhoneNumber._validate(phone_number, None)
except PydanticCustomError:
phone_number = parse_phone(text=phone_number)
if phone_number is None:
await message.reply(
text="Некорректный номер телефона, попробуйте ещё раз",
)

View File

@@ -3,13 +3,15 @@ import re
from aiogram import types
from aiogram.types import User as TelegramUser
from pydantic_core import PydanticCustomError
from pydantic_extra_types.phone_numbers import PhoneNumber
BIRTHDATE_REGEXP = re.compile(r"^(?P<day>\d{2}).(?P<month>\d{2}).(?P<year>\d{4})$")
def parse_date(message: types.Message) -> datetime.date:
if (re_match := BIRTHDATE_REGEXP.match(message.text)) is None:
def parse_date(text: str) -> datetime.date | None:
if (re_match := BIRTHDATE_REGEXP.match(text)) is None:
return
return datetime.date(
@@ -19,7 +21,18 @@ def parse_date(message: types.Message) -> datetime.date:
)
def parse_phone(text: str) -> str | None:
phone = text
if not phone.startswith("+"):
phone = f"+{phone}"
try:
PhoneNumber._validate(phone, None)
except PydanticCustomError:
return
return phone
def get_telegram_user_full_name(user: TelegramUser) -> str:
name_parts = (user.first_name, user.last_name)
return " ".join(filter(None, name_parts))