Init project

This commit is contained in:
2026-01-15 09:47:43 +03:00
commit 3e515c66ec
59 changed files with 5101 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
from .dispatcher import setup_dispatcher
__all__ = (
# dispatcher
"setup_dispatcher",
)

View File

@@ -0,0 +1,47 @@
import enum
import uuid
from typing import Any
from aiogram.filters.callback_data import CallbackData
from pydantic import Field
class PaginatorCallbackData(CallbackData, prefix=""):
page: int = 1
class MenuCallbackData(CallbackData, prefix="menu"):
pass
class SubscriptionsCallbackData(PaginatorCallbackData, prefix="subscriptions"):
pass
class AddSubscriptionCallbackData(CallbackData, prefix="subscription_add"):
pass
class ConfirmAnswerEnum(str, enum.Enum):
YES = "yes"
NO = "no"
class AddSubscriptionConfirmCallbackData(CallbackData, prefix="subscription_add_confirm"):
answer: ConfirmAnswerEnum
class SubscriptionActionEnum(str, enum.Enum):
SHOW = "show"
DELETE = "delete"
EDIT = "edit"
BACK = "back"
class SubscriptionCallbackData(CallbackData, prefix="subscription"):
to_user_id: uuid.UUID
action: SubscriptionActionEnum = SubscriptionActionEnum.SHOW
class AddSubscriptionPoolsCallbackData(PaginatorCallbackData, prefix="subscription_add_pool"):
to_user_id: uuid.UUID

View File

@@ -0,0 +1,26 @@
from birthday_pool_bot.dto import BankEnum
BANKS_MAP = {
"Т-Банк": BankEnum.T_BANK,
"Сбер": BankEnum.SBER,
"Альфа-Банк": BankEnum.ALFA_BANK,
}
BANKS_TITLE_MAP = {value: key for key, value in BANKS_MAP.items()}
PROFILE_BUTTON = "Профиль"
SUBSCRIPTIONS_BUTTON = "Мои подписки"
SET_PROFILE_NAME = "Изменить имя"
SET_PROFILE_PHONE = "Изменить номер телефона"
SET_PROFILE_GIFT_PAYMENT_DATA_BUTTON = "Изменить банковские данные для подарка"
SET_PROFILE_BIRTHDATE_BUTTON = "Изменить день рождения"
SHARE_CONTACT_BUTTON = "Поделиться контактом"
CREATE_POOL_BUTTON = "Хочу создать сбор денег"
JOIN_EXISTING_POOL_BUTTON = "Хочу присоединиться к сборам"
DECLINE_POOL_BUTTON = "Не хочу участвовать в сборе денег"
USE_PROFILE_GIFT_PAYMENT_DATA = "Использовать данные из профиля"
SKIP_BUTTON = "Пропустить"
BACK_BUTTON = "Назад"
ERROR_MESSAGE = "Какой-то сбой, попробуйте ещё раз"

View File

