import uuid from aiogram import Bot, types from aiogram.enums import ParseMode from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder from pydantic_filters import PagePagination from birthday_pool_bot.dto import BankEnum, Pool, Subscription, User from birthday_pool_bot.repositories.repositories import ( PoolsRepository, SubscriptionsRepository, UsersRepository, ) from . import constants from .callback_data import ( ConfirmAnswerEnum, MenuCallbackData, NewSubscriptionCallbackData, NewSubscriptionConfirmCallbackData, NotifyNewPoolCallbackData, PoolActionEnum, PoolCallbackData, PoolsCallbackData, PoolsBackCallbackData, SubscriptionActionEnum, SubscriptionCallbackData, SubscribeToNewPoolAnswerEnum, 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.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( 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=NewSubscriptionCallbackData().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: 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() # 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_new_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_new_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_new_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_new_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_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, ): page = 1 per_page = 5 if callback_query is not None: callback_data = SubscriptionsCallbackData.unpack(callback_query.data) page = callback_data.page 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)) 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 pools else "Нет сборов" keyboard = InlineKeyboardBuilder() 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=PoolsCallbackData(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=PoolsCallbackData(page=page + 1).pack(), )) keyboard.row(*navigation_row) keyboard.row( types.InlineKeyboardButton( text="Назад", callback_data=PoolsBackCallbackData().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 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() 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_new_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_new_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_new_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=NewSubscriptionConfirmCallbackData( answer=ConfirmAnswerEnum.YES, ).pack(), ) keyboard.button( text="Нет", callback_data=NewSubscriptionConfirmCallbackData( 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, ) async def notify_user_about_new_subscription_pool( subscription: Subscription, user: User, pool: Pool, pool_owner: User, bot: Bot, ): if user.telegram_id is None: return text = ( f"Создан новый сбор для {subscription.name}\n\n" "Собирает пользователь: **\n\n" "Сбор денег происходит на:\n" f"_Телефон_: {pool.payment_data.phone}\n" f"_Банк_: {pool.payment_data.bank}\n\n" ) keyboard = InlineKeyboardBuilder() keyboard.button( text="Подписаться", callback_data=NotifyNewPoolCallbackData( action=SubscribeToNewPoolAnswerEnum.YES, ).pack(), ) keyboard.button( text="Закрыть", callback_data=NotifyNewPoolCallbackData( action=SubscribeToNewPoolAnswerEnum.NO, ).pack() ) keyboard.adjust(2) reply_markup = keyboard.as_markup() await bot.send_message( chat_id=user.telegram_id, text=text, parse_mode=ParseMode.MARKDOWN, reply_markup=reply_markup, )