Fix bugs, add README and Makefile

This commit is contained in:
2026-01-16 20:39:51 +03:00
parent 3e515c66ec
commit a065c6b7be
30 changed files with 695 additions and 286 deletions

View File

@@ -4,13 +4,14 @@ WORKDIR /app
RUN pip install uv && apt update && apt install -y git RUN pip install uv && apt update && apt install -y git
COPY pyproject.toml uv.lock README.md /app COPY pyproject.toml uv.lock README.md /app
COPY birthday_pool_bot /app RUN uv sync
COPY birthday_pool_bot /app/birthday_pool_bot
ENTRYPOINT ["uv", "run"] ENTRYPOINT ["uv", "run"]
FROM core AS app FROM core AS app
RUN uv sync --group sqlite RUN uv sync --group sqlite --group postgresql
ENTRYPOINT ["uv", "run", "python", "-m", "birthday_pool_bot"] ENTRYPOINT ["uv", "run", "python", "-m", "birthday_pool_bot"]

5
Makefile Normal file
View File

@@ -0,0 +1,5 @@
lint:
uv run ruff check
test:
echo "Not implemented"

View File

@@ -1,4 +1,29 @@
# Birthday Pool Bot # Birthday Pool Bot
## Configuration ## Run
Copy environment template file
```shell
cp .env.example .env
```
And edit `.env`
Install requirements
```shell
uv sync --all-groups
```
Run
```shell
uv run python -m birthday_pool_bot -e .env run
```
## TODO
* Поддержать рассылку пользователям о том, что появились новые сборы для их подписки (если они ещё
не участвуют в сборе)
* Добавить возможность добавлять ссылку на сбор денег Сбера, когда происходит оповещение о
наступающем дне рождения
* Добавить раздел с настройками времени оповещения о предстоящих событиях
* Добавить возможность инициировать сбор денег по другому поводу (кроме ДР)
* Добавить возможность отписаться во время нотификации о предстоящем ДР

View File

@@ -46,6 +46,7 @@ class Pool(BaseModel):
payment_data: PaymentData payment_data: PaymentData
owner: User | None = None owner: User | None = None
birthday_user: User | None = None
class PoolFilter(BaseFilter): class PoolFilter(BaseFilter):

View File

@@ -1,8 +1,8 @@
import uuid from typing import Any
from datetime import timedelta
from typing import AsyncContextManager, AsyncGenerator, Protocol
from birthday_pool_bot.dto import Pool, User import pathlib
from datetime import timedelta
from typing import AsyncContextManager, Generator, Protocol
class MessengerInterface(Protocol): class MessengerInterface(Protocol):

View File

@@ -2,6 +2,7 @@ import asyncio
import typer import typer
from birthday_pool_bot.repositories import RepositoriesContainer
from .service import NotificationsService from .service import NotificationsService
from .settings import NotificationsSettings from .settings import NotificationsSettings

View File

@@ -6,6 +6,7 @@ from aiogram.enums import ParseMode
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from birthday_pool_bot.repositories import RepositoriesContainer
from .settings import NotificationsSettings from .settings import NotificationsSettings

View File

@@ -1,7 +1,8 @@
import contextvars import contextvars
from contextlib import asynccontextmanager, contextmanager from contextlib import asynccontextmanager, contextmanager
from typing import Generic, TypeVar from typing import AsyncGenerator, Generator, Generic, TypeVar
from pydantic_filters import BasePagination, BaseSort
from pydantic_filters.drivers.sqlalchemy import append_to_statement from pydantic_filters.drivers.sqlalchemy import append_to_statement
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
@@ -9,7 +10,9 @@ from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import delete, insert, select, update from sqlmodel import delete, insert, select, update
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from birthday_pool_bot.interfaces import RepositoryInterface from .exceptions import AlreadyExistsError, HaveNoSessionError
from .settings import RepositorySettings
from .tables import BaseSQLModel
DTOType = TypeVar("DTOType") DTOType = TypeVar("DTOType")

View File

@@ -12,7 +12,7 @@ from .migrator import Migrator
def callback(ctx: typer.Context): def callback(ctx: typer.Context):
ctx.obj = ctx.obj or {} ctx.obj = ctx.obj or {}
settings = ctx.obj["settings"] settings: Settings = ctx.obj["settings"]
ctx.obj["migrator"] = Migrator(settings=settings.repository) ctx.obj["migrator"] = Migrator(settings=settings.repository)

View File

@@ -27,7 +27,7 @@ def upgrade() -> None:
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("birthday", sa.Date(), nullable=True), sa.Column("birthday", sa.Date(), nullable=True),
sa.Column("phone", sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("phone", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("telegram_id", sa.Integer(), nullable=True), sa.Column("telegram_id", sa.BigInteger(), nullable=True),
sa.Column("gift_payment_data", sa.JSON(), nullable=True), sa.Column("gift_payment_data", sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint("id"), sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"), sa.UniqueConstraint("id"),

View File

@@ -1,4 +1,7 @@
from pydantic_filters import OffsetPagination import uuid
from typing import AsyncGenerator
from pydantic_filters import BasePagination, OffsetPagination
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from birthday_pool_bot.dto import Pool, PoolFilter from birthday_pool_bot.dto import Pool, PoolFilter
@@ -14,20 +17,38 @@ class PoolsRepository(BaseRepository[Pool, PoolFilter]):
filter_ = PoolFilter(birthday_user_id={birthday_user_id}) filter_ = PoolFilter(birthday_user_id={birthday_user_id})
return await self.get_items_count(filter_=filter_) return await self.get_items_count(filter_=filter_)
async def get_pools_by_birthday_user_id(self, birthday_user_id: uuid.UUID) -> list[Pool]: async def get_pools_by_birthday_user_id(
self,
birthday_user_id: uuid.UUID,
pagination: BasePagination | None = None,
with_owner: bool = False,
) -> AsyncGenerator[Pool]:
filter_ = PoolFilter(birthday_user_id={birthday_user_id}) filter_ = PoolFilter(birthday_user_id={birthday_user_id})
return [pool async for pool in self.get_items(filter_=filter_)] options = []
if with_owner:
options.append(joinedload(DBPool.owner))
pools_generator = self.get_items(
filter_=filter_,
pagination=pagination,
options=options,
)
async for pool in pools_generator:
yield pool
async def get_pool_by_id( async def get_pool_by_id(
self, self,
pool_id: uuid.UUID, pool_id: uuid.UUID,
with_owner: bool = False, with_owner: bool = False,
with_birthday_user: bool = False,
) -> Pool | None: ) -> Pool | None:
filter_ = PoolFilter(id={pool_id}) filter_ = PoolFilter(id={pool_id})
pagination = OffsetPagination(limit=1) pagination = OffsetPagination(limit=1)
options = [] options = []
if with_owner: if with_owner:
options.append(joinedload(DBPool.owner)) options.append(joinedload(DBPool.owner))
if with_birthday_user:
options.append(joinedload(DBPool.birthday_user))
pools_generator = self.get_items( pools_generator = self.get_items(
filter_=filter_, filter_=filter_,

View File

@@ -1,4 +1,5 @@
import uuid import uuid
from typing import AsyncGenerator, Iterable
from pydantic_filters import BasePagination, OffsetPagination from pydantic_filters import BasePagination, OffsetPagination
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload

View File

@@ -1,8 +1,9 @@
import uuid
from datetime import date from datetime import date
from typing import Container, Sequence, TypeVar from typing import Container, Sequence, TypeVar
from pydantic_filters import OffsetPagination from pydantic_filters import BasePagination, OffsetPagination
from sqlalchemy import Column, func, inspect, select from sqlalchemy import Column, func, inspect, or_, select, update
from sqlmodel import SQLModel from sqlmodel import SQLModel
from birthday_pool_bot.dto import User, UserFilter from birthday_pool_bot.dto import User, UserFilter

View File

@@ -1,13 +1,12 @@
import uuid import uuid
import enum
from datetime import date from datetime import date
from typing import Self, List from typing import Any, Self
import sqlalchemy as sa import sqlalchemy as sa
from pydantic import BaseModel
from sqlmodel import SQLModel, Field, Relationship from sqlmodel import SQLModel, Field, Relationship
from birthday_pool_bot.dto import ( from birthday_pool_bot.dto import (
BankEnum,
PaymentData as DTOPaymentData, PaymentData as DTOPaymentData,
Pool as DTOPool, Pool as DTOPool,
Subscription as DTOSubscription, Subscription as DTOSubscription,
@@ -46,7 +45,10 @@ class User(BaseSQLModel, table=True):
name: str | None = Field(nullable=True) name: str | None = Field(nullable=True)
birthday: date | None = Field(nullable=True) birthday: date | None = Field(nullable=True)
phone: str | None = Field(default=None, nullable=True) phone: str | None = Field(default=None, nullable=True)
telegram_id: int | None = Field(default=None, nullable=True) telegram_id: int | None = Field(
default=None,
sa_column=sa.Column(sa.BigInteger(), nullable=True),
)
gift_payment_data: dict | None = Field( gift_payment_data: dict | None = Field(
sa_column=sa.Column(sa.JSON, nullable=True), sa_column=sa.Column(sa.JSON, nullable=True),
default_factory=dict, default_factory=dict,
@@ -118,6 +120,12 @@ class Pool(BaseSQLModel, table=True):
"lazy": None, "lazy": None,
}, },
) )
birthday_user: User = Relationship(
sa_relationship_kwargs={
"primaryjoin": "User.id == Pool.birthday_user_id",
"lazy": None,
},
)
@classmethod @classmethod
def from_item(cls, item: DTOPool) -> Self: def from_item(cls, item: DTOPool) -> Self:
@@ -128,6 +136,11 @@ class Pool(BaseSQLModel, table=True):
description=item.description, description=item.description,
payment_data=item.payment_data.model_dump_json(), payment_data=item.payment_data.model_dump_json(),
owner=None if item.owner is None else DTOUser.from_item(item.owner), owner=None if item.owner is None else DTOUser.from_item(item.owner),
birthday_user=(
None
if item.birthday_user is None else
DTOUser.from_item(item.birthday_user)
),
) )
def to_item(self) -> DTOPool: def to_item(self) -> DTOPool:
@@ -137,7 +150,8 @@ class Pool(BaseSQLModel, table=True):
birthday_user_id=self.birthday_user_id, birthday_user_id=self.birthday_user_id,
description=self.description, description=self.description,
payment_data=DTOPaymentData.model_validate_json(self.payment_data), payment_data=DTOPaymentData.model_validate_json(self.payment_data),
owner=None if self.owner is None else self.owner.to_item() owner=None if self.owner is None else self.owner.to_item(),
birthday_user=None if self.birthday_user is None else self.birthday_user.to_item(),
) )

View File

@@ -6,6 +6,8 @@ from .settings import TelegramBotSettings
__all__ = ( __all__ = (
# cli # cli
"get_cli", "get_cli",
# fabric
"get_telegram_bot_service",
# settings # settings
"TelegramBotSettings", "TelegramBotSettings",
) )

View File

@@ -3,6 +3,7 @@ import facet
from aiogram.fsm.storage.memory import MemoryStorage from aiogram.fsm.storage.memory import MemoryStorage
from birthday_pool_bot.repositories import RepositoriesContainer from birthday_pool_bot.repositories import RepositoriesContainer
from .settings import TelegramBotSettings
from .ui import setup_dispatcher from .ui import setup_dispatcher

View File

@@ -12,15 +12,15 @@ def get_telegram_bot_service(
repositories_container: RepositoriesContainer, repositories_container: RepositoriesContainer,
) -> BaseTelegramBotService: ) -> BaseTelegramBotService:
match settings.method: match settings.method:
case TelegramBotMethodEnum.POLLING: case TelegramBotMethodEnum.POLLING.value:
return TelegramBotPollingService( return TelegramBotPollingService(
settings=settings, settings=settings,
repositories_container=repositories_container, repositories_container=repositories_container,
) )
case TelegramBotMethodEnum.WEBHOOK: case TelegramBotMethodEnum.WEBHOOK.value:
return TelegramBotWebhookService( return TelegramBotWebhookService(
settings=settings, settings=settings,
repositories_container=repositories_container, repositories_container=repositories_container,
) )
case _: case _:
raise ValueError(f"Cannot create telegram bot with method '{settings.method.value}'") raise ValueError(f"Cannot create telegram bot with method '{settings.method}'")

