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
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"]
FROM core AS app
RUN uv sync --group sqlite
RUN uv sync --group sqlite --group postgresql
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
## 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
owner: User | None = None
birthday_user: User | None = None
class PoolFilter(BaseFilter):

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import contextvars
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 sqlalchemy import func
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.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")

View File

@@ -12,7 +12,7 @@ from .migrator import Migrator
def callback(ctx: typer.Context):
ctx.obj = ctx.obj or {}
settings = ctx.obj["settings"]
settings: Settings = ctx.obj["settings"]
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("birthday", sa.Date(), 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.PrimaryKeyConstraint("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 birthday_pool_bot.dto import Pool, PoolFilter
@@ -14,20 +17,38 @@ class PoolsRepository(BaseRepository[Pool, PoolFilter]):
filter_ = PoolFilter(birthday_user_id={birthday_user_id})
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})
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(
self,
pool_id: uuid.UUID,
with_owner: bool = False,
with_birthday_user: bool = False,
) -> Pool | None:
filter_ = PoolFilter(id={pool_id})
pagination = OffsetPagination(limit=1)
options = []
if with_owner:
options.append(joinedload(DBPool.owner))
if with_birthday_user:
options.append(joinedload(DBPool.birthday_user))
pools_generator = self.get_items(
filter_=filter_,

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
import uuid
import enum
from datetime import date
from typing import Self, List
from typing import Any, Self
import sqlalchemy as sa
from pydantic import BaseModel
from sqlmodel import SQLModel, Field, Relationship
from birthday_pool_bot.dto import (
BankEnum,
PaymentData as DTOPaymentData,
Pool as DTOPool,
Subscription as DTOSubscription,
@@ -46,7 +45,10 @@ class User(BaseSQLModel, table=True):
name: str | None = Field(nullable=True)
birthday: date | None = Field(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(
sa_column=sa.Column(sa.JSON, nullable=True),
default_factory=dict,
@@ -118,6 +120,12 @@ class Pool(BaseSQLModel, table=True):
"lazy": None,
},
)
birthday_user: User = Relationship(
sa_relationship_kwargs={
"primaryjoin": "User.id == Pool.birthday_user_id",
"lazy": None,
},
)
@classmethod
def from_item(cls, item: DTOPool) -> Self:
@@ -128,6 +136,11 @@ class Pool(BaseSQLModel, table=True):
description=item.description,
payment_data=item.payment_data.model_dump_json(),
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:
@@ -137,7 +150,8 @@ class Pool(BaseSQLModel, table=True):
birthday_user_id=self.birthday_user_id,
description=self.description,
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__ = (
# cli
"get_cli",
# fabric
"get_telegram_bot_service",
# settings
"TelegramBotSettings",
)

View File

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

View File

@@ -12,15 +12,15 @@ def get_telegram_bot_service(
repositories_container: RepositoriesContainer,
) -> BaseTelegramBotService:
match settings.method:
case TelegramBotMethodEnum.POLLING:
case TelegramBotMethodEnum.POLLING.value:
return TelegramBotPollingService(
settings=settings,
repositories_container=repositories_container,
)
case TelegramBotMethodEnum.WEBHOOK:
case TelegramBotMethodEnum.WEBHOOK.value:
return TelegramBotWebhookService(
settings=settings,
repositories_container=repositories_container,
)
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):
async def listen_events(self):
await self._bot.delete_webhook()
await self._dispatcher._polling( # pylint: disable=protected-access
bot=self._bot,
polling_timeout=self._settings.timeout,

View File

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

View File

@@ -1,9 +1,7 @@
import enum
import uuid
from typing import Any
from aiogram.filters.callback_data import CallbackData
from pydantic import Field
class PaginatorCallbackData(CallbackData, prefix=""):
@@ -18,7 +16,7 @@ class SubscriptionsCallbackData(PaginatorCallbackData, prefix="subscriptions"):
pass
class AddSubscriptionCallbackData(CallbackData, prefix="subscription_add"):
class NewSubscriptionCallbackData(CallbackData, prefix="new_subscription"):
pass
@@ -27,7 +25,7 @@ class ConfirmAnswerEnum(str, enum.Enum):
NO = "no"
class AddSubscriptionConfirmCallbackData(CallbackData, prefix="subscription_add_confirm"):
class NewSubscriptionConfirmCallbackData(CallbackData, prefix="new_subscription_confirm"):
answer: ConfirmAnswerEnum
@@ -43,5 +41,19 @@ class SubscriptionCallbackData(CallbackData, prefix="subscription"):
action: SubscriptionActionEnum = SubscriptionActionEnum.SHOW
class AddSubscriptionPoolsCallbackData(PaginatorCallbackData, prefix="subscription_add_pool"):
to_user_id: uuid.UUID
class PoolsCallbackData(PaginatorCallbackData, prefix="pools"):
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
from aiogram.filters import Command
from aiogram_dialog import DialogManager
from birthday_pool_bot.repositories import RepositoriesContainer
from . import constants, handlers
from .callback_data import (
AddSubscriptionCallbackData,
AddSubscriptionConfirmCallbackData,
ConfirmAnswerEnum,
MenuCallbackData,
NewSubscriptionCallbackData,
NewSubscriptionConfirmCallbackData,
PoolActionEnum,
PoolCallbackData,
PoolsBackCallbackData,
PoolsCallbackData,
SubscriptionActionEnum,
SubscriptionCallbackData,
SubscriptionsCallbackData,
@@ -16,12 +19,12 @@ from .callback_data import (
from .middlewares import (
AuthMiddleware,
DependsMiddleware,
TypingMiddleware,
# TypingMiddleware,
)
from .states import (
AddSubscriptionPoolState,
AddSubscriptionState,
MenuState,
NewSubscriptionPoolState,
NewSubscriptionState,
SetProfileBirthdayState,
SetProfileGiftPaymentDataState,
SetProfileNameState,
@@ -33,7 +36,7 @@ def setup_dispatcher(
dispatcher: aiogram.Dispatcher,
repositories_container: RepositoriesContainer,
):
typing_middleware = TypingMiddleware()
# typing_middleware = TypingMiddleware()
auth_middleware = AuthMiddleware(users_repository=repositories_container.users)
users_repository_middleware = DependsMiddleware(
name="users_repository",
@@ -175,9 +178,9 @@ def setup_router(router: aiogram.Router) -> aiogram.Router:
SubscriptionsCallbackData.filter(),
)
router.callback_query.register(
handlers.add_subscription_callback_handler,
handlers.new_subscription_callback_handler,
MenuState.SUBSCRIPTIONS,
AddSubscriptionCallbackData.filter(),
NewSubscriptionCallbackData.filter(),
)
router.callback_query.register(
handlers.menu_callback_handler,
@@ -203,107 +206,132 @@ def setup_router(router: aiogram.Router) -> aiogram.Router:
### Add new subscription
router.message.register(
handlers.add_subscription_message_handler,
AddSubscriptionState.WAITING_FOR_PHONE,
handlers.new_subscription_message_handler,
NewSubscriptionState.WAITING_FOR_PHONE,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_user_message_handler,
AddSubscriptionState.WAITING_FOR_PHONE,
handlers.set_new_subscription_user_message_handler,
NewSubscriptionState.WAITING_FOR_PHONE,
)
router.message.register(
handlers.ask_add_subscription_user_message_handler,
AddSubscriptionState.WAITING_FOR_DATE,
handlers.ask_new_subscription_user_message_handler,
NewSubscriptionState.WAITING_FOR_DATE,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_user_birthday_message_handler,
AddSubscriptionState.WAITING_FOR_DATE,
handlers.set_new_subscription_user_birthday_message_handler,
NewSubscriptionState.WAITING_FOR_DATE,
)
router.message.register(
handlers.ask_add_subscription_user_message_handler,
AddSubscriptionState.WAITING_FOR_NAME,
handlers.ask_new_subscription_user_message_handler,
NewSubscriptionState.WAITING_FOR_NAME,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_name_message_handler,
AddSubscriptionState.WAITING_FOR_NAME,
handlers.set_new_subscription_name_message_handler,
NewSubscriptionState.WAITING_FOR_NAME,
)
router.message.register(
handlers.ask_add_subscription_pool_description_message_handler,
AddSubscriptionState.WAITING_FOR_POOL_DECISION,
handlers.ask_new_subscription_pool_description_message_handler,
NewSubscriptionState.WAITING_FOR_POOL_DECISION,
aiogram.F.text == constants.CREATE_POOL_BUTTON,
)
router.message.register(
handlers.show_add_subscription_pools_message_handler,
AddSubscriptionState.WAITING_FOR_POOL_DECISION,
handlers.show_new_subscription_choosing_pools_message_handler,
NewSubscriptionState.WAITING_FOR_POOL_DECISION,
aiogram.F.text == constants.JOIN_EXISTING_POOL_BUTTON,
)
router.message.register(
handlers.ask_add_subscription_confirmation_message_handler,
AddSubscriptionState.WAITING_FOR_POOL_DECISION,
handlers.ask_new_subscription_confirmation_message_handler,
NewSubscriptionState.WAITING_FOR_POOL_DECISION,
aiogram.F.text == constants.DECLINE_POOL_BUTTON,
)
router.message.register(
handlers.ask_add_subscription_name_message_handler,
AddSubscriptionState.WAITING_FOR_POOL_DECISION,
handlers.ask_new_subscription_name_message_handler,
NewSubscriptionState.WAITING_FOR_POOL_DECISION,
aiogram.F.text == constants.BACK_BUTTON,
)
router.callback_query.register(
handlers.confirm_add_subscription_callback_handler,
AddSubscriptionState.WAITING_FOR_CONFIRMATION,
AddSubscriptionConfirmCallbackData.filter(
handlers.confirm_new_subscription_callback_handler,
NewSubscriptionState.WAITING_FOR_CONFIRMATION,
NewSubscriptionConfirmCallbackData.filter(
aiogram.F.answer == ConfirmAnswerEnum.YES,
),
)
router.callback_query.register(
handlers.subscriptions_callback_handler,
AddSubscriptionState.WAITING_FOR_CONFIRMATION,
AddSubscriptionConfirmCallbackData.filter(
NewSubscriptionState.WAITING_FOR_CONFIRMATION,
NewSubscriptionConfirmCallbackData.filter(
aiogram.F.answer == ConfirmAnswerEnum.NO,
),
)
#### Add new subscription pool
router.message.register(
handlers.ask_add_subscription_pool_payment_phone_message_handler,
AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
handlers.ask_new_subscription_pool_payment_phone_message_handler,
NewSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
aiogram.F.text == constants.SKIP_BUTTON,
)
router.message.register(
handlers.ask_add_subscription_pool_decision_message_handler,
AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
handlers.ask_new_subscription_pool_decision_message_handler,
NewSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_pool_description_message_handler,
AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
handlers.set_new_subscription_pool_description_message_handler,
NewSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
)
router.message.register(
handlers.set_add_subscription_pool_payment_data_from_profile_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE,
handlers.set_new_subscription_pool_payment_data_from_profile_message_handler,
NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE,
aiogram.F.text == constants.USE_PROFILE_GIFT_PAYMENT_DATA,
)
router.message.register(
handlers.ask_add_subscription_pool_description_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE,
handlers.ask_new_subscription_pool_description_message_handler,
NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_pool_payment_phone_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE,
handlers.set_new_subscription_pool_payment_phone_message_handler,
NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE,
)
router.message.register(
handlers.set_add_subscription_pool_payment_phone_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK,
handlers.set_new_subscription_pool_payment_phone_message_handler,
NewSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_pool_payment_bank_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK,
handlers.set_new_subscription_pool_payment_bank_message_handler,
NewSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK,
)
#### 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
router.message.register(handlers.fallback_message_handler)

View File

@@ -1,21 +1,13 @@
import asyncio
import uuid
from aiogram import types, F, Router
from aiogram import types
from aiogram.enums import MessageOriginType, ParseMode
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
from pydantic_core import PydanticCustomError
from pydantic_filters import PagePagination
from pydantic_extra_types.phone_numbers import PhoneNumber
from birthday_pool_bot.dto import (
BankEnum,
PaymentData,
Pool,
Subscription,
SubscriptionFilter,
User,
)
from birthday_pool_bot.repositories.repositories import (
@@ -24,18 +16,11 @@ from birthday_pool_bot.repositories.repositories import (
UsersRepository,
)
from . import constants, logic
from .callback_data import (
AddSubscriptionCallbackData,
AddSubscriptionConfirmCallbackData,
ConfirmAnswerEnum,
MenuCallbackData,
SubscriptionCallbackData,
SubscriptionsCallbackData,
)
from .callback_data import PoolCallbackData, SubscriptionCallbackData
from .exceptions import FlowInternalError
from .states import (
AddSubscriptionPoolState,
AddSubscriptionState,
NewSubscriptionPoolState,
NewSubscriptionState,
MenuState,
SetProfileBirthdayState,
SetProfileGiftPaymentDataState,
@@ -322,31 +307,31 @@ async def delete_subscription_callback(
await state.set_state(MenuState.SUBSCRIPTIONS)
async def add_subscription_message_handler(
async def new_subscription_message_handler(
message: types.Message,
state: FSMContext,
):
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
async def add_subscription_callback_handler(
async def new_subscription_callback_handler(
callback_query: types.CallbackQuery,
state: FSMContext,
):
await logic.ask_add_subscription_user(callback_query=callback_query)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(callback_query=callback_query)
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,
state: FSMContext,
):
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
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,
state: FSMContext,
user: User,
@@ -370,15 +355,15 @@ async def set_add_subscription_user_message_handler(
await message.reply(
text="Пользователь скрыл свои данные, попробуйте отправить номер телефона или его контакт",
)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
if message.forward_origin.type != MessageOriginType.USER:
await message.reply(
text="Это сообщение не от пользователя, попробуйте ещё раз",
)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
user_name = get_telegram_user_full_name(message.forward_origin.sender_user)
user_telegram_id = message.forward_origin.sender_user.id
@@ -390,8 +375,8 @@ async def set_add_subscription_user_message_handler(
try:
PhoneNumber._validate(user_phone, None)
except PydanticCustomError:
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
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:
await message.reply("Нельзя подписаться на самого себя")
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
async with subscriptions_repository.transaction():
@@ -418,8 +403,8 @@ async def set_add_subscription_user_message_handler(
if subscription is not None:
await message.reply(text="У вас уже есть подписка на этого человека")
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
await state.update_data(
@@ -427,65 +412,65 @@ async def set_add_subscription_user_message_handler(
)
if subscription_user.birthday is None:
await logic.ask_add_subscription_user_birthday(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_DATE)
await logic.ask_new_subscription_user_birthday(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_DATE)
return
await logic.ask_add_subscription_name(
await logic.ask_new_subscription_name(
message=message,
users_repository=users_repository,
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,
state: FSMContext,
):
await logic.ask_add_subscription_user_birthday(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_DATE)
await logic.ask_new_subscription_user_birthday(message=message)
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,
state: FSMContext,
users_repository: UsersRepository,
):
birthday = parse_date(message=message)
if birthday is None:
await logic.ask_add_subscription_user_birthday(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_DATE)
await logic.ask_new_subscription_user_birthday(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_DATE)
return
state_data = await state.get_data()
subscription_user_id = state_data.get("subscription_user_id")
if subscription_user_id is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
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_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
subscription_user.birthday = birthday
await users_repository.update_user(user=subscription_user)
await logic.ask_add_subscription_name(
await logic.ask_new_subscription_name(
message=message,
users_repository=users_repository,
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,
state: FSMContext,
users_repository: UsersRepository,
@@ -494,26 +479,26 @@ async def ask_add_subscription_name_message_handler(
subscription_user_id = state_data.get("subscription_user_id")
if subscription_user_id is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
try:
await logic.ask_add_subscription_name(
await logic.ask_new_subscription_name(
message=message,
users_repository=users_repository,
subscription_user_id=subscription_user_id,
)
except FlowInternalError:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
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,
state: FSMContext,
user: User,
@@ -525,29 +510,29 @@ async def set_add_subscription_name_message_handler(
subscription_user_id = state_data.get("subscription_user_id")
if subscription_user_id is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
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 or subscription_user.birthday is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
await state.update_data(subscription_name=message.text)
await logic.ask_add_subscription_pool_decision(
await logic.ask_new_subscription_pool_decision(
message=message,
pools_repository=pools_repository,
users_repository=users_repository,
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,
state: FSMContext,
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")
if subscription_user_id is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
try:
await logic.ask_add_subscription_pool_decision(
await logic.ask_new_subscription_pool_decision(
message=message,
pools_repository=pools_repository,
users_repository=users_repository,
@@ -570,72 +555,235 @@ async def ask_add_subscription_pool_decision_message_handler(
)
except FlowInternalError:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
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,
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 message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
try:
await logic.show_add_subscription_pools(
await logic.show_new_subscription_choosing_pools(
user=user,
users_repository=users_repository,
pools_repository=pools_repository,
subscription_user_id=subscription_user_id,
message=message,
)
except FlowInternalError:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
await logic.ask_add_subscription_pool_description(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION)
await state.set_state(NewSubscriptionState.WAITING_FOR_CHOOSE_POOL)
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,
state: FSMContext,
):
await logic.ask_add_subscription_pool_description(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION)
await logic.ask_new_subscription_pool_description(message=message)
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,
state: FSMContext,
):
description = message.text
await state.update_data(subscription_pool_description=description)
await logic.ask_add_subscription_pool_payment_phone(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
await logic.ask_new_subscription_pool_payment_phone(message=message)
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,
state: FSMContext,
user: User,
):
await logic.ask_add_subscription_pool_payment_phone(message=message, user=user)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
await logic.ask_new_subscription_pool_payment_phone(message=message, user=user)
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,
state: FSMContext,
):
@@ -646,8 +794,8 @@ async def set_add_subscription_pool_payment_phone_message_handler(
phone_number = message.text
else:
await message.reply(text="Некорректный номер телефона, попробуйте ещё раз")
await logic.ask_add_subscription_pool_payment_phone(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
await logic.ask_new_subscription_pool_payment_phone(message=message)
await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
return
try:
@@ -656,16 +804,16 @@ async def set_add_subscription_pool_payment_phone_message_handler(
await message.reply(
text="Некорректный номер телефона, попробуйте ещё раз",
)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
return
await state.update_data(subscription_pool_phone=phone_number)
await logic.ask_add_subscription_pool_payment_bank(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK)
await logic.ask_new_subscription_pool_payment_bank(message=message)
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,
state: FSMContext,
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:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_pool_payment_phone(message=message, user=user)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
await logic.ask_new_subscription_pool_payment_phone(message=message, user=user)
await state.set_state(NewSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE)
return
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")
if not all((subscription_user_id, subscription_name)):
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
await state.update_data(
subscription_pool_phone=user.gift_payment_data.phone,
subscription_pool_bank=user.gift_payment_data.bank,
)
await logic.ask_add_subscription_confirmation(
await logic.ask_new_subscription_confirmation(
message=message,
users_repository=users_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_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,
state: FSMContext,
):
await logic.ask_add_subscription_pool_payment_bank(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_PAYMENT_BANK)
await logic.ask_new_subscription_pool_payment_bank(message=message)
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,
state: FSMContext,
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")
if not all((subscription_user_id, subscription_name)):
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
if subscription_pool_phone is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_pool_decision(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION)
await logic.ask_new_subscription_pool_decision(message=message)
await state.set_state(NewSubscriptionPoolState.WAITING_FOR_DESCRIPTION)
return
async with users_repository.transaction():
@@ -741,8 +889,8 @@ async def set_add_subscription_pool_payment_bank_message_handler(
)
if subscription_user is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
bank_name = message.text
@@ -754,7 +902,7 @@ async def set_add_subscription_pool_payment_bank_message_handler(
bank = constants.BANKS_MAP[bank_name]
await state.update_data(subscription_pool_bank=bank)
await logic.ask_add_subscription_confirmation(
await logic.ask_new_subscription_confirmation(
message=message,
users_repository=users_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_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,
state: FSMContext,
users_repository: UsersRepository,
@@ -782,8 +930,8 @@ async def ask_add_subscription_confirmation_message_handler(
subscription_pool_bank = state_data.get("subscription_pool_bank")
if not all((subscription_user_id, subscription_name)):
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
async with users_repository.transaction():
@@ -792,12 +940,12 @@ async def ask_add_subscription_confirmation_message_handler(
)
if subscription_user is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
return
try:
await logic.ask_add_subscription_confirmation(
await logic.ask_new_subscription_confirmation(
message=message,
users_repository=users_repository,
pools_repository=pools_repository,
@@ -810,14 +958,14 @@ async def ask_add_subscription_confirmation_message_handler(
)
except FlowInternalError:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
await logic.ask_new_subscription_user(message=message)
await state.set_state(NewSubscriptionState.WAITING_FOR_PHONE)
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,
state: FSMContext,
user: User,
@@ -833,9 +981,9 @@ async def confirm_add_subscription_callback_handler(
subscription_pool_phone = state_data.get("subscription_pool_phone")
subscription_pool_bank = state_data.get("subscription_pool_bank")
if not all((subscription_user_id, subscription_name)):
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
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
async with users_repository.transaction():
@@ -843,9 +991,9 @@ async def confirm_add_subscription_callback_handler(
user_id=subscription_user_id,
)
if subscription_user is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
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
subscription_pool = None
@@ -855,9 +1003,9 @@ async def confirm_add_subscription_callback_handler(
pool_id=subscription_pool_id,
)
if subscription_pool is None:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
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
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.enums import ParseMode
from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
from pydantic_filters import PagePagination
from birthday_pool_bot.dto import User
from birthday_pool_bot.repositories.repositories import UsersRepository
from birthday_pool_bot.dto import BankEnum, User
from birthday_pool_bot.repositories.repositories import (
PoolsRepository,
SubscriptionsRepository,
UsersRepository,
)
from . import constants
from .callback_data import (
AddSubscriptionCallbackData,
AddSubscriptionConfirmCallbackData,
ConfirmAnswerEnum,
MenuCallbackData,
NewSubscriptionCallbackData,
NewSubscriptionConfirmCallbackData,
PoolActionEnum,
PoolCallbackData,
PoolsCallbackData,
PoolsBackCallbackData,
SubscriptionActionEnum,
SubscriptionCallbackData,
SubscriptionsCallbackData,
@@ -200,12 +209,12 @@ async def show_subscriptions(
text = "Мои подписки:" if subscriptions else "Нет подписок"
keyboard = InlineKeyboardBuilder()
for subscription in subscriptions:
keyboard.button(
keyboard.row(types.InlineKeyboardButton(
text=subscription.name,
callback_data=SubscriptionCallbackData(
to_user_id=subscription.to_user_id,
).pack(),
)
))
navigation_row = []
if page > 1:
navigation_row.append(types.InlineKeyboardButton(
@@ -226,7 +235,7 @@ async def show_subscriptions(
keyboard.row(
types.InlineKeyboardButton(
text="Добавить",
callback_data=AddSubscriptionCallbackData().pack(),
callback_data=NewSubscriptionCallbackData().pack(),
),
types.InlineKeyboardButton(
text="Назад",
@@ -273,7 +282,12 @@ async def show_subscription(
)
if subscription.pool is not None:
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:
text += "Вы участвуете в сборе денег\n\n"
keyboard = InlineKeyboardBuilder()
@@ -333,7 +347,7 @@ async def delete_subscription(
await callback_query.answer(text="Подписка успешно удалена")
async def ask_add_subscription_user(
async def ask_new_subscription_user(
callback_query: types.CallbackQuery | 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,
):
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,
users_repository: UsersRepository,
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,
pools_repository: PoolsRepository,
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,
users_repository: UsersRepository,
pools_repository: PoolsRepository,
subscription_user_id: uuid.UUID,
message: types.Message | 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)
page = callback_data.page
async with subscriptions_repository.transaction():
total = await subscriptions_repository.get_user_subscriptions_count(user_id=user.id)
async with pools_repository.transaction():
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))
subscriptions = [
subscription
async for subscription in subscriptions_repository.get_user_subscriptions(
user_id=user.id,
pools = [
pool
async for pool in pools_repository.get_pools_by_birthday_user_id(
birthday_user_id=subscription_user_id,
pagination=PagePagination(page=page, per_page=per_page),
with_owner=True,
)
]
text = "Сборы:" if subscriptions else "Нет сборов"
text = "Сборы:" if pools else "Нет сборов"
keyboard = InlineKeyboardBuilder()
for subscription in subscriptions:
keyboard.button(
text=subscription.name,
callback_data=SubscriptionCallbackData(
to_user_id=subscription.to_user_id,
for pool in pools:
keyboard.row(types.InlineKeyboardButton(
text=pool.owner.name or "Аноним",
callback_data=PoolCallbackData(
id=pool.id,
).pack(),
)
))
navigation_row = []
if page > 1:
navigation_row.append(types.InlineKeyboardButton(
text="<",
callback_data=SubscriptionsCallbackData(page=page - 1).pack(),
callback_data=PoolsCallbackData(page=page - 1).pack(),
))
if pages_count > 1:
navigation_row.append(types.InlineKeyboardButton(
@@ -504,17 +523,13 @@ async def show_add_subscription_pools(
if page < pages_count:
navigation_row.append(types.InlineKeyboardButton(
text=">",
callback_data=SubscriptionsCallbackData(page=page + 1).pack(),
callback_data=PoolsCallbackData(page=page + 1).pack(),
))
keyboard.row(*navigation_row)
keyboard.row(
types.InlineKeyboardButton(
text="Добавить",
callback_data=AddSubscriptionCallbackData().pack(),
),
types.InlineKeyboardButton(
text="Назад",
callback_data=MenuCallbackData().pack(),
callback_data=PoolsBackCallbackData().pack(),
),
)
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,
):
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,
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,
):
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,
users_repository: UsersRepository,
pools_repository: PoolsRepository,
@@ -663,13 +741,13 @@ async def ask_add_subscription_confirmation(
keyboard = InlineKeyboardBuilder()
keyboard.button(
text="Да",
callback_data=AddSubscriptionConfirmCallbackData(
callback_data=NewSubscriptionConfirmCallbackData(
answer=ConfirmAnswerEnum.YES,
).pack(),
)
keyboard.button(
text="Нет",
callback_data=AddSubscriptionConfirmCallbackData(
callback_data=NewSubscriptionConfirmCallbackData(
answer=ConfirmAnswerEnum.NO,
).pack(),
)

View File

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

View File

@@ -5,7 +5,7 @@ from aiogram import types
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:

View File

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

View File

@@ -7,7 +7,7 @@ from birthday_pool_bot.telegram_bot.settings import TelegramBotSettings
class TelegramBotWebhookSettings(TelegramBotSettings):
method: Literal[TelegramBotMethodEnum.WEBHOOK.value] = TelegramBotMethodEnum.WEBHOOK
method: Literal[TelegramBotMethodEnum.WEBHOOK.value] = TelegramBotMethodEnum.WEBHOOK.value
root_url: AnyHttpUrl
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",
"uvicorn>=0.38.0",
"fastapi>=0.124.4",
"yarl>=1.22.0",
]
[dependency-groups]
sqlite = [
"aiosqlite>=0.21.0",
]
postgresql = [
"asyncpg>=0.31.0",
]
dev = [
"ruff>=0.14.11",
]
[tool.uv]
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" },
]
[[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]]
name = "attrs"
version = "25.4.0"
@@ -196,9 +228,11 @@ name = "birthday-pool-bot"
version = "0.0.0"
source = { virtual = "." }
dependencies = [
{ name = "aiogram" },
{ name = "alembic" },
{ name = "apscheduler" },
{ name = "facet" },
{ name = "fastapi" },
{ name = "pydantic" },
{ name = "pydantic-extra-types", extra = ["phonenumbers"] },
{ name = "pydantic-filters" },
@@ -207,23 +241,28 @@ dependencies = [
{ name = "sqlalchemy" },
{ name = "sqlmodel" },
{ name = "typer" },
{ name = "uvicorn" },
{ name = "yarl" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
postgresql = [
{ name = "asyncpg" },
]
sqlite = [
{ name = "aiosqlite" },
]
telegram = [
{ name = "aiogram" },
{ name = "fastapi" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "aiogram", specifier = ">=3.23.0" },
{ name = "alembic", specifier = ">=1.17.2" },
{ name = "apscheduler", specifier = ">=3.11.2" },
{ name = "facet", specifier = ">=0.10.1" },
{ name = "fastapi", specifier = ">=0.124.4" },
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "pydantic-extra-types", extras = ["phonenumbers"], specifier = ">=2.10.6" },
{ 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 = "sqlmodel", specifier = ">=0.0.27" },
{ name = "typer", specifier = ">=0.20.0" },
{ name = "uvicorn", specifier = ">=0.38.0" },
{ name = "yarl", specifier = ">=1.22.0" },
]
[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" }]
telegram = [
{ name = "aiogram", specifier = ">=3.23.0" },
{ name = "fastapi", specifier = ">=0.124.4" },
{ name = "uvicorn", specifier = ">=0.38.0" },
]
[[package]]
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" },
]
[[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]]
name = "shellingham"
version = "1.5.4"