@@ -0,0 +1,311 @@
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,
SubscriptionActionEnum,
SubscriptionCallbackData,
SubscriptionsCallbackData,
)
from .middlewares import (
AuthMiddleware,
DependsMiddleware,
TypingMiddleware,
)
from .states import (
AddSubscriptionPoolState,
AddSubscriptionState,
MenuState,
SetProfileBirthdayState,
SetProfileGiftPaymentDataState,
SetProfileNameState,
SetProfilePhoneState,
)
def setup_dispatcher(
dispatcher: aiogram.Dispatcher,
repositories_container: RepositoriesContainer,
):
typing_middleware = TypingMiddleware()
auth_middleware = AuthMiddleware(users_repository=repositories_container.users)
users_repository_middleware = DependsMiddleware(
name="users_repository",
object_=repositories_container.users,
)
pools_repository_middleware = DependsMiddleware(
name="pools_repository",
object_=repositories_container.pools,
)
subscriptions_repository_middleware = DependsMiddleware(
name="subscriptions_repository",
object_=repositories_container.subscriptions,
)
middlewares = [
# typing_middleware,
auth_middleware,
users_repository_middleware,
pools_repository_middleware,
subscriptions_repository_middleware,
]
router = aiogram.Router()
for middleware in middlewares:
dispatcher.update.outer_middleware(middleware)
router.callback_query.middleware(middleware)
router.message.middleware(middleware)
router = setup_router(router=router)
dispatcher.include_router(router)
def setup_router(router: aiogram.Router) -> aiogram.Router:
# Menu
router.message.register(handlers.menu_message_handler, Command("start"))
router.message.register(handlers.menu_message_handler, Command("menu"))
router.message.register(
handlers.profile_message_handler,
MenuState.MENU,
aiogram.F.text == constants.PROFILE_BUTTON,
)
router.message.register(
handlers.subscriptions_message_handler,
MenuState.MENU,
aiogram.F.text == constants.SUBSCRIPTIONS_BUTTON,
)
## Profile
router.message.register(
handlers.ask_profile_name_message_handler,
MenuState.PROFILE,
aiogram.F.text == constants.SET_PROFILE_NAME,
)
router.message.register(
handlers.ask_profile_phone_message_handler,
MenuState.PROFILE,
aiogram.F.text == constants.SET_PROFILE_PHONE,
)
router.message.register(
handlers.ask_profile_birthdate_message_handler,
MenuState.PROFILE,
aiogram.F.text == constants.SET_PROFILE_BIRTHDATE_BUTTON,
)
router.message.register(
handlers.ask_profile_gift_payment_phone_message_handler,
MenuState.PROFILE,
aiogram.F.text == constants.SET_PROFILE_GIFT_PAYMENT_DATA_BUTTON,
)
router.message.register(
handlers.menu_message_handler,
MenuState.PROFILE,
aiogram.F.text == constants.BACK_BUTTON,
)
### Set profile name
router.message.register(
handlers.profile_message_handler,
SetProfileNameState.WAITING_FOR_NAME,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_profile_name_message_handler,
SetProfileNameState.WAITING_FOR_NAME,
)
### Set profile phone
router.message.register(
handlers.profile_message_handler,
SetProfilePhoneState.WAITING_FOR_PHONE,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_profile_phone_message_handler,
SetProfilePhoneState.WAITING_FOR_PHONE,
)
### Set profile birthday
router.message.register(
handlers.profile_message_handler,
SetProfileBirthdayState.WAITING_FOR_DATE,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_profile_birthdate_message_handler,
SetProfileBirthdayState.WAITING_FOR_DATE,
)
### Set profile gift payment data
router.message.register(
handlers.profile_message_handler,
SetProfileGiftPaymentDataState.WAITING_FOR_PHONE,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_profile_gift_payment_phone_message_handler,
SetProfileGiftPaymentDataState.WAITING_FOR_PHONE,
)
router.message.register(
handlers.ask_profile_gift_payment_phone_message_handler,
SetProfileGiftPaymentDataState.WAITING_FOR_BANK,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_profile_gift_payment_bank_message_handler,
SetProfileGiftPaymentDataState.WAITING_FOR_BANK,
aiogram.F.text.in_(constants.BANKS_MAP),
)
## Subscriptions
router.callback_query.register(
handlers.subscription_callback_handler,
MenuState.SUBSCRIPTIONS,
SubscriptionCallbackData.filter(
aiogram.F.action == SubscriptionActionEnum.SHOW,
),
)
router.callback_query.register(
handlers.subscriptions_callback_handler,
MenuState.SUBSCRIPTIONS,
SubscriptionsCallbackData.filter(),
)
router.callback_query.register(
handlers.add_subscription_callback_handler,
MenuState.SUBSCRIPTIONS,
AddSubscriptionCallbackData.filter(),
)
router.callback_query.register(
handlers.menu_callback_handler,
MenuState.SUBSCRIPTIONS,
MenuCallbackData.filter(),
)
### Subscription interaction
router.callback_query.register(
handlers.delete_subscription_callback,
MenuState.SUBSCRIPTIONS,
SubscriptionCallbackData.filter(
aiogram.F.action == SubscriptionActionEnum.DELETE,
),
)
router.callback_query.register(
handlers.subscriptions_callback_handler,
MenuState.SUBSCRIPTIONS,
SubscriptionCallbackData.filter(
aiogram.F.action == SubscriptionActionEnum.BACK,
),
)
### Add new subscription
router.message.register(
handlers.add_subscription_message_handler,
AddSubscriptionState.WAITING_FOR_PHONE,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_user_message_handler,
AddSubscriptionState.WAITING_FOR_PHONE,
)
router.message.register(
handlers.ask_add_subscription_user_message_handler,
AddSubscriptionState.WAITING_FOR_DATE,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_user_birthday_message_handler,
AddSubscriptionState.WAITING_FOR_DATE,
)
router.message.register(
handlers.ask_add_subscription_user_message_handler,
AddSubscriptionState.WAITING_FOR_NAME,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_name_message_handler,
AddSubscriptionState.WAITING_FOR_NAME,
)
router.message.register(
handlers.ask_add_subscription_pool_description_message_handler,
AddSubscriptionState.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,
aiogram.F.text == constants.JOIN_EXISTING_POOL_BUTTON,
)
router.message.register(
handlers.ask_add_subscription_confirmation_message_handler,
AddSubscriptionState.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,
aiogram.F.text == constants.BACK_BUTTON,
)
router.callback_query.register(
handlers.confirm_add_subscription_callback_handler,
AddSubscriptionState.WAITING_FOR_CONFIRMATION,
AddSubscriptionConfirmCallbackData.filter(
aiogram.F.answer == ConfirmAnswerEnum.YES,
),
)
router.callback_query.register(
handlers.subscriptions_callback_handler,
AddSubscriptionState.WAITING_FOR_CONFIRMATION,
AddSubscriptionConfirmCallbackData.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,
aiogram.F.text == constants.SKIP_BUTTON,
)
router.message.register(
handlers.ask_add_subscription_pool_decision_message_handler,
AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_pool_description_message_handler,
AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION,
)
router.message.register(
handlers.set_add_subscription_pool_payment_data_from_profile_message_handler,
AddSubscriptionPoolState.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,
aiogram.F.text == constants.BACK_BUTTON,
)
router.message.register(
handlers.set_add_subscription_pool_payment_phone_message_handler,
AddSubscriptionPoolState.WAITING_FOR_PAYMENT_PHONE,
)
router.message.register(
handlers.set_add_subscription_pool_payment_phone_message_handler,
AddSubscriptionPoolState.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,
)
#### Choose pool for subscription
# Fallback
router.message.register(handlers.fallback_message_handler)
return router

View File

@@ -0,0 +1,6 @@
class TelegramBotUIError(Exception):
pass
class FlowInternalError(TelegramBotUIError):
pass

View File