View File

@@ -3,6 +3,7 @@ from birthday_pool_bot.telegram_bot.base import BaseTelegramBotService
class TelegramBotPollingService(BaseTelegramBotService): class TelegramBotPollingService(BaseTelegramBotService):
async def listen_events(self): async def listen_events(self):
await self._bot.delete_webhook()
await self._dispatcher._polling( # pylint: disable=protected-access await self._dispatcher._polling( # pylint: disable=protected-access
bot=self._bot, bot=self._bot,
polling_timeout=self._settings.timeout, polling_timeout=self._settings.timeout,

View File

@@ -8,7 +8,7 @@ from birthday_pool_bot.telegram_bot.settings import TelegramBotSettings
class TelegramBotPollingSettings(TelegramBotSettings): class TelegramBotPollingSettings(TelegramBotSettings):
method: Literal[TelegramBotMethodEnum.POLLING.value] = ( method: Literal[TelegramBotMethodEnum.POLLING.value] = (
TelegramBotMethodEnum.POLLING TelegramBotMethodEnum.POLLING.value
) )
timeout: PositiveInt = 10 # in seconds timeout: PositiveInt = 10 # in seconds

View File

@@ -1,9 +1,7 @@
import enum import enum
import uuid import uuid
from typing import Any
from aiogram.filters.callback_data import CallbackData from aiogram.filters.callback_data import CallbackData
from pydantic import Field
class PaginatorCallbackData(CallbackData, prefix=""): class PaginatorCallbackData(CallbackData, prefix=""):
@@ -18,7 +16,7 @@ class SubscriptionsCallbackData(PaginatorCallbackData, prefix="subscriptions"):
pass pass
class AddSubscriptionCallbackData(CallbackData, prefix="subscription_add"): class NewSubscriptionCallbackData(CallbackData, prefix="new_subscription"):
pass pass
@@ -27,7 +25,7 @@ class ConfirmAnswerEnum(str, enum.Enum):
NO = "no" NO = "no"
class AddSubscriptionConfirmCallbackData(CallbackData, prefix="subscription_add_confirm"): class NewSubscriptionConfirmCallbackData(CallbackData, prefix="new_subscription_confirm"):
answer: ConfirmAnswerEnum answer: ConfirmAnswerEnum
@@ -43,5 +41,19 @@ class SubscriptionCallbackData(CallbackData, prefix="subscription"):
action: SubscriptionActionEnum = SubscriptionActionEnum.SHOW action: SubscriptionActionEnum = SubscriptionActionEnum.SHOW
class AddSubscriptionPoolsCallbackData(PaginatorCallbackData, prefix="subscription_add_pool"): class PoolsCallbackData(PaginatorCallbackData, prefix="pools"):
to_user_id: uuid.UUID pass
class PoolsBackCallbackData(CallbackData, prefix="pools_back"):
pass
class PoolActionEnum(str, enum.Enum):
SHOW = "show"
CHOOSE = "choose"
class PoolCallbackData(CallbackData, prefix="pool"):
id: uuid.UUID
action: PoolActionEnum = PoolActionEnum.SHOW

View File

@@ -1,14 +1,17 @@
import aiogram import aiogram
from aiogram.filters import Command from aiogram.filters import Command
from aiogram_dialog import DialogManager
from birthday_pool_bot.repositories import RepositoriesContainer from birthday_pool_bot.repositories import RepositoriesContainer
from . import constants, handlers from . import constants, handlers
from .callback_data import ( from .callback_data import (
AddSubscriptionCallbackData,
AddSubscriptionConfirmCallbackData,
ConfirmAnswerEnum, ConfirmAnswerEnum,
MenuCallbackData, MenuCallbackData,
NewSubscriptionCallbackData,
NewSubscriptionConfirmCallbackData,
PoolActionEnum,
PoolCallbackData,
PoolsBackCallbackData,
PoolsCallbackData,
SubscriptionActionEnum, SubscriptionActionEnum,
SubscriptionCallbackData, SubscriptionCallbackData,
SubscriptionsCallbackData, SubscriptionsCallbackData,
@@ -16,12 +19,12 @@ from .callback_data import (
from .middlewares import ( from .middlewares import (
AuthMiddleware, AuthMiddleware,
DependsMiddleware, DependsMiddleware,
TypingMiddleware, # TypingMiddleware,
) )
from .states import ( from .states import (
AddSubscriptionPoolState,
AddSubscriptionState,
MenuState, MenuState,
NewSubscriptionPoolState,
NewSubscriptionState,
SetProfileBirthdayState, SetProfileBirthdayState,
SetProfileGiftPaymentDataState, SetProfileGiftPaymentDataState,
SetProfileNameState, SetProfileNameState,
@@ -33,7 +36,7 @@ def setup_dispatcher(
dispatcher: aiogram.Dispatcher, dispatcher: aiogram.Dispatcher,
repositories_container: RepositoriesContainer, repositories_container: RepositoriesContainer,
): ):
typing_middleware = TypingMiddleware() # typing_middleware = TypingMiddleware()
auth_middleware = AuthMiddleware(users_repository=repositories_container.users) auth_middleware = AuthMiddleware(users_repository=repositories_container.users)
users_repository_middleware = DependsMiddleware( users_repository_middleware = DependsMiddleware(
name="users_repository", name="users_repository",
@@ -175,9 +178,9 @@ def setup_router(router: aiogram.Router) -> aiogram.Router:
SubscriptionsCallbackData.filter(), SubscriptionsCallbackData.filter(),
) )
router.callback_query.register( router.callback_query.register(
handlers.add_subscription_callback_handler, handlers.new_subscription_callback_handler,
MenuState.SUBSCRIPTIONS, MenuState.SUBSCRIPTIONS,
AddSubscriptionCallbackData.filter(), NewSubscriptionCallbackData.filter(),
) )
router.callback_query.register( router.callback_query.register(
handlers.menu_callback_handler, handlers.menu_callback_handler,
@@ -203,107 +206,132 @@ def setup_router(router: aiogram.Router) -> aiogram.Router:
### Add new subscription ### Add new subscription
router.message.register( router.message.register(
handlers.add_subscription_message_handler, handlers.new_subscription_message_handler,
AddSubscriptionState.WAITING_FOR_PHONE, NewSubscriptionState.WAITING_FOR_PHONE,
aiogram.F.text == constants.BACK_BUTTON, aiogram.F.text == constants.BACK_BUTTON,
) )
router.message.register( router.message.register(
handlers.set_add_subscription_user_message_handler, handlers.set_new_subscription_user_message_handler,
AddSubscriptionState.WAITING_FOR_PHONE, NewSubscriptionState.WAITING_FOR_PHONE,
) )
router.message.register( router.message.register(
handlers.ask_add_subscription_user_message_handler, handlers.ask_new_subscription_user_message_handler,
AddSubscriptionState.WAITING_FOR_DATE, NewSubscriptionState.WAITING_FOR_DATE,
aiogram.F.text == constants.BACK_BUTTON, aiogram.F.text == constants.BACK_BUTTON,
) )
router.message.register( router.message.register(
handlers.set_add_subscription_user_birthday_message_handler, handlers.set_new_subscription_user_birthday_message_handler,
AddSubscriptionState.WAITING_FOR_DATE, NewSubscriptionState.WAITING_FOR_DATE,
) )
router.message.register( router.message.register(
handlers.ask_add_subscription_user_message_handler, handlers.ask_new_subscription_user_message_handler,
AddSubscriptionState.WAITING_FOR_NAME, NewSubscriptionState.WAITING_FOR_NAME,
aiogram.F.text == constants.BACK_BUTTON, aiogram.F.text == constants.BACK_BUTTON,
) )
router.message.register( router.message.register(
handlers.set_add_subscription_name_message_handler, handlers.set_new_subscription_name_message_handler,
AddSubscriptionState.WAITING_FOR_NAME, NewSubscriptionState.WAITING_FOR_NAME,
) )
router.message.register( router.message.register(
handlers.ask_add_subscription_pool_description_message_handler, handlers.ask_new_subscription_pool_description_message_handler,
AddSubscriptionState.WAITING_FOR_POOL_DECISION, NewSubscriptionState.WAITING_FOR_POOL_DECISION,
aiogram.F.text == constants.CREATE_POOL_BUTTON, aiogram.F.text == constants.CREATE_POOL_BUTTON,
) )
router.message.register( router.message.register(
handlers.show_add_subscription_pools_message_handler, handlers.show_new_subscription_choosing_pools_message_handler,
AddSubscriptionState.WAITING_FOR_POOL_DECISION, NewSubscriptionState.WAITING_FOR_POOL_DECISION,
aiogram.F.text == constants.JOIN_EXISTING_POOL_BUTTON, aiogram.F.text == constants.JOIN_EXISTING_POOL_BUTTON,
) )
router.message.register( router.message.register(
handlers.ask_add_subscription_confirmation_message_handler, handlers.ask_new_subscription_confirmation_message_handler,
AddSubscriptionState.WAITING_FOR_POOL_DECISION, NewSubscriptionState.WAITING_FOR_POOL_DECISION,
aiogram.F.text == constants.DECLINE_POOL_BUTTON, aiogram.F.text == constants.DECLINE_POOL_BUTTON,
) )
router.message.register( router.message.register(
handlers.ask_add_subscription_name_message_handler, handlers.ask_new_subscription_name_message_handler,
AddSubscriptionState.WAITING_FOR_POOL_DECISION, NewSubscriptionState.WAITING_FOR_POOL_DECISION,
aiogram.F.text == constants.BACK_BUTTON, aiogram.F.text == constants.BACK_BUTTON,
) )
router.callback_query.register( router.callback_query.register(
handlers.confirm_add_subscription_callback_handler, handlers.confirm_new_subscription_callback_handler,
AddSubscriptionState.WAITING_FOR_CONFIRMATION, NewSubscriptionState.WAITING_FOR_CONFIRMATION,
AddSubscriptionConfirmCallbackData.filter( NewSubscriptionConfirmCallbackData.filter(
aiogram.F.answer == ConfirmAnswerEnum.YES, aiogram.F.answer == ConfirmAnswerEnum.YES,
), ),
) )
router.callback_query.register( router.callback_query.register(
handlers.subscriptions_callback_handler, handlers.subscriptions_callback_handler,
AddSubscriptionState.WAITING_FOR_CONFIRMATION, NewSubscriptionState.WAITING_FOR_CONFIRMATION,
AddSubscriptionConfirmCallbackData.filter( NewSubscriptionConfirmCallbackData.filter(
aiogram.F.answer == ConfirmAnswerEnum.NO, aiogram.F.answer == ConfirmAnswerEnum.NO,
), ),
) )
#### Add new subscription pool #### Add new subscription pool
router.message.register( router.message.register(
handlers.ask_add_subscription_pool_payment_phone_message_handler, handlers.ask_new_subscription_pool_payment_phone_message_handler,
AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION, NewSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
aiogram.F.text == constants.SKIP_BUTTON, aiogram.F.text == constants.SKIP_BUTTON,
) )
router.message.register( router.message.register(
handlers.ask_add_subscription_pool_decision_message_handler, handlers.ask_new_subscription_pool_decision_message_handler,
AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION, NewSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
aiogram.F.text == constants.BACK_BUTTON, aiogram.F.text == constants.BACK_BUTTON,
) )
router.message.register( router.message.register(
handlers.set_add_subscription_pool_description_message_handler, handlers.set_new_subscription_pool_description_message_handler,
AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION, NewSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
) )
router.message.register( router.message.register(
handlers.set_add_subscription_pool_payment_data_from_profile_message_handler, handlers.set_new_subscription_pool_payment_data_from_profile_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE, NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE,
aiogram.F.text == constants.USE_PROFILE_GIFT_PAYMENT_DATA, aiogram.F.text == constants.USE_PROFILE_GIFT_PAYMENT_DATA,
) )
router.message.register( router.message.register(
handlers.ask_add_subscription_pool_description_message_handler, handlers.ask_new_subscription_pool_description_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE, NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE,
aiogram.F.text == constants.BACK_BUTTON, aiogram.F.text == constants.BACK_BUTTON,
) )
router.message.register( router.message.register(
handlers.set_add_subscription_pool_payment_phone_message_handler, handlers.set_new_subscription_pool_payment_phone_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE, NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE,
) )
router.message.register( router.message.register(
handlers.set_add_subscription_pool_payment_phone_message_handler, handlers.set_new_subscription_pool_payment_phone_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK, NewSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK,
aiogram.F.text == constants.BACK_BUTTON, aiogram.F.text == constants.BACK_BUTTON,
) )
router.message.register( router.message.register(
handlers.set_add_subscription_pool_payment_bank_message_handler, handlers.set_new_subscription_pool_payment_bank_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK, NewSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK,
) )
#### Choose pool for subscription #### Choose pool for subscription
router.callback_query.register(
handlers.show_new_subscription_choosing_pools_callback_handler,
NewSubscriptionState.WAITING_FOR_CHOOSE_POOL,
PoolsCallbackData.filter(),
)
router.callback_query.register(
handlers.ask_new_subscription_pool_decision_callback_handler,
NewSubscriptionState.WAITING_FOR_CHOOSE_POOL,
PoolsBackCallbackData.filter(),
)
router.callback_query.register(
handlers.show_new_subscription_choosing_pool_callback_handler,
NewSubscriptionState.WAITING_FOR_CHOOSE_POOL,
PoolCallbackData.filter(aiogram.F.action == PoolActionEnum.SHOW),
)
router.callback_query.register(
handlers.choose_new_subscription_pool_callback_handler,
NewSubscriptionState.WAITING_FOR_CHOOSE_POOL,
PoolCallbackData.filter(aiogram.F.action == PoolActionEnum.CHOOSE),
)
router.callback_query.register(
handlers.show_new_subscription_choosing_pools_callback_handler,
NewSubscriptionState.WAITING_FOR_CHOOSE_POOL,
PoolsCallbackData.filter(),
)
# Fallback # Fallback
router.message.register(handlers.fallback_message_handler) router.message.register(handlers.fallback_message_handler)

View File

@@ -1,21 +1,13 @@
import asyncio from aiogram import types
import uuid
from aiogram import types, F, Router
from aiogram.enums import MessageOriginType, ParseMode from aiogram.enums import MessageOriginType, ParseMode
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
from pydantic_core import PydanticCustomError from pydantic_core import PydanticCustomError
from pydantic_filters import PagePagination
from pydantic_extra_types.phone_numbers import PhoneNumber from pydantic_extra_types.phone_numbers import PhoneNumber
from birthday_pool_bot.dto import ( from birthday_pool_bot.dto import (
BankEnum,
PaymentData, PaymentData,
Pool, Pool,
Subscription, Subscription,
SubscriptionFilter,
User, User,
) )
from birthday_pool_bot.repositories.repositories import ( from birthday_pool_bot.repositories.repositories import (
@@ -24,18 +16,11 @@ from birthday_pool_bot.repositories.repositories import (
UsersRepository, UsersRepository,
) )
from . import constants, logic from . import constants, logic
from .callback_data import ( from .callback_data import PoolCallbackData, SubscriptionCallbackData
AddSubscriptionCallbackData,
AddSubscriptionConfirmCallbackData,
ConfirmAnswerEnum,
MenuCallbackData,
SubscriptionCallbackData,
SubscriptionsCallbackData,
)
from .exceptions import FlowInternalError from .exceptions import FlowInternalError
from .states import ( from .states import (
AddSubscriptionPoolState, NewSubscriptionPoolState,
AddSubscriptionState, NewSubscriptionState,
MenuState, MenuState,
SetProfileBirthdayState, SetProfileBirthdayState,
SetProfileGiftPaymentDataState, SetProfileGiftPaymentDataState,
@@ -322,31 +307,31 @@ async def delete_subscription_callback(
await state.set_state(MenuState.SUBSCRIPTIONS) await state.set_state(MenuState.SUBSCRIPTIONS)
async def add_subscription_message_handler( async def new_subscription_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
): ):
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
async def add_subscription_callback_handler( async def new_subscription_callback_handler(
callback_query: types.CallbackQuery, callback_query: types.CallbackQuery,
state: FSMContext, state: FSMContext,
): ):
await logic.ask_add_subscription_user(callback_query=callback_query) await logic.ask_new_subscription_user(callback_query=callback_query)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
async def ask_add_subscription_user_message_handler( async def ask_new_subscription_user_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
): ):
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
async def set_add_subscription_user_message_handler( async def set_new_subscription_user_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
user: User, user: User,
@@ -370,15 +355,15 @@ async def set_add_subscription_user_message_handler(
await message.reply( await message.reply(
text="Пользователь скрыл свои данные, попробуйте отправить номер телефона или его контакт", text="Пользователь скрыл свои данные, попробуйте отправить номер телефона или его контакт",
) )
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
if message.forward_origin.type != MessageOriginType.USER: if message.forward_origin.type != MessageOriginType.USER:
await message.reply( await message.reply(
text="Это сообщение не от пользователя, попробуйте ещё раз", text="Это сообщение не от пользователя, попробуйте ещё раз",
) )
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
user_name = get_telegram_user_full_name(message.forward_origin.sender_user) user_name = get_telegram_user_full_name(message.forward_origin.sender_user)
user_telegram_id = message.forward_origin.sender_user.id user_telegram_id = message.forward_origin.sender_user.id
@@ -390,8 +375,8 @@ async def set_add_subscription_user_message_handler(
try: try:
PhoneNumber._validate(user_phone, None) PhoneNumber._validate(user_phone, None)
except PydanticCustomError: except PydanticCustomError:
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
subscription_user = await users_repository.get_user_by_phone(phone=user_phone) subscription_user = await users_repository.get_user_by_phone(phone=user_phone)
@@ -406,8 +391,8 @@ async def set_add_subscription_user_message_handler(
if subscription_user.id == user.id: if subscription_user.id == user.id:
await message.reply("Нельзя подписаться на самого себя") await message.reply("Нельзя подписаться на самого себя")
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
async with subscriptions_repository.transaction(): async with subscriptions_repository.transaction():
@@ -418,8 +403,8 @@ async def set_add_subscription_user_message_handler(
if subscription is not None: if subscription is not None:
await message.reply(text="У вас уже есть подписка на этого человека") await message.reply(text="У вас уже есть подписка на этого человека")
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
await state.update_data( await state.update_data(
@@ -427,65 +412,65 @@ async def set_add_subscription_user_message_handler(
) )
if subscription_user.birthday is None: if subscription_user.birthday is None:
await logic.ask_add_subscription_user_birthday(message=message) await logic.ask_new_subscription_user_birthday(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_DATE) await state.set_state(NewSubscriptionState.WAITING_FOR_DATE)
return return
await logic.ask_add_subscription_name( await logic.ask_new_subscription_name(
message=message, message=message,
users_repository=users_repository, users_repository=users_repository,
subscription_user_id=subscription_user.id, subscription_user_id=subscription_user.id,
) )
await state.set_state(AddSubscriptionState.WAITING_FOR_NAME) await state.set_state(NewSubscriptionState.WAITING_FOR_NAME)
async def ask_add_subscription_user_birthday_message_handler( async def ask_new_subscription_user_birthday_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
): ):
await logic.ask_add_subscription_user_birthday(message=message) await logic.ask_new_subscription_user_birthday(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_DATE) await state.set_state(NewSubscriptionState.WAITING_FOR_DATE)
async def set_add_subscription_user_birthday_message_handler( async def set_new_subscription_user_birthday_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
users_repository: UsersRepository, users_repository: UsersRepository,
): ):
birthday = parse_date(message=message) birthday = parse_date(message=message)
if birthday is None: if birthday is None:
await logic.ask_add_subscription_user_birthday(message=message) await logic.ask_new_subscription_user_birthday(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_DATE) await state.set_state(NewSubscriptionState.WAITING_FOR_DATE)
return return
state_data = await state.get_data() state_data = await state.get_data()
subscription_user_id = state_data.get("subscription_user_id") subscription_user_id = state_data.get("subscription_user_id")
if subscription_user_id is None: if subscription_user_id is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
async with users_repository.transaction(): async with users_repository.transaction():
subscription_user = await users_repository.get_user_by_id(user_id=subscription_user_id) subscription_user = await users_repository.get_user_by_id(user_id=subscription_user_id)
if subscription_user is None: if subscription_user is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
subscription_user.birthday = birthday subscription_user.birthday = birthday
await users_repository.update_user(user=subscription_user) await users_repository.update_user(user=subscription_user)
await logic.ask_add_subscription_name( await logic.ask_new_subscription_name(
message=message, message=message,
users_repository=users_repository, users_repository=users_repository,
subscription_user_id=subscription_user.id, subscription_user_id=subscription_user.id,
) )
await state.set_state(AddSubscriptionState.WAITING_FOR_NAME) await state.set_state(NewSubscriptionState.WAITING_FOR_NAME)
async def ask_add_subscription_name_message_handler( async def ask_new_subscription_name_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
users_repository: UsersRepository, users_repository: UsersRepository,
@@ -494,26 +479,26 @@ async def ask_add_subscription_name_message_handler(
subscription_user_id = state_data.get("subscription_user_id") subscription_user_id = state_data.get("subscription_user_id")
if subscription_user_id is None: if subscription_user_id is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
try: try:
await logic.ask_add_subscription_name( await logic.ask_new_subscription_name(
message=message, message=message,
users_repository=users_repository, users_repository=users_repository,
subscription_user_id=subscription_user_id, subscription_user_id=subscription_user_id,
) )
except FlowInternalError: except FlowInternalError:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
await state.set_state(AddSubscriptionState.WAITING_FOR_NAME) await state.set_state(NewSubscriptionState.WAITING_FOR_NAME)
async def set_add_subscription_name_message_handler( async def set_new_subscription_name_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
user: User, user: User,
@@ -525,29 +510,29 @@ async def set_add_subscription_name_message_handler(
subscription_user_id = state_data.get("subscription_user_id") subscription_user_id = state_data.get("subscription_user_id")
if subscription_user_id is None: if subscription_user_id is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
async with users_repository.transaction(): async with users_repository.transaction():
subscription_user = await users_repository.get_user_by_id(user_id=subscription_user_id) subscription_user = await users_repository.get_user_by_id(user_id=subscription_user_id)
if subscription_user is None or subscription_user.birthday is None: if subscription_user is None or subscription_user.birthday is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
await state.update_data(subscription_name=message.text) await state.update_data(subscription_name=message.text)
await logic.ask_add_subscription_pool_decision( await logic.ask_new_subscription_pool_decision(
message=message, message=message,
pools_repository=pools_repository, pools_repository=pools_repository,
users_repository=users_repository, users_repository=users_repository,
subscription_user_id=subscription_user_id, subscription_user_id=subscription_user_id,
) )
await state.set_state(AddSubscriptionState.WAITING_FOR_POOL_DECISION) await state.set_state(NewSubscriptionState.WAITING_FOR_POOL_DECISION)
async def ask_add_subscription_pool_decision_message_handler( async def ask_new_subscription_pool_decision_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
pools_repository: PoolsRepository, pools_repository: PoolsRepository,
@@ -557,12 +542,12 @@ async def ask_add_subscription_pool_decision_message_handler(
subscription_user_id = state_data.get("subscription_user_id") subscription_user_id = state_data.get("subscription_user_id")
if subscription_user_id is None: if subscription_user_id is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
try: try:
await logic.ask_add_subscription_pool_decision( await logic.ask_new_subscription_pool_decision(
message=message, message=message,
pools_repository=pools_repository, pools_repository=pools_repository,
users_repository=users_repository, users_repository=users_repository,
@@ -570,72 +555,235 @@ async def ask_add_subscription_pool_decision_message_handler(
) )
except FlowInternalError: except FlowInternalError:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
await state.set_state(AddSubscriptionState.WAITING_FOR_POOL_DECISION) await state.set_state(NewSubscriptionState.WAITING_FOR_POOL_DECISION)
async def show_add_subscription_pools_message_handler( async def ask_new_subscription_pool_decision_callback_handler(
callback_query: types.Message,
state: FSMContext,
pools_repository: PoolsRepository,
users_repository: UsersRepository,
):
state_data = await state.get_data()
subscription_user_id = state_data.get("subscription_user_id")
if subscription_user_id is None:
await callback_query.answer(text=constants.ERROR_MESSAGE)
await logic.ask_new_subscription_user(message=callback_query.message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
try:
await logic.ask_new_subscription_pool_decision(
message=callback_query.message,
pools_repository=pools_repository,
users_repository=users_repository,
subscription_user_id=subscription_user_id,
)
except FlowInternalError:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_new_subscription_user(message=callback_query.message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
await state.set_state(NewSubscriptionState.WAITING_FOR_POOL_DECISION)
async def show_new_subscription_choosing_pools_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
user: User, user: User,
users_repository: UsersRepository,
pools_repository: PoolsRepository, pools_repository: PoolsRepository,
): ):
state_data = await state.get_data() state_data = await state.get_data()
subscription_user_id = state_data.get("subscription_user_id") subscription_user_id = state_data.get("subscription_user_id")
if subscription_user_id is None: if subscription_user_id is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
try: try:
await logic.show_add_subscription_pools( await logic.show_new_subscription_choosing_pools(
user=user, user=user,
users_repository=users_repository,
pools_repository=pools_repository, pools_repository=pools_repository,
subscription_user_id=subscription_user_id,
message=message, message=message,
) )
except FlowInternalError: except FlowInternalError:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
await logic.ask_add_subscription_pool_description(message=message) await state.set_state(NewSubscriptionState.WAITING_FOR_CHOOSE_POOL)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION)
async def ask_add_subscription_pool_description_message_handler( async def show_new_subscription_choosing_pools_callback_handler(
callback_query: types.CallbackQuery,
state: FSMContext,
user: User,
users_repository: UsersRepository,
pools_repository: PoolsRepository,
):
state_data = await state.get_data()
subscription_user_id = state_data.get("subscription_user_id")
if subscription_user_id is None:
await callback_query.answer(text=constants.ERROR_MESSAGE)
await logic.ask_new_subscription_user(message=callback_query.message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
try:
await logic.show_new_subscription_choosing_pools(
user=user,
users_repository=users_repository,
pools_repository=pools_repository,
subscription_user_id=subscription_user_id,
message=callback_query.message,
)
except FlowInternalError:
await callback_query.answer(text=constants.ERROR_MESSAGE)
await logic.ask_new_subscription_user(message=callback_query.message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
await state.set_state(NewSubscriptionState.WAITING_FOR_CHOOSE_POOL)
async def show_new_subscription_choosing_pool_callback_handler(
callback_query: types.CallbackQuery,
state: FSMContext,
pools_repository: PoolsRepository,
):
callback_data = PoolCallbackData.unpack(callback_query.data)
try:
await logic.show_new_subscription_choosing_pool(
pools_repository=pools_repository,
pool_id=callback_data.id,
callback_query=callback_query,
)
except FlowInternalError:
await callback_query.answer(text=constants.ERROR_MESSAGE)
await logic.show_new_subscription_choosing_pools(
user=user,
users_repository=users_repository,
pools_repository=pools_repository,
subscription_user_id=subscription_user_id,
)
await state.set_state(NewSubscriptionState.WAITING_FOR_CHOOSE_POOL)
return
await state.set_state(NewSubscriptionState.WAITING_FOR_CHOOSE_POOL)
async def choose_new_subscription_pool_callback_handler(
callback_query: types.CallbackQuery,
state: FSMContext,
users_repository: UsersRepository,
pools_repository: PoolsRepository,
):
state_data = await state.get_data()
subscription_user_id = state_data.get("subscription_user_id")
subscription_name = state_data.get("subscription_name")
if not all((subscription_user_id, subscription_name)):
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
async with users_repository.transaction():
subscription_user = await users_repository.get_user_by_id(
user_id=subscription_user_id,
)
if subscription_user is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
callback_data = PoolCallbackData.unpack(callback_query.data)
async with pools_repository.transaction():
subscription_pool = await pools_repository.get_pool_by_id(
pool_id=callback_data.id,
)
if subscription_pool is None:
await callback_query.answer(text=constants.ERROR_MESSAGE)
await logic.show_new_subscription_choosing_pools(
user=user,
users_repository=users_repository,
pools_repository=pools_repository,
subscription_user_id=subscription_user_id,
callback_query=callback_query,
)
await state.set_state(NewSubscriptionState.WAITING_FOR_CHOOSE_POOL)
return
await state.update_data(
subscription_pool_id=callback_data.id,
)
try:
await logic.ask_new_subscription_confirmation(
message=callback_query.message,
users_repository=users_repository,
pools_repository=pools_repository,
subscription_name=subscription_name,
subscription_user_id=subscription_user_id,
subscription_pool_id=callback_data.id,
)
except FlowInternalError:
await callback_query.answer(text=constants.ERROR_MESSAGE)
await logic.show_new_subscription_choosing_pools(
message=callback_query.message,
users_repository=users_repository,
pools_repository=pools_repository,
subscription_name=subscription_name,
subscription_user_id=subscription_user_id,
subscription_pool_id=callback_data.id,
)
await state.set_state(NewSubscriptionState.WAITING_FOR_CHOOSE_POOL)
return
await state.set_state(NewSubscriptionState.WAITING_FOR_CONFIRMATION)
async def ask_new_subscription_pool_description_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
): ):
await logic.ask_add_subscription_pool_description(message=message) await logic.ask_new_subscription_pool_description(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION) await state.set_state(NewSubscriptionPoolState.WAITING_FOR_DESCRIPTION)
async def set_add_subscription_pool_description_message_handler( async def set_new_subscription_pool_description_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
): ):
description = message.text description = message.text
await state.update_data(subscription_pool_description=description) await state.update_data(subscription_pool_description=description)
await logic.ask_add_subscription_pool_payment_phone(message=message) await logic.ask_new_subscription_pool_payment_phone(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE) await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
async def ask_add_subscription_pool_payment_phone_message_handler( async def ask_new_subscription_pool_payment_phone_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
user: User, user: User,
): ):
await logic.ask_add_subscription_pool_payment_phone(message=message, user=user) await logic.ask_new_subscription_pool_payment_phone(message=message, user=user)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE) await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
async def set_add_subscription_pool_payment_phone_message_handler( async def set_new_subscription_pool_payment_phone_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
): ):
@@ -646,8 +794,8 @@ async def set_add_subscription_pool_payment_phone_message_handler(
phone_number = message.text phone_number = message.text
else: else:
await message.reply(text="Некорректный номер телефона, попробуйте ещё раз") await message.reply(text="Некорректный номер телефона, попробуйте ещё раз")
await logic.ask_add_subscription_pool_payment_phone(message=message) await logic.ask_new_subscription_pool_payment_phone(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE) await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
return return
try: try:
@@ -656,16 +804,16 @@ async def set_add_subscription_pool_payment_phone_message_handler(
await message.reply( await message.reply(
text="Некорректный номер телефона, попробуйте ещё раз", text="Некорректный номер телефона, попробуйте ещё раз",
) )
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE) await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
return return
await state.update_data(subscription_pool_phone=phone_number) await state.update_data(subscription_pool_phone=phone_number)
await logic.ask_add_subscription_pool_payment_bank(message=message) await logic.ask_new_subscription_pool_payment_bank(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK) await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK)
async def set_add_subscription_pool_payment_data_from_profile_message_handler( async def set_new_subscription_pool_payment_data_from_profile_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
user: User, user: User,
@@ -674,8 +822,8 @@ async def set_add_subscription_pool_payment_data_from_profile_message_handler(
): ):
if user.gift_payment_data is None: if user.gift_payment_data is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_pool_payment_phone(message=message, user=user) await logic.ask_new_subscription_pool_payment_phone(message=message, user=user)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE) await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
return return
state_data = await state.get_data() state_data = await state.get_data()
@@ -684,15 +832,15 @@ async def set_add_subscription_pool_payment_data_from_profile_message_handler(
subscription_pool_description = state_data.get("subscription_pool_description") subscription_pool_description = state_data.get("subscription_pool_description")
if not all((subscription_user_id, subscription_name)): if not all((subscription_user_id, subscription_name)):
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
await state.update_data( await state.update_data(
subscription_pool_phone=user.gift_payment_data.phone, subscription_pool_phone=user.gift_payment_data.phone,
subscription_pool_bank=user.gift_payment_data.bank, subscription_pool_bank=user.gift_payment_data.bank,
) )
await logic.ask_add_subscription_confirmation( await logic.ask_new_subscription_confirmation(
message=message, message=message,
users_repository=users_repository, users_repository=users_repository,
pools_repository=pools_repository, pools_repository=pools_repository,
@@ -702,18 +850,18 @@ async def set_add_subscription_pool_payment_data_from_profile_message_handler(
subscription_pool_phone=user.gift_payment_data.phone, subscription_pool_phone=user.gift_payment_data.phone,
subscription_pool_bank=user.gift_payment_data.bank, subscription_pool_bank=user.gift_payment_data.bank,
) )
await state.set_state(AddSubscriptionState.WAITING_FOR_CONFIRMATION) await state.set_state(NewSubscriptionState.WAITING_FOR_CONFIRMATION)
async def ask_add_subscription_pool_payment_bank_message_handler( async def ask_new_subscription_pool_payment_bank_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
): ):
await logic.ask_add_subscription_pool_payment_bank(message=message) await logic.ask_new_subscription_pool_payment_bank(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK) await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK)
async def set_add_subscription_pool_payment_bank_message_handler( async def set_new_subscription_pool_payment_bank_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
users_repository: UsersRepository, users_repository: UsersRepository,
@@ -726,13 +874,13 @@ async def set_add_subscription_pool_payment_bank_message_handler(
subscription_pool_phone = state_data.get("subscription_pool_phone") subscription_pool_phone = state_data.get("subscription_pool_phone")
if not all((subscription_user_id, subscription_name)): if not all((subscription_user_id, subscription_name)):
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
if subscription_pool_phone is None: if subscription_pool_phone is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_pool_decision(message=message) await logic.ask_new_subscription_pool_decision(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION) await state.set_state(NewSubscriptionPoolState.WAITING_FOR_DESCRIPTION)
return return
async with users_repository.transaction(): async with users_repository.transaction():
@@ -741,8 +889,8 @@ async def set_add_subscription_pool_payment_bank_message_handler(
) )
if subscription_user is None: if subscription_user is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
bank_name = message.text bank_name = message.text
@@ -754,7 +902,7 @@ async def set_add_subscription_pool_payment_bank_message_handler(
bank = constants.BANKS_MAP[bank_name] bank = constants.BANKS_MAP[bank_name]
await state.update_data(subscription_pool_bank=bank) await state.update_data(subscription_pool_bank=bank)
await logic.ask_add_subscription_confirmation( await logic.ask_new_subscription_confirmation(
message=message, message=message,
users_repository=users_repository, users_repository=users_repository,
pools_repository=pools_repository, pools_repository=pools_repository,
@@ -764,10 +912,10 @@ async def set_add_subscription_pool_payment_bank_message_handler(
subscription_pool_phone=subscription_pool_phone, subscription_pool_phone=subscription_pool_phone,
subscription_pool_bank=bank, subscription_pool_bank=bank,
) )
await state.set_state(AddSubscriptionState.WAITING_FOR_CONFIRMATION) await state.set_state(NewSubscriptionState.WAITING_FOR_CONFIRMATION)
async def ask_add_subscription_confirmation_message_handler( async def ask_new_subscription_confirmation_message_handler(
message: types.Message, message: types.Message,
state: FSMContext, state: FSMContext,
users_repository: UsersRepository, users_repository: UsersRepository,
@@ -782,8 +930,8 @@ async def ask_add_subscription_confirmation_message_handler(
subscription_pool_bank = state_data.get("subscription_pool_bank") subscription_pool_bank = state_data.get("subscription_pool_bank")
if not all((subscription_user_id, subscription_name)): if not all((subscription_user_id, subscription_name)):
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
async with users_repository.transaction(): async with users_repository.transaction():
@@ -792,12 +940,12 @@ async def ask_add_subscription_confirmation_message_handler(
) )
if subscription_user is None: if subscription_user is None:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
try: try:
await logic.ask_add_subscription_confirmation( await logic.ask_new_subscription_confirmation(
message=message, message=message,
users_repository=users_repository, users_repository=users_repository,
pools_repository=pools_repository, pools_repository=pools_repository,
@@ -810,14 +958,14 @@ async def ask_add_subscription_confirmation_message_handler(
) )
except FlowInternalError: except FlowInternalError:
await message.reply(text=constants.ERROR_MESSAGE) await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
await state.set_state(AddSubscriptionState.WAITING_FOR_CONFIRMATION) await state.set_state(NewSubscriptionState.WAITING_FOR_CONFIRMATION)
async def confirm_add_subscription_callback_handler( async def confirm_new_subscription_callback_handler(
callback_query: types.CallbackQuery, callback_query: types.CallbackQuery,
state: FSMContext, state: FSMContext,
user: User, user: User,
@@ -833,9 +981,9 @@ async def confirm_add_subscription_callback_handler(
subscription_pool_phone = state_data.get("subscription_pool_phone") subscription_pool_phone = state_data.get("subscription_pool_phone")
subscription_pool_bank = state_data.get("subscription_pool_bank") subscription_pool_bank = state_data.get("subscription_pool_bank")
if not all((subscription_user_id, subscription_name)): if not all((subscription_user_id, subscription_name)):
await message.reply(text=constants.ERROR_MESSAGE) await callback_query.answer(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=callback_query.message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
async with users_repository.transaction(): async with users_repository.transaction():
@@ -843,9 +991,9 @@ async def confirm_add_subscription_callback_handler(
user_id=subscription_user_id, user_id=subscription_user_id,
) )
if subscription_user is None: if subscription_user is None:
await message.reply(text=constants.ERROR_MESSAGE) await callback_query.answer(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=callback_query.message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
subscription_pool = None subscription_pool = None
@@ -855,9 +1003,9 @@ async def confirm_add_subscription_callback_handler(
pool_id=subscription_pool_id, pool_id=subscription_pool_id,
) )
if subscription_pool is None: if subscription_pool is None:
await message.reply(text=constants.ERROR_MESSAGE) await callback_query.answer(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message) await logic.ask_new_subscription_user(message=callback_query.message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE) await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return return
if subscription_pool is None and all((subscription_pool_phone, subscription_pool_bank)): if subscription_pool is None and all((subscription_pool_phone, subscription_pool_bank)):

View File

@@ -1,17 +1,26 @@
import uuid
from aiogram import types from aiogram import types
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
from pydantic_filters import PagePagination from pydantic_filters import PagePagination
from birthday_pool_bot.dto import User from birthday_pool_bot.dto import BankEnum, User
from birthday_pool_bot.repositories.repositories import UsersRepository from birthday_pool_bot.repositories.repositories import (
PoolsRepository,
SubscriptionsRepository,
UsersRepository,
)
from . import constants from . import constants
from .callback_data import ( from .callback_data import (
AddSubscriptionCallbackData,
AddSubscriptionConfirmCallbackData,
ConfirmAnswerEnum, ConfirmAnswerEnum,
MenuCallbackData, MenuCallbackData,
NewSubscriptionCallbackData,
NewSubscriptionConfirmCallbackData,
PoolActionEnum,
PoolCallbackData,
PoolsCallbackData,
PoolsBackCallbackData,
SubscriptionActionEnum, SubscriptionActionEnum,
SubscriptionCallbackData, SubscriptionCallbackData,
SubscriptionsCallbackData, SubscriptionsCallbackData,
@@ -200,12 +209,12 @@ async def show_subscriptions(
text = "Мои подписки:" if subscriptions else "Нет подписок" text = "Мои подписки:" if subscriptions else "Нет подписок"
keyboard = InlineKeyboardBuilder() keyboard = InlineKeyboardBuilder()
for subscription in subscriptions: for subscription in subscriptions:
keyboard.button( keyboard.row(types.InlineKeyboardButton(
text=subscription.name, text=subscription.name,
callback_data=SubscriptionCallbackData( callback_data=SubscriptionCallbackData(
to_user_id=subscription.to_user_id, to_user_id=subscription.to_user_id,
).pack(), ).pack(),
) ))
navigation_row = [] navigation_row = []
if page > 1: if page > 1:
navigation_row.append(types.InlineKeyboardButton( navigation_row.append(types.InlineKeyboardButton(
@@ -226,7 +235,7 @@ async def show_subscriptions(
keyboard.row( keyboard.row(
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="Добавить", text="Добавить",
callback_data=AddSubscriptionCallbackData().pack(), callback_data=NewSubscriptionCallbackData().pack(),
), ),
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="Назад", text="Назад",
@@ -273,7 +282,12 @@ async def show_subscription(
) )
if subscription.pool is not None: if subscription.pool is not None:
if subscription.pool.owner_id == from_user_id: if subscription.pool.owner_id == from_user_id:
text += "Вы собираете деньги\n\n" bank_title = constants.BANKS_TITLE_MAP[subscription.pool.payment_data.bank]
text += (
"Вы собираете деньги на:\n"
f"_Телефон_: {subscription.pool.payment_data.phone}\n"
f"анк_: {bank_title}\n\n"
)
else: else:
text += "Вы участвуете в сборе денег\n\n" text += "Вы участвуете в сборе денег\n\n"
keyboard = InlineKeyboardBuilder() keyboard = InlineKeyboardBuilder()
@@ -333,7 +347,7 @@ async def delete_subscription(
await callback_query.answer(text="Подписка успешно удалена") await callback_query.answer(text="Подписка успешно удалена")
async def ask_add_subscription_user( async def ask_new_subscription_user(
callback_query: types.CallbackQuery | None = None, callback_query: types.CallbackQuery | None = None,
message: types.Message | None = None, message: types.Message | None = None,
): ):
@@ -367,7 +381,7 @@ async def ask_add_subscription_user(
) )
async def ask_add_subscription_user_birthday( async def ask_new_subscription_user_birthday(
message: types.Message, message: types.Message,
): ):
text = ( text = (
@@ -388,7 +402,7 @@ async def ask_add_subscription_user_birthday(
) )
async def ask_add_subscription_name( async def ask_new_subscription_name(
message: types.Message, message: types.Message,
users_repository: UsersRepository, users_repository: UsersRepository,
subscription_user_id: uuid.UUID, subscription_user_id: uuid.UUID,
@@ -422,7 +436,7 @@ async def ask_add_subscription_name(
) )
async def ask_add_subscription_pool_decision( async def ask_new_subscription_pool_decision(
message: types.Message, message: types.Message,
pools_repository: PoolsRepository, pools_repository: PoolsRepository,
users_repository: UsersRepository, users_repository: UsersRepository,
@@ -458,9 +472,11 @@ async def ask_add_subscription_pool_decision(
) )
async def show_add_subscription_pools( async def show_new_subscription_choosing_pools(
user: User, user: User,
users_repository: UsersRepository,
pools_repository: PoolsRepository, pools_repository: PoolsRepository,
subscription_user_id: uuid.UUID,
message: types.Message | None = None, message: types.Message | None = None,
callback_query: types.CallbackQuery | None = None, callback_query: types.CallbackQuery | None = None,
): ):
@@ -470,31 +486,34 @@ async def show_add_subscription_pools(
callback_data = SubscriptionsCallbackData.unpack(callback_query.data) callback_data = SubscriptionsCallbackData.unpack(callback_query.data)
page = callback_data.page page = callback_data.page
async with subscriptions_repository.transaction(): async with pools_repository.transaction():
total = await subscriptions_repository.get_user_subscriptions_count(user_id=user.id) total = await pools_repository.get_pools_by_birthday_user_id_count(
birthday_user_id=subscription_user_id,
)
pages_count = total // per_page + int(bool(total % per_page)) pages_count = total // per_page + int(bool(total % per_page))
subscriptions = [ pools = [
subscription pool
async for subscription in subscriptions_repository.get_user_subscriptions( async for pool in pools_repository.get_pools_by_birthday_user_id(
user_id=user.id, birthday_user_id=subscription_user_id,
pagination=PagePagination(page=page, per_page=per_page), pagination=PagePagination(page=page, per_page=per_page),
with_owner=True,
) )
] ]
text = "Сборы:" if subscriptions else "Нет сборов" text = "Сборы:" if pools else "Нет сборов"
keyboard = InlineKeyboardBuilder() keyboard = InlineKeyboardBuilder()
for subscription in subscriptions: for pool in pools:
keyboard.button( keyboard.row(types.InlineKeyboardButton(
text=subscription.name, text=pool.owner.name or "Аноним",
callback_data=SubscriptionCallbackData( callback_data=PoolCallbackData(
to_user_id=subscription.to_user_id, id=pool.id,
).pack(), ).pack(),
) ))
navigation_row = [] navigation_row = []
if page > 1: if page > 1:
navigation_row.append(types.InlineKeyboardButton( navigation_row.append(types.InlineKeyboardButton(
text="<", text="<",
callback_data=SubscriptionsCallbackData(page=page - 1).pack(), callback_data=PoolsCallbackData(page=page - 1).pack(),
)) ))
if pages_count > 1: if pages_count > 1:
navigation_row.append(types.InlineKeyboardButton( navigation_row.append(types.InlineKeyboardButton(
@@ -504,17 +523,13 @@ async def show_add_subscription_pools(
if page < pages_count: if page < pages_count:
navigation_row.append(types.InlineKeyboardButton( navigation_row.append(types.InlineKeyboardButton(
text=">", text=">",
callback_data=SubscriptionsCallbackData(page=page + 1).pack(), callback_data=PoolsCallbackData(page=page + 1).pack(),
)) ))
keyboard.row(*navigation_row) keyboard.row(*navigation_row)
keyboard.row( keyboard.row(
types.InlineKeyboardButton(
text="Добавить",
callback_data=AddSubscriptionCallbackData().pack(),
),
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="Назад", text="Назад",
callback_data=MenuCallbackData().pack(), callback_data=PoolsBackCallbackData().pack(),
), ),
) )
reply_markup = keyboard.as_markup() reply_markup = keyboard.as_markup()
@@ -535,7 +550,70 @@ async def show_add_subscription_pools(
) )
async def ask_add_subscription_pool_description( async def show_new_subscription_choosing_pool(
pools_repository: PoolsRepository,
pool_id: uuid.UUID,
message: types.Message | None = None,
callback_query: types.CallbackQuery | None = None,
):
async with pools_repository.transaction():
pool = await pools_repository.get_pool_by_id(
pool_id=pool_id,
with_owner=True,
with_birthday_user=True,
)
if pool is None:
raise FlowInternalError()
owner_name = (
pool.owner.name
or pool.owner.phone
or pool.owner.telegram_id
)
birthday_user_name = (
pool.birthday_user.name
or pool.birthday_user.phone
or pool.birthday_user.telegram_id
)
bank_name = constants.BANKS_TITLE_MAP[pool.payment_data.bank]
text = (
f"*{owner_name}* собирает деньги для {birthday_user_name}\n\n"
"Данные для отправки подарка:\n"
f"_Телефон_: {pool.payment_data.phone}\n"
f"анк_: {bank_name}\n"
)
keyboard = InlineKeyboardBuilder()
keyboard.button(
text="Выбрать",
callback_data=PoolCallbackData(
id=pool_id,
action=PoolActionEnum.CHOOSE,
).pack(),
)
keyboard.button(
text="Назад",
callback_data=PoolsCallbackData().pack()
)
keyboard.adjust(2)
reply_markup = keyboard.as_markup()
if message is not None:
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
elif callback_query is not None:
await callback_query.message.edit_text(
text=text,
parse_mode=ParseMode.MARKDOWN,
)
await callback_query.message.edit_reply_markup(
reply_markup=reply_markup,
)
async def ask_new_subscription_pool_description(
message: types.Message, message: types.Message,
): ):
keyboard = ReplyKeyboardBuilder() keyboard = ReplyKeyboardBuilder()
@@ -554,7 +632,7 @@ async def ask_add_subscription_pool_description(
) )
async def ask_add_subscription_pool_payment_phone( async def ask_new_subscription_pool_payment_phone(
message: types.Message, message: types.Message,
user: User, user: User,
): ):
@@ -589,7 +667,7 @@ async def ask_add_subscription_pool_payment_phone(
) )
async def ask_add_subscription_pool_payment_bank( async def ask_new_subscription_pool_payment_bank(
message: types.Message, message: types.Message,
): ):
text = "Выберите банк для приёма платежей" text = "Выберите банк для приёма платежей"
@@ -611,7 +689,7 @@ async def ask_add_subscription_pool_payment_bank(
) )
async def ask_add_subscription_confirmation( async def ask_new_subscription_confirmation(
message: types.Message, message: types.Message,
users_repository: UsersRepository, users_repository: UsersRepository,
pools_repository: PoolsRepository, pools_repository: PoolsRepository,
@@ -663,13 +741,13 @@ async def ask_add_subscription_confirmation(
keyboard = InlineKeyboardBuilder() keyboard = InlineKeyboardBuilder()
keyboard.button( keyboard.button(
text="Да", text="Да",
callback_data=AddSubscriptionConfirmCallbackData( callback_data=NewSubscriptionConfirmCallbackData(
answer=ConfirmAnswerEnum.YES, answer=ConfirmAnswerEnum.YES,
).pack(), ).pack(),
) )
keyboard.button( keyboard.button(
text="Нет", text="Нет",
callback_data=AddSubscriptionConfirmCallbackData( callback_data=NewSubscriptionConfirmCallbackData(
answer=ConfirmAnswerEnum.NO, answer=ConfirmAnswerEnum.NO,
).pack(), ).pack(),
) )

View File

@@ -24,15 +24,16 @@ class SetProfileGiftPaymentDataState(StatesGroup):
WAITING_FOR_BANK = State() WAITING_FOR_BANK = State()
class AddSubscriptionState(StatesGroup): class NewSubscriptionState(StatesGroup):
WAITING_FOR_PHONE = State() WAITING_FOR_PHONE = State()
WAITING_FOR_DATE = State() WAITING_FOR_DATE = State()
WAITING_FOR_NAME = State() WAITING_FOR_NAME = State()
WAITING_FOR_POOL_DECISION = State() WAITING_FOR_POOL_DECISION = State()
WAITING_FOR_CHOOSE_POOL = State()
WAITING_FOR_CONFIRMATION = State() WAITING_FOR_CONFIRMATION = State()
class AddSubscriptionPoolState(StatesGroup): class NewSubscriptionPoolState(StatesGroup):
WAITING_FOR_DESCRIPTION = State() WAITING_FOR_DESCRIPTION = State()
WAITING_FOR_PAYMENT_PHONE = State() WAITING_FOR_PAYMENT_PHONE = State()
WAITING_FOR_PAYMENT_BANK = State() WAITING_FOR_PAYMENT_BANK = State()

View File

@@ -5,7 +5,7 @@ from aiogram import types
from aiogram.types import User as TelegramUser from aiogram.types import User as TelegramUser
BIRTHDATE_REGEXP = re.compile("^(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4})$") 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: def parse_date(message: types.Message) -> datetime.date:

View File

@@ -3,6 +3,7 @@ from typing import Any
import aiogram import aiogram
import fastapi import fastapi
import uvicorn import uvicorn
import yarl
from birthday_pool_bot.telegram_bot.base import BaseTelegramBotService from birthday_pool_bot.telegram_bot.base import BaseTelegramBotService
@@ -14,9 +15,10 @@ class UvicornServer(uvicorn.Server):
class TelegramBotWebhookService(BaseTelegramBotService): class TelegramBotWebhookService(BaseTelegramBotService):
async def listen_events(self): async def listen_events(self):
webhook_url = yarl.URL(str(self._settings.root_url)) / self._settings.root_path.lstrip("/")
await self._bot.set_webhook( await self._bot.set_webhook(
url=self._settings.url, url=str(webhook_url),
secret_token=self._settings.secret_token, secret_token=self._settings.secret_access_key,
) )
await self.get_server().serve() await self.get_server().serve()
@@ -26,10 +28,9 @@ class TelegramBotWebhookService(BaseTelegramBotService):
root_url=self._settings.root_url, root_url=self._settings.root_url,
root_path=self._settings.root_path, root_path=self._settings.root_path,
) )
app.post("")(self.handler)
app.post(self._webhook_path)(self.handler) config = uvicorn.Config(app=app, host="0.0.0.0", port=self._settings.port)
config = uvicorn.Config(app=app, host="0.0.0.0", port=self._port)
return UvicornServer(config) return UvicornServer(config)
async def handler( async def handler(
@@ -37,7 +38,7 @@ class TelegramBotWebhookService(BaseTelegramBotService):
update: dict[str, Any], update: dict[str, Any],
x_telegram_bot_api_secret_token: str | None = fastapi.Header(None), x_telegram_bot_api_secret_token: str | None = fastapi.Header(None),
): ):
if x_telegram_bot_api_secret_token != self._secret_token: if x_telegram_bot_api_secret_token != self._settings.secret_access_key:
raise fastapi.HTTPException( raise fastapi.HTTPException(
status_code=fastapi.status.HTTP_403_FORBIDDEN, status_code=fastapi.status.HTTP_403_FORBIDDEN,
detail="Forbidden.", detail="Forbidden.",

View File

@@ -7,7 +7,7 @@ from birthday_pool_bot.telegram_bot.settings import TelegramBotSettings
class TelegramBotWebhookSettings(TelegramBotSettings): class TelegramBotWebhookSettings(TelegramBotSettings):
method: Literal[TelegramBotMethodEnum.WEBHOOK.value] = TelegramBotMethodEnum.WEBHOOK method: Literal[TelegramBotMethodEnum.WEBHOOK.value] = TelegramBotMethodEnum.WEBHOOK.value
root_url: AnyHttpUrl root_url: AnyHttpUrl
root_path: str = "/" root_path: str = "/"

View File

@@ -1,8 +0,0 @@
version: "3.9"
services:
telegram-bot:
notifications:
database:
volumes:

View File

@@ -19,12 +19,19 @@ dependencies = [
"aiogram>=3.23.0", "aiogram>=3.23.0",
"uvicorn>=0.38.0", "uvicorn>=0.38.0",
"fastapi>=0.124.4", "fastapi>=0.124.4",
"yarl>=1.22.0",
] ]
[dependency-groups] [dependency-groups]
sqlite = [ sqlite = [
"aiosqlite>=0.21.0", "aiosqlite>=0.21.0",
] ]
postgresql = [
"asyncpg>=0.31.0",
]
dev = [
"ruff>=0.14.11",
]
[tool.uv] [tool.uv]
package = false package = false

84
uv.lock generated
View File

@@ -182,6 +182,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" }, { url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" },
] ]
[[package]]
name = "asyncpg"
version = "0.31.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" },
{ url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" },
{ url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" },
{ url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" },
{ url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" },
{ url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" },
{ url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" },
{ url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" },
{ url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" },
{ url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" },
{ url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" },
{ url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" },
{ url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" },
{ url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" },
{ url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" },
{ url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" },
{ url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" },
{ url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" },
{ url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" },
{ url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" },
{ url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" },
{ url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" },
{ url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" },
]
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "25.4.0" version = "25.4.0"
@@ -196,9 +228,11 @@ name = "birthday-pool-bot"
version = "0.0.0" version = "0.0.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiogram" },
{ name = "alembic" }, { name = "alembic" },
{ name = "apscheduler" }, { name = "apscheduler" },
{ name = "facet" }, { name = "facet" },
{ name = "fastapi" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-extra-types", extra = ["phonenumbers"] }, { name = "pydantic-extra-types", extra = ["phonenumbers"] },
{ name = "pydantic-filters" }, { name = "pydantic-filters" },
@@ -207,23 +241,28 @@ dependencies = [
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "sqlmodel" }, { name = "sqlmodel" },
{ name = "typer" }, { name = "typer" },
{ name = "uvicorn" },
{ name = "yarl" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
dev = [
{ name = "ruff" },
]
postgresql = [
{ name = "asyncpg" },
]
sqlite = [ sqlite = [
{ name = "aiosqlite" }, { name = "aiosqlite" },
] ]
telegram = [
{ name = "aiogram" },
{ name = "fastapi" },
{ name = "uvicorn" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "aiogram", specifier = ">=3.23.0" },
{ name = "alembic", specifier = ">=1.17.2" }, { name = "alembic", specifier = ">=1.17.2" },
{ name = "apscheduler", specifier = ">=3.11.2" }, { name = "apscheduler", specifier = ">=3.11.2" },
{ name = "facet", specifier = ">=0.10.1" }, { name = "facet", specifier = ">=0.10.1" },
{ name = "fastapi", specifier = ">=0.124.4" },
{ name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic", specifier = ">=2.12.5" },
{ name = "pydantic-extra-types", extras = ["phonenumbers"], specifier = ">=2.10.6" }, { name = "pydantic-extra-types", extras = ["phonenumbers"], specifier = ">=2.10.6" },
{ name = "pydantic-filters", git = "https://github.com/OlegYurchik/pydantic-filters?rev=2ca8b822d59feaf5f19f36b570974d314ba5e330" }, { name = "pydantic-filters", git = "https://github.com/OlegYurchik/pydantic-filters?rev=2ca8b822d59feaf5f19f36b570974d314ba5e330" },
@@ -232,15 +271,14 @@ requires-dist = [
{ name = "sqlalchemy", specifier = ">=2.0.44" }, { name = "sqlalchemy", specifier = ">=2.0.44" },
{ name = "sqlmodel", specifier = ">=0.0.27" }, { name = "sqlmodel", specifier = ">=0.0.27" },
{ name = "typer", specifier = ">=0.20.0" }, { name = "typer", specifier = ">=0.20.0" },
{ name = "uvicorn", specifier = ">=0.38.0" },
{ name = "yarl", specifier = ">=1.22.0" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.14.11" }]
postgresql = [{ name = "asyncpg", specifier = ">=0.31.0" }]
sqlite = [{ name = "aiosqlite", specifier = ">=0.21.0" }] sqlite = [{ name = "aiosqlite", specifier = ">=0.21.0" }]
telegram = [
{ name = "aiogram", specifier = ">=3.23.0" },
{ name = "fastapi", specifier = ">=0.124.4" },
{ name = "uvicorn", specifier = ">=0.38.0" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
@@ -810,6 +848,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
] ]
[[package]]
name = "ruff"
version = "0.14.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" },
{ url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" },
{ url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" },
{ url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" },
{ url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" },
{ url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" },
{ url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" },
{ url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" },
{ url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" },
{ url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" },
{ url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" },
{ url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" },
{ url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" },
{ url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" },
{ url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" },
{ url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" },
{ url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" },
{ url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
]
[[package]] [[package]]
name = "shellingham" name = "shellingham"
version = "1.5.4" version = "1.5.4"