@@ -0,0 +1,894 @@
import asyncio
import uuid
from aiogram import types, F, Router
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 (
PoolsRepository,
SubscriptionsRepository,
UsersRepository,
)
from . import constants, logic
from .callback_data import (
AddSubscriptionCallbackData,
AddSubscriptionConfirmCallbackData,
ConfirmAnswerEnum,
MenuCallbackData,
SubscriptionCallbackData,
SubscriptionsCallbackData,
)
from .exceptions import FlowInternalError
from .states import (
AddSubscriptionPoolState,
AddSubscriptionState,
MenuState,
SetProfileBirthdayState,
SetProfileGiftPaymentDataState,
SetProfileNameState,
SetProfilePhoneState,
)
from .utils import get_telegram_user_full_name, parse_date
async def fallback_message_handler(
message: types.Message,
state: FSMContext,
):
await message.reply(text=constants.ERROR_MESSAGE)
await logic.show_menu(message=message)
await state.clear()
await state.set_state(MenuState.MENU)
async def menu_message_handler(
message: types.Message,
state: FSMContext,
):
await logic.show_menu(message=message)
await state.set_state(MenuState.MENU)
async def menu_callback_handler(
callback_query: types.CallbackQuery,
state: FSMContext,
):
await logic.show_menu(message=callback_query.message)
await state.set_state(MenuState.MENU)
async def profile_message_handler(
message: types.Message,
state: FSMContext,
user: User,
):
await logic.show_profile(message=message, user=user)
await state.set_state(MenuState.PROFILE)
async def ask_profile_name_message_handler(
message: types.Message,
state: FSMContext,
):
await logic.ask_profile_name(message=message)
await state.set_state(SetProfileNameState.WAITING_FOR_NAME)
async def set_profile_name_message_handler(
message: types.Message,
state: FSMContext,
user: User,
users_repository: UsersRepository,
):
user.name = message.text
async with users_repository.transaction():
user = await users_repository.update_user(user=user)
await logic.show_profile(message=message, user=user)
await state.set_state(MenuState.PROFILE)
async def ask_profile_phone_message_handler(
message: types.Message,
state: FSMContext,
):
await logic.ask_profile_phone(message=message)
await state.set_state(SetProfilePhoneState.WAITING_FOR_PHONE)
async def set_profile_phone_message_handler(
message: types.Message,
state: FSMContext,
user: User,
users_repository: UsersRepository,
):
if message.contact is None:
await message.reply(
text=(
"Некорректный ответ, пожалуйста, отправьте свой контакт нажатием на кнопку "
"`Поделиться контактом`"
),
parse_mode=ParseMode.MARKDOWN,
)
await logic.ask_profile_phone(message=message)
await state.set_state(SetProfilePhoneState.WAITING_FOR_PHONE)
return
phone_number = message.contact.phone_number
try:
PhoneNumber._validate(phone_number, None)
except PydanticCustomError:
await message.reply(
text="Некорректный номер телефон, попробуйте ещё раз",
)
await state.set_state(SetProfilePhoneState.WAITING_FOR_PHONE)
return
user.phone = phone_number
async with users_repository.transaction():
user = await users_repository.update_user(user=user)
await logic.show_profile(message=message, user=user)
await state.set_state(MenuState.PROFILE)
async def ask_profile_birthdate_message_handler(
message: types.Message,
state: FSMContext,
):
await logic.ask_profile_birthdate(message=message)
await state.set_state(SetProfileBirthdayState.WAITING_FOR_DATE)
async def set_profile_birthdate_message_handler(
message: types.Message,
state: FSMContext,
user: User,
users_repository: UsersRepository,
):
birthday = parse_date(message=message)
if birthday is None:
await logic.ask_profile_birthdate(message=message)
await state.set_state(SetProfileBirthdayState.WAITING_FOR_DATE)
return
user.birthday = birthday
async with users_repository.transaction():
await users_repository.update_user(user=user)
await logic.show_profile(message=message, user=user)
await state.set_state(MenuState.PROFILE)
async def ask_profile_gift_payment_phone_message_handler(
message: types.Message,
state: FSMContext,
):
await logic.ask_profile_gift_payment_phone(message=message)
await state.set_state(SetProfileGiftPaymentDataState.WAITING_FOR_PHONE)
async def set_profile_gift_payment_phone_message_handler(
message: types.Message,
state: FSMContext,
):
phone_number = None
if message.contact is not None:
phone_number = message.contact.phone_number
elif message.text is not None:
phone_number = message.text
else:
await logic.ask_profile_gift_payment_phone(message=message)
await state.set_state(SetProfileGiftPaymentDataState.WAITING_FOR_PHONE)
return
try:
PhoneNumber._validate(phone_number, None)
except PydanticCustomError:
await message.reply(
text="Некорректный номер телефон, попробуйте ещё раз",
)
await state.set_state(SetProfileGiftPaymentDataState.WAITING_FOR_PHONE)
return
await state.update_data(gift_payment_phone=phone_number)
await logic.ask_profile_gift_payment_bank(
message=message,
)
await state.set_state(SetProfileGiftPaymentDataState.WAITING_FOR_BANK)
async def ask_profile_gift_payment_bank_message_handler(
message: types.Message,
state: FSMContext,
):
await logic.ask_profile_gift_payment_bank(message=message)
await state.set_state(SetProfileGiftPaymentDataState.WAITING_FOR_BANK)
async def set_profile_gift_payment_bank_message_handler(
message: types.Message,
state: FSMContext,
user: User,
users_repository: UsersRepository,
):
data = await state.get_data()
if "gift_payment_phone" not in data:
await message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_profile_gift_payment_phone(message=message)
await state.set_state(SetProfileGiftPaymentDataState.WAITING_FOR_PHONE)
return
phone = data["gift_payment_phone"]
bank_name = message.text
if bank_name not in constants.BANKS_MAP:
await message.reply(text="Неизвестный банк, попробуйте ещё раз")
await logic.ask_profile_gift_payment_bank(message=message)
await state.set_state(SetProfileGiftPaymentDataState.WAITING_FOR_BANK)
bank = constants.BANKS_MAP[bank_name]
user.gift_payment_data = PaymentData(
phone=phone,
bank=bank,
)
async with users_repository.transaction():
user = await users_repository.update_user(user=user)
await logic.show_profile(message=message, user=user)
await state.set_state(MenuState.PROFILE)
async def subscriptions_message_handler(
message: types.Message,
state: FSMContext,
user: User,
subscriptions_repository: SubscriptionsRepository,
):
await logic.show_subscriptions(
user=user,
subscriptions_repository=subscriptions_repository,
message=message,
)
await state.set_state(MenuState.SUBSCRIPTIONS)
async def subscriptions_callback_handler(
callback_query: types.CallbackQuery,
state: FSMContext,
user: User,
subscriptions_repository: SubscriptionsRepository,
):
await logic.show_subscriptions(
user=user,
subscriptions_repository=subscriptions_repository,
message=callback_query.message,
)
await state.set_state(MenuState.SUBSCRIPTIONS)
async def subscription_callback_handler(
callback_query: types.CallbackQuery,
state: FSMContext,
user: User,
subscriptions_repository: SubscriptionsRepository,
):
callback_data = SubscriptionCallbackData.unpack(callback_query.data)
await logic.show_subscription(
from_user_id=user.id,
to_user_id=callback_data.to_user_id,
subscriptions_repository=subscriptions_repository,
callback_query=callback_query,
)
await state.set_state(MenuState.SUBSCRIPTIONS)
async def delete_subscription_callback(
callback_query: types.CallbackQuery,
state: FSMContext,
user: User,
subscriptions_repository: SubscriptionsRepository,
):
callback_data = SubscriptionCallbackData.unpack(callback_query.data)
await logic.delete_subscription(
from_user_id=user.id,
to_user_id=callback_data.to_user_id,
subscriptions_repository=subscriptions_repository,
callback_query=callback_query,
)
await logic.show_subscriptions(
user=user,
subscriptions_repository=subscriptions_repository,
message=callback_query.message,
)
await state.set_state(MenuState.SUBSCRIPTIONS)
async def add_subscription_message_handler(
message: types.Message,
state: FSMContext,
):
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
async def add_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)
async def ask_add_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)
async def set_add_subscription_user_message_handler(
message: types.Message,
state: FSMContext,
user: User,
users_repository: UsersRepository,
subscriptions_repository: SubscriptionsRepository,
):
user_name, user_phone, user_telegram_id = None, None, None
async with users_repository.transaction():
if message.contact is not None:
subscription_users = await users_repository.get_users_by_primary_keys(
phone=message.contact.phone_number,
telegram_id=message.contact.user_id,
)
subscription_user = await users_repository.merge_users(*subscription_users)
user_name = get_telegram_user_full_name(message.contact)
user_phone = message.contact.phone_number
user_telegram_id = message.contact.user_id
elif message.forward_origin is not None:
if message.forward_origin.type == MessageOriginType.HIDDEN_USER:
await message.reply(
text="Пользователь скрыл свои данные, попробуйте отправить номер телефона или его контакт",
)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.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)
return
user_name = get_telegram_user_full_name(message.forward_origin.sender_user)
user_telegram_id = message.forward_origin.sender_user.id
subscription_user = await users_repository.get_user_by_telegram_id(
telegram_id=user_telegram_id,
)
elif message.text:
user_phone = message.text
try:
PhoneNumber._validate(user_phone, None)
except PydanticCustomError:
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
return
subscription_user = await users_repository.get_user_by_phone(phone=user_phone)
if subscription_user is None:
subscription_user = User(
name=user_name,
phone=user_phone,
telegram_id=user_telegram_id,
)
async with users_repository.transaction():
subscription_user = await users_repository.create_user(user=subscription_user)
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)
return
async with subscriptions_repository.transaction():
subscription = await subscriptions_repository.get_subscription(
from_user_id=user.id,
to_user_id=subscription_user.id,
)
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)
return
await state.update_data(
subscription_user_id=subscription_user.id,
)
if subscription_user.birthday is None:
await logic.ask_add_subscription_user_birthday(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_DATE)
return
await logic.ask_add_subscription_name(
message=message,
users_repository=users_repository,
subscription_user_id=subscription_user.id,
)
await state.set_state(AddSubscriptionState.WAITING_FOR_NAME)
async def ask_add_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)
async def set_add_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)
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)
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)
return
subscription_user.birthday = birthday
await users_repository.update_user(user=subscription_user)
await logic.ask_add_subscription_name(
message=message,
users_repository=users_repository,
subscription_user_id=subscription_user.id,
)
await state.set_state(AddSubscriptionState.WAITING_FOR_NAME)
async def ask_add_subscription_name_message_handler(
message: types.Message,
state: FSMContext,
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 message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
return
try:
await logic.ask_add_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)
return
await state.set_state(AddSubscriptionState.WAITING_FOR_NAME)
async def set_add_subscription_name_message_handler(
message: types.Message,
state: FSMContext,
user: User,
pools_repository: PoolsRepository,
subscriptions_repository: SubscriptionsRepository,
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 message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.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)
return
await state.update_data(subscription_name=message.text)
await logic.ask_add_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)
async def ask_add_subscription_pool_decision_message_handler(
message: 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 message.reply(text=constants.ERROR_MESSAGE)
await logic.ask_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
return
try:
await logic.ask_add_subscription_pool_decision(
message=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_add_subscription_user(message=message)
await state.set_state(AddSubscriptionState.WAITING_FOR_PHONE)
return
await state.set_state(AddSubscriptionState.WAITING_FOR_POOL_DECISION)
async def show_add_subscription_pools_message_handler(
message: types.Message,
state: FSMContext,
user: User,
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)
return
try:
await logic.show_add_subscription_pools(
user=user,
pools_repository=pools_repository,
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)
return
await logic.ask_add_subscription_pool_description(message=message)
await state.set_state(AddSubscriptionPoolState.WAITING_FOR_DESCRIPTION)
async def ask_add_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)
async def set_add_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)
async def ask_add_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)
async def set_add_subscription_pool_payment_phone_message_handler(
message: types.Message,
state: FSMContext,
):
phone_number = None
if message.contact is not None:
phone_number = message.contact.phone_number
elif message.text is not None:
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)
return
try:
PhoneNumber._validate(phone_number, None)
except PydanticCustomError:
await message.reply(
text="Некорректный номер телефона, попробуйте ещё раз",
)
await state.set_state(AddSubscriptionPoolState.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)
async def set_add_subscription_pool_payment_data_from_profile_message_handler(
message: types.Message,
state: FSMContext,
user: User,
users_repository: UsersRepository,
pools_repository: PoolsRepository,
):
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)
return
state_data = await state.get_data()
subscription_user_id = state_data.get("subscription_user_id")
subscription_name = state_data.get("subscription_name")
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)
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(
message=message,
users_repository=users_repository,
pools_repository=pools_repository,
subscription_name=subscription_name,
subscription_user_id=subscription_user_id,
subscription_pool_description=subscription_pool_description,
subscription_pool_phone=user.gift_payment_data.phone,
subscription_pool_bank=user.gift_payment_data.bank,
)
await state.set_state(AddSubscriptionState.WAITING_FOR_CONFIRMATION)
async def ask_add_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)
async def set_add_subscription_pool_payment_bank_message_handler(
message: types.Message,
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")
subscription_pool_description = state_data.get("subscription_pool_description")
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)
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)
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)
return
bank_name = message.text
if bank_name not in constants.BANKS_MAP:
await message.reply(text="Неизвестный банк, попробуйте ещё раз")
await logic.ask_profile_gift_payment_bank(message=message)
await state.set_state(SetProfileGiftPaymentDataState.WAITING_FOR_BANK)
bank = constants.BANKS_MAP[bank_name]
await state.update_data(subscription_pool_bank=bank)
await logic.ask_add_subscription_confirmation(
message=message,
users_repository=users_repository,
pools_repository=pools_repository,
subscription_name=subscription_name,
subscription_user_id=subscription_user_id,
subscription_pool_description=subscription_pool_description,
subscription_pool_phone=subscription_pool_phone,
subscription_pool_bank=bank,
)
await state.set_state(AddSubscriptionState.WAITING_FOR_CONFIRMATION)
async def ask_add_subscription_confirmation_message_handler(
message: types.Message,
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")
subscription_pool_id = state_data.get("subscription_pool_id")
subscription_pool_description = state_data.get("subscription_pool_description")
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)
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)
return
try:
await logic.ask_add_subscription_confirmation(
message=message,
users_repository=users_repository,
pools_repository=pools_repository,
subscription_name=subscription_name,
subscription_user_id=subscription_user_id,
subscription_pool_id=subscription_pool_id,
subscription_pool_description=subscription_pool_description,
subscription_pool_phone=subscription_pool_phone,
subscription_pool_bank=subscription_pool_bank,
)
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)
return
await state.set_state(AddSubscriptionState.WAITING_FOR_CONFIRMATION)
async def confirm_add_subscription_callback_handler(
callback_query: types.CallbackQuery,
state: FSMContext,
user: User,
pools_repository: PoolsRepository,
subscriptions_repository: SubscriptionsRepository,
users_repository: UsersRepository,
):
state_data = await state.get_data()
subscription_user_id = state_data.get("subscription_user_id")
subscription_name = state_data.get("subscription_name")
subscription_pool_id = state_data.get("subscription_pool_id")
subscription_pool_description = state_data.get("subscription_pool_description")
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)
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)
return
subscription_pool = None
if subscription_pool_id is not None:
async with pools_repository.transaction():
subscription_pool = await pools_repository.get_pool_by_id(
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)
return
if subscription_pool is None and all((subscription_pool_phone, subscription_pool_bank)):
subscription_pool = Pool(
owner_id=user.id,
birthday_user_id=subscription_user.id,
description=subscription_pool_description,
payment_data=PaymentData(
phone=subscription_pool_phone,
bank=subscription_pool_bank,
),
)
async with pools_repository.transaction():
subscription_pool = await pools_repository.create_pool(pool=subscription_pool)
subscription_pool_id = subscription_pool.id
subscription = Subscription(
from_user_id=user.id,
to_user_id=subscription_user_id,
name=subscription_name,
pool_id=subscription_pool_id,
)
async with subscriptions_repository.transaction():
await subscriptions_repository.create_subscription(
subscription=subscription,
)
await logic.show_subscriptions(
user=user,
subscriptions_repository=subscriptions_repository,
message=callback_query.message,
)
await state.clear()
await state.set_state(MenuState.SUBSCRIPTIONS)

View File

@@ -0,0 +1,683 @@
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 . import constants
from .callback_data import (
AddSubscriptionCallbackData,
AddSubscriptionConfirmCallbackData,
ConfirmAnswerEnum,
MenuCallbackData,
SubscriptionActionEnum,
SubscriptionCallbackData,
SubscriptionsCallbackData,
)
from .exceptions import FlowInternalError
async def show_menu(message: types.Message) -> None:
text = (
"Привет! Я – Birthday Pool Bot. "
"Я помогаю собирать подарки на дни рождения.\n"
)
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.PROFILE_BUTTON)
keyboard.button(text=constants.SUBSCRIPTIONS_BUTTON)
keyboard.adjust(1)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def show_profile(
message: types.Message,
user: User,
):
birthday_str = user.birthday.strftime("%d.%m.%Y") if user.birthday else 'Не указано'
text = (
f"👤 *Имя*: {user.name or 'Не указано'}\n\n"
f"📱 *Телефон*: {user.phone}\n\n"
f"🎂 *День рождения*: {birthday_str}\n\n"
)
if user.gift_payment_data is not None:
bank_title = constants.BANKS_TITLE_MAP[user.gift_payment_data.bank]
text += (
"💳️ *Получать подарки на*:\n"
f"🏦 _Банк_: {bank_title}\n"
f"📱 _Телефон_: {user.gift_payment_data.phone}\n\n"
)
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.SET_PROFILE_NAME)
keyboard.button(text=constants.SET_PROFILE_PHONE)
keyboard.button(text=constants.SET_PROFILE_BIRTHDATE_BUTTON)
keyboard.button(text=constants.SET_PROFILE_GIFT_PAYMENT_DATA_BUTTON)
keyboard.button(text=constants.BACK_BUTTON)
keyboard.adjust(2)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_profile_name(message: types.Message):
text = "👤 Напишите своё имя"
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.BACK_BUTTON)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_profile_phone(message: types.Message):
text = "📱 Отправь свой контакт для подтверждения телефона"
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.SHARE_CONTACT_BUTTON, request_contact=True)
keyboard.button(text=constants.BACK_BUTTON)
keyboard.adjust(1)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_profile_gift_payment_phone(message: types.Message):
text = (
"*Введите номер телефона для получения подарков*\n"
"Пример: `+79123456789`\n\n"
"ИЛИ\n\n"
"*Просто поделитесь своим контактом*"
)
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.SHARE_CONTACT_BUTTON, request_contact=True)
keyboard.button(text=constants.BACK_BUTTON)
keyboard.adjust(1)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_profile_gift_payment_bank(
message: types.Message,
):
text = "🏦 Выберите банк для приёма платежей"
keyboard = ReplyKeyboardBuilder()
for title, type_ in constants.BANKS_MAP.items():
keyboard.button(text=title)
keyboard.adjust(3)
keyboard.row(types.KeyboardButton(text=constants.BACK_BUTTON))
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_profile_birthdate(message: types.Message):
text = (
"🎂 Отправьте свою дату рождения в формате dd.mm.yyyy\n"
"Пример: `26.05.1995`."
)
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.BACK_BUTTON)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def show_subscriptions(
user: User,
subscriptions_repository: SubscriptionsRepository,
message: types.Message | None = None,
callback_query: types.CallbackQuery | None = None,
):
page = 1
per_page = 5
if callback_query is not None:
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)
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,
pagination=PagePagination(page=page, per_page=per_page),
)
]
text = "Мои подписки:" if subscriptions else "Нет подписок"
keyboard = InlineKeyboardBuilder()
for subscription in subscriptions:
keyboard.button(
text=subscription.name,
callback_data=SubscriptionCallbackData(
to_user_id=subscription.to_user_id,
).pack(),
)
navigation_row = []
if page > 1:
navigation_row.append(types.InlineKeyboardButton(
text="<",
callback_data=SubscriptionsCallbackData(page=page - 1).pack(),
))
if pages_count > 1:
navigation_row.append(types.InlineKeyboardButton(
text=f"{page}/{pages_count}",
callback_data="null",
))
if page < pages_count:
navigation_row.append(types.InlineKeyboardButton(
text=">",
callback_data=SubscriptionsCallbackData(page=page + 1).pack(),
))
keyboard.row(*navigation_row)
keyboard.row(
types.InlineKeyboardButton(
text="Добавить",
callback_data=AddSubscriptionCallbackData().pack(),
),
types.InlineKeyboardButton(
text="Назад",
callback_data=MenuCallbackData().pack(),
),
)
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)
await callback_query.message.edit_reply_markup(
reply_markup=reply_markup,
)
async def show_subscription(
from_user_id: uuid.UUID,
to_user_id: uuid.UUID,
subscriptions_repository: SubscriptionsRepository,
callback_query: types.CallbackQuery | None = None,
message: types.Message | None = None,
):
async with subscriptions_repository.transaction():
subscription = await subscriptions_repository.get_subscription(
from_user_id=from_user_id,
to_user_id=to_user_id,
with_to_user=True,
with_pool=True,
with_pool_owner=True,
)
if subscription is None:
raise FlowInternalError()
birthday_str = subscription.to_user.birthday.strftime("%d.%m.%Y")
text = (
f"*Имя*: {subscription.name}\n\n"
f"🎂 *День рождения*: {birthday_str}\n\n"
)
if subscription.pool is not None:
if subscription.pool.owner_id == from_user_id:
text += "Вы собираете деньги\n\n"
else:
text += "Вы участвуете в сборе денег\n\n"
keyboard = InlineKeyboardBuilder()
# keyboard.button(
# text="Изменить",
# callback_data=SubscriptionCallbackData(
# to_user_id=subscription.to_user_id,
# action=SubscriptionActionEnum.EDIT,
# ).pack(),
# )
keyboard.button(
text="Удалить",
callback_data=SubscriptionCallbackData(
to_user_id=subscription.to_user_id,
action=SubscriptionActionEnum.DELETE,
).pack(),
)
keyboard.button(
text="Назад",
callback_data=SubscriptionsCallbackData().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,
)
return
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)
return
async def delete_subscription(
from_user_id: uuid.UUID,
to_user_id: uuid.UUID,
subscriptions_repository: SubscriptionsRepository,
callback_query: types.CallbackQuery | None = None,
message: types.Message | None = None,
):
async with subscriptions_repository.transaction():
await subscriptions_repository.delete_subscription(
from_user_id=from_user_id,
to_user_id=to_user_id,
)
if message is not None:
await message.reply(text="Подписка успешно удалена")
elif callback_query is not None:
await callback_query.answer(text="Подписка успешно удалена")
async def ask_add_subscription_user(
callback_query: types.CallbackQuery | None = None,
message: types.Message | None = None,
):
text = (
"*Введите номер телефона будущего именниника*\n"
"Пример: `+79123456789`\n\n"
"ИЛИ\n\n"
"*Перешлите сообщение пользователя*\n\n"
"ИЛИ\n\n"
"*Просто поделитесь его контактом*"
)
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.BACK_BUTTON)
keyboard.adjust(1)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
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.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_add_subscription_user_birthday(
message: types.Message,
):
text = (
"Отправьте дату рождения пользователя в формате dd.mm.yyyy\n"
"Пример: `26.05.1995`."
)
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.BACK_BUTTON)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_add_subscription_name(
message: types.Message,
users_repository: UsersRepository,
subscription_user_id: uuid.UUID,
):
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:
raise FlowInternalError()
text = (
"Введите имя человека, на которого хотите подписаться\n\n"
)
keyboard = ReplyKeyboardBuilder()
if subscription_user.name is not None:
text += (
"ИЛИ\n\n"
f"Используйте имя из профиля пользователя: `{subscription_user.name}`\n\n"
)
keyboard.button(text=subscription_user.name)
keyboard.button(text=constants.BACK_BUTTON)
keyboard.adjust(1)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_add_subscription_pool_decision(
message: types.Message,
pools_repository: PoolsRepository,
users_repository: UsersRepository,
subscription_user_id: uuid.UUID,
):
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:
raise FlowInternalError()
async with pools_repository.transaction():
pools_count = await pools_repository.get_pools_by_birthday_user_id_count(
birthday_user_id=subscription_user_id,
)
text = "Хотите ли вы участвовать в сборе денег для этого пользователя?"
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.CREATE_POOL_BUTTON)
if pools_count > 0:
keyboard.button(text=constants.JOIN_EXISTING_POOL_BUTTON)
keyboard.button(text=constants.DECLINE_POOL_BUTTON)
keyboard.button(text=constants.BACK_BUTTON)
keyboard.adjust(1)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def show_add_subscription_pools(
user: User,
pools_repository: PoolsRepository,
message: types.Message | None = None,
callback_query: types.CallbackQuery | None = None,
):
page = 1
per_page = 5
if callback_query is not None:
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)
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,
pagination=PagePagination(page=page, per_page=per_page),
)
]
text = "Сборы:" if subscriptions else "Нет сборов"
keyboard = InlineKeyboardBuilder()
for subscription in subscriptions:
keyboard.button(
text=subscription.name,
callback_data=SubscriptionCallbackData(
to_user_id=subscription.to_user_id,
).pack(),
)
navigation_row = []
if page > 1:
navigation_row.append(types.InlineKeyboardButton(
text="<",
callback_data=SubscriptionsCallbackData(page=page - 1).pack(),
))
if pages_count > 1:
navigation_row.append(types.InlineKeyboardButton(
text=f"{page}/{pages_count}",
callback_data="null",
))
if page < pages_count:
navigation_row.append(types.InlineKeyboardButton(
text=">",
callback_data=SubscriptionsCallbackData(page=page + 1).pack(),
))
keyboard.row(*navigation_row)
keyboard.row(
types.InlineKeyboardButton(
text="Добавить",
callback_data=AddSubscriptionCallbackData().pack(),
),
types.InlineKeyboardButton(
text="Назад",
callback_data=MenuCallbackData().pack(),
),
)
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_add_subscription_pool_description(
message: types.Message,
):
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.SKIP_BUTTON)
keyboard.button(text=constants.BACK_BUTTON)
keyboard.adjust(1)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text="Введи описание для сбора",
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_add_subscription_pool_payment_phone(
message: types.Message,
user: User,
):
text = (
"*Введите номер телефона, на который будете принимать сбор денег*\n\n"
"ИЛИ\n\n"
"*Отправьте свой контакт*\n\n"
)
if user.gift_payment_data is not None:
bank_title = constants.BANKS_TITLE_MAP[user.gift_payment_data.bank]
text += (
"ИЛИ\n\n"
"*Используйте данные из своего профиля*:\n"
f"📱 _Телефон_: {user.gift_payment_data.phone}\n"
f"🏦 _Банк_: {bank_title}\n\n"
)
keyboard = ReplyKeyboardBuilder()
keyboard.button(text=constants.SHARE_CONTACT_BUTTON, request_contact=True)
if user.gift_payment_data is not None:
keyboard.button(text=constants.USE_PROFILE_GIFT_PAYMENT_DATA)
keyboard.button(text=constants.BACK_BUTTON)
keyboard.adjust(1)
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_add_subscription_pool_payment_bank(
message: types.Message,
):
text = "Выберите банк для приёма платежей"
keyboard = ReplyKeyboardBuilder()
for title, type_ in constants.BANKS_MAP.items():
keyboard.button(text=title)
keyboard.adjust(3)
keyboard.row(types.KeyboardButton(text=constants.BACK_BUTTON))
reply_markup = keyboard.as_markup(
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)
async def ask_add_subscription_confirmation(
message: types.Message,
users_repository: UsersRepository,
pools_repository: PoolsRepository,
subscription_name: str,
subscription_user_id: uuid.UUID,
subscription_pool_id: uuid.UUID | None = None,
subscription_pool_description: str | None = None,
subscription_pool_phone: str | None = None,
subscription_pool_bank: BankEnum | None = None,
):
async with users_repository.transaction():
subscription_user = await users_repository.get_user_by_id(
user_id=subscription_user_id,
)
if subscription_user is None:
raise FlowInternalError()
subscription_pool = None
if subscription_pool_id is not None:
async with pools_repository.transaction():
subscription_pool = await pools_repository.get_pool_by_id(
pool_id=subscription_pool_id,
with_owner=True,
)
if subscription_pool is None:
raise FlowInternalError()
text = (
f"Вы хотите подписаться на пользователя {subscription_name}?\n\n"
f"🎂 *День рождения*: {subscription_user.birthday.strftime('%d.%m.%Y')}\n\n"
)
if all((subscription_pool_description, subscription_pool_phone, subscription_pool_bank)):
bank_title = constants.BANKS_TITLE_MAP[subscription_pool_bank]
text += (
"💳️ *Вы собираете деньги на*:\n"
f"📱 _Телефон_: {subscription_pool_phone}\n"
f"🏦 _Банк_: {bank_title}\n\n"
)
if subscription_pool_description:
text += (
"_Описание_:\n"
"```\n"
f"{subscription_pool_description}"
"```\n\n"
)
elif subscription_pool is not None:
text += "💳️ ️️️*Вы участвуете в сборе денег*\n\n"
keyboard = InlineKeyboardBuilder()
keyboard.button(
text="Да",
callback_data=AddSubscriptionConfirmCallbackData(
answer=ConfirmAnswerEnum.YES,
).pack(),
)
keyboard.button(
text="Нет",
callback_data=AddSubscriptionConfirmCallbackData(
answer=ConfirmAnswerEnum.NO,
).pack(),
)
keyboard.adjust(2)
reply_markup = keyboard.as_markup()
await message.reply(
text=text,
parse_mode=ParseMode.MARKDOWN,
reply_markup=reply_markup,
)

View File

@@ -0,0 +1,13 @@
from .auth import AuthMiddleware
from .depends import DependsMiddleware
from .typing import TypingMiddleware
__all__ = (
# auth
"AuthMiddleware",
# depends
"DependsMiddleware",
# typing
"TypingMiddleware",
)

View File

@@ -0,0 +1,47 @@
from typing import Any, Awaitable, Callable
import aiogram
from aiogram.types import TelegramObject, User as TelegramUser
from birthday_pool_bot.dto import User
from birthday_pool_bot.repositories.repositories import UsersRepository
from birthday_pool_bot.telegram_bot.ui.utils import get_telegram_user_full_name
class AuthMiddleware(aiogram.BaseMiddleware):
def __init__(self, users_repository: UsersRepository):
self._users_repository = users_repository
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
):
telegram_user = self.get_telegram_user(event=event)
if telegram_user is None:
return await handler(event, data)
async with self._users_repository.transaction():
user = await self._users_repository.get_user_by_telegram_id(
telegram_id=telegram_user.id,
)
if user is None:
user = User(
name=get_telegram_user_full_name(user=telegram_user),
telegram_id=telegram_user.id,
)
user = await self._users_repository.create_user(user=user)
data["user"] = user
return await handler(event, data)
def get_telegram_user(self, event: TelegramObject) -> TelegramUser | None:
if hasattr(event, "from_user") and isinstance(event.from_user, TelegramUser):
return event.from_user
if hasattr(event, "user") and isinstance(event.user, TelegramUser):
return event.user
if hasattr(event, "new_chat_member") and event.new_chat_member:
return event.new_chat_member.user

View File

@@ -0,0 +1,19 @@
from typing import Any, Awaitable, Callable
import aiogram
from aiogram.types import TelegramObject
class DependsMiddleware(aiogram.BaseMiddleware):
def __init__(self, name: str, object_: Any):
self._name = name
self._object = object_
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
):
data[self._name] = self._object
return await handler(event, data)

View File

@@ -0,0 +1,36 @@
import asyncio
import contextlib
from typing import Any, Awaitable, Callable
import aiogram
from aiogram import types
class TypingMiddleware(aiogram.BaseMiddleware):
async def __call__(
self,
handler: Callable[[types.TelegramObject, dict[str, Any]], Awaitable[Any]],
event: types.TelegramObject,
data: dict[str, Any],
):
if not isinstance(event, types.Message):
return await handler(event, data)
bot = data.get("bot")
chat_id = event.chat.id
async with send_typing_action(bot=bot, chat_id=chat_id):
return await handler(event, data)
@contextlib.asynccontextmanager
async def send_typing_action(bot: aiogram.Bot, chat_id: int):
async def background_task():
while True:
await bot.send_chat_action(chat_id=chat_id, action="typing")
await asyncio.sleep(3)
task = asyncio.create_task(background_task())
try:
yield
finally:
task.cancel()

View File

@@ -0,0 +1,38 @@
from aiogram.fsm.state import State, StatesGroup
class MenuState(StatesGroup):
MENU = State()
PROFILE = State()
SUBSCRIPTIONS = State()
class SetProfileNameState(StatesGroup):
WAITING_FOR_NAME = State()
class SetProfilePhoneState(StatesGroup):
WAITING_FOR_PHONE = State()
class SetProfileBirthdayState(StatesGroup):
WAITING_FOR_DATE = State()
class SetProfileGiftPaymentDataState(StatesGroup):
WAITING_FOR_PHONE = State()
WAITING_FOR_BANK = State()
class AddSubscriptionState(StatesGroup):
WAITING_FOR_PHONE = State()
WAITING_FOR_DATE = State()
WAITING_FOR_NAME = State()
WAITING_FOR_POOL_DECISION = State()
WAITING_FOR_CONFIRMATION = State()
class AddSubscriptionPoolState(StatesGroup):
WAITING_FOR_DESCRIPTION = State()
WAITING_FOR_PAYMENT_PHONE = State()
WAITING_FOR_PAYMENT_BANK = State()

View File

@@ -0,0 +1,25 @@
import datetime
import re
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})$")
def parse_date(message: types.Message) -> datetime.date:
if (re_match := BIRTHDATE_REGEXP.match(message.text)) is None:
return
return datetime.date(
year=int(re_match.group("year")),
month=int(re_match.group("month")),
day=int(re_match.group("day")),
)
def get_telegram_user_full_name(user: TelegramUser) -> str:
name_parts = (user.first_name, user.last_name)
return " ".join(filter(None, name_parts))